Compare commits
382 Commits
7
.github/workflows/bridge.yml
vendored
7
.github/workflows/bridge.yml
vendored
@@ -8,6 +8,7 @@ on:
|
|||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: "3.16.9"
|
FLUTTER_VERSION: "3.16.9"
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
|
RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
generate_bridge:
|
generate_bridge:
|
||||||
@@ -37,10 +38,8 @@ jobs:
|
|||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
libclang-10-dev \
|
libclang-10-dev \
|
||||||
libclang-dev \
|
|
||||||
libgtk-3-dev \
|
libgtk-3-dev \
|
||||||
llvm-10-dev \
|
llvm-10-dev \
|
||||||
llvm-dev \
|
|
||||||
nasm \
|
nasm \
|
||||||
ninja-build \
|
ninja-build \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
@@ -49,9 +48,9 @@ jobs:
|
|||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
uses: dtolnay/rust-toolchain@v1
|
uses: dtolnay/rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: ${{ env.RUST_VERSION }}
|
||||||
targets: ${{ matrix.job.target }}
|
targets: ${{ matrix.job.target }}
|
||||||
components: ''
|
components: "rustfmt"
|
||||||
|
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
243
.github/workflows/build-macos-arm64.yml
vendored
243
.github/workflows/build-macos-arm64.yml
vendored
@@ -7,55 +7,224 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
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.16.9"
|
FLUTTER_VERSION: "3.19.6"
|
||||||
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"
|
||||||
FLUTTER_ELINUX_COMMIT_ID: "c02bd16e1630f5bd690b85c5c2456ac1920e25af"
|
|
||||||
TAG_NAME: "nightly"
|
TAG_NAME: "nightly"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
# vcpkg version: 2023.10.19
|
# vcpkg version: 2024.03.25
|
||||||
# for multiarch gcc compatibility
|
VCPKG_COMMIT_ID: "a34c873a9717a888f58dc05268dea15592c2f0ff"
|
||||||
VCPKG_COMMIT_ID: "8eb57355a4ffb410a2e94c07b4dca2dffbee8e50"
|
VERSION: "1.2.5"
|
||||||
VERSION: "1.2.4"
|
NDK_VERSION: "r26d"
|
||||||
NDK_VERSION: "r26b"
|
|
||||||
#signing keys env variable checks
|
#signing keys env variable checks
|
||||||
ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
|
ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}"
|
||||||
MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}'
|
MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}"
|
||||||
# To make a custom build with your own servers set the below secret values
|
# To make a custom build with your own servers set the below secret values
|
||||||
RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}'
|
RS_PUB_KEY: "${{ secrets.RS_PUB_KEY }}"
|
||||||
RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}'
|
RENDEZVOUS_SERVER: "${{ secrets.RENDEZVOUS_SERVER }}"
|
||||||
API_SERVER: '${{ secrets.API_SERVER }}'
|
API_SERVER: "${{ secrets.API_SERVER }}"
|
||||||
UPLOAD_ARTIFACT: true
|
UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}"
|
||||||
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-for-macOS-arm64:
|
build-appimage:
|
||||||
name: build-for-macOS-arm64
|
name: Build image ${{ matrix.job.target }}
|
||||||
runs-on: [self-hosted, macOS, ARM64]
|
runs-on: ubuntu-20.04
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- {
|
||||||
|
target: x86_64-unknown-linux-gnu,
|
||||||
|
arch: x86_64,
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: aarch64-unknown-linux-gnu,
|
||||||
|
arch: aarch64,
|
||||||
|
}
|
||||||
steps:
|
steps:
|
||||||
#- name: Import the codesign cert
|
- name: Checkout source code
|
||||||
# if: env.MACOS_P12_BASE64 != null
|
uses: actions/checkout@v3
|
||||||
# uses: apple-actions/import-codesign-certs@v1
|
|
||||||
# continue-on-error: true
|
- name: Rename Binary
|
||||||
# with:
|
run: |
|
||||||
# p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
|
sudo apt-get update -y
|
||||||
# p12-password: ${{ secrets.MACOS_P12_PASSWORD }}
|
sudo apt-get install -y wget libarchive-tools
|
||||||
# keychain: rustdesk
|
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
||||||
|
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
|
||||||
#- name: Check sign and import sign key
|
|
||||||
# if: env.MACOS_P12_BASE64 != null
|
- name: Patch archlinux PKGBUILD
|
||||||
# run: |
|
if: matrix.job.arch == 'x86_64'
|
||||||
# security default-keychain -s rustdesk.keychain
|
run: |
|
||||||
# security find-identity -v
|
sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD
|
||||||
|
if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then
|
||||||
- name: Run
|
sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD
|
||||||
|
fi
|
||||||
|
bsdtar -zxvf rustdesk-${{ env.VERSION }}.deb
|
||||||
|
tar -xvf ./data.tar.xz
|
||||||
|
case ${{ matrix.job.arch }} in
|
||||||
|
aarch64)
|
||||||
|
mkdir -p flutter/build/linux/arm64/release/bundle
|
||||||
|
cp -rf usr/lib/rustdesk/* flutter/build/linux/arm64/release/bundle/
|
||||||
|
;;
|
||||||
|
x86_64)
|
||||||
|
mkdir -p flutter/build/linux/x64/release/bundle
|
||||||
|
cp -rf usr/lib/rustdesk/* flutter/build/linux/x64/release/bundle/
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
- name: Build archlinux package
|
||||||
|
if: matrix.job.arch == 'x86_64'
|
||||||
|
uses: rustdesk-org/arch-makepkg-action@master
|
||||||
|
with:
|
||||||
|
packages: >
|
||||||
|
llvm
|
||||||
|
clang
|
||||||
|
libva
|
||||||
|
libvdpau
|
||||||
|
rust
|
||||||
|
gstreamer
|
||||||
|
unzip
|
||||||
|
git
|
||||||
|
cmake
|
||||||
|
gcc
|
||||||
|
curl
|
||||||
|
wget
|
||||||
|
nasm
|
||||||
|
zip
|
||||||
|
make
|
||||||
|
pkg-config
|
||||||
|
clang
|
||||||
|
gtk3
|
||||||
|
xdotool
|
||||||
|
libxcb
|
||||||
|
libxfixes
|
||||||
|
alsa-lib
|
||||||
|
pipewire
|
||||||
|
python
|
||||||
|
ttf-arphic-uming
|
||||||
|
libappindicator-gtk3
|
||||||
|
pam
|
||||||
|
gst-plugins-base
|
||||||
|
gst-plugin-pipewire
|
||||||
|
scripts: |
|
||||||
|
cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f
|
||||||
|
|
||||||
|
- name: Publish archlinux package
|
||||||
|
if: matrix.job.arch == 'x86_64'
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
files: |
|
||||||
|
res/rustdesk-${{ env.VERSION }}*.zst
|
||||||
|
|
||||||
|
- name: Build appimage package
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd /opt/build
|
# set-up appimage-builder
|
||||||
#./update_mac_template.sh
|
pushd /tmp
|
||||||
#security default-keychain -s rustdesk.keychain
|
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
|
||||||
#security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
|
chmod +x appimage-builder-x86_64.AppImage
|
||||||
./agent.sh
|
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
|
||||||
|
popd
|
||||||
|
# run appimage-builder
|
||||||
|
pushd appimage
|
||||||
|
sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml
|
||||||
|
|
||||||
|
- name: Publish appimage package
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
files: |
|
||||||
|
./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
|
||||||
|
|
||||||
|
build-flatpak:
|
||||||
|
name: Build Flatpak ${{ matrix.job.target }}
|
||||||
|
runs-on: ${{ matrix.job.on }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
job:
|
||||||
|
- {
|
||||||
|
target: x86_64-unknown-linux-gnu,
|
||||||
|
distro: ubuntu18.04,
|
||||||
|
on: ubuntu-20.04,
|
||||||
|
arch: x86_64,
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
target: aarch64-unknown-linux-gnu,
|
||||||
|
# try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub"
|
||||||
|
distro: ubuntu22.04,
|
||||||
|
on: [self-hosted, Linux, ARM64],
|
||||||
|
arch: aarch64,
|
||||||
|
}
|
||||||
|
steps:
|
||||||
|
- name: Checkout source code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Rename Binary
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y wget
|
||||||
|
wget https://github.com/rustdesk/rustdesk/releases/download/nightly/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb
|
||||||
|
mv rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.deb rustdesk-${{ env.VERSION }}.deb
|
||||||
|
|
||||||
|
- uses: rustdesk-org/run-on-arch-action@amd64-support
|
||||||
|
name: Build rustdesk flatpak package for ${{ matrix.job.arch }}
|
||||||
|
id: rpm
|
||||||
|
with:
|
||||||
|
arch: ${{ matrix.job.arch }}
|
||||||
|
distro: ${{ matrix.job.distro }}
|
||||||
|
githubToken: ${{ github.token }}
|
||||||
|
setup: |
|
||||||
|
ls -l "${PWD}"
|
||||||
|
dockerRunArgs: |
|
||||||
|
--volume "${PWD}:/workspace"
|
||||||
|
shell: /bin/bash
|
||||||
|
install: |
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
rpm \
|
||||||
|
wget
|
||||||
|
run: |
|
||||||
|
# disable git safe.directory
|
||||||
|
git config --global --add safe.directory "*"
|
||||||
|
pushd /workspace
|
||||||
|
# install
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y \
|
||||||
|
cmake \
|
||||||
|
curl \
|
||||||
|
flatpak \
|
||||||
|
flatpak-builder \
|
||||||
|
gcc \
|
||||||
|
git \
|
||||||
|
g++ \
|
||||||
|
libgtk-3-dev \
|
||||||
|
nasm \
|
||||||
|
wget
|
||||||
|
# flatpak deps
|
||||||
|
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08
|
||||||
|
flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08
|
||||||
|
# package
|
||||||
|
pushd flatpak
|
||||||
|
git clone https://github.com/flathub/shared-modules.git --depth=1
|
||||||
|
flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json
|
||||||
|
flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak com.rustdesk.RustDesk
|
||||||
|
|
||||||
|
- name: Publish flatpak package
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
prerelease: true
|
||||||
|
tag_name: ${{ env.TAG_NAME }}
|
||||||
|
files: |
|
||||||
|
flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.flatpak
|
||||||
|
|||||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -21,6 +21,9 @@ on:
|
|||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "README.md"
|
- "README.md"
|
||||||
|
- "res/**"
|
||||||
|
- "appimage/**"
|
||||||
|
- "flatpak/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ensure_cargo_fmt:
|
# ensure_cargo_fmt:
|
||||||
@@ -71,12 +74,12 @@ jobs:
|
|||||||
# - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
# - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||||
# - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
# - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true }
|
||||||
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
# - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
|
||||||
# - { target: i686-pc-windows-msvc , os: windows-2019 }
|
# - { target: i686-pc-windows-msvc , os: windows-2022 }
|
||||||
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
# - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
|
||||||
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
# - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||||
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
# - { target: x86_64-apple-darwin , os: macos-10.15 }
|
||||||
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
|
# - { target: x86_64-pc-windows-gnu , os: windows-2022 }
|
||||||
# - { target: x86_64-pc-windows-msvc , os: windows-2019 }
|
# - { target: x86_64-pc-windows-msvc , os: windows-2022 }
|
||||||
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
|
- { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 }
|
||||||
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
# - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
|
||||||
steps:
|
steps:
|
||||||
@@ -103,6 +106,7 @@ jobs:
|
|||||||
gcc \
|
gcc \
|
||||||
git \
|
git \
|
||||||
g++ \
|
g++ \
|
||||||
|
libpam0g-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
libgstreamer1.0-dev \
|
libgstreamer1.0-dev \
|
||||||
libgstreamer-plugins-base1.0-dev \
|
libgstreamer-plugins-base1.0-dev \
|
||||||
|
|||||||
1487
.github/workflows/flutter-build.yml
vendored
1487
.github/workflows/flutter-build.yml
vendored
File diff suppressed because it is too large
Load Diff
3
.github/workflows/flutter-ci.yml
vendored
3
.github/workflows/flutter-ci.yml
vendored
@@ -13,6 +13,9 @@ on:
|
|||||||
- ".github/**"
|
- ".github/**"
|
||||||
- "docs/**"
|
- "docs/**"
|
||||||
- "README.md"
|
- "README.md"
|
||||||
|
- "res/**"
|
||||||
|
- "appimage/**"
|
||||||
|
- "flatpak/**"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-ci:
|
run-ci:
|
||||||
|
|||||||
22
.github/workflows/flutter-tag.yml
vendored
22
.github/workflows/flutter-tag.yml
vendored
@@ -15,24 +15,4 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
with:
|
with:
|
||||||
upload-artifact: true
|
upload-artifact: true
|
||||||
upload-tag: ${{ env.GITHUB_REF_NAME }}
|
upload-tag: ${{ github.ref_name }}
|
||||||
|
|
||||||
update-fdroid-version-file:
|
|
||||||
name: Publish RustDesk version file for F-Droid updater
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Generate RustDesk version file
|
|
||||||
run: |
|
|
||||||
UPSTREAM_VERNAME="$GITHUB_REF_NAME"
|
|
||||||
UPSTREAM_VERCODE="$(echo "$UPSTREAM_VERNAME" | tr -d '.')"
|
|
||||||
echo "versionName=$UPSTREAM_VERNAME" > rustdesk-version.txt
|
|
||||||
echo "versionCode=$UPSTREAM_VERCODE" >> rustdesk-version.txt
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Publish RustDesk version file
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
prerelease: true
|
|
||||||
tag_name: "fdroid-version"
|
|
||||||
files: |
|
|
||||||
./rustdesk-version.txt
|
|
||||||
4
.github/workflows/history.yml
vendored
4
.github/workflows/history.yml
vendored
@@ -8,7 +8,7 @@ env:
|
|||||||
TAG_NAME: "tmp"
|
TAG_NAME: "tmp"
|
||||||
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
FLUTTER_RUST_BRIDGE_VERSION: "1.80.1"
|
||||||
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
|
||||||
VERSION: "1.2.4"
|
VERSION: "1.2.5"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-for-history-windows:
|
build-for-history-windows:
|
||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
job:
|
job:
|
||||||
- { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
- { target: x86_64-pc-windows-msvc, os: windows-2022, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source code
|
- name: Checkout source code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ on:
|
|||||||
description: 'Target'
|
description: 'Target'
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
default: 'windows-2019'
|
default: 'windows-2022'
|
||||||
configuration:
|
configuration:
|
||||||
description: 'Configuration'
|
description: 'Configuration'
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -51,4 +51,6 @@ lib/generated_bridge.dart
|
|||||||
# build cache in examples
|
# build cache in examples
|
||||||
examples/**/target/
|
examples/**/target/
|
||||||
# ===
|
# ===
|
||||||
vcpkg_installed
|
vcpkg_installed
|
||||||
|
flutter/lib/generated_plugin_registrant.dart
|
||||||
|
libsciter.dylib
|
||||||
|
|||||||
441
Cargo.lock
generated
441
Cargo.lock
generated
@@ -115,17 +115,6 @@ dependencies = [
|
|||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "amf"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
|
||||||
dependencies = [
|
|
||||||
"bindgen 0.59.2",
|
|
||||||
"cc",
|
|
||||||
"gpu_common",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android-tzdata"
|
name = "android-tzdata"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -311,9 +300,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.4.5"
|
version = "0.4.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5"
|
checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flate2",
|
"flate2",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -528,6 +517,12 @@ version = "0.21.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64ct"
|
name = "base64ct"
|
||||||
version = "1.6.0"
|
version = "1.6.0"
|
||||||
@@ -768,9 +763,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.5.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde 1.0.190",
|
"serde 1.0.190",
|
||||||
]
|
]
|
||||||
@@ -1106,7 +1101,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "confy"
|
name = "confy"
|
||||||
version = "0.4.0-2"
|
version = "0.4.0-2"
|
||||||
source = "git+https://github.com/open-trade/confy#7855cd3c32b1a60b44e5076ee8f6b4131da10350"
|
source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc5611bb382479"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"directories-next",
|
"directories-next",
|
||||||
"serde 1.0.190",
|
"serde 1.0.190",
|
||||||
@@ -1836,9 +1831,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encoding_rs"
|
name = "encoding_rs"
|
||||||
version = "0.8.33"
|
version = "0.8.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
|
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
]
|
]
|
||||||
@@ -2097,7 +2092,7 @@ version = "0.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"spin 0.9.8",
|
"spin",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2649,36 +2644,6 @@ dependencies = [
|
|||||||
"system-deps 6.1.2",
|
"system-deps 6.1.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gpu_common"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
|
||||||
dependencies = [
|
|
||||||
"bindgen 0.59.2",
|
|
||||||
"cc",
|
|
||||||
"log",
|
|
||||||
"serde 1.0.190",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json 1.0.107",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gpucodec"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
|
||||||
dependencies = [
|
|
||||||
"amf",
|
|
||||||
"bindgen 0.59.2",
|
|
||||||
"cc",
|
|
||||||
"gpu_common",
|
|
||||||
"log",
|
|
||||||
"nv",
|
|
||||||
"serde 1.0.190",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json 1.0.107",
|
|
||||||
"vpl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gstreamer"
|
name = "gstreamer"
|
||||||
version = "0.16.7"
|
version = "0.16.7"
|
||||||
@@ -2866,9 +2831,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "h2"
|
name = "h2"
|
||||||
version = "0.3.24"
|
version = "0.3.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9"
|
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -2917,6 +2882,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"backtrace",
|
"backtrace",
|
||||||
|
"base64 0.22.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"confy",
|
"confy",
|
||||||
@@ -2928,6 +2894,7 @@ dependencies = [
|
|||||||
"flexi_logger",
|
"flexi_logger",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"httparse",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@@ -2936,19 +2903,24 @@ dependencies = [
|
|||||||
"osascript",
|
"osascript",
|
||||||
"protobuf",
|
"protobuf",
|
||||||
"protobuf-codegen",
|
"protobuf-codegen",
|
||||||
"quinn",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"regex",
|
"regex",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-platform-verifier",
|
||||||
"serde 1.0.190",
|
"serde 1.0.190",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json 1.0.107",
|
"serde_json 1.0.107",
|
||||||
"socket2 0.3.19",
|
"socket2 0.3.19",
|
||||||
"sodiumoxide",
|
"sodiumoxide",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-socks",
|
"tokio-native-tls",
|
||||||
|
"tokio-rustls 0.26.0",
|
||||||
|
"tokio-socks 0.5.2",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml 0.7.8",
|
"toml 0.7.8",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
"zstd 0.13.0",
|
"zstd 0.13.0",
|
||||||
@@ -3025,9 +2997,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "0.2.9"
|
version = "0.2.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
|
checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"fnv",
|
"fnv",
|
||||||
@@ -3036,9 +3008,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http-body"
|
name = "http-body"
|
||||||
version = "0.4.5"
|
version = "0.4.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
|
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"http",
|
"http",
|
||||||
@@ -3065,8 +3037,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hwcodec"
|
name = "hwcodec"
|
||||||
version = "0.2.0"
|
version = "0.4.15"
|
||||||
source = "git+https://github.com/21pages/hwcodec?branch=stable#52e1da2aae86acec5f374bc065f5921945b55e7b"
|
source = "git+https://github.com/21pages/hwcodec#1d504ee590c15472813fecc22cee4b8149b2b8cd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.59.2",
|
"bindgen 0.59.2",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -3078,9 +3050,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.27"
|
version = "0.14.28"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
|
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
@@ -3093,7 +3065,7 @@ dependencies = [
|
|||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa 1.0.9",
|
"itoa 1.0.9",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.4.10",
|
"socket2 0.5.5",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -3111,7 +3083,7 @@ dependencies = [
|
|||||||
"hyper",
|
"hyper",
|
||||||
"rustls 0.21.10",
|
"rustls 0.21.10",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls",
|
"tokio-rustls 0.24.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3791,15 +3763,6 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "miow"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "359f76430b20a79f9e20e115b3428614e654f04fab314482fc0fda0ebd3c6044"
|
|
||||||
dependencies = [
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mouce"
|
name = "mouce"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -4134,17 +4097,6 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nv"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
|
||||||
dependencies = [
|
|
||||||
"bindgen 0.59.2",
|
|
||||||
"cc",
|
|
||||||
"gpu_common",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc"
|
name = "objc"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -4432,12 +4384,11 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "parity-tokio-ipc"
|
name = "parity-tokio-ipc"
|
||||||
version = "0.7.3-3"
|
version = "0.7.3-3"
|
||||||
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#f2d1fcf8fb002335d9a62bec308559d40698694d"
|
source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#e8448ade10d6d972d0b2307646424b36ab202ff5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"miow",
|
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"tokio",
|
"tokio",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
@@ -4616,7 +4567,7 @@ version = "1.5.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa"
|
checksum = "9a4a0cfc5fb21a09dc6af4bf834cf10d4a32fccd9e2ea468c4b1751a097487aa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.21.5",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"line-wrap",
|
"line-wrap",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
@@ -4762,9 +4713,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf"
|
name = "protobuf"
|
||||||
version = "3.3.0"
|
version = "3.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190"
|
checksum = "58678a64de2fced2bdec6bca052a6716a0efe692d6e3f53d1bda6a1def64cfc0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -4774,9 +4725,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf-codegen"
|
name = "protobuf-codegen"
|
||||||
version = "3.3.0"
|
version = "3.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6e85514a216b1c73111d9032e26cc7a5ecb1bb3d4d9539e91fb72a4395060f78"
|
checksum = "32777b0b3f6538d9d2e012b3fad85c7e4b9244b5958d04a6415f4333782b7a77"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -4789,9 +4740,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf-parse"
|
name = "protobuf-parse"
|
||||||
version = "3.3.0"
|
version = "3.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77d6fbd6697c9e531873e81cec565a85e226b99a0f10e1acc079be057fe2fcba"
|
checksum = "96cb37955261126624a25b5e6bda40ae34cf3989d52a783087ca6091b29b5642"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
@@ -4805,9 +4756,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "protobuf-support"
|
name = "protobuf-support"
|
||||||
version = "3.3.0"
|
version = "3.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c"
|
checksum = "e1ed294a835b0f30810e13616b1cd34943c6d1e84a8f3b0dcfe466d256c3e7e7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
@@ -4860,56 +4811,6 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn"
|
|
||||||
version = "0.9.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2e8b432585672228923edbbf64b8b12c14e1112f62e88737655b4a083dbcd78e"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"pin-project-lite",
|
|
||||||
"quinn-proto",
|
|
||||||
"quinn-udp",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls 0.20.9",
|
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"webpki",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-proto"
|
|
||||||
version = "0.9.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "94b0b33c13a79f669c85defaf4c275dc86a0c0372807d0ca3d78e0bb87274863"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"ring 0.16.20",
|
|
||||||
"rustc-hash",
|
|
||||||
"rustls 0.20.9",
|
|
||||||
"rustls-native-certs",
|
|
||||||
"slab",
|
|
||||||
"thiserror",
|
|
||||||
"tinyvec",
|
|
||||||
"tracing",
|
|
||||||
"webpki",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quinn-udp"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"quinn-proto",
|
|
||||||
"socket2 0.4.10",
|
|
||||||
"tracing",
|
|
||||||
"windows-sys 0.42.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "0.6.13"
|
version = "0.6.13"
|
||||||
@@ -5224,10 +5125,10 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.11.23"
|
version = "0.11.23"
|
||||||
source = "git+https://github.com/rustdesk-org/reqwest"
|
source = "git+https://github.com/rustdesk-org/reqwest#9cb758c9fb2f4edc62eb790acfd45a6a3da21ed3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-compression",
|
"async-compression",
|
||||||
"base64",
|
"base64 0.21.5",
|
||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
@@ -5247,8 +5148,8 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustls 0.21.10",
|
"rustls 0.21.10",
|
||||||
"rustls-native-certs",
|
"rustls-native-certs 0.6.3",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile 1.0.3",
|
||||||
"serde 1.0.190",
|
"serde 1.0.190",
|
||||||
"serde_json 1.0.107",
|
"serde_json 1.0.107",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@@ -5256,32 +5157,18 @@ dependencies = [
|
|||||||
"system-configuration",
|
"system-configuration",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
"tokio-rustls",
|
"tokio-rustls 0.24.1",
|
||||||
|
"tokio-socks 0.5.1",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots",
|
"webpki-roots 0.25.4",
|
||||||
"winreg 0.50.0",
|
"winreg 0.50.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ring"
|
|
||||||
version = "0.16.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"spin 0.5.2",
|
|
||||||
"untrusted 0.7.1",
|
|
||||||
"web-sys",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.5"
|
version = "0.17.5"
|
||||||
@@ -5291,8 +5178,8 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"libc",
|
"libc",
|
||||||
"spin 0.9.8",
|
"spin",
|
||||||
"untrusted 0.9.0",
|
"untrusted",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5402,14 +5289,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.4"
|
version = "1.2.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-wakelock",
|
"android-wakelock",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
"arboard",
|
"arboard",
|
||||||
"async-process",
|
"async-process",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64",
|
|
||||||
"bytes",
|
"bytes",
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
@@ -5500,7 +5386,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.2.4"
|
version = "1.2.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"brotli",
|
"brotli",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
@@ -5551,17 +5437,6 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls"
|
|
||||||
version = "0.20.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99"
|
|
||||||
dependencies = [
|
|
||||||
"ring 0.16.20",
|
|
||||||
"sct",
|
|
||||||
"webpki",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.21.10"
|
version = "0.21.10"
|
||||||
@@ -5569,11 +5444,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
|
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"ring 0.17.5",
|
"ring",
|
||||||
"rustls-webpki",
|
"rustls-webpki 0.101.7",
|
||||||
"sct",
|
"sct",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c4d6d8ad9f2492485e13453acbb291dd08f64441b6609c491f1c2cd2c6b4fe1"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki 0.102.2",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-native-certs"
|
name = "rustls-native-certs"
|
||||||
version = "0.6.3"
|
version = "0.6.3"
|
||||||
@@ -5581,7 +5471,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"openssl-probe",
|
"openssl-probe",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile 1.0.3",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-native-certs"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
|
||||||
|
dependencies = [
|
||||||
|
"openssl-probe",
|
||||||
|
"rustls-pemfile 2.1.2",
|
||||||
|
"rustls-pki-types",
|
||||||
"schannel",
|
"schannel",
|
||||||
"security-framework",
|
"security-framework",
|
||||||
]
|
]
|
||||||
@@ -5592,17 +5495,71 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
|
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.21.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pemfile"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.0",
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-platform-verifier"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b5f0d26fa1ce3c790f9590868f0109289a044acb954525f933e2aa3b871c157d"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
|
"core-foundation-sys 0.8.4",
|
||||||
|
"jni 0.19.0",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"rustls 0.23.4",
|
||||||
|
"rustls-native-certs 0.7.0",
|
||||||
|
"rustls-platform-verifier-android",
|
||||||
|
"rustls-webpki 0.102.2",
|
||||||
|
"security-framework",
|
||||||
|
"security-framework-sys",
|
||||||
|
"webpki-roots 0.26.1",
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-platform-verifier-android"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.101.7"
|
version = "0.101.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring 0.17.5",
|
"ring",
|
||||||
"untrusted 0.9.0",
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.102.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5683,7 +5640,6 @@ dependencies = [
|
|||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"dbus",
|
"dbus",
|
||||||
"docopt",
|
"docopt",
|
||||||
"gpucodec",
|
|
||||||
"gstreamer",
|
"gstreamer",
|
||||||
"gstreamer-app",
|
"gstreamer-app",
|
||||||
"gstreamer-video",
|
"gstreamer-video",
|
||||||
@@ -5712,28 +5668,29 @@ version = "0.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring 0.17.5",
|
"ring",
|
||||||
"untrusted 0.9.0",
|
"untrusted",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework"
|
name = "security-framework"
|
||||||
version = "2.9.2"
|
version = "2.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
|
checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
"core-foundation 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"core-foundation-sys 0.8.4",
|
"core-foundation-sys 0.8.4",
|
||||||
"libc",
|
"libc",
|
||||||
|
"num-bigint",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "security-framework-sys"
|
name = "security-framework-sys"
|
||||||
version = "2.9.1"
|
version = "2.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
|
checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation-sys 0.8.4",
|
"core-foundation-sys 0.8.4",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -5971,12 +5928,6 @@ dependencies = [
|
|||||||
"serde 1.0.190",
|
"serde 1.0.190",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "spin"
|
|
||||||
version = "0.5.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
@@ -6408,9 +6359,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.36.0"
|
version = "1.37.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -6456,10 +6407,33 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||||
|
dependencies = [
|
||||||
|
"rustls 0.23.4",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-socks"
|
name = "tokio-socks"
|
||||||
version = "0.5.1-2"
|
version = "0.5.1"
|
||||||
source = "git+https://github.com/open-trade/tokio-socks#14a5c2564fa20a2765ea53d03c573ee2b7e20421"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
"futures-util",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-socks"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "git+https://github.com/rustdesk-org/tokio-socks#51037c93f8be34196fd2b6de9f674e8dfae3d01e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"either",
|
"either",
|
||||||
@@ -6689,9 +6663,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.4"
|
version = "0.2.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
@@ -6782,12 +6756,6 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "untrusted"
|
|
||||||
version = "0.7.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -6900,17 +6868,6 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "vpl"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "git+https://github.com/21pages/gpucodec#546f7f644ce15a35b833c1531a4fead4b34a1b3b"
|
|
||||||
dependencies = [
|
|
||||||
"bindgen 0.59.2",
|
|
||||||
"cc",
|
|
||||||
"gpu_common",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "waker-fn"
|
name = "waker-fn"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -7133,20 +7090,19 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki"
|
name = "webpki-roots"
|
||||||
version = "0.22.4"
|
version = "0.25.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53"
|
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||||
dependencies = [
|
|
||||||
"ring 0.17.5",
|
|
||||||
"untrusted 0.9.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webpki-roots"
|
name = "webpki-roots"
|
||||||
version = "0.25.3"
|
version = "0.26.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10"
|
checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
@@ -7370,21 +7326,6 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-sys"
|
|
||||||
version = "0.42.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm 0.42.2",
|
|
||||||
"windows_aarch64_msvc 0.42.2",
|
|
||||||
"windows_i686_gnu 0.42.2",
|
|
||||||
"windows_i686_msvc 0.42.2",
|
|
||||||
"windows_x86_64_gnu 0.42.2",
|
|
||||||
"windows_x86_64_gnullvm 0.42.2",
|
|
||||||
"windows_x86_64_msvc 0.42.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.45.0"
|
version = "0.45.0"
|
||||||
@@ -7919,6 +7860,12 @@ dependencies = [
|
|||||||
"syn 2.0.55",
|
"syn 2.0.55",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zip"
|
name = "zip"
|
||||||
version = "0.6.6"
|
version = "0.6.6"
|
||||||
|
|||||||
33
Cargo.toml
33
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk"
|
name = "rustdesk"
|
||||||
version = "1.2.4"
|
version = "1.2.5"
|
||||||
authors = ["rustdesk <info@rustdesk.com>"]
|
authors = ["rustdesk <info@rustdesk.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build= "build.rs"
|
build= "build.rs"
|
||||||
@@ -19,19 +19,14 @@ path = "src/naming.rs"
|
|||||||
[features]
|
[features]
|
||||||
inline = []
|
inline = []
|
||||||
cli = []
|
cli = []
|
||||||
flutter_texture_render = []
|
|
||||||
appimage = []
|
|
||||||
flatpak = []
|
|
||||||
use_samplerate = ["samplerate"]
|
use_samplerate = ["samplerate"]
|
||||||
use_rubato = ["rubato"]
|
use_rubato = ["rubato"]
|
||||||
use_dasp = ["dasp"]
|
use_dasp = ["dasp"]
|
||||||
flutter = ["flutter_rust_bridge"]
|
flutter = ["flutter_rust_bridge"]
|
||||||
default = ["use_dasp"]
|
default = ["use_dasp"]
|
||||||
hwcodec = ["scrap/hwcodec"]
|
hwcodec = ["scrap/hwcodec"]
|
||||||
gpucodec = ["scrap/gpucodec"]
|
vram = ["scrap/vram"]
|
||||||
mediacodec = ["scrap/mediacodec"]
|
mediacodec = ["scrap/mediacodec"]
|
||||||
linux_headless = ["pam" ]
|
|
||||||
virtual_display_driver = ["virtual_display"]
|
|
||||||
plugin_framework = []
|
plugin_framework = []
|
||||||
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
|
linux-pkg-config = ["magnum-opus/linux-pkg-config", "scrap/linux-pkg-config"]
|
||||||
unix-file-copy-paste = [
|
unix-file-copy-paste = [
|
||||||
@@ -65,7 +60,6 @@ samplerate = { version = "0.2", optional = true }
|
|||||||
uuid = { version = "1.3", features = ["v4"] }
|
uuid = { version = "1.3", features = ["v4"] }
|
||||||
clap = "4.2"
|
clap = "4.2"
|
||||||
rpassword = "7.2"
|
rpassword = "7.2"
|
||||||
base64 = "0.21"
|
|
||||||
num_cpus = "1.15"
|
num_cpus = "1.15"
|
||||||
bytes = { version = "1.4", features = ["serde"] }
|
bytes = { version = "1.4", features = ["serde"] }
|
||||||
default-net = "0.14"
|
default-net = "0.14"
|
||||||
@@ -100,10 +94,23 @@ system_shutdown = "4.0"
|
|||||||
qrcode-generator = "4.1"
|
qrcode-generator = "4.1"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
winapi = { version = "0.3", features = ["winuser", "wincrypt", "shellscalingapi", "pdh", "synchapi", "memoryapi", "shellapi"] }
|
winapi = { version = "0.3", features = [
|
||||||
|
"winuser",
|
||||||
|
"wincrypt",
|
||||||
|
"shellscalingapi",
|
||||||
|
"pdh",
|
||||||
|
"synchapi",
|
||||||
|
"memoryapi",
|
||||||
|
"shellapi",
|
||||||
|
"devguid",
|
||||||
|
"setupapi",
|
||||||
|
"cguid",
|
||||||
|
"cfgmgr32",
|
||||||
|
"ioapiset",
|
||||||
|
] }
|
||||||
winreg = "0.11"
|
winreg = "0.11"
|
||||||
windows-service = "0.6"
|
windows-service = "0.6"
|
||||||
virtual_display = { path = "libs/virtual_display", optional = true }
|
virtual_display = { path = "libs/virtual_display" }
|
||||||
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
|
||||||
shared_memory = "0.12"
|
shared_memory = "0.12"
|
||||||
tauri-winrt-notification = "0.1.2"
|
tauri-winrt-notification = "0.1.2"
|
||||||
@@ -132,10 +139,10 @@ wallpaper = { git = "https://github.com/21pages/wallpaper.rs" }
|
|||||||
|
|
||||||
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||||
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
# https://github.com/rustdesk/rustdesk-server-pro/issues/189, using native-tls for better tls support
|
||||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "native-tls", "gzip"], default-features=false }
|
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "native-tls", "gzip"], default-features=false }
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||||
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocking", "socks", "json", "rustls-tls", "rustls-tls-native-roots", "gzip"], default-features=false }
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
psimple = { package = "libpulse-simple-binding", version = "2.27" }
|
||||||
@@ -146,7 +153,7 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
|
|||||||
evdev = { git="https://github.com/fufesou/evdev" }
|
evdev = { git="https://github.com/fufesou/evdev" }
|
||||||
dbus = "0.9"
|
dbus = "0.9"
|
||||||
dbus-crossroads = "0.5"
|
dbus-crossroads = "0.5"
|
||||||
pam = { git="https://github.com/fufesou/pam", optional = true }
|
pam = { git="https://github.com/fufesou/pam" }
|
||||||
users = { version = "0.11" }
|
users = { version = "0.11" }
|
||||||
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true}
|
||||||
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
x11rb = {version = "0.12", features = ["all-extensions"], optional = true}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ RUN apt update -y && \
|
|||||||
libxcb-shape0-dev \
|
libxcb-shape0-dev \
|
||||||
libxcb-xfixes0-dev \
|
libxcb-xfixes0-dev \
|
||||||
libasound2-dev \
|
libasound2-dev \
|
||||||
|
libpam0g-dev \
|
||||||
libpulse-dev \
|
libpulse-dev \
|
||||||
make \
|
make \
|
||||||
cmake \
|
cmake \
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
version: 1
|
version: 1
|
||||||
script:
|
script:
|
||||||
- rm -rf ./AppDir || true
|
- rm -rf ./AppDir || true
|
||||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
- bsdtar -zxvf rustdesk.deb
|
||||||
- tar -xvf ./data.tar.xz
|
- tar -xvf ./data.tar.xz
|
||||||
- mkdir ./AppDir
|
- mkdir ./AppDir
|
||||||
- mv ./usr ./AppDir/usr
|
- mv ./usr ./AppDir/usr
|
||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.4
|
version: 1.2.5
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
@@ -26,18 +26,18 @@ AppDir:
|
|||||||
- arm64
|
- arm64
|
||||||
allow_unauthenticated: true
|
allow_unauthenticated: true
|
||||||
sources:
|
sources:
|
||||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse
|
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal main restricted universe multiverse
|
||||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse
|
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-updates main restricted universe multiverse
|
||||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted
|
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-backports main restricted
|
||||||
universe multiverse
|
universe multiverse
|
||||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||||
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted
|
- sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ focal-security main restricted
|
||||||
universe multiverse
|
universe multiverse
|
||||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
|
||||||
include:
|
include:
|
||||||
- libc6
|
- libc6:arm64
|
||||||
- libgtk-3-0
|
- libgtk-3-0
|
||||||
- libxcb-randr0
|
- libxcb-randr0
|
||||||
- libxdo3
|
- libxdo3
|
||||||
@@ -51,9 +51,15 @@ AppDir:
|
|||||||
- libva-x11-2
|
- libva-x11-2
|
||||||
- libvdpau1
|
- libvdpau1
|
||||||
- libgstreamer-plugins-base1.0-0
|
- libgstreamer-plugins-base1.0-0
|
||||||
|
- gstreamer1.0-pipewire
|
||||||
|
- libwayland-client0
|
||||||
- libwayland-cursor0
|
- libwayland-cursor0
|
||||||
- libwayland-egl1
|
- libwayland-egl1
|
||||||
- libpulse0
|
- libpulse0
|
||||||
|
- packagekit-gtk3-module
|
||||||
|
- libcanberra-gtk3-module
|
||||||
|
- libpam0g
|
||||||
|
- libdrm2
|
||||||
exclude:
|
exclude:
|
||||||
- humanity-icon-theme
|
- humanity-icon-theme
|
||||||
- hicolor-icon-theme
|
- hicolor-icon-theme
|
||||||
@@ -69,8 +75,11 @@ AppDir:
|
|||||||
- usr/share/doc/*/TODO.*
|
- usr/share/doc/*/TODO.*
|
||||||
runtime:
|
runtime:
|
||||||
env:
|
env:
|
||||||
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
|
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules
|
||||||
GDK_BACKEND: x11
|
GDK_BACKEND: x11
|
||||||
|
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64
|
||||||
|
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||||
|
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0
|
||||||
test:
|
test:
|
||||||
fedora-30:
|
fedora-30:
|
||||||
image: appimagecrafters/tests-env:fedora-30
|
image: appimagecrafters/tests-env:fedora-30
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
version: 1
|
version: 1
|
||||||
script:
|
script:
|
||||||
- rm -rf ./AppDir || true
|
- rm -rf ./AppDir || true
|
||||||
- bsdtar -zxvf ../rustdesk-1.2.4.deb
|
- bsdtar -zxvf rustdesk.deb
|
||||||
- tar -xvf ./data.tar.xz
|
- tar -xvf ./data.tar.xz
|
||||||
- mkdir ./AppDir
|
- mkdir ./AppDir
|
||||||
- mv ./usr ./AppDir/usr
|
- mv ./usr ./AppDir/usr
|
||||||
@@ -18,7 +18,7 @@ AppDir:
|
|||||||
id: rustdesk
|
id: rustdesk
|
||||||
name: rustdesk
|
name: rustdesk
|
||||||
icon: rustdesk
|
icon: rustdesk
|
||||||
version: 1.2.4
|
version: 1.2.5
|
||||||
exec: usr/lib/rustdesk/rustdesk
|
exec: usr/lib/rustdesk/rustdesk
|
||||||
exec_args: $@
|
exec_args: $@
|
||||||
apt:
|
apt:
|
||||||
@@ -26,18 +26,16 @@ AppDir:
|
|||||||
- amd64
|
- amd64
|
||||||
allow_unauthenticated: true
|
allow_unauthenticated: true
|
||||||
sources:
|
sources:
|
||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal main restricted
|
||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates main restricted
|
||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal universe
|
||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates universe
|
||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal multiverse
|
||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-updates multiverse
|
||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-backports main restricted
|
||||||
universe multiverse
|
universe multiverse
|
||||||
- sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted
|
- sourceline: deb http://archive.ubuntu.com/ubuntu/ focal-security main restricted
|
||||||
universe multiverse
|
universe multiverse
|
||||||
- sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu
|
|
||||||
bionic main
|
|
||||||
include:
|
include:
|
||||||
- libc6:amd64
|
- libc6:amd64
|
||||||
- libgtk-3-0
|
- libgtk-3-0
|
||||||
@@ -54,9 +52,14 @@ AppDir:
|
|||||||
- libvdpau1
|
- libvdpau1
|
||||||
- libgstreamer-plugins-base1.0-0
|
- libgstreamer-plugins-base1.0-0
|
||||||
- gstreamer1.0-pipewire
|
- gstreamer1.0-pipewire
|
||||||
|
- libwayland-client0
|
||||||
- libwayland-cursor0
|
- libwayland-cursor0
|
||||||
- libwayland-egl1
|
- libwayland-egl1
|
||||||
- libpulse0
|
- libpulse0
|
||||||
|
- packagekit-gtk3-module
|
||||||
|
- libcanberra-gtk3-module
|
||||||
|
- libpam0g
|
||||||
|
- libdrm2
|
||||||
exclude:
|
exclude:
|
||||||
- humanity-icon-theme
|
- humanity-icon-theme
|
||||||
- hicolor-icon-theme
|
- hicolor-icon-theme
|
||||||
@@ -72,8 +75,11 @@ AppDir:
|
|||||||
- usr/share/doc/*/TODO.*
|
- usr/share/doc/*/TODO.*
|
||||||
runtime:
|
runtime:
|
||||||
env:
|
env:
|
||||||
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
|
GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules
|
||||||
GDK_BACKEND: x11
|
GDK_BACKEND: x11
|
||||||
|
APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64
|
||||||
|
GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||||
|
GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0
|
||||||
test:
|
test:
|
||||||
fedora-30:
|
fedora-30:
|
||||||
image: appimagecrafters/tests-env:fedora-30
|
image: appimagecrafters/tests-env:fedora-30
|
||||||
|
|||||||
29
build.py
29
build.py
@@ -33,9 +33,9 @@ def get_arch() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def system2(cmd):
|
def system2(cmd):
|
||||||
err = os.system(cmd)
|
exit_code = os.system(cmd)
|
||||||
if err != 0:
|
if exit_code != 0:
|
||||||
print(f"Error occurred when executing: {cmd}. Exiting.")
|
sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
|
|
||||||
@@ -118,9 +118,9 @@ def make_parser():
|
|||||||
'' if windows or osx else ', need libva-dev, libvdpau-dev.')
|
'' if windows or osx else ', need libva-dev, libvdpau-dev.')
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--gpucodec',
|
'--vram',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help='Enable feature gpucodec, only available on windows now.'
|
help='Enable feature vram, only available on windows now.'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--portable',
|
'--portable',
|
||||||
@@ -132,16 +132,6 @@ def make_parser():
|
|||||||
action='store_true',
|
action='store_true',
|
||||||
help='Build with unix file copy paste feature'
|
help='Build with unix file copy paste feature'
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
'--flatpak',
|
|
||||||
action='store_true',
|
|
||||||
help='Build rustdesk libs with the flatpak feature enabled'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
'--appimage',
|
|
||||||
action='store_true',
|
|
||||||
help='Build rustdesk libs with the appimage feature enabled'
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--skip-cargo',
|
'--skip-cargo',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
@@ -282,15 +272,10 @@ def get_features(args):
|
|||||||
features = ['inline'] if not args.flutter else []
|
features = ['inline'] if not args.flutter else []
|
||||||
if args.hwcodec:
|
if args.hwcodec:
|
||||||
features.append('hwcodec')
|
features.append('hwcodec')
|
||||||
if args.gpucodec:
|
if args.vram:
|
||||||
features.append('gpucodec')
|
features.append('vram')
|
||||||
if args.flutter:
|
if args.flutter:
|
||||||
features.append('flutter')
|
features.append('flutter')
|
||||||
features.append('flutter_texture_render')
|
|
||||||
if args.flatpak:
|
|
||||||
features.append('flatpak')
|
|
||||||
if args.appimage:
|
|
||||||
features.append('appimage')
|
|
||||||
if args.unix_file_copy_paste:
|
if args.unix_file_copy_paste:
|
||||||
features.append('unix-file-copy-paste')
|
features.append('unix-file-copy-paste')
|
||||||
print("features:", features)
|
print("features:", features)
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="../res/logo-header.svg" alt="RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn"><br>
|
<img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
|
||||||
<a href="#free-public-servers">Máy chủ</a> •
|
<a href="#free-public-servers">Server</a> •
|
||||||
<a href="#raw-steps-to-build">Build</a> •
|
<a href="#raw-steps-to-build">Build</a> •
|
||||||
<a href="#how-to-build-with-docker">Docker</a> •
|
<a href="#how-to-build-with-docker">Docker</a> •
|
||||||
<a href="#file-structure">Cấu trúc tệp tin</a> •
|
<a href="#file-structure">Structure</a> •
|
||||||
<a href="#snapshot">Snapshot</a><br>
|
<a href="#snapshot">Snapshot</a><br>
|
||||||
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
[<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
|
||||||
<b>Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và <a href="https://github.com/rustdesk/doc.rustdesk.com">tài liệu</a> sang ngôn ngữ bản địa của bạn</b>
|
<b>Chúng tôi rất hoan nghênh sự hỗ trợ của bạn trong việc dịch trang README, trang giao diện người dùng của RustDesk - <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> và trang tài liệu của RustDesk - <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> sang Tiếng Việt</b>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Chat với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
Hãy trao đổi với chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
|
||||||
|
|
||||||
[](https://ko-fi.com/I2I04VU09)
|
[](https://ko-fi.com/I2I04VU09)
|
||||||
|
|
||||||
Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằng ngôn ngữ Rust. Hoạt động tức thì, không cần phải cài đặt. Bạn có toàn quyền điểu khiển với dữ liệu của bạn mà không cần phải lo lắng về sự bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi, [tự cài đặt máy chủ](https://rustdesk.com/server), hay thậm chí [tự tạo máy chủ rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
|
RustDesk là một phần mềm điểu khiển máy tính từ xa mã nguồn mở, được viết bằng Rust. Nó hoạt động ngay sau khi cài đặt, không yêu cầu cấu hình phức tạp. Bạn có toàn quyền kiểm soát với dữ liệu của mình mà không cần phải lo lắng về vấn đề bảo mật. Bạn có thể sử dụng máy chủ rendezvous/relay của chúng tôi hoặc [tự cài đặt máy chủ của riêng mình](https://rustdesk.com/server) hay thậm chí [tự tạo máy chủ rendezvous/relay cho riêng bạn](https://github.com/rustdesk/rustdesk-server-demo).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](CONTRIBUTING.md).
|
**RustDesk** luôn hoan nghênh mọi đóng góp từ mọi người. Hãy xem tệp [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) để bắt đầu.
|
||||||
|
|
||||||
[**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
|
[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
|
||||||
|
[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
|
||||||
[**CÁC BẢN PHÂN PHÁT MÃ NHỊ PHÂN**](https://github.com/rustdesk/rustdesk/releases)
|
[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/FAQreleases/tag/nightly)
|
||||||
|
|
||||||
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
|
||||||
alt="Get it on F-Droid"
|
alt="Get it on F-Droid"
|
||||||
@@ -29,28 +31,25 @@ Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
Phiên bản cho máy tính sử dụng [sciter](https://sciter.com/) cho giao diện của phần mềm, vậy nên bạn cần tự tải về thư viện sciter.
|
Phiên bản máy tính sử dụng __Flutter__ hoặc __Sciter__ (đã lỗi thời) cho giao diện người dùng (GUI). Hướng dẫn này chỉ áp dụng cho phiên bản Sciter, vì nó thân thiện và dễ bắt đầu hơn. Hãy kiểm tra [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) của chúng tôi để xây dựng phiên bản Flutter.
|
||||||
|
|
||||||
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
|
Vui lòng tự tải thư viện `Sciter` về máy theo hướng dẫn cho từng hệ điều hành.
|
||||||
[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
|
|
||||||
[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
|
||||||
|
|
||||||
Phiên bản cho điện thoại sử dụng Flutter. Chúng tôi sẽ chuyển sang sử dụng Flutter thay cho Sciter cho phiên bản máy tính.
|
[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
|
||||||
|
|
||||||
## Cách để build
|
## Các bước build cơ bản
|
||||||
|
|
||||||
- Chuẩn bị môi trường phát triển Rust và môi trường build C++
|
- Chuẩn bị môi trường phát triển Rust và môi trường biên dịch C++
|
||||||
|
|
||||||
- Tải và cài [vcpkg](https://github.com/microsoft/vcpkg), và đặt biến môi trường `VCPKG_ROOT` sao cho đúng.
|
- Tải và cài đặt [`vcpkg`](https://github.com/microsoft/vcpkg), và thiết lập biến môi trường `VCPKG_ROOT`.
|
||||||
|
|
||||||
- Đối với Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
|
|
||||||
- Đối với Linux/MacOS: vcpkg install libvpx libyuv opus aom
|
|
||||||
|
|
||||||
|
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static`
|
||||||
|
- Linux/MacOS: `vcpkg install libvpx libyuv opus aom`
|
||||||
- Chạy lệnh `cargo run`
|
- Chạy lệnh `cargo run`
|
||||||
|
|
||||||
## [Build](https://rustdesk.com/docs/en/dev/build/)
|
## [Build](https://rustdesk.com/docs/en/dev/build/)
|
||||||
|
|
||||||
## Cách để build cho Linux
|
## Cách build cho Linux
|
||||||
|
|
||||||
### Ubuntu 18 (Debian 10)
|
### Ubuntu 18 (Debian 10)
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
|
|||||||
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cách cài vcpkg
|
### Cách cài đặt `vcpkg`
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/microsoft/vcpkg
|
git clone https://github.com/microsoft/vcpkg
|
||||||
@@ -82,7 +81,7 @@ export VCPKG_ROOT=$HOME/vcpkg
|
|||||||
vcpkg/vcpkg install libvpx libyuv opus aom
|
vcpkg/vcpkg install libvpx libyuv opus aom
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cách sửa lỗi libvpx (Dành cho hệ điều hành Fedora)
|
### Cách sửa lỗi `libvpx` (Dành cho hệ điều hành Fedora)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd vcpkg/buildtrees/libvpx/src
|
cd vcpkg/buildtrees/libvpx/src
|
||||||
@@ -95,7 +94,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
|
|||||||
cd
|
cd
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cách build
|
### Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
@@ -108,9 +107,9 @@ mv libsciter-gtk.so target/debug
|
|||||||
VCPKG_ROOT=$HOME/vcpkg cargo run
|
VCPKG_ROOT=$HOME/vcpkg cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Cách để build sử dụng Docker
|
## Cách build bằng Docker
|
||||||
|
|
||||||
Bắt đầu bằng cách sao chép repo này về máy tính và build cái Docker cointainer:
|
Bắt đầu bằng cách sao chép repo này về máy tính của bạn và tạo Docker container:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/rustdesk/rustdesk
|
git clone https://github.com/rustdesk/rustdesk
|
||||||
@@ -118,37 +117,37 @@ cd rustdesk
|
|||||||
docker build -t "rustdesk-builder" .
|
docker build -t "rustdesk-builder" .
|
||||||
```
|
```
|
||||||
|
|
||||||
Rồi mỗi khi bạn chạy ứng dụng, thì hãy chạy lệnh này:
|
Sau đó, mỗi khi bạn chạy ứng dụng, thì hãy chạy dòng lệnh sau:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
|
||||||
```
|
```
|
||||||
|
|
||||||
Chú ý: Lần build đầu tiên có thể sẽ mất lâu hơn truớc khi các dependecies đuợc lưu lại, những lần build sau sẽ nhanh hơn. Hơn nũa, nếu bạn cần cung cấp các cài đặt lệnh khác cho lệnh build, bạn có thể đặt những cài đặt lệnh này vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ nếu bạn cần build phiên bản đuợc tối ưu hóa, bạn sẽ chạy lệnh trên cùng với cài đặt lệnh ‘--release’. Kết quả build sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
|
Lưu ý rằng **lần build đầu tiên có thể mất thời gian hơn trước khi các dependencies được lưu vào bộ nhớ cache**, nhưng các lần build sau sẽ nhanh hơn. Ngoài ra, nếu bạn cần chỉ định các đối số khác cho lệnh build, bạn có thể thêm chúng vào cuối lệnh ở phần `<OPTIONAL-ARGS>`. Ví dụ, nếu bạn muốn build phiên bản tối ưu hóa, bạn sẽ chạy lệnh trên với tùy chọn `--release`. Kết quả biên dịch sẽ được lưu trong thư mục target trên máy tính của bạn, và có thể chạy với lệnh:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
target/debug/rustdesk
|
target/debug/rustdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
Nếu bạn đang chạy bản build đuợc tối ưu hóa, thì bạn có thể chạy với lệnh:
|
Nếu bạn đang chạy bản build được tối ưu hóa, thì bạn có thể chạy với lệnh:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
target/release/rustdesk
|
target/release/rustdesk
|
||||||
```
|
```
|
||||||
|
|
||||||
Hãy đảm bảo là bạn đang chạy những lệnh này từ thu mục rễ của repo RustDesk, vì nếu không thì ứng dụng có thể sẽ không tìm đuợc những tệp tài nguyên cần thiết. Cũng như nhớ rằng những lệnh con của cargo như `install` hoặc `run` hiện chưa được hỗ trợ bởi phương pháp này vì chúng sẽ cài đặt hoặc chạy ứng dụng trong container thay vì trên máy tính của bạn.
|
Hãy đảm bảo rằng bạn đang chạy các lệnh này từ gốc của thư mục **RustDesk**, nếu không, ứng dụng có thể không thể tìm thấy các tệp tài nguyên cần thiết. Hãy lưu ý rằng các câu lệnh con khác của **cargo** như **install** hoặc **run** hiện không được hỗ trợ qua phương pháp này, vì chúng sẽ cài đặt hoặc chạy chương trình bên trong **container** thay vì trên máy tính của bạn.
|
||||||
|
|
||||||
## Cấu trúc tệp tin
|
## Cấu trúc tệp tin
|
||||||
|
|
||||||
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác
|
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, cấu hình, tcp/udp wrapper, protobuf, fs functions để truyền file, và một số hàm tiện ích khác
|
||||||
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: để ghi lại màn hình
|
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ghi lại màn hình
|
||||||
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: để điều khiển máy tính/con chuột trên những nền tảng khác nhau
|
- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: điều khiển máy tính/chuột trên các nền tảng khác nhau
|
||||||
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng
|
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: giao diện người dùng
|
||||||
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng
|
- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: các dịch vụ âm thanh, clipboard, đầu vào, video và các kết nối mạng
|
||||||
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: để bắt đầu kết nối với một peer
|
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bắt đầu kết nối với một peer
|
||||||
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Để liên lạc với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi cho kết nối trực tiếp (TCP hole punching) hoặc kết nối được relayed.
|
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: giao tiếp với [rustdesk-server](https://github.com/rustdesk/rustdesk-server), đợi kết nối trực tiếp (TCP hole punching) hoặc kết nối được chuyển tiếp.
|
||||||
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
|
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: mã nguồn riêng cho mỗi nền tảng
|
||||||
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành cho điện thoại
|
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Mã Flutter dành máy tính và điện thoại
|
||||||
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
|
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Mã JavaScript dành cho giao diện trên web bằng Flutter
|
||||||
|
|
||||||
## Snapshot
|
## Snapshot
|
||||||
|
|||||||
@@ -8,17 +8,31 @@
|
|||||||
"modules": [
|
"modules": [
|
||||||
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
|
"shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
|
||||||
"xdotool.json",
|
"xdotool.json",
|
||||||
|
{
|
||||||
|
"name": "pam",
|
||||||
|
"buildsystem": "simple",
|
||||||
|
"build-commands": [
|
||||||
|
"./configure --disable-selinux --prefix=/app && make -j4 install"
|
||||||
|
],
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"url": "https://github.com/linux-pam/linux-pam/releases/download/v1.3.1/Linux-PAM-1.3.1.tar.xz",
|
||||||
|
"sha256": "eff47a4ecd833fbf18de9686632a70ee8d0794b79aecb217ebd0ce11db4cd0db"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "rustdesk",
|
"name": "rustdesk",
|
||||||
"buildsystem": "simple",
|
"buildsystem": "simple",
|
||||||
"build-commands": [
|
"build-commands": [
|
||||||
"bsdtar -zxvf rustdesk-1.2.4.deb",
|
"bsdtar -zxvf rustdesk.deb",
|
||||||
"tar -xvf ./data.tar.xz",
|
"tar -xvf ./data.tar.xz",
|
||||||
"cp -r ./usr/* /app/",
|
"cp -r ./usr/* /app/",
|
||||||
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
|
"mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
|
||||||
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
|
"mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
|
||||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop",
|
"mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop",
|
||||||
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop",
|
"sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop",
|
||||||
"mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
|
"mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
|
||||||
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
|
"for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
|
||||||
],
|
],
|
||||||
@@ -26,7 +40,7 @@
|
|||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"path": "../rustdesk-1.2.4.deb"
|
"path": "./rustdesk.deb"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
|
|||||||
@@ -108,4 +108,3 @@ dependencies {
|
|||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } }
|
||||||
}
|
}
|
||||||
|
|
||||||
apply plugin: 'com.google.gms.google-services'
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "768133699366",
|
|
||||||
"firebase_url": "https://rustdesk.firebaseio.com",
|
|
||||||
"project_id": "rustdesk",
|
|
||||||
"storage_bucket": "rustdesk.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:768133699366:android:5fc9015370e344457993e7",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "com.carriez.flutter_hbb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyAPOsKcXjrAR-7Z148sYr_gdB_JQZkamTM"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"services": {
|
|
||||||
"appinvite_service": {
|
|
||||||
"other_platform_oauth_client": [
|
|
||||||
{
|
|
||||||
"client_id": "768133699366-s9gdfsijefsd5g1nura4kmfne42lencn.apps.googleusercontent.com",
|
|
||||||
"client_type": 3
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
package="com.carriez.flutter_hbb">
|
package="com.carriez.flutter_hbb">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import ffi.FFI
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.*
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import android.media.projection.MediaProjection
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
|
||||||
|
const val AUDIO_SAMPLE_RATE = 48000
|
||||||
|
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
||||||
|
|
||||||
|
class AudioRecordHandle(private var context: Context, private var isVideoStart: ()->Boolean, private var isAudioStart: ()->Boolean) {
|
||||||
|
private val logTag = "LOG_AUDIO_RECORD_HANDLE"
|
||||||
|
|
||||||
|
private var audioRecorder: AudioRecord? = null
|
||||||
|
private var audioReader: AudioReader? = null
|
||||||
|
private var minBufferSize = 0
|
||||||
|
private var audioRecordStat = false
|
||||||
|
private var audioThread: Thread? = null
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun createAudioRecorder(inVoiceCall: Boolean, mediaProjection: MediaProjection?): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (ActivityCompat.checkSelfPermission(
|
||||||
|
context,
|
||||||
|
Manifest.permission.RECORD_AUDIO
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
Log.d(logTag, "createAudioRecorder failed, no RECORD_AUDIO permission")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = AudioRecord.Builder()
|
||||||
|
.setAudioFormat(
|
||||||
|
AudioFormat.Builder()
|
||||||
|
.setEncoding(AUDIO_ENCODING)
|
||||||
|
.setSampleRate(AUDIO_SAMPLE_RATE)
|
||||||
|
.setChannelMask(AUDIO_CHANNEL_MASK).build()
|
||||||
|
);
|
||||||
|
if (inVoiceCall) {
|
||||||
|
builder.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
|
||||||
|
} else {
|
||||||
|
mediaProjection?.let {
|
||||||
|
var apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
||||||
|
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build();
|
||||||
|
builder.setAudioPlaybackCaptureConfig(apcc);
|
||||||
|
} ?: let {
|
||||||
|
Log.d(logTag, "createAudioRecorder failed, mediaProjection null")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audioRecorder = builder.build()
|
||||||
|
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
private fun checkAudioReader() {
|
||||||
|
if (audioReader != null && minBufferSize != 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// read f32 to byte , length * 4
|
||||||
|
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
||||||
|
AUDIO_SAMPLE_RATE,
|
||||||
|
AUDIO_CHANNEL_MASK,
|
||||||
|
AUDIO_ENCODING
|
||||||
|
)
|
||||||
|
if (minBufferSize == 0) {
|
||||||
|
Log.d(logTag, "get min buffer size fail!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioReader = AudioReader(minBufferSize, 4)
|
||||||
|
Log.d(logTag, "init audioData len:$minBufferSize")
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun startAudioRecorder() {
|
||||||
|
checkAudioReader()
|
||||||
|
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
||||||
|
try {
|
||||||
|
FFI.setFrameRawEnable("audio", true)
|
||||||
|
audioRecorder!!.startRecording()
|
||||||
|
audioRecordStat = true
|
||||||
|
audioThread = thread {
|
||||||
|
while (audioRecordStat) {
|
||||||
|
audioReader!!.readSync(audioRecorder!!)?.let {
|
||||||
|
FFI.onAudioFrameUpdate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// let's release here rather than onDestroy to avoid threading issue
|
||||||
|
audioRecorder?.release()
|
||||||
|
audioRecorder = null
|
||||||
|
minBufferSize = 0
|
||||||
|
FFI.setFrameRawEnable("audio", false)
|
||||||
|
Log.d(logTag, "Exit audio thread")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail:$e")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "startAudioRecorder fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallStarted(mediaProjection: MediaProjection?): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (isVideoStart() || isAudioStart()) {
|
||||||
|
if (!switchToVoiceCall(mediaProjection)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!switchToVoiceCall(mediaProjection)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallClosed(mediaProjection: MediaProjection?): Boolean {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (isVideoStart()) {
|
||||||
|
switchOutVoiceCall(mediaProjection)
|
||||||
|
}
|
||||||
|
tryReleaseAudio()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun switchToVoiceCall(mediaProjection: MediaProjection?): Boolean {
|
||||||
|
audioRecorder?.let {
|
||||||
|
if (it.getAudioSource() == MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
audioThread = null
|
||||||
|
|
||||||
|
if (!createAudioRecorder(true, mediaProjection)) {
|
||||||
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
startAudioRecorder()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
|
fun switchOutVoiceCall(mediaProjection: MediaProjection?): Boolean {
|
||||||
|
audioRecorder?.let {
|
||||||
|
if (it.getAudioSource() != MediaRecorder.AudioSource.VOICE_COMMUNICATION) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
|
||||||
|
if (!createAudioRecorder(false, mediaProjection)) {
|
||||||
|
Log.e(logTag, "createAudioRecorder fail")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
startAudioRecorder()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun tryReleaseAudio() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isAudioStart() || isVideoStart()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
audioThread = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
Log.d(logTag, "destroy audio record handle")
|
||||||
|
|
||||||
|
audioRecordStat = false
|
||||||
|
audioThread?.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ package com.carriez.flutter_hbb
|
|||||||
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
|
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import ffi.FFI
|
||||||
|
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -15,10 +17,20 @@ import android.os.Build
|
|||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
|
import android.media.MediaCodecInfo
|
||||||
|
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
||||||
|
import android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar
|
||||||
|
import android.media.MediaCodecList
|
||||||
|
import android.media.MediaFormat
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONObject
|
||||||
import com.hjq.permissions.XXPermissions
|
import com.hjq.permissions.XXPermissions
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
@@ -30,6 +42,9 @@ class MainActivity : FlutterActivity() {
|
|||||||
private val logTag = "mMainActivity"
|
private val logTag = "mMainActivity"
|
||||||
private var mainService: MainService? = null
|
private var mainService: MainService? = null
|
||||||
|
|
||||||
|
private var isAudioStart = false
|
||||||
|
private val audioRecordHandle = AudioRecordHandle(this, { false }, { isAudioStart })
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
if (MainService.isReady) {
|
if (MainService.isReady) {
|
||||||
@@ -42,6 +57,7 @@ class MainActivity : FlutterActivity() {
|
|||||||
channelTag
|
channelTag
|
||||||
)
|
)
|
||||||
initFlutterChannel(flutterMethodChannel!!)
|
initFlutterChannel(flutterMethodChannel!!)
|
||||||
|
thread { setCodecInfo() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
@@ -217,10 +233,145 @@ class MainActivity : FlutterActivity() {
|
|||||||
result.success(false)
|
result.success(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"on_voice_call_started" -> {
|
||||||
|
onVoiceCallStarted()
|
||||||
|
}
|
||||||
|
"on_voice_call_closed" -> {
|
||||||
|
onVoiceCallClosed()
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
result.error("-1", "No such method", null)
|
result.error("-1", "No such method", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setCodecInfo() {
|
||||||
|
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
||||||
|
val codecs = codecList.codecInfos
|
||||||
|
val codecArray = JSONArray()
|
||||||
|
|
||||||
|
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||||
|
var w = 0
|
||||||
|
var h = 0
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
val m = windowManager.maximumWindowMetrics
|
||||||
|
w = m.bounds.width()
|
||||||
|
h = m.bounds.height()
|
||||||
|
} else {
|
||||||
|
val dm = DisplayMetrics()
|
||||||
|
windowManager.defaultDisplay.getRealMetrics(dm)
|
||||||
|
w = dm.widthPixels
|
||||||
|
h = dm.heightPixels
|
||||||
|
}
|
||||||
|
val align = 64
|
||||||
|
w = (w + align - 1) / align * align
|
||||||
|
h = (h + align - 1) / align * align
|
||||||
|
codecs.forEach { codec ->
|
||||||
|
val codecObject = JSONObject()
|
||||||
|
codecObject.put("name", codec.name)
|
||||||
|
codecObject.put("is_encoder", codec.isEncoder)
|
||||||
|
var hw: Boolean? = null;
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
hw = codec.isHardwareAccelerated
|
||||||
|
} else {
|
||||||
|
// https://chromium.googlesource.com/external/webrtc/+/HEAD/sdk/android/src/java/org/webrtc/MediaCodecUtils.java#29
|
||||||
|
// https://chromium.googlesource.com/external/webrtc/+/master/sdk/android/api/org/webrtc/HardwareVideoEncoderFactory.java#229
|
||||||
|
if (listOf("OMX.google.", "OMX.SEC.", "c2.android").any { codec.name.startsWith(it, true) }) {
|
||||||
|
hw = false
|
||||||
|
} else if (listOf("c2.qti", "OMX.qcom.video", "OMX.Exynos", "OMX.hisi", "OMX.MTK", "OMX.Intel", "OMX.Nvidia").any { codec.name.startsWith(it, true) }) {
|
||||||
|
hw = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hw != true) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
codecObject.put("hw", hw)
|
||||||
|
var mime_type = ""
|
||||||
|
codec.supportedTypes.forEach { type ->
|
||||||
|
if (listOf("video/avc", "video/hevc").contains(type)) { // "video/x-vnd.on2.vp8", "video/x-vnd.on2.vp9", "video/av01"
|
||||||
|
mime_type = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mime_type.isNotEmpty()) {
|
||||||
|
codecObject.put("mime_type", mime_type)
|
||||||
|
val caps = codec.getCapabilitiesForType(mime_type)
|
||||||
|
if (codec.isEncoder) {
|
||||||
|
// Encoder‘s max_height and max_width are interchangeable
|
||||||
|
if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codecObject.put("min_width", caps.videoCapabilities.supportedWidths.lower)
|
||||||
|
codecObject.put("max_width", caps.videoCapabilities.supportedWidths.upper)
|
||||||
|
codecObject.put("min_height", caps.videoCapabilities.supportedHeights.lower)
|
||||||
|
codecObject.put("max_height", caps.videoCapabilities.supportedHeights.upper)
|
||||||
|
val surface = caps.colorFormats.contains(COLOR_FormatSurface);
|
||||||
|
codecObject.put("surface", surface)
|
||||||
|
val nv12 = caps.colorFormats.contains(COLOR_FormatYUV420SemiPlanar)
|
||||||
|
codecObject.put("nv12", nv12)
|
||||||
|
if (!(nv12 || surface)) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
codecObject.put("min_bitrate", caps.videoCapabilities.bitrateRange.lower / 1000)
|
||||||
|
codecObject.put("max_bitrate", caps.videoCapabilities.bitrateRange.upper / 1000)
|
||||||
|
if (!codec.isEncoder) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
codecObject.put("low_latency", caps.isFeatureSupported(MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!codec.isEncoder) {
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
codecArray.put(codecObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val result = JSONObject()
|
||||||
|
result.put("version", Build.VERSION.SDK_INT)
|
||||||
|
result.put("w", w)
|
||||||
|
result.put("h", h)
|
||||||
|
result.put("codecs", codecArray)
|
||||||
|
FFI.setCodecInfo(result.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onVoiceCallStarted() {
|
||||||
|
var ok = false
|
||||||
|
mainService?.let {
|
||||||
|
ok = it.onVoiceCallStarted()
|
||||||
|
} ?: let {
|
||||||
|
isAudioStart = true
|
||||||
|
ok = audioRecordHandle.onVoiceCallStarted(null)
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
// Rarely happens, So we just add log and msgbox here.
|
||||||
|
Log.e(logTag, "onVoiceCallStarted fail")
|
||||||
|
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
|
"title" to "Voice call",
|
||||||
|
"text" to "Failed to start voice call."))
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "onVoiceCallStarted success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onVoiceCallClosed() {
|
||||||
|
var ok = false
|
||||||
|
mainService?.let {
|
||||||
|
ok = it.onVoiceCallClosed()
|
||||||
|
} ?: let {
|
||||||
|
isAudioStart = false
|
||||||
|
ok = audioRecordHandle.onVoiceCallClosed(null)
|
||||||
|
}
|
||||||
|
if (!ok) {
|
||||||
|
// Rarely happens, So we just add log and msgbox here.
|
||||||
|
Log.e(logTag, "onVoiceCallClosed fail")
|
||||||
|
flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
|
"title" to "Voice call",
|
||||||
|
"text" to "Failed to stop voice call."))
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "onVoiceCallClosed success")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.carriez.flutter_hbb
|
package com.carriez.flutter_hbb
|
||||||
|
|
||||||
|
import ffi.FFI
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture screen,get video and audio,send to rust.
|
* Capture screen,get video and audio,send to rust.
|
||||||
* Dispatch notifications
|
* Dispatch notifications
|
||||||
@@ -52,22 +54,14 @@ const val NOTIFY_ID_OFFSET = 100
|
|||||||
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
|
const val MIME_TYPE = MediaFormat.MIMETYPE_VIDEO_VP9
|
||||||
|
|
||||||
// video const
|
// video const
|
||||||
|
|
||||||
const val MAX_SCREEN_SIZE = 1200
|
const val MAX_SCREEN_SIZE = 1200
|
||||||
|
|
||||||
const val VIDEO_KEY_BIT_RATE = 1024_000
|
const val VIDEO_KEY_BIT_RATE = 1024_000
|
||||||
const val VIDEO_KEY_FRAME_RATE = 30
|
const val VIDEO_KEY_FRAME_RATE = 30
|
||||||
|
|
||||||
// audio const
|
|
||||||
const val AUDIO_ENCODING = AudioFormat.ENCODING_PCM_FLOAT // ENCODING_OPUS need API 30
|
|
||||||
const val AUDIO_SAMPLE_RATE = 48000
|
|
||||||
const val AUDIO_CHANNEL_MASK = AudioFormat.CHANNEL_IN_STEREO
|
|
||||||
|
|
||||||
class MainService : Service() {
|
class MainService : Service() {
|
||||||
|
|
||||||
init {
|
|
||||||
System.loadLibrary("rustdesk")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@RequiresApi(Build.VERSION_CODES.N)
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
|
fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
|
||||||
@@ -141,10 +135,51 @@ class MainService : Service() {
|
|||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"update_voice_call_state" -> {
|
||||||
|
try {
|
||||||
|
val jsonObject = JSONObject(arg1)
|
||||||
|
val id = jsonObject["id"] as Int
|
||||||
|
val username = jsonObject["name"] as String
|
||||||
|
val peerId = jsonObject["peer_id"] as String
|
||||||
|
val inVoiceCall = jsonObject["in_voice_call"] as Boolean
|
||||||
|
val incomingVoiceCall = jsonObject["incoming_voice_call"] as Boolean
|
||||||
|
if (!inVoiceCall) {
|
||||||
|
if (incomingVoiceCall) {
|
||||||
|
voiceCallRequestNotification(id, "Voice Call Request", username, peerId)
|
||||||
|
} else {
|
||||||
|
if (!audioRecordHandle.switchOutVoiceCall(mediaProjection)) {
|
||||||
|
Log.e(logTag, "switchOutVoiceCall fail")
|
||||||
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
|
"title" to "Voice call",
|
||||||
|
"text" to "Failed to switch out voice call."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!audioRecordHandle.switchToVoiceCall(mediaProjection)) {
|
||||||
|
Log.e(logTag, "switchToVoiceCall fail")
|
||||||
|
MainActivity.flutterMethodChannel?.invokeMethod("msgbox", mapOf(
|
||||||
|
"type" to "custom-nook-nocancel-hasclose-error",
|
||||||
|
"title" to "Voice call",
|
||||||
|
"text" to "Failed to switch to voice call."))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
"stop_capture" -> {
|
"stop_capture" -> {
|
||||||
Log.d(logTag, "from rust:stop_capture")
|
Log.d(logTag, "from rust:stop_capture")
|
||||||
stopCapture()
|
stopCapture()
|
||||||
}
|
}
|
||||||
|
"is_hardware_codec" -> {
|
||||||
|
val isHwCodec = arg1.toBoolean()
|
||||||
|
if (isHardwareCodec != isHwCodec) {
|
||||||
|
isHardwareCodec = isHwCodec
|
||||||
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,38 +191,28 @@ class MainService : Service() {
|
|||||||
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
private val powerManager: PowerManager by lazy { applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager }
|
||||||
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
|
private val wakeLock: PowerManager.WakeLock by lazy { powerManager.newWakeLock(PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "rustdesk:wakelock")}
|
||||||
|
|
||||||
// jvm call rust
|
|
||||||
private external fun init(ctx: Context)
|
|
||||||
|
|
||||||
/// When app start on boot, app_dir will not be passed from flutter
|
|
||||||
/// so pass a app_dir here to rust server
|
|
||||||
private external fun startServer(app_dir: String)
|
|
||||||
private external fun startService()
|
|
||||||
private external fun onVideoFrameUpdate(buf: ByteBuffer)
|
|
||||||
private external fun onAudioFrameUpdate(buf: ByteBuffer)
|
|
||||||
private external fun translateLocale(localeName: String, input: String): String
|
|
||||||
private external fun refreshScreen()
|
|
||||||
private external fun setFrameRawEnable(name: String, value: Boolean)
|
|
||||||
// private external fun sendVp9(data: ByteArray)
|
|
||||||
|
|
||||||
private fun translate(input: String): String {
|
private fun translate(input: String): String {
|
||||||
Log.d(logTag, "translate:$LOCAL_NAME")
|
Log.d(logTag, "translate:$LOCAL_NAME")
|
||||||
return translateLocale(LOCAL_NAME, input)
|
return FFI.translateLocale(LOCAL_NAME, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private var _isReady = false // media permission ready status
|
private var _isReady = false // media permission ready status
|
||||||
private var _isStart = false // screen capture start status
|
private var _isStart = false // screen capture start status
|
||||||
|
private var _isAudioStart = false // audio capture start status
|
||||||
val isReady: Boolean
|
val isReady: Boolean
|
||||||
get() = _isReady
|
get() = _isReady
|
||||||
val isStart: Boolean
|
val isStart: Boolean
|
||||||
get() = _isStart
|
get() = _isStart
|
||||||
|
val isAudioStart: Boolean
|
||||||
|
get() = _isAudioStart
|
||||||
}
|
}
|
||||||
|
|
||||||
private val logTag = "LOG_SERVICE"
|
private val logTag = "LOG_SERVICE"
|
||||||
private val useVP9 = false
|
private val useVP9 = false
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
|
private var reuseVirtualDisplay = Build.VERSION.SDK_INT > 33
|
||||||
|
|
||||||
// video
|
// video
|
||||||
private var mediaProjection: MediaProjection? = null
|
private var mediaProjection: MediaProjection? = null
|
||||||
@@ -198,10 +223,7 @@ class MainService : Service() {
|
|||||||
private var virtualDisplay: VirtualDisplay? = null
|
private var virtualDisplay: VirtualDisplay? = null
|
||||||
|
|
||||||
// audio
|
// audio
|
||||||
private var audioRecorder: AudioRecord? = null
|
private val audioRecordHandle = AudioRecordHandle(this, { isStart }, { isAudioStart })
|
||||||
private var audioReader: AudioReader? = null
|
|
||||||
private var minBufferSize = 0
|
|
||||||
private var audioRecordStat = false
|
|
||||||
|
|
||||||
// notification
|
// notification
|
||||||
private lateinit var notificationManager: NotificationManager
|
private lateinit var notificationManager: NotificationManager
|
||||||
@@ -210,8 +232,8 @@ class MainService : Service() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Log.d(logTag,"MainService onCreate")
|
Log.d(logTag,"MainService onCreate, sdk int:${Build.VERSION.SDK_INT} reuseVirtualDisplay:$reuseVirtualDisplay")
|
||||||
init(this)
|
FFI.init(this)
|
||||||
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
|
||||||
start()
|
start()
|
||||||
serviceLooper = looper
|
serviceLooper = looper
|
||||||
@@ -223,7 +245,7 @@ class MainService : Service() {
|
|||||||
// keep the config dir same with flutter
|
// keep the config dir same with flutter
|
||||||
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
|
||||||
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
|
||||||
startServer(configPath)
|
FFI.startServer(configPath, "")
|
||||||
|
|
||||||
createForegroundNotification()
|
createForegroundNotification()
|
||||||
}
|
}
|
||||||
@@ -233,6 +255,7 @@ class MainService : Service() {
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isHardwareCodec: Boolean? = null;
|
||||||
private fun updateScreenInfo(orientation: Int) {
|
private fun updateScreenInfo(orientation: Int) {
|
||||||
var w: Int
|
var w: Int
|
||||||
var h: Int
|
var h: Int
|
||||||
@@ -265,7 +288,7 @@ class MainService : Service() {
|
|||||||
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
Log.d(logTag,"updateScreenInfo:w:$w,h:$h")
|
||||||
var scale = 1
|
var scale = 1
|
||||||
if (w != 0 && h != 0) {
|
if (w != 0 && h != 0) {
|
||||||
if (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE) {
|
if (isHardwareCodec == false && (w > MAX_SCREEN_SIZE || h > MAX_SCREEN_SIZE)) {
|
||||||
scale = 2
|
scale = 2
|
||||||
w /= scale
|
w /= scale
|
||||||
h /= scale
|
h /= scale
|
||||||
@@ -278,7 +301,7 @@ class MainService : Service() {
|
|||||||
SCREEN_INFO.dpi = dpi
|
SCREEN_INFO.dpi = dpi
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
stopCapture()
|
stopCapture()
|
||||||
refreshScreen()
|
FFI.refreshScreen()
|
||||||
startCapture()
|
startCapture()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,7 +329,7 @@ class MainService : Service() {
|
|||||||
createForegroundNotification()
|
createForegroundNotification()
|
||||||
|
|
||||||
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
|
||||||
startService()
|
FFI.startService()
|
||||||
}
|
}
|
||||||
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
|
Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
|
||||||
val mediaProjectionManager =
|
val mediaProjectionManager =
|
||||||
@@ -354,12 +377,13 @@ class MainService : Service() {
|
|||||||
).apply {
|
).apply {
|
||||||
setOnImageAvailableListener({ imageReader: ImageReader ->
|
setOnImageAvailableListener({ imageReader: ImageReader ->
|
||||||
try {
|
try {
|
||||||
|
// If not call acquireLatestImage, listener will not be called again
|
||||||
imageReader.acquireLatestImage().use { image ->
|
imageReader.acquireLatestImage().use { image ->
|
||||||
if (image == null) return@setOnImageAvailableListener
|
if (image == null || !isStart) return@setOnImageAvailableListener
|
||||||
val planes = image.planes
|
val planes = image.planes
|
||||||
val buffer = planes[0].buffer
|
val buffer = planes[0].buffer
|
||||||
buffer.rewind()
|
buffer.rewind()
|
||||||
onVideoFrameUpdate(buffer)
|
FFI.onVideoFrameUpdate(buffer)
|
||||||
}
|
}
|
||||||
} catch (ignored: java.lang.Exception) {
|
} catch (ignored: java.lang.Exception) {
|
||||||
}
|
}
|
||||||
@@ -370,6 +394,14 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallStarted(): Boolean {
|
||||||
|
return audioRecordHandle.onVoiceCallStarted(mediaProjection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onVoiceCallClosed(): Boolean {
|
||||||
|
return audioRecordHandle.onVoiceCallClosed(mediaProjection)
|
||||||
|
}
|
||||||
|
|
||||||
fun startCapture(): Boolean {
|
fun startCapture(): Boolean {
|
||||||
if (isStart) {
|
if (isStart) {
|
||||||
return true
|
return true
|
||||||
@@ -378,6 +410,7 @@ class MainService : Service() {
|
|||||||
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
Log.w(logTag, "startCapture fail,mediaProjection is null")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScreenInfo(resources.configuration.orientation)
|
updateScreenInfo(resources.configuration.orientation)
|
||||||
Log.d(logTag, "Start Capture")
|
Log.d(logTag, "Start Capture")
|
||||||
surface = createSurface()
|
surface = createSurface()
|
||||||
@@ -389,47 +422,66 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
startAudioRecorder()
|
if (!audioRecordHandle.createAudioRecorder(false, mediaProjection)) {
|
||||||
|
Log.d(logTag, "createAudioRecorder fail")
|
||||||
|
} else {
|
||||||
|
Log.d(logTag, "audio recorder start")
|
||||||
|
audioRecordHandle.startAudioRecorder()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
_isStart = true
|
_isStart = true
|
||||||
setFrameRawEnable("video",true)
|
FFI.setFrameRawEnable("video",true)
|
||||||
setFrameRawEnable("audio",true)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun stopCapture() {
|
fun stopCapture() {
|
||||||
Log.d(logTag, "Stop Capture")
|
Log.d(logTag, "Stop Capture")
|
||||||
setFrameRawEnable("video",false)
|
FFI.setFrameRawEnable("video",false)
|
||||||
setFrameRawEnable("audio",false)
|
|
||||||
_isStart = false
|
_isStart = false
|
||||||
// release video
|
// release video
|
||||||
virtualDisplay?.release()
|
if (reuseVirtualDisplay) {
|
||||||
surface?.release()
|
// The virtual display video projection can be paused by calling `setSurface(null)`.
|
||||||
|
// https://developer.android.com/reference/android/hardware/display/VirtualDisplay.Callback
|
||||||
|
// https://learn.microsoft.com/en-us/dotnet/api/android.hardware.display.virtualdisplay.callback.onpaused?view=net-android-34.0
|
||||||
|
virtualDisplay?.setSurface(null)
|
||||||
|
} else {
|
||||||
|
virtualDisplay?.release()
|
||||||
|
}
|
||||||
|
// suface needs to be release after `imageReader.close()` to imageReader access released surface
|
||||||
|
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
|
||||||
imageReader?.close()
|
imageReader?.close()
|
||||||
|
imageReader = null
|
||||||
videoEncoder?.let {
|
videoEncoder?.let {
|
||||||
it.signalEndOfInputStream()
|
it.signalEndOfInputStream()
|
||||||
it.stop()
|
it.stop()
|
||||||
it.release()
|
it.release()
|
||||||
}
|
}
|
||||||
virtualDisplay = null
|
if (!reuseVirtualDisplay) {
|
||||||
|
virtualDisplay = null
|
||||||
|
}
|
||||||
videoEncoder = null
|
videoEncoder = null
|
||||||
|
// suface needs to be release after `imageReader.close()` to imageReader access released surface
|
||||||
|
// https://github.com/rustdesk/rustdesk/issues/4118#issuecomment-1515666629
|
||||||
|
surface?.release()
|
||||||
|
|
||||||
// release audio
|
// release audio
|
||||||
audioRecordStat = false
|
_isAudioStart = false
|
||||||
audioRecorder?.release()
|
audioRecordHandle.tryReleaseAudio()
|
||||||
audioRecorder = null
|
|
||||||
minBufferSize = 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
Log.d(logTag, "destroy service")
|
Log.d(logTag, "destroy service")
|
||||||
_isReady = false
|
_isReady = false
|
||||||
|
_isAudioStart = false
|
||||||
|
|
||||||
stopCapture()
|
stopCapture()
|
||||||
imageReader?.close()
|
|
||||||
imageReader = null
|
if (reuseVirtualDisplay) {
|
||||||
|
virtualDisplay?.release()
|
||||||
|
virtualDisplay = null
|
||||||
|
}
|
||||||
|
|
||||||
mediaProjection = null
|
mediaProjection = null
|
||||||
checkMediaPermission()
|
checkMediaPermission()
|
||||||
@@ -459,11 +511,7 @@ class MainService : Service() {
|
|||||||
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
Log.d(logTag, "startRawVideoRecorder failed,surface is null")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
virtualDisplay = mp.createVirtualDisplay(
|
createOrSetVirtualDisplay(mp, surface!!)
|
||||||
"RustDeskVD",
|
|
||||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
|
||||||
surface, null, null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startVP9VideoRecorder(mp: MediaProjection) {
|
private fun startVP9VideoRecorder(mp: MediaProjection) {
|
||||||
@@ -475,11 +523,28 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
it.setCallback(cb)
|
it.setCallback(cb)
|
||||||
it.start()
|
it.start()
|
||||||
virtualDisplay = mp.createVirtualDisplay(
|
createOrSetVirtualDisplay(mp, surface!!)
|
||||||
"RustDeskVD",
|
}
|
||||||
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
}
|
||||||
surface, null, null
|
|
||||||
)
|
// https://github.com/bk138/droidVNC-NG/blob/b79af62db5a1c08ed94e6a91464859ffed6f4e97/app/src/main/java/net/christianbeier/droidvnc_ng/MediaProjectionService.java#L250
|
||||||
|
// Reuse virtualDisplay if it exists, to avoid media projection confirmation dialog every connection.
|
||||||
|
private fun createOrSetVirtualDisplay(mp: MediaProjection, s: Surface) {
|
||||||
|
try {
|
||||||
|
virtualDisplay?.let {
|
||||||
|
it.resize(SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi)
|
||||||
|
it.setSurface(s)
|
||||||
|
} ?: let {
|
||||||
|
virtualDisplay = mp.createVirtualDisplay(
|
||||||
|
"RustDeskVD",
|
||||||
|
SCREEN_INFO.width, SCREEN_INFO.height, SCREEN_INFO.dpi, VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||||
|
s, null, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
Log.w(logTag, "createOrSetVirtualDisplay: got SecurityException, re-requesting confirmation");
|
||||||
|
// This initiates a prompt dialog for the user to confirm screen projection.
|
||||||
|
requestMediaProjection()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,7 +572,6 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun createMediaCodec() {
|
private fun createMediaCodec() {
|
||||||
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
|
Log.d(logTag, "MediaFormat.MIMETYPE_VIDEO_VP9 :$MIME_TYPE")
|
||||||
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
|
videoEncoder = MediaCodec.createEncoderByType(MIME_TYPE)
|
||||||
@@ -527,76 +591,6 @@ class MainService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun startAudioRecorder() {
|
|
||||||
checkAudioRecorder()
|
|
||||||
if (audioReader != null && audioRecorder != null && minBufferSize != 0) {
|
|
||||||
try {
|
|
||||||
audioRecorder!!.startRecording()
|
|
||||||
audioRecordStat = true
|
|
||||||
thread {
|
|
||||||
while (audioRecordStat) {
|
|
||||||
audioReader!!.readSync(audioRecorder!!)?.let {
|
|
||||||
onAudioFrameUpdate(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(logTag, "Exit audio thread")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.d(logTag, "startAudioRecorder fail:$e")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(logTag, "startAudioRecorder fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.M)
|
|
||||||
private fun checkAudioRecorder() {
|
|
||||||
if (audioRecorder != null && audioRecorder != null && minBufferSize != 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// read f32 to byte , length * 4
|
|
||||||
minBufferSize = 2 * 4 * AudioRecord.getMinBufferSize(
|
|
||||||
AUDIO_SAMPLE_RATE,
|
|
||||||
AUDIO_CHANNEL_MASK,
|
|
||||||
AUDIO_ENCODING
|
|
||||||
)
|
|
||||||
if (minBufferSize == 0) {
|
|
||||||
Log.d(logTag, "get min buffer size fail!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
audioReader = AudioReader(minBufferSize, 4)
|
|
||||||
Log.d(logTag, "init audioData len:$minBufferSize")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
mediaProjection?.let {
|
|
||||||
val apcc = AudioPlaybackCaptureConfiguration.Builder(it)
|
|
||||||
.addMatchingUsage(AudioAttributes.USAGE_MEDIA)
|
|
||||||
.addMatchingUsage(AudioAttributes.USAGE_ALARM)
|
|
||||||
.addMatchingUsage(AudioAttributes.USAGE_GAME)
|
|
||||||
.addMatchingUsage(AudioAttributes.USAGE_UNKNOWN).build()
|
|
||||||
if (ActivityCompat.checkSelfPermission(
|
|
||||||
this,
|
|
||||||
Manifest.permission.RECORD_AUDIO
|
|
||||||
) != PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
audioRecorder = AudioRecord.Builder()
|
|
||||||
.setAudioFormat(
|
|
||||||
AudioFormat.Builder()
|
|
||||||
.setEncoding(AUDIO_ENCODING)
|
|
||||||
.setSampleRate(AUDIO_SAMPLE_RATE)
|
|
||||||
.setChannelMask(AUDIO_CHANNEL_MASK).build()
|
|
||||||
)
|
|
||||||
.setAudioPlaybackCaptureConfig(apcc)
|
|
||||||
.setBufferSizeInBytes(minBufferSize).build()
|
|
||||||
Log.d(logTag, "createAudioRecorder done,minBufferSize:$minBufferSize")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(logTag, "createAudioRecorder fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initNotification() {
|
private fun initNotification() {
|
||||||
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
notificationChannel = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@@ -681,6 +675,21 @@ class MainService : Service() {
|
|||||||
notificationManager.notify(getClientNotifyID(clientID), notification)
|
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun voiceCallRequestNotification(
|
||||||
|
clientID: Int,
|
||||||
|
type: String,
|
||||||
|
username: String,
|
||||||
|
peerId: String
|
||||||
|
) {
|
||||||
|
val notification = notificationBuilder
|
||||||
|
.setOngoing(false)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
|
.setContentTitle(translate("Do you accept?"))
|
||||||
|
.setContentText("$type:$username-$peerId")
|
||||||
|
.build()
|
||||||
|
notificationManager.notify(getClientNotifyID(clientID), notification)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getClientNotifyID(clientID: Int): Int {
|
private fun getClientNotifyID(clientID: Int): Int {
|
||||||
return clientID + NOTIFY_ID_OFFSET
|
return clientID + NOTIFY_ID_OFFSET
|
||||||
}
|
}
|
||||||
|
|||||||
22
flutter/android/app/src/main/kotlin/ffi.kt
Normal file
22
flutter/android/app/src/main/kotlin/ffi.kt
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// ffi.kt
|
||||||
|
|
||||||
|
package ffi
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
|
||||||
|
object FFI {
|
||||||
|
init {
|
||||||
|
System.loadLibrary("rustdesk")
|
||||||
|
}
|
||||||
|
|
||||||
|
external fun init(ctx: Context)
|
||||||
|
external fun startServer(app_dir: String, custom_client_config: String)
|
||||||
|
external fun startService()
|
||||||
|
external fun onVideoFrameUpdate(buf: ByteBuffer)
|
||||||
|
external fun onAudioFrameUpdate(buf: ByteBuffer)
|
||||||
|
external fun translateLocale(localeName: String, input: String): String
|
||||||
|
external fun refreshScreen()
|
||||||
|
external fun setFrameRawEnable(name: String, value: Boolean)
|
||||||
|
external fun setCodecInfo(info: String)
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="0 0 1024 1024"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" class="icon" viewBox="-186 -186 1365 1365"><path d="M608 160c141.16 0 256 114.84 256 256 0 17.67 14.33 32 32 32s32-14.33 32-32c0-85.48-33.29-165.83-93.73-226.27C773.83 129.29 693.47 96 608 96c-17.67 0-32 14.33-32 32s14.33 32 32 32zm-24 168c61.76 0 112 50.24 112 112 0 17.67 14.33 32 32 32s32-14.33 32-32c0-97.05-78.95-176-176-176-17.67 0-32 14.33-32 32s14.33 32 32 32z"/><path d="M808.3 561.21c-12.76-3.83-25.7-6.2-38.46-7.03-60.3-4.5-116.45 18.9-146.55 61.08-22.6 31.67-45.66 50.01-68.52 54.5-17.71 3.48-33.12-1.7-45.49-5.85-2.66-.9-5.18-1.74-7.68-2.49-93.84-28.17-156.49-108.42-155.9-199.7.16-24.14 16.38-45.98 42.34-56.99 43.75-18.56 77.35-54 92.17-97.22 7.02-20.48 9.65-41.57 7.8-62.68-2.66-31.78-15.1-61.85-35.96-86.96-21.1-25.39-49.51-44-82.16-53.8-4.07-1.22-8.22-2.31-12.35-3.23-30.63-6.87-62.7-4.49-92.73 6.88-29.24 11.07-54.56 29.86-73.23 54.33a476.073 476.073 0 0 0-36.42 55.34 477.675 477.675 0 0 0-17.24 33.81C109.84 312.17 95.73 376.76 96 443.15c.26 63.78 13.7 126.26 39.95 185.7 27.55 62.39 69.3 119.84 120.74 166.11 54.14 48.71 117.6 84.85 188.63 107.4C499.02 919.41 554.33 928 610.21 928c10.99 0 22.01-.33 33.03-1 17.64-1.07 31.08-16.23 30.01-33.87-1.07-17.64-16.22-31.08-33.87-30.01-59.19 3.57-117.96-3.75-174.69-21.76C342.78 802.66 244.31 715.78 194.5 603c-46.76-105.9-46.21-221.33 1.55-325.03 4.55-9.87 9.57-19.72 14.92-29.26 9.29-16.54 19.89-32.64 31.5-47.86 23.47-30.77 64.09-45.87 101.07-37.58 2.66.6 5.33 1.3 7.95 2.08 40.93 12.29 69.48 45.6 72.75 84.86 0 .05.01.1.01.15 1.07 12.15-.47 24.39-4.58 36.37-8.94 26.06-29.58 47.59-56.63 59.07-23.58 10.01-43.63 25.72-57.99 45.45-15.12 20.78-23.2 45-23.36 70.05-.37 57.15 19 114.29 54.53 160.91 36.46 47.83 87.28 82.58 146.96 100.49 1.5.45 3.44 1.1 5.69 1.86 29.79 10.01 108.9 36.59 186.49-72.13 16.95-23.75 52.2-37.26 89.81-34.42l.36.03c7.97.51 16.17 2.02 24.34 4.47 22.12 6.64 42.04 25.38 56.11 52.77 16.97 33.04 21.71 72.53 12.1 100.56l-.16.47c-5.54 16.05-17.78 29.48-34.47 37.8-15.82 7.89-22.24 27.1-14.36 42.92s27.1 22.24 42.92 14.36c31.78-15.85 55.36-42.19 66.41-74.2l.18-.53c15.23-44.4 9.22-102.11-15.68-150.61-22.07-43.02-55.68-73.15-94.62-84.84z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -139,4 +139,4 @@ SPEC CHECKSUMS:
|
|||||||
|
|
||||||
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
|
PODFILE CHECKSUM: d4cb12ad5d3bdb3352770b1d3db237584e155156
|
||||||
|
|
||||||
COCOAPODS: 1.12.1
|
COCOAPODS: 1.15.2
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastUpgradeCheck = 1430;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
97C146ED1CF9000F007C117D = {
|
97C146ED1CF9000F007C117D = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
|||||||
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/main.dart';
|
import 'package:flutter_hbb/main.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/utils/platform_channel.dart';
|
import 'package:flutter_hbb/utils/platform_channel.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
import 'package:uni_links/uni_links.dart';
|
import 'package:uni_links/uni_links.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
@@ -55,6 +55,12 @@ var isMobile = isAndroid || isIOS;
|
|||||||
var version = '';
|
var version = '';
|
||||||
int androidVersion = 0;
|
int androidVersion = 0;
|
||||||
|
|
||||||
|
// Only used on Linux.
|
||||||
|
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux.
|
||||||
|
// https://stackoverflow.com/questions/8193613/gtk-window-resize-disable-without-going-back-to-default
|
||||||
|
// So we need to use this flag to enable/disable resizable.
|
||||||
|
bool _linuxWindowResizable = true;
|
||||||
|
|
||||||
/// only available for Windows target
|
/// only available for Windows target
|
||||||
int windowsBuildNumber = 0;
|
int windowsBuildNumber = 0;
|
||||||
DesktopType? desktopType;
|
DesktopType? desktopType;
|
||||||
@@ -544,7 +550,8 @@ class MyTheme {
|
|||||||
Get.changeThemeMode(mode);
|
Get.changeThemeMode(mode);
|
||||||
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
if (desktopType == DesktopType.main || isAndroid || isIOS) {
|
||||||
if (mode == ThemeMode.system) {
|
if (mode == ThemeMode.system) {
|
||||||
await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
|
await bind.mainSetLocalOption(
|
||||||
|
key: kCommConfKeyTheme, value: defaultOptionTheme);
|
||||||
} else {
|
} else {
|
||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: kCommConfKeyTheme, value: mode.toShortString());
|
key: kCommConfKeyTheme, value: mode.toShortString());
|
||||||
@@ -852,30 +859,10 @@ class OverlayDialogManager {
|
|||||||
final overlayState = _overlayKeyState.state;
|
final overlayState = _overlayKeyState.state;
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
|
|
||||||
// compute overlay position
|
final overlay = makeMobileActionsOverlayEntry(
|
||||||
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
|
() => hideMobileActionsOverlay(),
|
||||||
final screenH = MediaQuery.of(globalKey.currentContext!).size.height;
|
ffi: ffi,
|
||||||
const double overlayW = 200;
|
);
|
||||||
const double overlayH = 45;
|
|
||||||
final left = (screenW - overlayW) / 2;
|
|
||||||
final top = screenH - overlayH - 80;
|
|
||||||
|
|
||||||
final overlay = OverlayEntry(builder: (context) {
|
|
||||||
final session = ffi ?? gFFI;
|
|
||||||
return DraggableMobileActions(
|
|
||||||
position: Offset(left, top),
|
|
||||||
width: overlayW,
|
|
||||||
height: overlayH,
|
|
||||||
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
|
||||||
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
|
|
||||||
onRecentPressed: () async {
|
|
||||||
session.inputModel.sendMouse('down', MouseButtons.wheel);
|
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
|
||||||
session.inputModel.sendMouse('up', MouseButtons.wheel);
|
|
||||||
},
|
|
||||||
onHidePressed: () => hideMobileActionsOverlay(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
overlayState.insert(overlay);
|
overlayState.insert(overlay);
|
||||||
_mobileActionsOverlayEntry = overlay;
|
_mobileActionsOverlayEntry = overlay;
|
||||||
mobileActionsOverlayVisible.value = true;
|
mobileActionsOverlayVisible.value = true;
|
||||||
@@ -903,6 +890,45 @@ class OverlayDialogManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
makeMobileActionsOverlayEntry(VoidCallback? onHide, {FFI? ffi}) {
|
||||||
|
final position = SimpleWrapper(Offset(0, 0));
|
||||||
|
makeMobileActions(BuildContext context, double s) {
|
||||||
|
final scale = s < 0.85 ? 0.85 : s;
|
||||||
|
final session = ffi ?? gFFI;
|
||||||
|
// compute overlay position
|
||||||
|
final screenW = MediaQuery.of(context).size.width;
|
||||||
|
final screenH = MediaQuery.of(context).size.height;
|
||||||
|
const double overlayW = 200;
|
||||||
|
const double overlayH = 45;
|
||||||
|
final left = (screenW - overlayW * scale) / 2;
|
||||||
|
final top = screenH - (overlayH + 80) * scale;
|
||||||
|
position.value = Offset(left, top);
|
||||||
|
return DraggableMobileActions(
|
||||||
|
scale: scale,
|
||||||
|
position: position,
|
||||||
|
width: overlayW,
|
||||||
|
height: overlayH,
|
||||||
|
onBackPressed: () => session.inputModel.tap(MouseButtons.right),
|
||||||
|
onHomePressed: () => session.inputModel.tap(MouseButtons.wheel),
|
||||||
|
onRecentPressed: () async {
|
||||||
|
session.inputModel.sendMouse('down', MouseButtons.wheel);
|
||||||
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
|
session.inputModel.sendMouse('up', MouseButtons.wheel);
|
||||||
|
},
|
||||||
|
onHidePressed: onHide,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return OverlayEntry(builder: (context) {
|
||||||
|
if (isDesktop) {
|
||||||
|
final c = Provider.of<CanvasModel>(context);
|
||||||
|
return makeMobileActions(context, c.scale * 2.0);
|
||||||
|
} else {
|
||||||
|
return makeMobileActions(globalKey.currentContext!, 1.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
|
void showToast(String text, {Duration timeout = const Duration(seconds: 3)}) {
|
||||||
final overlayState = globalKey.currentState?.overlay;
|
final overlayState = globalKey.currentState?.overlay;
|
||||||
if (overlayState == null) return;
|
if (overlayState == null) return;
|
||||||
@@ -982,7 +1008,7 @@ class CustomAlertDialog extends StatelessWidget {
|
|||||||
return KeyEventResult.handled; // avoid TextField exception on escape
|
return KeyEventResult.handled; // avoid TextField exception on escape
|
||||||
} else if (!tabTapped &&
|
} else if (!tabTapped &&
|
||||||
onSubmit != null &&
|
onSubmit != null &&
|
||||||
key.logicalKey == LogicalKeyboardKey.enter) {
|
(key.logicalKey == LogicalKeyboardKey.enter || key.logicalKey == LogicalKeyboardKey.numpadEnter)) {
|
||||||
if (key is RawKeyDownEvent) onSubmit?.call();
|
if (key is RawKeyDownEvent) onSubmit?.call();
|
||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
} else if (key.logicalKey == LogicalKeyboardKey.tab) {
|
||||||
@@ -1401,7 +1427,7 @@ bool option2bool(String option, String value) {
|
|||||||
res = value != "N";
|
res = value != "N";
|
||||||
} else if (option.startsWith("allow-") ||
|
} else if (option.startsWith("allow-") ||
|
||||||
option == "stop-service" ||
|
option == "stop-service" ||
|
||||||
option == "direct-server" ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
option == "stop-rendezvous-service" ||
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = value == "Y";
|
res = value == "Y";
|
||||||
@@ -1415,13 +1441,13 @@ bool option2bool(String option, String value) {
|
|||||||
String bool2option(String option, bool b) {
|
String bool2option(String option, bool b) {
|
||||||
String res;
|
String res;
|
||||||
if (option.startsWith('enable-')) {
|
if (option.startsWith('enable-')) {
|
||||||
res = b ? '' : 'N';
|
res = b ? defaultOptionYes : 'N';
|
||||||
} else if (option.startsWith('allow-') ||
|
} else if (option.startsWith('allow-') ||
|
||||||
option == "stop-service" ||
|
option == "stop-service" ||
|
||||||
option == "direct-server" ||
|
option == kOptionDirectServer ||
|
||||||
option == "stop-rendezvous-service" ||
|
option == "stop-rendezvous-service" ||
|
||||||
option == kOptionForceAlwaysRelay) {
|
option == kOptionForceAlwaysRelay) {
|
||||||
res = b ? 'Y' : '';
|
res = b ? 'Y' : defaultOptionNo;
|
||||||
} else {
|
} else {
|
||||||
assert(false);
|
assert(false);
|
||||||
res = b ? 'Y' : 'N';
|
res = b ? 'Y' : 'N';
|
||||||
@@ -1493,6 +1519,12 @@ Widget getPlatformImage(String platform, {double size = 50}) {
|
|||||||
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class OffsetDevicePixelRatio {
|
||||||
|
Offset offset;
|
||||||
|
final double devicePixelRatio;
|
||||||
|
OffsetDevicePixelRatio(this.offset, this.devicePixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
class LastWindowPosition {
|
class LastWindowPosition {
|
||||||
double? width;
|
double? width;
|
||||||
double? height;
|
double? height;
|
||||||
@@ -1554,16 +1586,13 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
|||||||
late Offset position;
|
late Offset position;
|
||||||
late Size sz;
|
late Size sz;
|
||||||
late bool isMaximized;
|
late bool isMaximized;
|
||||||
bool isFullscreen = stateGlobal.fullscreen.isTrue ||
|
bool isFullscreen = stateGlobal.fullscreen.isTrue;
|
||||||
(isMacOS && stateGlobal.closeOnFullscreen == true);
|
setPreFrame() {
|
||||||
setFrameIfMaximized() {
|
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
||||||
if (isMaximized) {
|
var lpos = LastWindowPosition.loadFromString(pos);
|
||||||
final pos = bind.getLocalFlutterOption(k: windowFramePrefix + type.name);
|
position = Offset(
|
||||||
var lpos = LastWindowPosition.loadFromString(pos);
|
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
||||||
position = Offset(
|
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
||||||
lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
|
|
||||||
sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -1572,26 +1601,33 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
|
|||||||
// `await windowManager.isMaximized()` will always return true
|
// `await windowManager.isMaximized()` will always return true
|
||||||
// if is not resizable. The reason is unknown.
|
// if is not resizable. The reason is unknown.
|
||||||
//
|
//
|
||||||
// `windowManager.setResizable(!bind.isIncomingOnly());` in main.dart
|
// `setResizable(!bind.isIncomingOnly());` in main.dart
|
||||||
isMaximized =
|
isMaximized =
|
||||||
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
|
bind.isIncomingOnly() ? false : await windowManager.isMaximized();
|
||||||
position = await windowManager.getPosition();
|
if (isFullscreen || isMaximized) {
|
||||||
sz = await windowManager.getSize();
|
setPreFrame();
|
||||||
setFrameIfMaximized();
|
} else {
|
||||||
|
position = await windowManager.getPosition();
|
||||||
|
sz = await windowManager.getSize();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
final wc = WindowController.fromWindowId(windowId!);
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
isMaximized = await wc.isMaximized();
|
isMaximized = await wc.isMaximized();
|
||||||
final Rect frame;
|
if (isFullscreen || isMaximized) {
|
||||||
try {
|
setPreFrame();
|
||||||
frame = await wc.getFrame();
|
} else {
|
||||||
} catch (e) {
|
final Rect frame;
|
||||||
debugPrint("Failed to get frame of window $windowId, it may be hidden");
|
try {
|
||||||
return;
|
frame = await wc.getFrame();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(
|
||||||
|
"Failed to get frame of window $windowId, it may be hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
position = frame.topLeft;
|
||||||
|
sz = frame.size;
|
||||||
}
|
}
|
||||||
position = frame.topLeft;
|
|
||||||
sz = frame.size;
|
|
||||||
setFrameIfMaximized();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
@@ -1625,7 +1661,7 @@ Future _saveSessionWindowPosition(WindowType windowType, int windowId,
|
|||||||
final remoteList = await DesktopMultiWindow.invokeMethod(
|
final remoteList = await DesktopMultiWindow.invokeMethod(
|
||||||
windowId, kWindowEventGetRemoteList, null);
|
windowId, kWindowEventGetRemoteList, null);
|
||||||
getPeerPos(String peerId) {
|
getPeerPos(String peerId) {
|
||||||
if (isMaximized) {
|
if (isMaximized || isFullscreen) {
|
||||||
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
||||||
id: peerId, k: windowFramePrefix + windowType.name);
|
id: peerId, k: windowFramePrefix + windowType.name);
|
||||||
var lpos = LastWindowPosition.loadFromString(peerPos);
|
var lpos = LastWindowPosition.loadFromString(peerPos);
|
||||||
@@ -1682,8 +1718,15 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
|
|||||||
return Size(restoreWidth, restoreHeight);
|
return Size(restoreWidth, restoreHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isPointInRect(Offset point, Rect rect) {
|
||||||
|
return point.dx >= rect.left &&
|
||||||
|
point.dx <= rect.right &&
|
||||||
|
point.dy >= rect.top &&
|
||||||
|
point.dy <= rect.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
/// return null means center
|
/// return null means center
|
||||||
Future<Offset?> _adjustRestoreMainWindowOffset(
|
Future<OffsetDevicePixelRatio?> _adjustRestoreMainWindowOffset(
|
||||||
double? left,
|
double? left,
|
||||||
double? top,
|
double? top,
|
||||||
double? width,
|
double? width,
|
||||||
@@ -1697,9 +1740,13 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
|||||||
double? frameTop;
|
double? frameTop;
|
||||||
double? frameRight;
|
double? frameRight;
|
||||||
double? frameBottom;
|
double? frameBottom;
|
||||||
|
double devicePixelRatio = 1.0;
|
||||||
|
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
for (final screen in await window_size.getScreenList()) {
|
for (final screen in await window_size.getScreenList()) {
|
||||||
|
if (isPointInRect(Offset(left, top), screen.visibleFrame)) {
|
||||||
|
devicePixelRatio = screen.scaleFactor;
|
||||||
|
}
|
||||||
frameLeft = frameLeft == null
|
frameLeft = frameLeft == null
|
||||||
? screen.visibleFrame.left
|
? screen.visibleFrame.left
|
||||||
: min(screen.visibleFrame.left, frameLeft);
|
: min(screen.visibleFrame.left, frameLeft);
|
||||||
@@ -1733,7 +1780,7 @@ Future<Offset?> _adjustRestoreMainWindowOffset(
|
|||||||
top < frameTop!) {
|
top < frameTop!) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return Offset(left, top);
|
return OffsetDevicePixelRatio(Offset(left, top), devicePixelRatio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1759,15 +1806,10 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
// No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
|
||||||
// Though "open in tabs" is true and the new window restore peer position, it's ok.
|
// Though "open in tabs" is true and the new window restore peer position, it's ok.
|
||||||
if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
|
if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
|
||||||
// If the restore position is called by main window, and the peer id is not null
|
final peerPos = bind.mainGetPeerFlutterOptionSync(
|
||||||
// then we may need to get the position by reading the peer config.
|
id: peerId, k: windowFramePrefix + type.name);
|
||||||
// Because the session may not be read at this time.
|
if (peerPos.isNotEmpty) {
|
||||||
if (desktopType == DesktopType.main) {
|
pos = peerPos;
|
||||||
pos = bind.mainGetPeerFlutterOptionSync(
|
|
||||||
id: peerId, k: windowFramePrefix + type.name);
|
|
||||||
} else {
|
|
||||||
pos = await bind.sessionGetFlutterOptionByPeerId(
|
|
||||||
id: peerId, k: windowFramePrefix + type.name);
|
|
||||||
}
|
}
|
||||||
isRemotePeerPos = pos != null;
|
isRemotePeerPos = pos != null;
|
||||||
}
|
}
|
||||||
@@ -1798,22 +1840,47 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
}
|
}
|
||||||
|
|
||||||
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
|
||||||
final offset = await _adjustRestoreMainWindowOffset(
|
final offsetDevicePixelRatio = await _adjustRestoreMainWindowOffset(
|
||||||
lpos.offsetWidth,
|
lpos.offsetWidth,
|
||||||
lpos.offsetHeight,
|
lpos.offsetHeight,
|
||||||
size.width,
|
size.width,
|
||||||
size.height,
|
size.height,
|
||||||
);
|
);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}");
|
"restore lpos: ${size.width}/${size.height}, offset:${offsetDevicePixelRatio?.offset.dx}/${offsetDevicePixelRatio?.offset.dy}, devicePixelRatio:${offsetDevicePixelRatio?.devicePixelRatio}, isMaximized: ${lpos.isMaximized}, isFullscreen: ${lpos.isFullscreen}");
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case WindowType.Main:
|
case WindowType.Main:
|
||||||
|
// https://github.com/rustdesk/rustdesk/issues/8038
|
||||||
|
// `setBounds()` in `window_manager` will use the current devicePixelRatio.
|
||||||
|
// So we need to adjust the offset by the scale factor.
|
||||||
|
// https://github.com/rustdesk-org/window_manager/blob/f19acdb008645366339444a359a45c3257c8b32e/windows/window_manager.cpp#L701
|
||||||
|
if (isWindows) {
|
||||||
|
double? curDevicePixelRatio;
|
||||||
|
Offset curPos = await windowManager.getPosition();
|
||||||
|
for (final screen in await window_size.getScreenList()) {
|
||||||
|
if (isPointInRect(curPos, screen.visibleFrame)) {
|
||||||
|
curDevicePixelRatio = screen.scaleFactor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (curDevicePixelRatio != null &&
|
||||||
|
curDevicePixelRatio != 0 &&
|
||||||
|
offsetDevicePixelRatio != null) {
|
||||||
|
if (offsetDevicePixelRatio.devicePixelRatio != 0) {
|
||||||
|
final scale =
|
||||||
|
offsetDevicePixelRatio.devicePixelRatio / curDevicePixelRatio;
|
||||||
|
offsetDevicePixelRatio.offset =
|
||||||
|
offsetDevicePixelRatio.offset.scale(scale, scale);
|
||||||
|
debugPrint(
|
||||||
|
"restore new offset: ${offsetDevicePixelRatio.offset.dx}/${offsetDevicePixelRatio.offset.dy}, scale:$scale");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
restorePos() async {
|
restorePos() async {
|
||||||
if (offset == null) {
|
if (offsetDevicePixelRatio == null) {
|
||||||
await windowManager.center();
|
await windowManager.center();
|
||||||
} else {
|
} else {
|
||||||
await windowManager.setPosition(offset);
|
await windowManager.setPosition(offsetDevicePixelRatio.offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lpos.isMaximized == true) {
|
if (lpos.isMaximized == true) {
|
||||||
@@ -1831,19 +1898,27 @@ Future<bool> restoreWindowPosition(WindowType type,
|
|||||||
default:
|
default:
|
||||||
final wc = WindowController.fromWindowId(windowId!);
|
final wc = WindowController.fromWindowId(windowId!);
|
||||||
restoreFrame() async {
|
restoreFrame() async {
|
||||||
if (offset == null) {
|
if (offsetDevicePixelRatio == null) {
|
||||||
await wc.center();
|
await wc.center();
|
||||||
} else {
|
} else {
|
||||||
final frame =
|
final frame = Rect.fromLTWH(offsetDevicePixelRatio.offset.dx,
|
||||||
Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height);
|
offsetDevicePixelRatio.offset.dy, size.width, size.height);
|
||||||
await wc.setFrame(frame);
|
await wc.setFrame(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lpos.isFullscreen == true) {
|
if (lpos.isFullscreen == true) {
|
||||||
await restoreFrame();
|
if (!isMacOS) {
|
||||||
|
await restoreFrame();
|
||||||
|
}
|
||||||
// An duration is needed to avoid the window being restored after fullscreen.
|
// An duration is needed to avoid the window being restored after fullscreen.
|
||||||
Future.delayed(Duration(milliseconds: 300), () async {
|
Future.delayed(Duration(milliseconds: 300), () async {
|
||||||
stateGlobal.setFullscreen(true);
|
if (kWindowId == windowId) {
|
||||||
|
stateGlobal.setFullscreen(true);
|
||||||
|
} else {
|
||||||
|
// If is not current window, we need to send a fullscreen message to `windowId`
|
||||||
|
DesktopMultiWindow.invokeMethod(
|
||||||
|
windowId, kWindowEventSetFullscreen, 'true');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else if (lpos.isMaximized == true) {
|
} else if (lpos.isMaximized == true) {
|
||||||
await restoreFrame();
|
await restoreFrame();
|
||||||
@@ -2309,7 +2384,13 @@ Future<void> onActiveWindowChanged() async {
|
|||||||
await windowManager.setPreventClose(false);
|
await windowManager.setPreventClose(false);
|
||||||
await windowManager.close();
|
await windowManager.close();
|
||||||
if (isMacOS) {
|
if (isMacOS) {
|
||||||
RdPlatformChannel.instance.terminate();
|
// If we call without delay, `flutter/macos/Runner/MainFlutterWindow.swift` can handle the "terminate" event.
|
||||||
|
// But the app will not close.
|
||||||
|
//
|
||||||
|
// No idea why we need to delay here, `terminate()` itself is also an async function.
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
RdPlatformChannel.instance.terminate();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2743,11 +2824,6 @@ sessionRefreshVideo(SessionID sessionId, PeerInfo pi) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isChooseDisplayToOpenInNewWindow(PeerInfo pi, SessionID sessionId) =>
|
|
||||||
pi.isSupportMultiDisplay &&
|
|
||||||
useTextureRender &&
|
|
||||||
bind.sessionGetDisplaysAsIndividualWindows(sessionId: sessionId) == 'Y';
|
|
||||||
|
|
||||||
Future<List<Rect>> getScreenListWayland() async {
|
Future<List<Rect>> getScreenListWayland() async {
|
||||||
final screenRectList = <Rect>[];
|
final screenRectList = <Rect>[];
|
||||||
if (isMainDesktopWindow) {
|
if (isMainDesktopWindow) {
|
||||||
@@ -2846,6 +2922,15 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi,
|
|||||||
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
kMainWindowId, kWindowEventOpenMonitorSession, jsonEncode(args));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setNewConnectWindowFrame(int windowId, String peerId, Rect? screenRect) async {
|
||||||
|
if (screenRect == null) {
|
||||||
|
await restoreWindowPosition(WindowType.RemoteDesktop,
|
||||||
|
windowId: windowId, peerId: peerId);
|
||||||
|
} else {
|
||||||
|
await tryMoveToScreenAndSetFullscreen(screenRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
|
tryMoveToScreenAndSetFullscreen(Rect? screenRect) async {
|
||||||
if (screenRect == null) {
|
if (screenRect == null) {
|
||||||
return;
|
return;
|
||||||
@@ -2978,16 +3063,16 @@ Future<bool> setServerConfig(
|
|||||||
}
|
}
|
||||||
// id
|
// id
|
||||||
if (config.idServer.isNotEmpty && errMsgs != null) {
|
if (config.idServer.isNotEmpty && errMsgs != null) {
|
||||||
errMsgs[0].value =
|
errMsgs[0].value = translate(await bind.mainTestIfValidServer(
|
||||||
translate(await bind.mainTestIfValidServer(server: config.idServer));
|
server: config.idServer, testWithProxy: true));
|
||||||
if (errMsgs[0].isNotEmpty) {
|
if (errMsgs[0].isNotEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// relay
|
// relay
|
||||||
if (config.relayServer.isNotEmpty && errMsgs != null) {
|
if (config.relayServer.isNotEmpty && errMsgs != null) {
|
||||||
errMsgs[1].value =
|
errMsgs[1].value = translate(await bind.mainTestIfValidServer(
|
||||||
translate(await bind.mainTestIfValidServer(server: config.relayServer));
|
server: config.relayServer, testWithProxy: true));
|
||||||
if (errMsgs[1].isNotEmpty) {
|
if (errMsgs[1].isNotEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3106,6 +3191,27 @@ Color? disabledTextColor(BuildContext context, bool enabled) {
|
|||||||
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
|
: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget loadPowered(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
launchUrl(Uri.parse('https://rustdesk.com'));
|
||||||
|
},
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 0.5,
|
||||||
|
child: Text(
|
||||||
|
translate("powered_by_me"),
|
||||||
|
overflow: TextOverflow.clip,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall
|
||||||
|
?.copyWith(fontSize: 9, decoration: TextDecoration.underline),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
).marginOnly(top: 6);
|
||||||
|
}
|
||||||
|
|
||||||
// max 300 x 60
|
// max 300 x 60
|
||||||
Widget loadLogo() {
|
Widget loadLogo() {
|
||||||
return FutureBuilder<ByteData>(
|
return FutureBuilder<ByteData>(
|
||||||
@@ -3155,3 +3261,114 @@ bool isInHomePage() {
|
|||||||
final controller = Get.find<DesktopTabController>();
|
final controller = Get.find<DesktopTabController>();
|
||||||
return controller.state.value.selected == 0;
|
return controller.state.value.selected == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildPresetPasswordWarning() {
|
||||||
|
return FutureBuilder<bool>(
|
||||||
|
future: bind.isPresetPassword(),
|
||||||
|
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
|
||||||
|
} else if (snapshot.hasError) {
|
||||||
|
return Text(
|
||||||
|
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
||||||
|
} else if (snapshot.hasData && snapshot.data == true) {
|
||||||
|
return Container(
|
||||||
|
color: Colors.yellow,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
child: Text(
|
||||||
|
translate("Security Alert"),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
fontSize:
|
||||||
|
18, // https://github.com/rustdesk/rustdesk-server-pro/issues/261
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
)).paddingOnly(bottom: 8),
|
||||||
|
Text(
|
||||||
|
translate("preset_password_warning"),
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
).paddingAll(8),
|
||||||
|
); // Show a warning message if the Future completed with true
|
||||||
|
} else {
|
||||||
|
return SizedBox
|
||||||
|
.shrink(); // Show nothing if the Future completed with false or null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/leanflutter/window_manager/blob/87dd7a50b4cb47a375b9fc697f05e56eea0a2ab3/lib/src/widgets/virtual_window_frame.dart#L44
|
||||||
|
Widget buildVirtualWindowFrame(BuildContext context, Widget child) {
|
||||||
|
boxShadow() => isMainDesktopWindow
|
||||||
|
? <BoxShadow>[
|
||||||
|
if (stateGlobal.fullscreen.isFalse || stateGlobal.isMaximized.isFalse)
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
offset: Offset(
|
||||||
|
0.0,
|
||||||
|
stateGlobal.isFocused.isTrue
|
||||||
|
? kFrameBoxShadowOffsetFocused
|
||||||
|
: kFrameBoxShadowOffsetUnfocused),
|
||||||
|
blurRadius: kFrameBoxShadowBlurRadius,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null;
|
||||||
|
return Obx(
|
||||||
|
() => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isMainDesktopWindow
|
||||||
|
? Colors.transparent
|
||||||
|
: Theme.of(context).colorScheme.background,
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: stateGlobal.windowBorderWidth.value,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
|
||||||
|
? 0
|
||||||
|
: kFrameBorderRadius,
|
||||||
|
),
|
||||||
|
boxShadow: boxShadow(),
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
(stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.isTrue)
|
||||||
|
? 0
|
||||||
|
: kFrameClipRRectBorderRadius,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get windowEdgeSize => isLinux && !_linuxWindowResizable ? 0.0 : kWindowEdgeSize;
|
||||||
|
|
||||||
|
// `windowManager.setResizable(false)` will reset the window size to the default size on Linux and then set unresizable.
|
||||||
|
// See _linuxWindowResizable for more details.
|
||||||
|
// So we use `setResizable()` instead of `windowManager.setResizable()`.
|
||||||
|
//
|
||||||
|
// We can only call `windowManager.setResizable(false)` if we need the default size on Linux.
|
||||||
|
setResizable(bool resizable) {
|
||||||
|
if (isLinux) {
|
||||||
|
_linuxWindowResizable = resizable;
|
||||||
|
stateGlobal.refreshResizeEdgeSize();
|
||||||
|
} else {
|
||||||
|
windowManager.setResizable(resizable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isOptionFixed(String key) => bind.mainIsOptionFixed(key: key);
|
||||||
|
|
||||||
|
final isCustomClient = bind.isCustomClient();
|
||||||
|
get defaultOptionLang => isCustomClient ? 'default' : '';
|
||||||
|
get defaultOptionTheme => isCustomClient ? 'system' : '';
|
||||||
|
get defaultOptionYes => isCustomClient ? 'Y' : '';
|
||||||
|
get defaultOptionNo => isCustomClient ? 'N' : '';
|
||||||
|
get defaultOptionWhitelist => isCustomClient ? ',' : '';
|
||||||
|
get defaultOptionAccessMode => isCustomClient ? 'custom' : '';
|
||||||
|
get defaultOptionApproveMode => isCustomClient ? 'password-click' : '';
|
||||||
|
|||||||
@@ -168,6 +168,29 @@ class ShowRemoteCursorState {
|
|||||||
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ShowRemoteCursorLockState {
|
||||||
|
static String tag(String id) => 'show_remote_cursor_lock_$id';
|
||||||
|
|
||||||
|
static void init(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (!Get.isRegistered(tag: key)) {
|
||||||
|
final RxBool state = false.obs;
|
||||||
|
Get.put(state, tag: key);
|
||||||
|
} else {
|
||||||
|
Get.find<RxBool>(tag: key).value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void delete(String id) {
|
||||||
|
final key = tag(id);
|
||||||
|
if (Get.isRegistered(tag: key)) {
|
||||||
|
Get.delete(tag: key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static RxBool find(String id) => Get.find<RxBool>(tag: tag(id));
|
||||||
|
}
|
||||||
|
|
||||||
class KeyboardEnabledState {
|
class KeyboardEnabledState {
|
||||||
static String tag(String id) => 'keyboard_enabled_$id';
|
static String tag(String id) => 'keyboard_enabled_$id';
|
||||||
|
|
||||||
@@ -315,9 +338,10 @@ initSharedStates(String id) {
|
|||||||
CurrentDisplayState.init(id);
|
CurrentDisplayState.init(id);
|
||||||
KeyboardEnabledState.init(id);
|
KeyboardEnabledState.init(id);
|
||||||
ShowRemoteCursorState.init(id);
|
ShowRemoteCursorState.init(id);
|
||||||
|
ShowRemoteCursorLockState.init(id);
|
||||||
RemoteCursorMovedState.init(id);
|
RemoteCursorMovedState.init(id);
|
||||||
FingerprintState.init(id);
|
FingerprintState.init(id);
|
||||||
PeerBoolOption.init(id, 'zoom-cursor', () => false);
|
PeerBoolOption.init(id, kOptionZoomCursor, () => false);
|
||||||
UnreadChatCountState.init(id);
|
UnreadChatCountState.init(id);
|
||||||
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
|
if (isMobile) ConnectionTypeState.init(id); // desktop in other places
|
||||||
}
|
}
|
||||||
@@ -327,10 +351,11 @@ removeSharedStates(String id) {
|
|||||||
BlockInputState.delete(id);
|
BlockInputState.delete(id);
|
||||||
CurrentDisplayState.delete(id);
|
CurrentDisplayState.delete(id);
|
||||||
ShowRemoteCursorState.delete(id);
|
ShowRemoteCursorState.delete(id);
|
||||||
|
ShowRemoteCursorLockState.delete(id);
|
||||||
KeyboardEnabledState.delete(id);
|
KeyboardEnabledState.delete(id);
|
||||||
RemoteCursorMovedState.delete(id);
|
RemoteCursorMovedState.delete(id);
|
||||||
FingerprintState.delete(id);
|
FingerprintState.delete(id);
|
||||||
PeerBoolOption.delete(id, 'zoom-cursor');
|
PeerBoolOption.delete(id, kOptionZoomCursor);
|
||||||
UnreadChatCountState.delete(id);
|
UnreadChatCountState.delete(id);
|
||||||
if (isMobile) ConnectionTypeState.delete(id);
|
if (isMobile) ConnectionTypeState.delete(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
|
|||||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
import 'package:flutter_hbb/common/widgets/peer_card.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
|
||||||
import 'package:flutter_hbb/models/ab_model.dart';
|
import 'package:flutter_hbb/models/ab_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
@@ -189,41 +190,60 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
if (!names.contains(gFFI.abModel.currentName.value)) {
|
if (!names.contains(gFFI.abModel.currentName.value)) {
|
||||||
return Offstage();
|
return Offstage();
|
||||||
}
|
}
|
||||||
|
// order: personal, divider, character order
|
||||||
|
// https://pub.dev/packages/dropdown_button2#3-dropdownbutton2-with-items-of-different-heights-like-dividers
|
||||||
|
final personalAddressBookName = gFFI.abModel.personalAddressBookName();
|
||||||
|
bool contains = names.remove(personalAddressBookName);
|
||||||
|
names.sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase()));
|
||||||
|
if (contains) {
|
||||||
|
names.insert(0, personalAddressBookName);
|
||||||
|
}
|
||||||
|
final items = names
|
||||||
|
.map((e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Tooltip(
|
||||||
|
waitDuration: Duration(milliseconds: 500),
|
||||||
|
message: gFFI.abModel.translatedName(e),
|
||||||
|
child: Text(
|
||||||
|
gFFI.abModel.translatedName(e),
|
||||||
|
style: TextStyle(fontSize: 14.0),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)))
|
||||||
|
.toList();
|
||||||
|
var menuItemStyleData = MenuItemStyleData(height: 36);
|
||||||
|
if (contains && items.length > 1) {
|
||||||
|
items.insert(1, DropdownMenuItem(enabled: false, child: Divider()));
|
||||||
|
List<double> customHeights = List.filled(items.length, 36);
|
||||||
|
customHeights[1] = 4;
|
||||||
|
menuItemStyleData = MenuItemStyleData(customHeights: customHeights);
|
||||||
|
}
|
||||||
final TextEditingController textEditingController = TextEditingController();
|
final TextEditingController textEditingController = TextEditingController();
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kOptionCurrentAbName);
|
||||||
return DropdownButton2<String>(
|
return DropdownButton2<String>(
|
||||||
value: gFFI.abModel.currentName.value,
|
value: gFFI.abModel.currentName.value,
|
||||||
onChanged: (value) {
|
onChanged: isOptFixed
|
||||||
if (value != null) {
|
? null
|
||||||
gFFI.abModel.setCurrentName(value);
|
: (value) {
|
||||||
bind.setLocalFlutterOption(k: 'current-ab-name', v: value);
|
if (value != null) {
|
||||||
}
|
gFFI.abModel.setCurrentName(value);
|
||||||
},
|
bind.setLocalFlutterOption(k: kOptionCurrentAbName, v: value);
|
||||||
|
}
|
||||||
|
},
|
||||||
underline: Container(
|
underline: Container(
|
||||||
height: 0.7,
|
height: 0.7,
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
color: Theme.of(context).dividerColor.withOpacity(0.1),
|
||||||
),
|
),
|
||||||
buttonStyleData: ButtonStyleData(height: 48),
|
buttonStyleData: ButtonStyleData(height: 48),
|
||||||
menuItemStyleData: MenuItemStyleData(height: 36),
|
menuItemStyleData: menuItemStyleData,
|
||||||
items: names
|
items: items,
|
||||||
.map((e) => DropdownMenuItem(
|
|
||||||
value: e,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Tooltip(
|
|
||||||
waitDuration: Duration(milliseconds: 500),
|
|
||||||
message: gFFI.abModel.translatedName(e),
|
|
||||||
child: Text(
|
|
||||||
gFFI.abModel.translatedName(e),
|
|
||||||
style: TextStyle(fontSize: 14.0),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)))
|
|
||||||
.toList(),
|
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
dropdownSearchData: DropdownSearchData(
|
dropdownSearchData: DropdownSearchData(
|
||||||
searchController: textEditingController,
|
searchController: textEditingController,
|
||||||
@@ -333,6 +353,7 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
|
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> syncMenuItem() {
|
MenuEntryBase<String> syncMenuItem() {
|
||||||
|
final isOptFixed = isOptionFixed(syncAbOption);
|
||||||
return MenuEntrySwitch<String>(
|
return MenuEntrySwitch<String>(
|
||||||
switchType: SwitchType.scheckbox,
|
switchType: SwitchType.scheckbox,
|
||||||
text: translate('Sync with recent sessions'),
|
text: translate('Sync with recent sessions'),
|
||||||
@@ -343,11 +364,13 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
gFFI.abModel.setShouldAsync(v);
|
gFFI.abModel.setShouldAsync(v);
|
||||||
},
|
},
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: (!isOptFixed).obs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> sortMenuItem() {
|
MenuEntryBase<String> sortMenuItem() {
|
||||||
|
final isOptFixed = isOptionFixed(sortAbTagsOption);
|
||||||
return MenuEntrySwitch<String>(
|
return MenuEntrySwitch<String>(
|
||||||
switchType: SwitchType.scheckbox,
|
switchType: SwitchType.scheckbox,
|
||||||
text: translate('Sort tags'),
|
text: translate('Sort tags'),
|
||||||
@@ -355,15 +378,18 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
return shouldSortTags();
|
return shouldSortTags();
|
||||||
},
|
},
|
||||||
setter: (bool v) async {
|
setter: (bool v) async {
|
||||||
bind.mainSetLocalOption(key: sortAbTagsOption, value: v ? 'Y' : '');
|
bind.mainSetLocalOption(
|
||||||
|
key: sortAbTagsOption, value: v ? 'Y' : defaultOptionNo);
|
||||||
gFFI.abModel.sortTags.value = v;
|
gFFI.abModel.sortTags.value = v;
|
||||||
},
|
},
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: (!isOptFixed).obs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
MenuEntryBase<String> filterMenuItem() {
|
MenuEntryBase<String> filterMenuItem() {
|
||||||
|
final isOptFixed = isOptionFixed(filterAbTagOption);
|
||||||
return MenuEntrySwitch<String>(
|
return MenuEntrySwitch<String>(
|
||||||
switchType: SwitchType.scheckbox,
|
switchType: SwitchType.scheckbox,
|
||||||
text: translate('Filter by intersection'),
|
text: translate('Filter by intersection'),
|
||||||
@@ -371,10 +397,12 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
return filterAbTagByIntersection();
|
return filterAbTagByIntersection();
|
||||||
},
|
},
|
||||||
setter: (bool v) async {
|
setter: (bool v) async {
|
||||||
bind.mainSetLocalOption(key: filterAbTagOption, value: v ? 'Y' : '');
|
bind.mainSetLocalOption(
|
||||||
|
key: filterAbTagOption, value: v ? 'Y' : defaultOptionNo);
|
||||||
gFFI.abModel.filterByIntersection.value = v;
|
gFFI.abModel.filterByIntersection.value = v;
|
||||||
},
|
},
|
||||||
dismissOnClicked: true,
|
dismissOnClicked: true,
|
||||||
|
enabled: (!isOptFixed).obs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,10 +413,11 @@ class _AddressBookState extends State<AddressBook> {
|
|||||||
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
if (canWrite) getEntry(translate("Add Tag"), abAddTag),
|
||||||
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
|
||||||
sortMenuItem(),
|
sortMenuItem(),
|
||||||
syncMenuItem(),
|
if (canWrite) syncMenuItem(),
|
||||||
filterMenuItem(),
|
filterMenuItem(),
|
||||||
if (!gFFI.abModel.legacyMode.value) MenuEntryDivider<String>(),
|
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||||
if (!gFFI.abModel.legacyMode.value)
|
MenuEntryDivider<String>(),
|
||||||
|
if (!gFFI.abModel.legacyMode.value && canWrite)
|
||||||
getEntry(translate("ab_web_console_tip"), () async {
|
getEntry(translate("ab_web_console_tip"), () async {
|
||||||
final url = await bind.mainGetApiServer();
|
final url = await bind.mainGetApiServer();
|
||||||
if (await canLaunchUrlString(url)) {
|
if (await canLaunchUrlString(url)) {
|
||||||
|
|||||||
56
flutter/lib/common/widgets/audio_input.dart
Normal file
56
flutter/lib/common/widgets/audio_input.dart
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
|
|
||||||
|
typedef AudioINputSetDevice = void Function(String device);
|
||||||
|
typedef AudioInputBuilder = Widget Function(
|
||||||
|
List<String> devices, String currentDevice, AudioINputSetDevice setDevice);
|
||||||
|
|
||||||
|
class AudioInput extends StatelessWidget {
|
||||||
|
final AudioInputBuilder builder;
|
||||||
|
|
||||||
|
const AudioInput({Key? key, required this.builder}) : super(key: key);
|
||||||
|
|
||||||
|
static String getDefault() {
|
||||||
|
if (isWindows) return translate('System Sound');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<String> getValue() async {
|
||||||
|
String device = await bind.mainGetOption(key: 'audio-input');
|
||||||
|
if (device.isNotEmpty) {
|
||||||
|
return device;
|
||||||
|
} else {
|
||||||
|
return getDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> setDevice(String device) async {
|
||||||
|
if (device == getDefault()) device = '';
|
||||||
|
await bind.mainSetOption(key: 'audio-input', value: device);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<Map<String, Object>> getDevicesInfo() async {
|
||||||
|
List<String> devices = (await bind.mainGetSoundInputs()).toList();
|
||||||
|
if (isWindows) {
|
||||||
|
devices.insert(0, translate('System Sound'));
|
||||||
|
}
|
||||||
|
String current = await getValue();
|
||||||
|
return {'devices': devices, 'current': current};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return futureBuilder(
|
||||||
|
future: getDevicesInfo(),
|
||||||
|
hasData: (data) {
|
||||||
|
String currentDevice = data['current'];
|
||||||
|
List<String> devices = data['devices'] as List<String>;
|
||||||
|
if (devices.isEmpty) {
|
||||||
|
return const Offstage();
|
||||||
|
}
|
||||||
|
return builder(devices, currentDevice, setDevice);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,11 +177,14 @@ void changeIdDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void changeWhiteList({Function()? callback}) async {
|
void changeWhiteList({Function()? callback}) async {
|
||||||
var newWhiteList = (await bind.mainGetOption(key: 'whitelist')).split(',');
|
final curWhiteList = await bind.mainGetOption(key: kOptionWhitelist);
|
||||||
var newWhiteListField = newWhiteList.join('\n');
|
var newWhiteListField = curWhiteList == defaultOptionWhitelist
|
||||||
|
? ''
|
||||||
|
: curWhiteList.split(',').join('\n');
|
||||||
var controller = TextEditingController(text: newWhiteListField);
|
var controller = TextEditingController(text: newWhiteListField);
|
||||||
var msg = "";
|
var msg = "";
|
||||||
var isInProgress = false;
|
var isInProgress = false;
|
||||||
|
final isOptFixed = isOptionFixed(kOptionWhitelist);
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title: Text(translate("IP Whitelisting")),
|
title: Text(translate("IP Whitelisting")),
|
||||||
@@ -201,6 +204,7 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
errorText: msg.isEmpty ? null : translate(msg),
|
errorText: msg.isEmpty ? null : translate(msg),
|
||||||
),
|
),
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
enabled: !isOptFixed,
|
||||||
autofocus: true),
|
autofocus: true),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -214,12 +218,13 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
dialogButton("Clear", onPressed: () async {
|
if (!isOptFixed)dialogButton("Clear", onPressed: () async {
|
||||||
await bind.mainSetOption(key: 'whitelist', value: '');
|
await bind.mainSetOption(
|
||||||
|
key: kOptionWhitelist, value: defaultOptionWhitelist);
|
||||||
callback?.call();
|
callback?.call();
|
||||||
close();
|
close();
|
||||||
}, isOutline: true),
|
}, isOutline: true),
|
||||||
dialogButton(
|
if (!isOptFixed) dialogButton(
|
||||||
"OK",
|
"OK",
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -248,7 +253,11 @@ void changeWhiteList({Function()? callback}) async {
|
|||||||
}
|
}
|
||||||
newWhiteList = ips.join(',');
|
newWhiteList = ips.join(',');
|
||||||
}
|
}
|
||||||
await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
|
if (newWhiteList.trim().isEmpty) {
|
||||||
|
newWhiteList = defaultOptionWhitelist;
|
||||||
|
}
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionWhitelist, value: newWhiteList);
|
||||||
callback?.call();
|
callback?.call();
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
@@ -298,7 +307,7 @@ Future<String> changeDirectAccessPort(
|
|||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
dialogButton("OK", onPressed: () async {
|
dialogButton("OK", onPressed: () async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: 'direct-access-port', value: controller.text);
|
key: kOptionDirectAccessPort, value: controller.text);
|
||||||
close();
|
close();
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -345,7 +354,7 @@ Future<String> changeAutoDisconnectTimeout(String old) async {
|
|||||||
dialogButton("Cancel", onPressed: close, isOutline: true),
|
dialogButton("Cancel", onPressed: close, isOutline: true),
|
||||||
dialogButton("OK", onPressed: () async {
|
dialogButton("OK", onPressed: () async {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
key: 'auto-disconnect-timeout', value: controller.text);
|
key: kOptionAutoDisconnectTimeout, value: controller.text);
|
||||||
close();
|
close();
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class DraggableChatWindow extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: Draggable(
|
: Draggable(
|
||||||
checkKeyboard: true,
|
checkKeyboard: true,
|
||||||
position: position,
|
position: SimpleWrapper(position),
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
chatModel: chatModel,
|
chatModel: chatModel,
|
||||||
@@ -166,15 +166,17 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
/// floating buttons of back/home/recent actions for android
|
/// floating buttons of back/home/recent actions for android
|
||||||
class DraggableMobileActions extends StatelessWidget {
|
class DraggableMobileActions extends StatelessWidget {
|
||||||
DraggableMobileActions(
|
DraggableMobileActions(
|
||||||
{this.position = Offset.zero,
|
{this.onBackPressed,
|
||||||
this.onBackPressed,
|
|
||||||
this.onRecentPressed,
|
this.onRecentPressed,
|
||||||
this.onHomePressed,
|
this.onHomePressed,
|
||||||
this.onHidePressed,
|
this.onHidePressed,
|
||||||
|
required this.position,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height});
|
required this.height,
|
||||||
|
required this.scale});
|
||||||
|
|
||||||
final Offset position;
|
final double scale;
|
||||||
|
final SimpleWrapper<Offset> position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final VoidCallback? onBackPressed;
|
final VoidCallback? onBackPressed;
|
||||||
@@ -186,8 +188,8 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Draggable(
|
return Draggable(
|
||||||
position: position,
|
position: position,
|
||||||
width: width,
|
width: scale * width,
|
||||||
height: height,
|
height: scale * height,
|
||||||
builder: (_, onPanUpdate) {
|
builder: (_, onPanUpdate) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onPanUpdate: onPanUpdate,
|
onPanUpdate: onPanUpdate,
|
||||||
@@ -197,7 +199,8 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: MyTheme.accent.withOpacity(0.4),
|
color: MyTheme.accent.withOpacity(0.4),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(15))),
|
borderRadius:
|
||||||
|
BorderRadius.all(Radius.circular(15 * scale))),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
@@ -205,17 +208,20 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onBackPressed,
|
onPressed: onBackPressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.arrow_back)),
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
iconSize: 24 * scale),
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onHomePressed,
|
onPressed: onHomePressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.home)),
|
icon: const Icon(Icons.home),
|
||||||
|
iconSize: 24 * scale),
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onRecentPressed,
|
onPressed: onRecentPressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.more_horiz)),
|
icon: const Icon(Icons.more_horiz),
|
||||||
|
iconSize: 24 * scale),
|
||||||
const VerticalDivider(
|
const VerticalDivider(
|
||||||
width: 0,
|
width: 0,
|
||||||
thickness: 2,
|
thickness: 2,
|
||||||
@@ -226,7 +232,8 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
onPressed: onHidePressed,
|
onPressed: onHidePressed,
|
||||||
splashRadius: kDesktopIconButtonSplashRadius,
|
splashRadius: kDesktopIconButtonSplashRadius,
|
||||||
icon: const Icon(Icons.keyboard_arrow_down)),
|
icon: const Icon(Icons.keyboard_arrow_down),
|
||||||
|
iconSize: 24 * scale),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
@@ -235,11 +242,11 @@ class DraggableMobileActions extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Draggable extends StatefulWidget {
|
class Draggable extends StatefulWidget {
|
||||||
const Draggable(
|
Draggable(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
this.checkKeyboard = false,
|
this.checkKeyboard = false,
|
||||||
this.checkScreenSize = false,
|
this.checkScreenSize = false,
|
||||||
this.position = Offset.zero,
|
required this.position,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
this.chatModel,
|
this.chatModel,
|
||||||
@@ -248,7 +255,7 @@ class Draggable extends StatefulWidget {
|
|||||||
|
|
||||||
final bool checkKeyboard;
|
final bool checkKeyboard;
|
||||||
final bool checkScreenSize;
|
final bool checkScreenSize;
|
||||||
final Offset position;
|
final SimpleWrapper<Offset> position;
|
||||||
final double width;
|
final double width;
|
||||||
final double height;
|
final double height;
|
||||||
final ChatModel? chatModel;
|
final ChatModel? chatModel;
|
||||||
@@ -259,7 +266,6 @@ class Draggable extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _DraggableState extends State<Draggable> {
|
class _DraggableState extends State<Draggable> {
|
||||||
late Offset _position;
|
|
||||||
late ChatModel? _chatModel;
|
late ChatModel? _chatModel;
|
||||||
bool _keyboardVisible = false;
|
bool _keyboardVisible = false;
|
||||||
double _saveHeight = 0;
|
double _saveHeight = 0;
|
||||||
@@ -268,35 +274,36 @@ class _DraggableState extends State<Draggable> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_position = widget.position;
|
|
||||||
_chatModel = widget.chatModel;
|
_chatModel = widget.chatModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get position => widget.position.value;
|
||||||
|
|
||||||
void onPanUpdate(DragUpdateDetails d) {
|
void onPanUpdate(DragUpdateDetails d) {
|
||||||
final offset = d.delta;
|
final offset = d.delta;
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
double x = 0;
|
double x = 0;
|
||||||
double y = 0;
|
double y = 0;
|
||||||
|
|
||||||
if (_position.dx + offset.dx + widget.width > size.width) {
|
if (position.dx + offset.dx + widget.width > size.width) {
|
||||||
x = size.width - widget.width;
|
x = size.width - widget.width;
|
||||||
} else if (_position.dx + offset.dx < 0) {
|
} else if (position.dx + offset.dx < 0) {
|
||||||
x = 0;
|
x = 0;
|
||||||
} else {
|
} else {
|
||||||
x = _position.dx + offset.dx;
|
x = position.dx + offset.dx;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_position.dy + offset.dy + widget.height > size.height) {
|
if (position.dy + offset.dy + widget.height > size.height) {
|
||||||
y = size.height - widget.height;
|
y = size.height - widget.height;
|
||||||
} else if (_position.dy + offset.dy < 0) {
|
} else if (position.dy + offset.dy < 0) {
|
||||||
y = 0;
|
y = 0;
|
||||||
} else {
|
} else {
|
||||||
y = _position.dy + offset.dy;
|
y = position.dy + offset.dy;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(x, y);
|
widget.position.value = Offset(x, y);
|
||||||
});
|
});
|
||||||
_chatModel?.setChatWindowPosition(_position);
|
_chatModel?.setChatWindowPosition(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkScreenSize() {}
|
checkScreenSize() {}
|
||||||
@@ -307,13 +314,13 @@ class _DraggableState extends State<Draggable> {
|
|||||||
|
|
||||||
// save
|
// save
|
||||||
if (!_keyboardVisible && currentVisible) {
|
if (!_keyboardVisible && currentVisible) {
|
||||||
_saveHeight = _position.dy;
|
_saveHeight = position.dy;
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
if (_lastBottomHeight > 0 && bottomHeight == 0) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, _saveHeight);
|
widget.position.value = Offset(position.dx, _saveHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,10 +328,10 @@ class _DraggableState extends State<Draggable> {
|
|||||||
if (_keyboardVisible && currentVisible) {
|
if (_keyboardVisible && currentVisible) {
|
||||||
final sumHeight = bottomHeight + widget.height;
|
final sumHeight = bottomHeight + widget.height;
|
||||||
final contextHeight = MediaQuery.of(context).size.height;
|
final contextHeight = MediaQuery.of(context).size.height;
|
||||||
if (sumHeight + _position.dy > contextHeight) {
|
if (sumHeight + position.dy > contextHeight) {
|
||||||
final y = contextHeight - sumHeight;
|
final y = contextHeight - sumHeight;
|
||||||
setState(() {
|
setState(() {
|
||||||
_position = Offset(_position.dx, y);
|
widget.position.value = Offset(position.dx, y);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,8 +350,8 @@ class _DraggableState extends State<Draggable> {
|
|||||||
}
|
}
|
||||||
return Stack(children: [
|
return Stack(children: [
|
||||||
Positioned(
|
Positioned(
|
||||||
top: _position.dy,
|
top: position.dy,
|
||||||
left: _position.dx,
|
left: position.dx,
|
||||||
width: widget.width,
|
width: widget.width,
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
child: widget.builder(context, onPanUpdate))
|
child: widget.builder(context, onPanUpdate))
|
||||||
|
|||||||
@@ -74,9 +74,11 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
];
|
];
|
||||||
RelativeRect? mobileTabContextMenuPos;
|
RelativeRect? mobileTabContextMenuPos;
|
||||||
|
|
||||||
|
final isOptVisiableFixed = isOptionFixed(kOptionPeerTabVisible);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
|
final uiType = bind.getLocalFlutterOption(k: kOptionPeerCardUiType);
|
||||||
if (uiType != '') {
|
if (uiType != '') {
|
||||||
peerCardUiType.value = int.parse(uiType) == 0
|
peerCardUiType.value = int.parse(uiType) == 0
|
||||||
? PeerUiType.grid
|
? PeerUiType.grid
|
||||||
@@ -85,7 +87,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
: PeerUiType.list;
|
: PeerUiType.list;
|
||||||
}
|
}
|
||||||
hideAbTagsPanel.value =
|
hideAbTagsPanel.value =
|
||||||
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
bind.mainGetLocalOption(key: kOptionHideAbTagsPanel).isNotEmpty;
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +175,13 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
child: Icon(model.tabIcon(t), color: color)
|
child: Icon(model.tabIcon(t), color: color)
|
||||||
.paddingSymmetric(horizontal: 4),
|
.paddingSymmetric(horizontal: 4),
|
||||||
).paddingSymmetric(horizontal: 4),
|
).paddingSymmetric(horizontal: 4),
|
||||||
onTap: () async {
|
onTap: isOptionFixed(kOptionPeerTabIndex)
|
||||||
await handleTabSelection(t);
|
? null
|
||||||
await bind.setLocalFlutterOption(
|
: () async {
|
||||||
k: PeerTabModel.kPeerTabIndex, v: t.toString());
|
await handleTabSelection(t);
|
||||||
},
|
await bind.setLocalFlutterOption(
|
||||||
|
k: kOptionPeerTabIndex, v: t.toString());
|
||||||
|
},
|
||||||
onHover: (value) => hover.value = value,
|
onHover: (value) => hover.value = value,
|
||||||
),
|
),
|
||||||
)));
|
)));
|
||||||
@@ -265,17 +269,22 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
if (!model.isEnabled[i]) continue;
|
if (!model.isEnabled[i]) continue;
|
||||||
items.add(PopupMenuItem(
|
items.add(PopupMenuItem(
|
||||||
height: kMinInteractiveDimension * 0.8,
|
height: kMinInteractiveDimension * 0.8,
|
||||||
onTap: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
onTap: isOptVisiableFixed
|
||||||
|
? null
|
||||||
|
: () => model.setTabVisible(i, !model.isVisibleEnabled[i]),
|
||||||
|
enabled: !isOptVisiableFixed,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: model.isVisibleEnabled[i],
|
value: model.isVisibleEnabled[i],
|
||||||
onChanged: (_) {
|
onChanged: isOptVisiableFixed
|
||||||
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
? null
|
||||||
if (Navigator.canPop(context)) {
|
: (_) {
|
||||||
Navigator.pop(context);
|
model.setTabVisible(i, !model.isVisibleEnabled[i]);
|
||||||
}
|
if (Navigator.canPop(context)) {
|
||||||
}),
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}),
|
||||||
Expanded(child: Text(model.tabTooltip(i))),
|
Expanded(child: Text(model.tabTooltip(i))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -332,8 +341,10 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
currentValue: model.isVisibleEnabled[tabIndex],
|
currentValue: model.isVisibleEnabled[tabIndex],
|
||||||
setter: (show) async {
|
setter: (show) async {
|
||||||
model.setTabVisible(tabIndex, show);
|
model.setTabVisible(tabIndex, show);
|
||||||
cancelFunc();
|
// Do not hide the current menu (checkbox)
|
||||||
}));
|
// cancelFunc();
|
||||||
|
},
|
||||||
|
enabled: (!isOptVisiableFixed).obs));
|
||||||
}
|
}
|
||||||
return mod_menu.PopupMenu(
|
return mod_menu.PopupMenu(
|
||||||
items: menu
|
items: menu
|
||||||
@@ -529,7 +540,7 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
Widget _toggleTags() {
|
Widget _toggleTags() {
|
||||||
return _hoverAction(
|
return _hoverAction(
|
||||||
context: context,
|
context: context,
|
||||||
toolTip: translate('Toggle tags'),
|
toolTip: translate('Toggle Tags'),
|
||||||
hoverableWhenfalse: hideAbTagsPanel,
|
hoverableWhenfalse: hideAbTagsPanel,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.tag_rounded,
|
Icons.tag_rounded,
|
||||||
@@ -537,7 +548,8 @@ class _PeerTabPageState extends State<PeerTabPage>
|
|||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await bind.mainSetLocalOption(
|
await bind.mainSetLocalOption(
|
||||||
key: "hideAbTagsPanel", value: hideAbTagsPanel.value ? "" : "Y");
|
key: kOptionHideAbTagsPanel,
|
||||||
|
value: hideAbTagsPanel.value ? defaultOptionNo : "Y");
|
||||||
hideAbTagsPanel.value = !hideAbTagsPanel.value;
|
hideAbTagsPanel.value = !hideAbTagsPanel.value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -799,16 +811,22 @@ class _PeerViewDropdownState extends State<PeerViewDropdown> {
|
|||||||
style: style),
|
style: style),
|
||||||
e,
|
e,
|
||||||
peerCardUiType.value,
|
peerCardUiType.value,
|
||||||
dense: true, (PeerUiType? v) async {
|
dense: true,
|
||||||
if (v != null) {
|
isOptionFixed(kOptionPeerCardUiType)
|
||||||
peerCardUiType.value = v;
|
? null
|
||||||
setState(() {});
|
: (PeerUiType? v) async {
|
||||||
await bind.setLocalFlutterOption(
|
if (v != null) {
|
||||||
k: "peer-card-ui-type",
|
peerCardUiType.value = v;
|
||||||
v: peerCardUiType.value.index.toString(),
|
setState(() {});
|
||||||
);
|
await bind.setLocalFlutterOption(
|
||||||
}
|
k: kOptionPeerCardUiType,
|
||||||
}),
|
v: peerCardUiType.value.index.toString(),
|
||||||
|
);
|
||||||
|
if (Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
@@ -852,7 +870,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
|||||||
if (!PeerSortType.values.contains(peerSort.value)) {
|
if (!PeerSortType.values.contains(peerSort.value)) {
|
||||||
peerSort.value = PeerSortType.remoteId;
|
peerSort.value = PeerSortType.remoteId;
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: "peer-sorting",
|
k: kOptionPeerSorting,
|
||||||
v: peerSort.value,
|
v: peerSort.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -882,7 +900,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
|
|||||||
if (v != null) {
|
if (v != null) {
|
||||||
peerSort.value = v;
|
peerSort.value = v;
|
||||||
await bind.setLocalFlutterOption(
|
await bind.setLocalFlutterOption(
|
||||||
k: "peer-sorting",
|
k: kOptionPeerSorting,
|
||||||
v: peerSort.value,
|
v: peerSort.value,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import 'dart:collection';
|
|||||||
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
import 'package:dynamic_layouts/dynamic_layouts.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'package:flutter_hbb/models/peer_tab_model.dart';
|
|
||||||
|
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../models/peer_model.dart';
|
import '../../models/peer_model.dart';
|
||||||
@@ -45,7 +45,7 @@ class LoadEvent {
|
|||||||
final peerSearchText = "".obs;
|
final peerSearchText = "".obs;
|
||||||
|
|
||||||
/// for peer sort, global obs value
|
/// for peer sort, global obs value
|
||||||
final peerSort = bind.getLocalFlutterOption(k: 'peer-sorting').obs;
|
final peerSort = bind.getLocalFlutterOption(k: kOptionPeerSorting).obs;
|
||||||
|
|
||||||
// list for listener
|
// list for listener
|
||||||
final obslist = [peerSearchText, peerSort].obs;
|
final obslist = [peerSearchText, peerSort].obs;
|
||||||
@@ -86,17 +86,6 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
var _queryCount = 0;
|
var _queryCount = 0;
|
||||||
var _exit = false;
|
var _exit = false;
|
||||||
|
|
||||||
late final mobileWidth = () {
|
|
||||||
const minWidth = 320.0;
|
|
||||||
final windowWidth = MediaQuery.of(context).size.width;
|
|
||||||
var width = windowWidth - 2 * space;
|
|
||||||
if (windowWidth > minWidth + 2 * space) {
|
|
||||||
final n = (windowWidth / (minWidth + 2 * space)).floor();
|
|
||||||
width = windowWidth / n - 2 * space;
|
|
||||||
}
|
|
||||||
return width;
|
|
||||||
}();
|
|
||||||
|
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
|
|
||||||
_PeersViewState() {
|
_PeersViewState() {
|
||||||
@@ -189,61 +178,60 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
onVisibilityChanged: onVisibilityChanged,
|
onVisibilityChanged: onVisibilityChanged,
|
||||||
child: widget.peerCardBuilder(peer),
|
child: widget.peerCardBuilder(peer),
|
||||||
);
|
);
|
||||||
final windowWidth = MediaQuery.of(context).size.width;
|
|
||||||
// `Provider.of<PeerTabModel>(context)` will causes infinete loop.
|
// `Provider.of<PeerTabModel>(context)` will causes infinete loop.
|
||||||
// Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
|
// Because `gFFI.peerTabModel.setCurrentTabCachedPeers(peers)` will trigger `notifyListeners()`.
|
||||||
//
|
//
|
||||||
// No need to listen the currentTab change event.
|
// No need to listen the currentTab change event.
|
||||||
// Because the currentTab change event will trigger the peers change event,
|
// Because the currentTab change event will trigger the peers change event,
|
||||||
// and the peers change event will trigger _buildPeersView().
|
// and the peers change event will trigger _buildPeersView().
|
||||||
final currentTab =
|
|
||||||
Provider.of<PeerTabModel>(context, listen: false).currentTab;
|
|
||||||
final hideAbTagsPanel =
|
|
||||||
bind.mainGetLocalOption(key: "hideAbTagsPanel").isNotEmpty;
|
|
||||||
return (isDesktop || isWebDesktop)
|
return (isDesktop || isWebDesktop)
|
||||||
? Obx(
|
? Obx(() => peerCardUiType.value == PeerUiType.list
|
||||||
() => SizedBox(
|
? Container(height: 45, child: visibilityChild)
|
||||||
width: peerCardUiType.value != PeerUiType.list
|
: peerCardUiType.value == PeerUiType.grid
|
||||||
? 220
|
? SizedBox(
|
||||||
: currentTab == PeerTabIndex.group.index ||
|
width: 220, height: 140, child: visibilityChild)
|
||||||
(currentTab == PeerTabIndex.ab.index &&
|
: SizedBox(
|
||||||
!hideAbTagsPanel)
|
width: 220, height: 42, child: visibilityChild))
|
||||||
? windowWidth - 390
|
: Container(child: visibilityChild);
|
||||||
: windowWidth - 227,
|
|
||||||
height: peerCardUiType.value == PeerUiType.grid
|
|
||||||
? 140
|
|
||||||
: peerCardUiType.value != PeerUiType.list
|
|
||||||
? 42
|
|
||||||
: 45,
|
|
||||||
child: visibilityChild,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SizedBox(width: mobileWidth, child: visibilityChild);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
child = DynamicGridView.builder(
|
child = ListView.builder(
|
||||||
gridDelegate: SliverGridDelegateWithWrapping(
|
|
||||||
mainAxisSpacing: space / 2, crossAxisSpacing: space),
|
|
||||||
itemCount: peers.length,
|
itemCount: peers.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
return buildOnePeer(peers[index]);
|
return buildOnePeer(peers[index]).marginOnly(
|
||||||
|
top: index == 0 ? 0 : space / 2, bottom: space / 2);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
child = DesktopScrollWrapper(
|
child = Obx(() => peerCardUiType.value == PeerUiType.list
|
||||||
scrollController: _scrollController,
|
? DesktopScrollWrapper(
|
||||||
child: DynamicGridView.builder(
|
scrollController: _scrollController,
|
||||||
controller: _scrollController,
|
child: ListView.builder(
|
||||||
physics: DraggableNeverScrollableScrollPhysics(),
|
controller: _scrollController,
|
||||||
gridDelegate: SliverGridDelegateWithWrapping(
|
physics: DraggableNeverScrollableScrollPhysics(),
|
||||||
mainAxisSpacing: space / 2, crossAxisSpacing: space),
|
itemCount: peers.length,
|
||||||
itemCount: peers.length,
|
itemBuilder: (BuildContext context, int index) {
|
||||||
itemBuilder: (BuildContext context, int index) {
|
return buildOnePeer(peers[index]).marginOnly(
|
||||||
return buildOnePeer(peers[index]);
|
right: space,
|
||||||
}),
|
top: index == 0 ? 0 : space / 2,
|
||||||
);
|
bottom: space / 2);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: DesktopScrollWrapper(
|
||||||
|
scrollController: _scrollController,
|
||||||
|
child: DynamicGridView.builder(
|
||||||
|
controller: _scrollController,
|
||||||
|
physics: DraggableNeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: SliverGridDelegateWithWrapping(
|
||||||
|
mainAxisSpacing: space / 2,
|
||||||
|
crossAxisSpacing: space),
|
||||||
|
itemCount: peers.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return buildOnePeer(peers[index]);
|
||||||
|
}),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateEvent == UpdateEvent.load) {
|
if (updateEvent == UpdateEvent.load) {
|
||||||
@@ -314,7 +302,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
|
|||||||
if (!PeerSortType.values.contains(sortedBy)) {
|
if (!PeerSortType.values.contains(sortedBy)) {
|
||||||
sortedBy = PeerSortType.remoteId;
|
sortedBy = PeerSortType.remoteId;
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: "peer-sorting",
|
k: kOptionPeerSorting,
|
||||||
v: sortedBy,
|
v: sortedBy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!handleTouch) {
|
if (!handleTouch) {
|
||||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,6 +222,9 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (handleTouch) {
|
if (handleTouch) {
|
||||||
|
if (isDesktop) {
|
||||||
|
ffi.cursorModel.trySetRemoteWindowCoords();
|
||||||
|
}
|
||||||
inputModel.sendMouse('down', MouseButtons.left);
|
inputModel.sendMouse('down', MouseButtons.left);
|
||||||
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
|
||||||
} else {
|
} else {
|
||||||
@@ -241,13 +244,16 @@ class _RawTouchGestureDetectorRegionState
|
|||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
|
ffi.cursorModel.updatePan(d.delta, d.localPosition, handleTouch);
|
||||||
}
|
}
|
||||||
|
|
||||||
onOneFingerPanEnd(DragEndDetails d) {
|
onOneFingerPanEnd(DragEndDetails d) {
|
||||||
if (lastDeviceKind != PointerDeviceKind.touch) {
|
if (lastDeviceKind != PointerDeviceKind.touch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isDesktop) {
|
||||||
|
ffi.cursorModel.clearRemoteWindowCoords();
|
||||||
|
}
|
||||||
inputModel.sendMouse('up', MouseButtons.left);
|
inputModel.sendMouse('up', MouseButtons.left);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,14 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
customImageQualityWidget(
|
customImageQualityWidget(
|
||||||
{required double initQuality,
|
{required double initQuality,
|
||||||
required double initFps,
|
required double initFps,
|
||||||
required Function(double) setQuality,
|
required Function(double)? setQuality,
|
||||||
required Function(double) setFps,
|
required Function(double)? setFps,
|
||||||
required bool showFps,
|
required bool showFps,
|
||||||
required bool showMoreQuality}) {
|
required bool showMoreQuality}) {
|
||||||
if (initQuality < kMinQuality ||
|
if (initQuality < kMinQuality ||
|
||||||
@@ -27,16 +26,12 @@ customImageQualityWidget(
|
|||||||
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
|
final RxBool moreQualityChecked = RxBool(qualityValue.value > kMaxQuality);
|
||||||
final debouncerQuality = Debouncer<double>(
|
final debouncerQuality = Debouncer<double>(
|
||||||
Duration(milliseconds: 1000),
|
Duration(milliseconds: 1000),
|
||||||
onChanged: (double v) {
|
onChanged: setQuality,
|
||||||
setQuality(v);
|
|
||||||
},
|
|
||||||
initialValue: qualityValue.value,
|
initialValue: qualityValue.value,
|
||||||
);
|
);
|
||||||
final debouncerFps = Debouncer<double>(
|
final debouncerFps = Debouncer<double>(
|
||||||
Duration(milliseconds: 1000),
|
Duration(milliseconds: 1000),
|
||||||
onChanged: (double v) {
|
onChanged: setFps,
|
||||||
setFps(v);
|
|
||||||
},
|
|
||||||
initialValue: fpsValue.value,
|
initialValue: fpsValue.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -62,10 +57,12 @@ customImageQualityWidget(
|
|||||||
divisions: moreQualityChecked.value
|
divisions: moreQualityChecked.value
|
||||||
? ((kMaxMoreQuality - kMinQuality) / 10).round()
|
? ((kMaxMoreQuality - kMinQuality) / 10).round()
|
||||||
: ((kMaxQuality - kMinQuality) / 5).round(),
|
: ((kMaxQuality - kMinQuality) / 5).round(),
|
||||||
onChanged: (double value) async {
|
onChanged: setQuality == null
|
||||||
qualityValue.value = value;
|
? null
|
||||||
debouncerQuality.value = value;
|
: (double value) async {
|
||||||
},
|
qualityValue.value = value;
|
||||||
|
debouncerQuality.value = value;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -124,10 +121,12 @@ customImageQualityWidget(
|
|||||||
min: kMinFps,
|
min: kMinFps,
|
||||||
max: kMaxFps,
|
max: kMaxFps,
|
||||||
divisions: ((kMaxFps - kMinFps) / 5).round(),
|
divisions: ((kMaxFps - kMinFps) / 5).round(),
|
||||||
onChanged: (double value) async {
|
onChanged: setFps == null
|
||||||
fpsValue.value = value;
|
? null
|
||||||
debouncerFps.value = value;
|
: (double value) async {
|
||||||
},
|
fpsValue.value = value;
|
||||||
|
debouncerFps.value = value;
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -152,21 +151,29 @@ customImageQualitySetting() {
|
|||||||
final qualityKey = 'custom_image_quality';
|
final qualityKey = 'custom_image_quality';
|
||||||
final fpsKey = 'custom-fps';
|
final fpsKey = 'custom-fps';
|
||||||
|
|
||||||
var initQuality =
|
final initQuality =
|
||||||
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
(double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
|
||||||
kDefaultQuality);
|
kDefaultQuality);
|
||||||
var initFps = (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
final isQuanlityFixed = isOptionFixed(qualityKey);
|
||||||
kDefaultFps);
|
final initFps =
|
||||||
|
(double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ??
|
||||||
|
kDefaultFps);
|
||||||
|
final isFpsFixed = isOptionFixed(fpsKey);
|
||||||
|
|
||||||
return customImageQualityWidget(
|
return customImageQualityWidget(
|
||||||
initQuality: initQuality,
|
initQuality: initQuality,
|
||||||
initFps: initFps,
|
initFps: initFps,
|
||||||
setQuality: (v) {
|
setQuality: isQuanlityFixed
|
||||||
bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
|
? null
|
||||||
},
|
: (v) {
|
||||||
setFps: (v) {
|
bind.mainSetUserDefaultOption(
|
||||||
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
key: qualityKey, value: v.toString());
|
||||||
},
|
},
|
||||||
|
setFps: isFpsFixed
|
||||||
|
? null
|
||||||
|
: (v) {
|
||||||
|
bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
|
||||||
|
},
|
||||||
showFps: true,
|
showFps: true,
|
||||||
showMoreQuality: true);
|
showMoreQuality: true);
|
||||||
}
|
}
|
||||||
@@ -208,27 +215,31 @@ List<Widget> ServerConfigImportExportWidgets(
|
|||||||
|
|
||||||
List<(String, String)> otherDefaultSettings() {
|
List<(String, String)> otherDefaultSettings() {
|
||||||
List<(String, String)> v = [
|
List<(String, String)> v = [
|
||||||
('View Mode', 'view_only'),
|
('View Mode', kOptionViewOnly),
|
||||||
if ((isDesktop || isWebDesktop)) ('show_monitors_tip', kKeyShowMonitorsToolbar),
|
if ((isDesktop || isWebDesktop))
|
||||||
if ((isDesktop || isWebDesktop)) ('Collapse toolbar', 'collapse_toolbar'),
|
('show_monitors_tip', kKeyShowMonitorsToolbar),
|
||||||
('Show remote cursor', 'show_remote_cursor'),
|
if ((isDesktop || isWebDesktop))
|
||||||
if ((isDesktop || isWebDesktop)) ('Zoom cursor', 'zoom-cursor'),
|
('Collapse toolbar', kOptionCollapseToolbar),
|
||||||
('Show quality monitor', 'show_quality_monitor'),
|
('Show remote cursor', kOptionShowRemoteCursor),
|
||||||
('Mute', 'disable_audio'),
|
('Follow remote cursor', kOptionFollowRemoteCursor),
|
||||||
if (isDesktop) ('Enable file copy and paste', 'enable_file_transfer'),
|
('Follow remote window focus', kOptionFollowRemoteWindow),
|
||||||
('Disable clipboard', 'disable_clipboard'),
|
if ((isDesktop || isWebDesktop)) ('Zoom cursor', kOptionZoomCursor),
|
||||||
('Lock after session end', 'lock_after_session_end'),
|
('Show quality monitor', kOptionShowQualityMonitor),
|
||||||
('Privacy mode', 'privacy_mode'),
|
('Mute', kOptionDisableAudio),
|
||||||
if (isMobile) ('Touch mode', 'touch-mode'),
|
if (isDesktop) ('Enable file copy and paste', kOptionEnableFileCopyPaste),
|
||||||
('True color (4:4:4)', 'i444'),
|
('Disable clipboard', kOptionDisableClipboard),
|
||||||
|
('Lock after session end', kOptionLockAfterSessionEnd),
|
||||||
|
('Privacy mode', kOptionPrivacyMode),
|
||||||
|
if (isMobile) ('Touch mode', kOptionTouchMode),
|
||||||
|
('True color (4:4:4)', kOptionI444),
|
||||||
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
('Reverse mouse wheel', kKeyReverseMouseWheel),
|
||||||
('swap-left-right-mouse', 'swap-left-right-mouse'),
|
('swap-left-right-mouse', kOptionSwapLeftRightMouse),
|
||||||
if (isDesktop && useTextureRender)
|
if (isDesktop && bind.mainGetUseTextureRender())
|
||||||
(
|
(
|
||||||
'Show displays as individual windows',
|
'Show displays as individual windows',
|
||||||
kKeyShowDisplaysAsIndividualWindows
|
kKeyShowDisplaysAsIndividualWindows
|
||||||
),
|
),
|
||||||
if (isDesktop && useTextureRender)
|
if (isDesktop && bind.mainGetUseTextureRender())
|
||||||
(
|
(
|
||||||
'Use all my displays for the remote session',
|
'Use all my displays for the remote session',
|
||||||
kKeyUseAllMyDisplaysForTheRemoteSession
|
kKeyUseAllMyDisplaysForTheRemoteSession
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:flutter_hbb/common/widgets/dialog.dart';
|
|||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
bool isEditOsPassword = false;
|
bool isEditOsPassword = false;
|
||||||
@@ -116,7 +115,10 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// paste
|
// paste
|
||||||
if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
|
if (isMobile &&
|
||||||
|
pi.platform != kPeerPlatformAndroid &&
|
||||||
|
perms['keyboard'] != false &&
|
||||||
|
perms['clipboard'] != false) {
|
||||||
v.add(TTextMenu(
|
v.add(TTextMenu(
|
||||||
child: Text(translate('Paste')),
|
child: Text(translate('Paste')),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
@@ -327,7 +329,7 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
final alternativeCodecs =
|
final alternativeCodecs =
|
||||||
await bind.sessionAlternativeCodecs(sessionId: sessionId);
|
await bind.sessionAlternativeCodecs(sessionId: sessionId);
|
||||||
final groupValue = await bind.sessionGetOption(
|
final groupValue = await bind.sessionGetOption(
|
||||||
sessionId: sessionId, arg: 'codec-preference') ??
|
sessionId: sessionId, arg: kOptionCodecPreference) ??
|
||||||
'';
|
'';
|
||||||
final List<bool> codecs = [];
|
final List<bool> codecs = [];
|
||||||
try {
|
try {
|
||||||
@@ -349,20 +351,25 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
onChanged(String? value) async {
|
onChanged(String? value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionPeerOption(
|
await bind.sessionPeerOption(
|
||||||
sessionId: sessionId, name: 'codec-preference', value: value);
|
sessionId: sessionId, name: kOptionCodecPreference, value: value);
|
||||||
bind.sessionChangePreferCodec(sessionId: sessionId);
|
bind.sessionChangePreferCodec(sessionId: sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
TRadioMenu<String> radio(String label, String value, bool enabled) {
|
TRadioMenu<String> radio(String label, String value, bool enabled) {
|
||||||
return TRadioMenu<String>(
|
return TRadioMenu<String>(
|
||||||
child: Text(translate(label)),
|
child: Text(label),
|
||||||
value: value,
|
value: value,
|
||||||
groupValue: groupValue,
|
groupValue: groupValue,
|
||||||
onChanged: enabled ? onChanged : null);
|
onChanged: enabled ? onChanged : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var autoLabel = translate('Auto');
|
||||||
|
if (groupValue == 'auto' &&
|
||||||
|
ffi.qualityMonitorModel.data.codecFormat != null) {
|
||||||
|
autoLabel = '$autoLabel (${ffi.qualityMonitorModel.data.codecFormat})';
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
radio('Auto', 'auto', true),
|
radio(autoLabel, 'auto', true),
|
||||||
if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
|
if (codecs[0]) radio('VP8', 'vp8', codecs[0]),
|
||||||
radio('VP9', 'vp9', true),
|
radio('VP9', 'vp9', true),
|
||||||
if (codecs[1]) radio('AV1', 'av1', codecs[1]),
|
if (codecs[1]) radio('AV1', 'av1', codecs[1]),
|
||||||
@@ -371,12 +378,11 @@ Future<List<TRadioMenu<String>>> toolbarCodec(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
Future<List<TToggleMenu>> toolbarCursor(
|
||||||
BuildContext context, String id, FFI ffi) async {
|
BuildContext context, String id, FFI ffi) async {
|
||||||
List<TToggleMenu> v = [];
|
List<TToggleMenu> v = [];
|
||||||
final ffiModel = ffi.ffiModel;
|
final ffiModel = ffi.ffiModel;
|
||||||
final pi = ffiModel.pi;
|
final pi = ffiModel.pi;
|
||||||
final perms = ffiModel.permissions;
|
|
||||||
final sessionId = ffi.sessionId;
|
final sessionId = ffi.sessionId;
|
||||||
|
|
||||||
// show remote cursor
|
// show remote cursor
|
||||||
@@ -384,12 +390,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
!ffi.canvasModel.cursorEmbedded &&
|
!ffi.canvasModel.cursorEmbedded &&
|
||||||
!pi.isWayland) {
|
!pi.isWayland) {
|
||||||
final state = ShowRemoteCursorState.find(id);
|
final state = ShowRemoteCursorState.find(id);
|
||||||
|
final lockState = ShowRemoteCursorLockState.find(id);
|
||||||
final enabled = !ffiModel.viewOnly;
|
final enabled = !ffiModel.viewOnly;
|
||||||
final option = 'show-remote-cursor';
|
final option = 'show-remote-cursor';
|
||||||
|
if (pi.currentDisplay == kAllDisplayValue ||
|
||||||
|
bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||||
|
lockState.value = false;
|
||||||
|
}
|
||||||
v.add(TToggleMenu(
|
v.add(TToggleMenu(
|
||||||
child: Text(translate('Show remote cursor')),
|
child: Text(translate('Show remote cursor')),
|
||||||
value: state.value,
|
value: state.value,
|
||||||
onChanged: enabled
|
onChanged: enabled && !lockState.value
|
||||||
? (value) async {
|
? (value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionToggleOption(
|
await bind.sessionToggleOption(
|
||||||
@@ -399,6 +410,67 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
}
|
}
|
||||||
: null));
|
: null));
|
||||||
}
|
}
|
||||||
|
// follow remote cursor
|
||||||
|
if (pi.platform != kPeerPlatformAndroid &&
|
||||||
|
!ffi.canvasModel.cursorEmbedded &&
|
||||||
|
!pi.isWayland &&
|
||||||
|
versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||||
|
pi.displays.length > 1 &&
|
||||||
|
pi.currentDisplay != kAllDisplayValue &&
|
||||||
|
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||||
|
final option = 'follow-remote-cursor';
|
||||||
|
final value =
|
||||||
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||||
|
final showCursorOption = 'show-remote-cursor';
|
||||||
|
final showCursorState = ShowRemoteCursorState.find(id);
|
||||||
|
final showCursorLockState = ShowRemoteCursorLockState.find(id);
|
||||||
|
final showCursorEnabled = bind.sessionGetToggleOptionSync(
|
||||||
|
sessionId: sessionId, arg: showCursorOption);
|
||||||
|
showCursorLockState.value = value;
|
||||||
|
if (value && !showCursorEnabled) {
|
||||||
|
await bind.sessionToggleOption(
|
||||||
|
sessionId: sessionId, value: showCursorOption);
|
||||||
|
showCursorState.value = bind.sessionGetToggleOptionSync(
|
||||||
|
sessionId: sessionId, arg: showCursorOption);
|
||||||
|
}
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
child: Text(translate('Follow remote cursor')),
|
||||||
|
value: value,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||||
|
value = bind.sessionGetToggleOptionSync(
|
||||||
|
sessionId: sessionId, arg: option);
|
||||||
|
showCursorLockState.value = value;
|
||||||
|
if (!showCursorEnabled) {
|
||||||
|
await bind.sessionToggleOption(
|
||||||
|
sessionId: sessionId, value: showCursorOption);
|
||||||
|
showCursorState.value = bind.sessionGetToggleOptionSync(
|
||||||
|
sessionId: sessionId, arg: showCursorOption);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
// follow remote window focus
|
||||||
|
if (pi.platform != kPeerPlatformAndroid &&
|
||||||
|
!ffi.canvasModel.cursorEmbedded &&
|
||||||
|
!pi.isWayland &&
|
||||||
|
versionCmp(pi.version, "1.2.4") >= 0 &&
|
||||||
|
pi.displays.length > 1 &&
|
||||||
|
pi.currentDisplay != kAllDisplayValue &&
|
||||||
|
!bind.sessionIsMultiUiSession(sessionId: sessionId)) {
|
||||||
|
final option = 'follow-remote-window';
|
||||||
|
final value =
|
||||||
|
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
||||||
|
v.add(TToggleMenu(
|
||||||
|
child: Text(translate('Follow remote window focus')),
|
||||||
|
value: value,
|
||||||
|
onChanged: (value) async {
|
||||||
|
if (value == null) return;
|
||||||
|
await bind.sessionToggleOption(sessionId: sessionId, value: option);
|
||||||
|
value = bind.sessionGetToggleOptionSync(
|
||||||
|
sessionId: sessionId, arg: option);
|
||||||
|
}));
|
||||||
|
}
|
||||||
// zoom cursor
|
// zoom cursor
|
||||||
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
|
final viewStyle = await bind.sessionGetViewStyle(sessionId: sessionId) ?? '';
|
||||||
if (!isMobile &&
|
if (!isMobile &&
|
||||||
@@ -417,6 +489,17 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<TToggleMenu>> toolbarDisplayToggle(
|
||||||
|
BuildContext context, String id, FFI ffi) async {
|
||||||
|
List<TToggleMenu> v = [];
|
||||||
|
final ffiModel = ffi.ffiModel;
|
||||||
|
final pi = ffiModel.pi;
|
||||||
|
final perms = ffiModel.permissions;
|
||||||
|
final sessionId = ffi.sessionId;
|
||||||
|
|
||||||
// show quality monitor
|
// show quality monitor
|
||||||
final option = 'show-quality-monitor';
|
final option = 'show-quality-monitor';
|
||||||
v.add(TToggleMenu(
|
v.add(TToggleMenu(
|
||||||
@@ -441,20 +524,27 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Mute'))));
|
child: Text(translate('Mute'))));
|
||||||
}
|
}
|
||||||
// file copy and paste
|
// file copy and paste
|
||||||
|
// If the version is less than 1.2.4, file copy and paste is supported on Windows only.
|
||||||
|
final isSupportIfPeer_1_2_3 = versionCmp(pi.version, '1.2.4') < 0 &&
|
||||||
|
isWindows &&
|
||||||
|
pi.platform == kPeerPlatformWindows;
|
||||||
|
// If the version is 1.2.4 or later, file copy and paste is supported when kPlatformAdditionsHasFileClipboard is set.
|
||||||
|
final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 &&
|
||||||
|
bind.mainHasFileClipboard() &&
|
||||||
|
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard);
|
||||||
if (ffiModel.keyboard &&
|
if (ffiModel.keyboard &&
|
||||||
perms['file'] != false &&
|
perms['file'] != false &&
|
||||||
bind.mainHasFileClipboard() &&
|
(isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) {
|
||||||
pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard)) {
|
|
||||||
final enabled = !ffiModel.viewOnly;
|
final enabled = !ffiModel.viewOnly;
|
||||||
final option = 'enable-file-transfer';
|
final value = bind.sessionGetToggleOptionSync(
|
||||||
final value =
|
sessionId: sessionId, arg: kOptionEnableFileCopyPaste);
|
||||||
bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option);
|
|
||||||
v.add(TToggleMenu(
|
v.add(TToggleMenu(
|
||||||
value: value,
|
value: value,
|
||||||
onChanged: enabled
|
onChanged: enabled
|
||||||
? (value) {
|
? (value) {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
bind.sessionToggleOption(sessionId: sessionId, value: option);
|
bind.sessionToggleOption(
|
||||||
|
sessionId: sessionId, value: kOptionEnableFileCopyPaste);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: Text(translate('Enable file copy and paste'))));
|
child: Text(translate('Enable file copy and paste'))));
|
||||||
@@ -477,7 +567,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Disable clipboard'))));
|
child: Text(translate('Disable clipboard'))));
|
||||||
}
|
}
|
||||||
// lock after session end
|
// lock after session end
|
||||||
if (ffiModel.keyboard) {
|
if (ffiModel.keyboard && !ffiModel.isPeerAndroid) {
|
||||||
final enabled = !ffiModel.viewOnly;
|
final enabled = !ffiModel.viewOnly;
|
||||||
final option = 'lock-after-session-end';
|
final option = 'lock-after-session-end';
|
||||||
final value =
|
final value =
|
||||||
@@ -493,7 +583,7 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
child: Text(translate('Lock after session end'))));
|
child: Text(translate('Lock after session end'))));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useTextureRender &&
|
if (bind.mainGetUseTextureRender() &&
|
||||||
pi.isSupportMultiDisplay &&
|
pi.isSupportMultiDisplay &&
|
||||||
PrivacyModeState.find(id).isEmpty &&
|
PrivacyModeState.find(id).isEmpty &&
|
||||||
pi.displaysCount.value > 1 &&
|
pi.displaysCount.value > 1 &&
|
||||||
@@ -512,7 +602,9 @@ Future<List<TToggleMenu>> toolbarDisplayToggle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
final isMultiScreens = !isWeb && (await getScreenRectList()).length > 1;
|
||||||
if (useTextureRender && pi.isSupportMultiDisplay && isMultiScreens) {
|
if (bind.mainGetUseTextureRender() &&
|
||||||
|
pi.isSupportMultiDisplay &&
|
||||||
|
isMultiScreens) {
|
||||||
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
final value = bind.sessionGetUseAllMyDisplaysForTheRemoteSession(
|
||||||
sessionId: ffi.sessionId) ==
|
sessionId: ffi.sessionId) ==
|
||||||
'Y';
|
'Y';
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ const kKeyTranslateMode = 'translate';
|
|||||||
const String kPlatformAdditionsIsWayland = "is_wayland";
|
const String kPlatformAdditionsIsWayland = "is_wayland";
|
||||||
const String kPlatformAdditionsHeadless = "headless";
|
const String kPlatformAdditionsHeadless = "headless";
|
||||||
const String kPlatformAdditionsIsInstalled = "is_installed";
|
const String kPlatformAdditionsIsInstalled = "is_installed";
|
||||||
const String kPlatformAdditionsVirtualDisplays = "virtual_displays";
|
const String kPlatformAdditionsIddImpl = "idd_impl";
|
||||||
|
const String kPlatformAdditionsRustDeskVirtualDisplays =
|
||||||
|
"rustdesk_virtual_displays";
|
||||||
|
const String kPlatformAdditionsAmyuniVirtualDisplays =
|
||||||
|
"amyuni_virtual_displays";
|
||||||
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
const String kPlatformAdditionsHasFileClipboard = "has_file_clipboard";
|
||||||
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
const String kPlatformAdditionsSupportedPrivacyModeImpl =
|
||||||
"supported_privacy_mode_impl";
|
"supported_privacy_mode_impl";
|
||||||
@@ -58,16 +62,79 @@ const String kWindowEventActiveSession = "active_session";
|
|||||||
const String kWindowEventActiveDisplaySession = "active_display_session";
|
const String kWindowEventActiveDisplaySession = "active_display_session";
|
||||||
const String kWindowEventGetRemoteList = "get_remote_list";
|
const String kWindowEventGetRemoteList = "get_remote_list";
|
||||||
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
const String kWindowEventGetSessionIdList = "get_session_id_list";
|
||||||
|
const String kWindowEventRemoteWindowCoords = "remote_window_coords";
|
||||||
|
const String kWindowEventSetFullscreen = "set_fullscreen";
|
||||||
|
|
||||||
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
|
||||||
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
const String kWindowEventGetCachedSessionData = "get_cached_session_data";
|
||||||
const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
const String kWindowEventOpenMonitorSession = "open_monitor_session";
|
||||||
|
|
||||||
|
const String kOptionViewStyle = "view_style";
|
||||||
|
const String kOptionScrollStyle = "scroll_style";
|
||||||
|
const String kOptionImageQuality = "image_quality";
|
||||||
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
|
||||||
|
const String kOptionTextureRender = "use-texture-render";
|
||||||
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
const String kOptionOpenInTabs = "allow-open-in-tabs";
|
||||||
const String kOptionOpenInWindows = "allow-open-in-windows";
|
const String kOptionOpenInWindows = "allow-open-in-windows";
|
||||||
const String kOptionForceAlwaysRelay = "force-always-relay";
|
const String kOptionForceAlwaysRelay = "force-always-relay";
|
||||||
const String kOptionViewOnly = "view-only";
|
const String kOptionViewOnly = "view_only";
|
||||||
|
const String kOptionEnableLanDiscovery = "enable-lan-discovery";
|
||||||
|
const String kOptionWhitelist = "whitelist";
|
||||||
|
const String kOptionEnableAbr = "enable-abr";
|
||||||
|
const String kOptionEnableRecordSession = "enable-record-session";
|
||||||
|
const String kOptionDirectServer = "direct-server";
|
||||||
|
const String kOptionDirectAccessPort = "direct-access-port";
|
||||||
|
const String kOptionAllowAutoDisconnect = "allow-auto-disconnect";
|
||||||
|
const String kOptionAutoDisconnectTimeout = "auto-disconnect-timeout";
|
||||||
|
const String kOptionEnableHwcodec = "enable-hwcodec";
|
||||||
|
const String kOptionAllowAutoRecordIncoming = "allow-auto-record-incoming";
|
||||||
|
const String kOptionVideoSaveDirectory = "video-save-directory";
|
||||||
|
const String kOptionAccessMode = "access-mode";
|
||||||
|
const String kOptionEnableKeyboard = "enable-keyboard";
|
||||||
|
// "Settings -> Security -> Permissions"
|
||||||
|
const String kOptionEnableClipboard = "enable-clipboard";
|
||||||
|
const String kOptionEnableFileTransfer = "enable-file-transfer";
|
||||||
|
const String kOptionEnableAudio = "enable-audio";
|
||||||
|
const String kOptionEnableTunnel = "enable-tunnel";
|
||||||
|
const String kOptionEnableRemoteRestart = "enable-remote-restart";
|
||||||
|
const String kOptionEnableBlockInput = "enable-block-input";
|
||||||
|
const String kOptionAllowRemoteConfigModification =
|
||||||
|
"allow-remote-config-modification";
|
||||||
|
const String kOptionVerificationMethod = "verification-method";
|
||||||
|
const String kOptionApproveMode = "approve-mode";
|
||||||
|
const String kOptionCollapseToolbar = "collapse_toolbar";
|
||||||
|
const String kOptionShowRemoteCursor = "show_remote_cursor";
|
||||||
|
const String kOptionFollowRemoteCursor = "follow_remote_cursor";
|
||||||
|
const String kOptionFollowRemoteWindow = "follow_remote_window";
|
||||||
|
const String kOptionZoomCursor = "zoom-cursor";
|
||||||
|
const String kOptionShowQualityMonitor = "show_quality_monitor";
|
||||||
|
const String kOptionDisableAudio = "disable_audio";
|
||||||
|
const String kOptionEnableFileCopyPaste = "enable-file-copy-paste";
|
||||||
|
// "Settings -> Display -> Other default options"
|
||||||
|
const String kOptionDisableClipboard = "disable_clipboard";
|
||||||
|
const String kOptionLockAfterSessionEnd = "lock_after_session_end";
|
||||||
|
const String kOptionPrivacyMode = "privacy_mode";
|
||||||
|
const String kOptionTouchMode = "touch-mode";
|
||||||
|
const String kOptionI444 = "i444";
|
||||||
|
const String kOptionSwapLeftRightMouse = "swap-left-right-mouse";
|
||||||
|
const String kOptionCodecPreference = "codec-preference";
|
||||||
|
const String kOptionRemoteMenubarDragLeft = "remote-menubar-drag-left";
|
||||||
|
const String kOptionRemoteMenubarDragRight = "remote-menubar-drag-right";
|
||||||
|
const String kOptionHideAbTagsPanel = "hideAbTagsPanel";
|
||||||
|
const String kOptionRemoteMenubarState = "remoteMenubarState";
|
||||||
|
const String kOptionPeerSorting = "peer-sorting";
|
||||||
|
const String kOptionPeerTabIndex = "peer-tab-index";
|
||||||
|
const String kOptionPeerTabOrder = "peer-tab-order";
|
||||||
|
const String kOptionPeerTabVisible = "peer-tab-visible";
|
||||||
|
const String kOptionPeerCardUiType = "peer-card-ui-type";
|
||||||
|
const String kOptionCurrentAbName = "current-ab-name";
|
||||||
|
const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs";
|
||||||
|
const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render";
|
||||||
|
const String kOptionEnableCheckUpdate = "enable-check-update";
|
||||||
|
const String kOptionAllowLinuxHeadless = "allow-linux-headless";
|
||||||
|
const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper";
|
||||||
|
|
||||||
|
const String kOptionToggleViewOnly = "view-only";
|
||||||
|
|
||||||
const String kUrlActionClose = "close";
|
const String kUrlActionClose = "close";
|
||||||
|
|
||||||
@@ -87,10 +154,13 @@ const String kKeyUseAllMyDisplaysForTheRemoteSession =
|
|||||||
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
const String kKeyShowMonitorsToolbar = 'show_monitors_toolbar';
|
||||||
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
|
const String kKeyReverseMouseWheel = "reverse_mouse_wheel";
|
||||||
|
|
||||||
|
const String kMsgboxTextWaitingForImage = 'Connected, waiting for image...';
|
||||||
|
|
||||||
// the executable name of the portable version
|
// the executable name of the portable version
|
||||||
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
|
||||||
|
|
||||||
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
|
const Color kColorWarn = Color.fromARGB(255, 245, 133, 59);
|
||||||
|
const Color kColorCanvas = Colors.black;
|
||||||
|
|
||||||
const int kMobileDefaultDisplayWidth = 720;
|
const int kMobileDefaultDisplayWidth = 720;
|
||||||
const int kMobileDefaultDisplayHeight = 1280;
|
const int kMobileDefaultDisplayHeight = 1280;
|
||||||
@@ -121,12 +191,11 @@ double kNewWindowOffset = isWindows
|
|||||||
? 30.0
|
? 30.0
|
||||||
: 50.0;
|
: 50.0;
|
||||||
|
|
||||||
EdgeInsets get kDragToResizeAreaPadding =>
|
EdgeInsets get kDragToResizeAreaPadding => !kUseCompatibleUiMode && isLinux
|
||||||
!kUseCompatibleUiMode && isLinux
|
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
||||||
? stateGlobal.fullscreen.isTrue || stateGlobal.isMaximized.value
|
? EdgeInsets.zero
|
||||||
? EdgeInsets.zero
|
: EdgeInsets.all(5.0)
|
||||||
: EdgeInsets.all(5.0)
|
: EdgeInsets.zero;
|
||||||
: EdgeInsets.zero;
|
|
||||||
// https://en.wikipedia.org/wiki/Non-breaking_space
|
// https://en.wikipedia.org/wiki/Non-breaking_space
|
||||||
const int $nbsp = 0x00A0;
|
const int $nbsp = 0x00A0;
|
||||||
|
|
||||||
@@ -150,9 +219,15 @@ const kDefaultScrollDuration = Duration(milliseconds: 50);
|
|||||||
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
|
||||||
const kFullScreenEdgeSize = 0.0;
|
const kFullScreenEdgeSize = 0.0;
|
||||||
const kMaximizeEdgeSize = 0.0;
|
const kMaximizeEdgeSize = 0.0;
|
||||||
var kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
// Do not use kWindowEdgeSize directly. Use `windowEdgeSize` in `common.dart` instead.
|
||||||
const kWindowBorderWidth = 1.0;
|
final kWindowEdgeSize = isWindows ? 1.0 : 5.0;
|
||||||
|
final kWindowBorderWidth = 1.0;
|
||||||
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
|
||||||
|
const kFrameBorderRadius = 12.0;
|
||||||
|
const kFrameClipRRectBorderRadius = 12.0;
|
||||||
|
const kFrameBoxShadowBlurRadius = 32.0;
|
||||||
|
const kFrameBoxShadowOffsetFocused = 4.0;
|
||||||
|
const kFrameBoxShadowOffsetUnfocused = 2.0;
|
||||||
|
|
||||||
const kInvalidValueStr = 'InvalidValueStr';
|
const kInvalidValueStr = 'InvalidValueStr';
|
||||||
|
|
||||||
@@ -198,12 +273,6 @@ const kRemoteImageQualityLow = 'low';
|
|||||||
/// [kRemoteImageQualityCustom] Custom image quality.
|
/// [kRemoteImageQualityCustom] Custom image quality.
|
||||||
const kRemoteImageQualityCustom = 'custom';
|
const kRemoteImageQualityCustom = 'custom';
|
||||||
|
|
||||||
/// [kRemoteAudioGuestToHost] Guest to host audio mode(default).
|
|
||||||
const kRemoteAudioGuestToHost = 'guest-to-host';
|
|
||||||
|
|
||||||
/// [kRemoteAudioDualWay] dual-way audio mode(default).
|
|
||||||
const kRemoteAudioDualWay = 'dual-way';
|
|
||||||
|
|
||||||
const kIgnoreDpi = true;
|
const kIgnoreDpi = true;
|
||||||
|
|
||||||
// ================================ mobile ================================
|
// ================================ mobile ================================
|
||||||
@@ -222,6 +291,7 @@ const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
|
|||||||
const kRequestIgnoreBatteryOptimizations =
|
const kRequestIgnoreBatteryOptimizations =
|
||||||
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
|
||||||
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
|
||||||
|
const kAndroid13Notification = "android.permission.POST_NOTIFICATIONS";
|
||||||
|
|
||||||
/// Android channel invoke type key
|
/// Android channel invoke type key
|
||||||
class AndroidChannel {
|
class AndroidChannel {
|
||||||
|
|||||||
@@ -262,7 +262,7 @@ class _ConnectionPageState extends State<ConnectionPage>
|
|||||||
void onWindowLeaveFullScreen() {
|
void onWindowLeaveFullScreen() {
|
||||||
// Restore edge border to default edge size.
|
// Restore edge border to default edge size.
|
||||||
stateGlobal.resizeEdgeSize.value =
|
stateGlobal.resizeEdgeSize.value =
|
||||||
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
|
stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : windowEdgeSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -69,44 +69,6 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildPresetPasswordWarning() {
|
|
||||||
return FutureBuilder<bool>(
|
|
||||||
future: bind.isPresetPassword(),
|
|
||||||
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
|
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
||||||
return CircularProgressIndicator(); // Show a loading spinner while waiting for the Future to complete
|
|
||||||
} else if (snapshot.hasError) {
|
|
||||||
return Text(
|
|
||||||
'Error: ${snapshot.error}'); // Show an error message if the Future completed with an error
|
|
||||||
} else if (snapshot.hasData && snapshot.data == true) {
|
|
||||||
return Container(
|
|
||||||
color: Colors.yellow,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
child: Text(
|
|
||||||
translate("Security Alert"),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.red,
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
)).paddingOnly(bottom: 8),
|
|
||||||
Text(
|
|
||||||
translate("preset_password_warning"),
|
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
).paddingAll(8),
|
|
||||||
); // Show a warning message if the Future completed with true
|
|
||||||
} else {
|
|
||||||
return SizedBox
|
|
||||||
.shrink(); // Show nothing if the Future completed with false or null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildLeftPane(BuildContext context) {
|
Widget buildLeftPane(BuildContext context) {
|
||||||
final isIncomingOnly = bind.isIncomingOnly();
|
final isIncomingOnly = bind.isIncomingOnly();
|
||||||
final isOutgoingOnly = bind.isOutgoingOnly();
|
final isOutgoingOnly = bind.isOutgoingOnly();
|
||||||
@@ -115,22 +77,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
if (bind.isCustomClient())
|
if (bind.isCustomClient())
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: MouseRegion(
|
child: loadPowered(context),
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
launchUrl(Uri.parse('https://rustdesk.com'));
|
|
||||||
},
|
|
||||||
child: Opacity(
|
|
||||||
opacity: 0.5,
|
|
||||||
child: Text(
|
|
||||||
translate("powered_by_me"),
|
|
||||||
overflow: TextOverflow.clip,
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
||||||
fontSize: 9, decoration: TextDecoration.underline),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
).marginOnly(top: 6),
|
|
||||||
),
|
),
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
@@ -206,7 +153,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
size: 22,
|
size: 22,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () => DesktopSettingPage.switch2page(0),
|
onTap: () => {
|
||||||
|
if (DesktopSettingPage.tabKeys.isNotEmpty)
|
||||||
|
{
|
||||||
|
DesktopSettingPage.switch2page(
|
||||||
|
DesktopSettingPage.tabKeys[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
onHover: (value) => _editHover.value = value,
|
onHover: (value) => _editHover.value = value,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -298,19 +251,20 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
RxBool hover = false.obs;
|
RxBool hover = false.obs;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: DesktopTabPage.onAddSetting,
|
onTap: DesktopTabPage.onAddSetting,
|
||||||
child: Obx(
|
child: Tooltip(
|
||||||
() => CircleAvatar(
|
message: translate('Settings'),
|
||||||
radius: 15,
|
child: Obx(
|
||||||
backgroundColor: hover.value
|
() => CircleAvatar(
|
||||||
? Theme.of(context).scaffoldBackgroundColor
|
radius: 15,
|
||||||
: Theme.of(context).colorScheme.background,
|
backgroundColor: hover.value
|
||||||
child: Tooltip(
|
? Theme.of(context).scaffoldBackgroundColor
|
||||||
message: translate('Settings'),
|
: Theme.of(context).colorScheme.background,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.more_vert_outlined,
|
Icons.more_vert_outlined,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
color: hover.value ? textColor : textColor?.withOpacity(0.5),
|
||||||
)),
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onHover: (value) => hover.value = value,
|
onHover: (value) => hover.value = value,
|
||||||
@@ -371,33 +325,36 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
),
|
),
|
||||||
AnimatedRotationWidget(
|
AnimatedRotationWidget(
|
||||||
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
onPressed: () => bind.mainUpdateTemporaryPassword(),
|
||||||
child: Obx(() => RotatedBox(
|
child: Tooltip(
|
||||||
quarterTurns: 2,
|
message: translate('Refresh Password'),
|
||||||
child: Tooltip(
|
child: Obx(() => RotatedBox(
|
||||||
message: translate('Refresh Password'),
|
quarterTurns: 2,
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.refresh,
|
Icons.refresh,
|
||||||
color: refreshHover.value
|
color: refreshHover.value
|
||||||
? textColor
|
? textColor
|
||||||
: Color(0xFFDDDDDD),
|
: Color(0xFFDDDDDD),
|
||||||
size: 22,
|
size: 22,
|
||||||
)))),
|
))),
|
||||||
|
),
|
||||||
onHover: (value) => refreshHover.value = value,
|
onHover: (value) => refreshHover.value = value,
|
||||||
).marginOnly(right: 8, top: 4),
|
).marginOnly(right: 8, top: 4),
|
||||||
if (!bind.isDisableSettings())
|
if (!bind.isDisableSettings())
|
||||||
InkWell(
|
InkWell(
|
||||||
child: Obx(
|
child: Tooltip(
|
||||||
() => Tooltip(
|
message: translate('Change Password'),
|
||||||
message: translate('Change Password'),
|
child: Obx(
|
||||||
child: Icon(
|
() => Icon(
|
||||||
Icons.edit,
|
Icons.edit,
|
||||||
color: editHover.value
|
color: editHover.value
|
||||||
? textColor
|
? textColor
|
||||||
: Color(0xFFDDDDDD),
|
: Color(0xFFDDDDDD),
|
||||||
size: 22,
|
size: 22,
|
||||||
)).marginOnly(right: 8, top: 4),
|
).marginOnly(right: 8, top: 4),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onTap: () => DesktopSettingPage.switch2page(0),
|
onTap: () => DesktopSettingPage.switch2page(
|
||||||
|
SettingsTabKey.safety),
|
||||||
onHover: (value) => editHover.value = value,
|
onHover: (value) => editHover.value = value,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -701,10 +658,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
Timer(const Duration(seconds: 1), () async {
|
if (!bind.isCustomClient()) {
|
||||||
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
Timer(const Duration(seconds: 1), () async {
|
||||||
if (updateUrl.isNotEmpty) setState(() {});
|
updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||||
});
|
if (updateUrl.isNotEmpty) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
_updateTimer = periodic_immediate(const Duration(seconds: 1), () async {
|
||||||
await gFFI.serverModel.fetchID();
|
await gFFI.serverModel.fetchID();
|
||||||
final error = await bind.mainGetError();
|
final error = await bind.mainGetError();
|
||||||
@@ -824,6 +783,12 @@ class _DesktopHomePageState extends State<DesktopHomePage>
|
|||||||
final screenRect = parseParamScreenRect(args);
|
final screenRect = parseParamScreenRect(args);
|
||||||
await rustDeskWinManager.openMonitorSession(
|
await rustDeskWinManager.openMonitorSession(
|
||||||
windowId, peerId, display, displayCount, screenRect);
|
windowId, peerId, display, displayCount, screenRect);
|
||||||
|
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
||||||
|
final windowId = int.tryParse(call.arguments);
|
||||||
|
if (windowId != null) {
|
||||||
|
return jsonEncode(
|
||||||
|
await rustDeskWinManager.getOtherRemoteWindowCoords(windowId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_uniLinksSubscription = listenUniLinks();
|
_uniLinksSubscription = listenUniLinks();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,8 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
@override
|
@override
|
||||||
State<DesktopTabPage> createState() => _DesktopTabPageState();
|
State<DesktopTabPage> createState() => _DesktopTabPageState();
|
||||||
|
|
||||||
static void onAddSetting({int initialPage = 0}) {
|
static void onAddSetting(
|
||||||
|
{SettingsTabKey initialPage = SettingsTabKey.general}) {
|
||||||
try {
|
try {
|
||||||
DesktopTabController tabController = Get.find();
|
DesktopTabController tabController = Get.find();
|
||||||
tabController.add(TabInfo(
|
tabController.add(TabInfo(
|
||||||
@@ -27,7 +28,7 @@ class DesktopTabPage extends StatefulWidget {
|
|||||||
unselectedIcon: Icons.build_outlined,
|
unselectedIcon: Icons.build_outlined,
|
||||||
page: DesktopSettingPage(
|
page: DesktopSettingPage(
|
||||||
key: const ValueKey(kTabLabelSettingPage),
|
key: const ValueKey(kTabLabelSettingPage),
|
||||||
initialPage: initialPage,
|
initialTabkey: initialPage,
|
||||||
)));
|
)));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrintStack(label: '$e');
|
debugPrintStack(label: '$e');
|
||||||
@@ -56,10 +57,10 @@ class _DesktopTabPageState extends State<DesktopTabPage> {
|
|||||||
tabController.onSelected = (key) {
|
tabController.onSelected = (key) {
|
||||||
if (key == kTabLabelHomePage) {
|
if (key == kTabLabelHomePage) {
|
||||||
windowManager.setSize(getIncomingOnlyHomeSize());
|
windowManager.setSize(getIncomingOnlyHomeSize());
|
||||||
windowManager.setResizable(false);
|
setResizable(false);
|
||||||
} else {
|
} else {
|
||||||
windowManager.setSize(getIncomingOnlySettingsSize());
|
windowManager.setSize(getIncomingOnlySettingsSize());
|
||||||
windowManager.setResizable(true);
|
setResizable(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,18 +91,21 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tabWidget = Container(
|
final child = Scaffold(
|
||||||
decoration: BoxDecoration(
|
backgroundColor: Theme.of(context).cardColor,
|
||||||
border: Border.all(color: MyTheme.color(context).border!)),
|
body: DesktopTab(
|
||||||
child: Scaffold(
|
controller: tabController,
|
||||||
backgroundColor: Theme.of(context).cardColor,
|
onWindowCloseButton: handleWindowCloseButton,
|
||||||
body: DesktopTab(
|
tail: const AddButton(),
|
||||||
controller: tabController,
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
onWindowCloseButton: handleWindowCloseButton,
|
));
|
||||||
tail: const AddButton().paddingOnly(left: 10),
|
final tabWidget = isLinux
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
? buildVirtualWindowFrame(context, child)
|
||||||
)),
|
: Container(
|
||||||
);
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
return isMacOS || kUseCompatibleUiMode
|
return isMacOS || kUseCompatibleUiMode
|
||||||
? tabWidget
|
? tabWidget
|
||||||
: SubWindowDragToResizeArea(
|
: SubWindowDragToResizeArea(
|
||||||
@@ -128,9 +131,9 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
|
|||||||
tabController.clear();
|
tabController.clear();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
|
||||||
final bool res;
|
final bool res;
|
||||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||||
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||||
res = true;
|
res = true;
|
||||||
} else {
|
} else {
|
||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
|
|||||||
@@ -97,21 +97,30 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tabWidget = Container(
|
final child = Scaffold(
|
||||||
decoration: BoxDecoration(
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
border: Border.all(color: MyTheme.color(context).border!)),
|
body: DesktopTab(
|
||||||
child: Scaffold(
|
controller: tabController,
|
||||||
backgroundColor: Theme.of(context).colorScheme.background,
|
onWindowCloseButton: () async {
|
||||||
body: DesktopTab(
|
tabController.clear();
|
||||||
controller: tabController,
|
return true;
|
||||||
onWindowCloseButton: () async {
|
},
|
||||||
tabController.clear();
|
tail: AddButton(),
|
||||||
return true;
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
},
|
),
|
||||||
tail: AddButton().paddingOnly(left: 10),
|
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
|
final tabWidget = isLinux
|
||||||
|
? buildVirtualWindowFrame(
|
||||||
|
context,
|
||||||
|
Scaffold(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
|
body: child),
|
||||||
|
)
|
||||||
|
: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: MyTheme.color(context).border!)),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
return isMacOS || kUseCompatibleUiMode
|
return isMacOS || kUseCompatibleUiMode
|
||||||
? tabWidget
|
? tabWidget
|
||||||
: Obx(
|
: Obx(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart';
|
||||||
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
|
|
||||||
import '../../consts.dart';
|
import '../../consts.dart';
|
||||||
import '../../common/widgets/overlay.dart';
|
import '../../common/widgets/overlay.dart';
|
||||||
@@ -15,7 +16,6 @@ import '../../common.dart';
|
|||||||
import '../../common/widgets/dialog.dart';
|
import '../../common/widgets/dialog.dart';
|
||||||
import '../../common/widgets/toolbar.dart';
|
import '../../common/widgets/toolbar.dart';
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
import '../../models/desktop_render_texture.dart';
|
|
||||||
import '../../models/platform_model.dart';
|
import '../../models/platform_model.dart';
|
||||||
import '../../common/shared_state.dart';
|
import '../../common/shared_state.dart';
|
||||||
import '../../utils/image.dart';
|
import '../../utils/image.dart';
|
||||||
@@ -93,7 +93,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
|
|
||||||
void _initStates(String id) {
|
void _initStates(String id) {
|
||||||
initSharedStates(id);
|
initSharedStates(id);
|
||||||
_zoomCursor = PeerBoolOption.find(id, 'zoom-cursor');
|
_zoomCursor = PeerBoolOption.find(id, kOptionZoomCursor);
|
||||||
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
_showRemoteCursor = ShowRemoteCursorState.find(id);
|
||||||
_keyboardEnabled = KeyboardEnabledState.find(id);
|
_keyboardEnabled = KeyboardEnabledState.find(id);
|
||||||
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
_remoteCursorMoved = RemoteCursorMovedState.find(id);
|
||||||
@@ -135,7 +135,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
_showRemoteCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'show-remote-cursor');
|
sessionId: sessionId, arg: 'show-remote-cursor');
|
||||||
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
_zoomCursor.value = bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: 'zoom-cursor');
|
sessionId: sessionId, arg: kOptionZoomCursor);
|
||||||
DesktopMultiWindow.addListener(this);
|
DesktopMultiWindow.addListener(this);
|
||||||
// if (!_isCustomCursorInited) {
|
// if (!_isCustomCursorInited) {
|
||||||
// customCursorController.registerNeedUpdateCursorCallback(
|
// customCursorController.registerNeedUpdateCursorCallback(
|
||||||
@@ -165,6 +165,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
// and let OS to handle events instead.
|
// and let OS to handle events instead.
|
||||||
_rawKeyFocusNode.unfocus();
|
_rawKeyFocusNode.unfocus();
|
||||||
}
|
}
|
||||||
|
stateGlobal.isFocused.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -174,6 +175,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
_isWindowBlur = false;
|
_isWindowBlur = false;
|
||||||
}
|
}
|
||||||
|
stateGlobal.isFocused.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -206,6 +208,22 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowEnterFullScreen() {
|
||||||
|
super.onWindowEnterFullScreen();
|
||||||
|
if (isMacOS) {
|
||||||
|
stateGlobal.setFullscreen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowLeaveFullScreen() {
|
||||||
|
super.onWindowLeaveFullScreen();
|
||||||
|
if (isMacOS) {
|
||||||
|
stateGlobal.setFullscreen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
|
final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
|
||||||
@@ -218,6 +236,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.inputModel.enterOrLeave(false);
|
_ffi.inputModel.enterOrLeave(false);
|
||||||
DesktopMultiWindow.removeListener(this);
|
DesktopMultiWindow.removeListener(this);
|
||||||
_ffi.dialogManager.hideMobileActionsOverlay();
|
_ffi.dialogManager.hideMobileActionsOverlay();
|
||||||
|
_ffi.imageModel.disposeImage();
|
||||||
|
_ffi.cursorModel.disposeImages();
|
||||||
_ffi.recordingModel.onClose();
|
_ffi.recordingModel.onClose();
|
||||||
_rawKeyFocusNode.dispose();
|
_rawKeyFocusNode.dispose();
|
||||||
await _ffi.close(closeSession: closeSession);
|
await _ffi.close(closeSession: closeSession);
|
||||||
@@ -258,7 +278,7 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
color: Colors.black,
|
color: kColorCanvas,
|
||||||
child: RawKeyFocusScope(
|
child: RawKeyFocusScope(
|
||||||
focusNode: _rawKeyFocusNode,
|
focusNode: _rawKeyFocusNode,
|
||||||
onFocusChange: (bool imageFocused) {
|
onFocusChange: (bool imageFocused) {
|
||||||
@@ -287,8 +307,30 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
_ffi.ffiModel.waitForFirstImage.isTrue
|
_ffi.ffiModel.waitForFirstImage.isTrue
|
||||||
? emptyOverlay()
|
? emptyOverlay()
|
||||||
: () {
|
: () {
|
||||||
_ffi.ffiModel.tryShowAndroidActionsOverlay();
|
if (!_ffi.ffiModel.isPeerAndroid) {
|
||||||
return Offstage();
|
return Offstage();
|
||||||
|
} else {
|
||||||
|
if (_ffi.connType == ConnType.defaultConn &&
|
||||||
|
_ffi.ffiModel.permissions['keyboard'] != false) {
|
||||||
|
Timer(
|
||||||
|
Duration(milliseconds: 10),
|
||||||
|
() => _ffi.dialogManager
|
||||||
|
.mobileActionsOverlayVisible.value = true);
|
||||||
|
}
|
||||||
|
return Obx(() => Offstage(
|
||||||
|
offstage: _ffi.dialogManager
|
||||||
|
.mobileActionsOverlayVisible.isFalse,
|
||||||
|
child: Overlay(initialEntries: [
|
||||||
|
makeMobileActionsOverlayEntry(
|
||||||
|
() => _ffi
|
||||||
|
.dialogManager
|
||||||
|
.mobileActionsOverlayVisible
|
||||||
|
.value = false,
|
||||||
|
ffi: _ffi,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
}
|
||||||
}(),
|
}(),
|
||||||
// Use Overlay to enable rebuild every time on menu button click.
|
// Use Overlay to enable rebuild every time on menu button click.
|
||||||
_ffi.ffiModel.pi.isSet.isTrue
|
_ffi.ffiModel.pi.isSet.isTrue
|
||||||
@@ -435,9 +477,8 @@ class _RemotePageState extends State<RemotePage>
|
|||||||
}, onExit: (evt) {
|
}, onExit: (evt) {
|
||||||
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true);
|
||||||
}, child: LayoutBuilder(builder: (context, constraints) {
|
}, child: LayoutBuilder(builder: (context, constraints) {
|
||||||
Future.delayed(Duration.zero, () {
|
final c = Provider.of<CanvasModel>(context, listen: false);
|
||||||
Provider.of<CanvasModel>(context, listen: false).updateViewStyle();
|
Future.delayed(Duration.zero, () => c.updateViewStyle());
|
||||||
});
|
|
||||||
final peerDisplay = CurrentDisplayState.find(widget.id);
|
final peerDisplay = CurrentDisplayState.find(widget.id);
|
||||||
return Obx(
|
return Obx(
|
||||||
() => _ffi.ffiModel.pi.isSet.isFalse
|
() => _ffi.ffiModel.pi.isSet.isFalse
|
||||||
@@ -572,12 +613,11 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
onHover: (evt) {},
|
onHover: (evt) {},
|
||||||
child: child);
|
child: child);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
|
||||||
final paintWidth = c.getDisplayWidth() * s;
|
final paintWidth = c.getDisplayWidth() * s;
|
||||||
final paintHeight = c.getDisplayHeight() * s;
|
final paintHeight = c.getDisplayHeight() * s;
|
||||||
final paintSize = Size(paintWidth, paintHeight);
|
final paintSize = Size(paintWidth, paintHeight);
|
||||||
final paintWidget = useTextureRender
|
final paintWidget = m.useTextureRender
|
||||||
? _BuildPaintTextureRender(
|
? _BuildPaintTextureRender(
|
||||||
c, s, Offset.zero, paintSize, isViewOriginal())
|
c, s, Offset.zero, paintSize, isViewOriginal())
|
||||||
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
: _buildScrollbarNonTextureRender(m, paintSize, s);
|
||||||
@@ -598,7 +638,7 @@ class _ImagePaintState extends State<ImagePaint> {
|
|||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
if (c.size.width > 0 && c.size.height > 0) {
|
if (c.size.width > 0 && c.size.height > 0) {
|
||||||
final paintWidget = useTextureRender
|
final paintWidget = m.useTextureRender
|
||||||
? _BuildPaintTextureRender(
|
? _BuildPaintTextureRender(
|
||||||
c,
|
c,
|
||||||
s,
|
s,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
import 'package:flutter_hbb/common/shared_state.dart';
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
|
import 'package:flutter_hbb/models/input_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
import 'package:flutter_hbb/desktop/pages/remote_page.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart';
|
||||||
@@ -107,107 +108,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
|
|
||||||
tabController.onRemoved = (_, id) => onRemoveId(id);
|
tabController.onRemoved = (_, id) => onRemoveId(id);
|
||||||
|
|
||||||
rustDeskWinManager.setMethodHandler((call, fromWindowId) async {
|
rustDeskWinManager.setMethodHandler(_remoteMethodHandler);
|
||||||
print(
|
|
||||||
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
|
||||||
|
|
||||||
dynamic returnValue;
|
|
||||||
// for simplify, just replace connectionId
|
|
||||||
if (call.method == kWindowEventNewRemoteDesktop) {
|
|
||||||
final args = jsonDecode(call.arguments);
|
|
||||||
final id = args['id'];
|
|
||||||
final switchUuid = args['switch_uuid'];
|
|
||||||
final sessionId = args['session_id'];
|
|
||||||
final tabWindowId = args['tab_window_id'];
|
|
||||||
final display = args['display'];
|
|
||||||
final displays = args['displays'];
|
|
||||||
final screenRect = parseParamScreenRect(args);
|
|
||||||
windowOnTop(windowId());
|
|
||||||
tryMoveToScreenAndSetFullscreen(screenRect);
|
|
||||||
if (tabController.length == 0) {
|
|
||||||
// Show the hidden window.
|
|
||||||
if (isMacOS && stateGlobal.closeOnFullscreen == true) {
|
|
||||||
stateGlobal.setFullscreen(true);
|
|
||||||
}
|
|
||||||
// Reset the state
|
|
||||||
stateGlobal.closeOnFullscreen = null;
|
|
||||||
}
|
|
||||||
ConnectionTypeState.init(id);
|
|
||||||
_toolbarState.setShow(
|
|
||||||
bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
|
||||||
tabController.add(TabInfo(
|
|
||||||
key: id,
|
|
||||||
label: id,
|
|
||||||
selectedIcon: selectedIcon,
|
|
||||||
unselectedIcon: unselectedIcon,
|
|
||||||
onTabCloseButton: () => tabController.closeBy(id),
|
|
||||||
page: RemotePage(
|
|
||||||
key: ValueKey(id),
|
|
||||||
id: id,
|
|
||||||
sessionId: sessionId == null ? null : SessionID(sessionId),
|
|
||||||
tabWindowId: tabWindowId,
|
|
||||||
display: display,
|
|
||||||
displays: displays?.cast<int>(),
|
|
||||||
password: args['password'],
|
|
||||||
toolbarState: _toolbarState,
|
|
||||||
tabController: tabController,
|
|
||||||
switchUuid: switchUuid,
|
|
||||||
forceRelay: args['forceRelay'],
|
|
||||||
isSharedPassword: args['isSharedPassword'],
|
|
||||||
),
|
|
||||||
));
|
|
||||||
} else if (call.method == kWindowDisableGrabKeyboard) {
|
|
||||||
// ???
|
|
||||||
} else if (call.method == "onDestroy") {
|
|
||||||
tabController.clear();
|
|
||||||
} else if (call.method == kWindowActionRebuild) {
|
|
||||||
reloadCurrentWindow();
|
|
||||||
} else if (call.method == kWindowEventActiveSession) {
|
|
||||||
final jumpOk = tabController.jumpToByKey(call.arguments);
|
|
||||||
if (jumpOk) {
|
|
||||||
windowOnTop(windowId());
|
|
||||||
}
|
|
||||||
return jumpOk;
|
|
||||||
} else if (call.method == kWindowEventActiveDisplaySession) {
|
|
||||||
final args = jsonDecode(call.arguments);
|
|
||||||
final id = args['id'];
|
|
||||||
final display = args['display'];
|
|
||||||
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
|
|
||||||
if (jumpOk) {
|
|
||||||
windowOnTop(windowId());
|
|
||||||
}
|
|
||||||
return jumpOk;
|
|
||||||
} else if (call.method == kWindowEventGetRemoteList) {
|
|
||||||
return tabController.state.value.tabs
|
|
||||||
.map((e) => e.key)
|
|
||||||
.toList()
|
|
||||||
.join(',');
|
|
||||||
} else if (call.method == kWindowEventGetSessionIdList) {
|
|
||||||
return tabController.state.value.tabs
|
|
||||||
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
|
|
||||||
.toList()
|
|
||||||
.join(';');
|
|
||||||
} else if (call.method == kWindowEventGetCachedSessionData) {
|
|
||||||
// Ready to show new window and close old tab.
|
|
||||||
final args = jsonDecode(call.arguments);
|
|
||||||
final id = args['id'];
|
|
||||||
final close = args['close'];
|
|
||||||
try {
|
|
||||||
final remotePage = tabController.state.value.tabs
|
|
||||||
.firstWhere((tab) => tab.key == id)
|
|
||||||
.page as RemotePage;
|
|
||||||
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Failed to get cached session data: $e');
|
|
||||||
}
|
|
||||||
if (close && returnValue != null) {
|
|
||||||
closeSessionOnDispose[id] = false;
|
|
||||||
tabController.closeBy(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_update_remote_count();
|
|
||||||
return returnValue;
|
|
||||||
});
|
|
||||||
if (!_isScreenRectSet) {
|
if (!_isScreenRectSet) {
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
restoreWindowPosition(
|
restoreWindowPosition(
|
||||||
@@ -230,103 +131,103 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tabWidget = Obx(
|
final child = Scaffold(
|
||||||
() => Container(
|
backgroundColor: Theme.of(context).colorScheme.background,
|
||||||
decoration: BoxDecoration(
|
body: DesktopTab(
|
||||||
border: Border.all(
|
controller: tabController,
|
||||||
color: MyTheme.color(context).border!,
|
onWindowCloseButton: handleWindowCloseButton,
|
||||||
width: stateGlobal.windowBorderWidth.value),
|
tail: const AddButton(),
|
||||||
),
|
pageViewBuilder: (pageView) => pageView,
|
||||||
child: Scaffold(
|
labelGetter: DesktopTab.tablabelGetter,
|
||||||
backgroundColor: Theme.of(context).colorScheme.background,
|
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
||||||
body: DesktopTab(
|
final connectionType = ConnectionTypeState.find(key);
|
||||||
controller: tabController,
|
if (!connectionType.isValid()) {
|
||||||
onWindowCloseButton: handleWindowCloseButton,
|
return Row(
|
||||||
tail: const AddButton().paddingOnly(left: 10),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
pageViewBuilder: (pageView) => pageView,
|
children: [
|
||||||
labelGetter: DesktopTab.tablabelGetter,
|
icon,
|
||||||
tabBuilder: (key, icon, label, themeConf) => Obx(() {
|
label,
|
||||||
final connectionType = ConnectionTypeState.find(key);
|
],
|
||||||
if (!connectionType.isValid()) {
|
);
|
||||||
return Row(
|
} else {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
bool secure =
|
||||||
children: [
|
connectionType.secure.value == ConnectionType.strSecure;
|
||||||
icon,
|
bool direct =
|
||||||
label,
|
connectionType.direct.value == ConnectionType.strDirect;
|
||||||
],
|
String msgConn;
|
||||||
);
|
if (secure && direct) {
|
||||||
} else {
|
msgConn = translate("Direct and encrypted connection");
|
||||||
bool secure =
|
} else if (secure && !direct) {
|
||||||
connectionType.secure.value == ConnectionType.strSecure;
|
msgConn = translate("Relayed and encrypted connection");
|
||||||
bool direct =
|
} else if (!secure && direct) {
|
||||||
connectionType.direct.value == ConnectionType.strDirect;
|
msgConn = translate("Direct and unencrypted connection");
|
||||||
String msgConn;
|
} else {
|
||||||
if (secure && direct) {
|
msgConn = translate("Relayed and unencrypted connection");
|
||||||
msgConn = translate("Direct and encrypted connection");
|
}
|
||||||
} else if (secure && !direct) {
|
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
||||||
msgConn = translate("Relayed and encrypted connection");
|
var fingerprint = FingerprintState.find(key).value;
|
||||||
} else if (!secure && direct) {
|
if (fingerprint.isEmpty) {
|
||||||
msgConn = translate("Direct and unencrypted connection");
|
fingerprint = 'N/A';
|
||||||
} else {
|
}
|
||||||
msgConn = translate("Relayed and unencrypted connection");
|
if (fingerprint.length > 5 * 8) {
|
||||||
}
|
var first = fingerprint.substring(0, 39);
|
||||||
var msgFingerprint = '${translate('Fingerprint')}:\n';
|
var second = fingerprint.substring(40);
|
||||||
var fingerprint = FingerprintState.find(key).value;
|
msgFingerprint += '$first\n$second';
|
||||||
if (fingerprint.isEmpty) {
|
} else {
|
||||||
fingerprint = 'N/A';
|
msgFingerprint += fingerprint;
|
||||||
}
|
}
|
||||||
if (fingerprint.length > 5 * 8) {
|
|
||||||
var first = fingerprint.substring(0, 39);
|
|
||||||
var second = fingerprint.substring(40);
|
|
||||||
msgFingerprint += '$first\n$second';
|
|
||||||
} else {
|
|
||||||
msgFingerprint += fingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
final tab = Row(
|
final tab = Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
icon,
|
icon,
|
||||||
Tooltip(
|
Tooltip(
|
||||||
message: '$msgConn\n$msgFingerprint',
|
message: '$msgConn\n$msgFingerprint',
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
'assets/${connectionType.secure.value}${connectionType.direct.value}.svg',
|
||||||
width: themeConf.iconSize,
|
width: themeConf.iconSize,
|
||||||
height: themeConf.iconSize,
|
height: themeConf.iconSize,
|
||||||
).paddingOnly(right: 5),
|
).paddingOnly(right: 5),
|
||||||
),
|
),
|
||||||
label,
|
label,
|
||||||
unreadMessageCountBuilder(UnreadChatCountState.find(key))
|
unreadMessageCountBuilder(UnreadChatCountState.find(key))
|
||||||
.marginOnly(left: 4),
|
.marginOnly(left: 4),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerDown: (e) {
|
onPointerDown: (e) {
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final remotePage = tabController.state.value.tabs
|
final remotePage = tabController.state.value.tabs
|
||||||
.firstWhere((tab) => tab.key == key)
|
.firstWhere((tab) => tab.key == key)
|
||||||
.page as RemotePage;
|
.page as RemotePage;
|
||||||
if (remotePage.ffi.ffiModel.pi.isSet.isTrue &&
|
if (remotePage.ffi.ffiModel.pi.isSet.isTrue && e.buttons == 2) {
|
||||||
e.buttons == 2) {
|
showRightMenu(
|
||||||
showRightMenu(
|
(CancelFunc cancelFunc) {
|
||||||
(CancelFunc cancelFunc) {
|
return _tabMenuBuilder(key, cancelFunc);
|
||||||
return _tabMenuBuilder(key, cancelFunc);
|
},
|
||||||
},
|
target: e.position,
|
||||||
target: e.position,
|
);
|
||||||
);
|
}
|
||||||
}
|
},
|
||||||
},
|
child: tab,
|
||||||
child: tab,
|
);
|
||||||
);
|
}
|
||||||
}
|
}),
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
final tabWidget = isLinux
|
||||||
|
? buildVirtualWindowFrame(context, child)
|
||||||
|
: Obx(() => Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: MyTheme.color(context).border!,
|
||||||
|
width: stateGlobal.windowBorderWidth.value),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
));
|
||||||
return isMacOS || kUseCompatibleUiMode
|
return isMacOS || kUseCompatibleUiMode
|
||||||
? tabWidget
|
? tabWidget
|
||||||
: Obx(() => SubWindowDragToResizeArea(
|
: Obx(() => SubWindowDragToResizeArea(
|
||||||
@@ -449,7 +350,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
|
|
||||||
void onRemoveId(String id) async {
|
void onRemoveId(String id) async {
|
||||||
if (tabController.state.value.tabs.isEmpty) {
|
if (tabController.state.value.tabs.isEmpty) {
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
|
||||||
// Keep calling until the window status is hidden.
|
// Keep calling until the window status is hidden.
|
||||||
//
|
//
|
||||||
// Workaround for Windows:
|
// Workaround for Windows:
|
||||||
@@ -483,9 +383,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
tabController.clear();
|
tabController.clear();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
|
||||||
final bool res;
|
final bool res;
|
||||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||||
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||||
res = true;
|
res = true;
|
||||||
} else {
|
} else {
|
||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
@@ -499,4 +399,132 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
|
|||||||
|
|
||||||
_update_remote_count() =>
|
_update_remote_count() =>
|
||||||
RemoteCountState.find().value = tabController.length;
|
RemoteCountState.find().value = tabController.length;
|
||||||
|
|
||||||
|
Future<dynamic> _remoteMethodHandler(call, fromWindowId) async {
|
||||||
|
print(
|
||||||
|
"[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
|
||||||
|
|
||||||
|
dynamic returnValue;
|
||||||
|
// for simplify, just replace connectionId
|
||||||
|
if (call.method == kWindowEventNewRemoteDesktop) {
|
||||||
|
final args = jsonDecode(call.arguments);
|
||||||
|
final id = args['id'];
|
||||||
|
final switchUuid = args['switch_uuid'];
|
||||||
|
final sessionId = args['session_id'];
|
||||||
|
final tabWindowId = args['tab_window_id'];
|
||||||
|
final display = args['display'];
|
||||||
|
final displays = args['displays'];
|
||||||
|
final screenRect = parseParamScreenRect(args);
|
||||||
|
Future.delayed(Duration.zero, () async {
|
||||||
|
if (stateGlobal.fullscreen.isTrue) {
|
||||||
|
await WindowController.fromWindowId(windowId()).setFullscreen(false);
|
||||||
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
|
}
|
||||||
|
await setNewConnectWindowFrame(windowId(), id!, screenRect);
|
||||||
|
Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async {
|
||||||
|
await windowOnTop(windowId());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ConnectionTypeState.init(id);
|
||||||
|
_toolbarState.setShow(
|
||||||
|
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
||||||
|
tabController.add(TabInfo(
|
||||||
|
key: id,
|
||||||
|
label: id,
|
||||||
|
selectedIcon: selectedIcon,
|
||||||
|
unselectedIcon: unselectedIcon,
|
||||||
|
onTabCloseButton: () => tabController.closeBy(id),
|
||||||
|
page: RemotePage(
|
||||||
|
key: ValueKey(id),
|
||||||
|
id: id,
|
||||||
|
sessionId: sessionId == null ? null : SessionID(sessionId),
|
||||||
|
tabWindowId: tabWindowId,
|
||||||
|
display: display,
|
||||||
|
displays: displays?.cast<int>(),
|
||||||
|
password: args['password'],
|
||||||
|
toolbarState: _toolbarState,
|
||||||
|
tabController: tabController,
|
||||||
|
switchUuid: switchUuid,
|
||||||
|
forceRelay: args['forceRelay'],
|
||||||
|
isSharedPassword: args['isSharedPassword'],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
} else if (call.method == kWindowDisableGrabKeyboard) {
|
||||||
|
// ???
|
||||||
|
} else if (call.method == "onDestroy") {
|
||||||
|
tabController.clear();
|
||||||
|
} else if (call.method == kWindowActionRebuild) {
|
||||||
|
reloadCurrentWindow();
|
||||||
|
} else if (call.method == kWindowEventActiveSession) {
|
||||||
|
final jumpOk = tabController.jumpToByKey(call.arguments);
|
||||||
|
if (jumpOk) {
|
||||||
|
windowOnTop(windowId());
|
||||||
|
}
|
||||||
|
return jumpOk;
|
||||||
|
} else if (call.method == kWindowEventActiveDisplaySession) {
|
||||||
|
final args = jsonDecode(call.arguments);
|
||||||
|
final id = args['id'];
|
||||||
|
final display = args['display'];
|
||||||
|
final jumpOk = tabController.jumpToByKeyAndDisplay(id, display);
|
||||||
|
if (jumpOk) {
|
||||||
|
windowOnTop(windowId());
|
||||||
|
}
|
||||||
|
return jumpOk;
|
||||||
|
} else if (call.method == kWindowEventGetRemoteList) {
|
||||||
|
return tabController.state.value.tabs
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList()
|
||||||
|
.join(',');
|
||||||
|
} else if (call.method == kWindowEventGetSessionIdList) {
|
||||||
|
return tabController.state.value.tabs
|
||||||
|
.map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
|
||||||
|
.toList()
|
||||||
|
.join(';');
|
||||||
|
} else if (call.method == kWindowEventGetCachedSessionData) {
|
||||||
|
// Ready to show new window and close old tab.
|
||||||
|
final args = jsonDecode(call.arguments);
|
||||||
|
final id = args['id'];
|
||||||
|
final close = args['close'];
|
||||||
|
try {
|
||||||
|
final remotePage = tabController.state.value.tabs
|
||||||
|
.firstWhere((tab) => tab.key == id)
|
||||||
|
.page as RemotePage;
|
||||||
|
returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Failed to get cached session data: $e');
|
||||||
|
}
|
||||||
|
if (close && returnValue != null) {
|
||||||
|
closeSessionOnDispose[id] = false;
|
||||||
|
tabController.closeBy(id);
|
||||||
|
}
|
||||||
|
} else if (call.method == kWindowEventRemoteWindowCoords) {
|
||||||
|
final remotePage =
|
||||||
|
tabController.state.value.selectedTabInfo.page as RemotePage;
|
||||||
|
final ffi = remotePage.ffi;
|
||||||
|
final displayRect = ffi.ffiModel.displaysRect();
|
||||||
|
if (displayRect != null) {
|
||||||
|
final wc = WindowController.fromWindowId(windowId());
|
||||||
|
Rect? frame;
|
||||||
|
try {
|
||||||
|
frame = await wc.getFrame();
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint(
|
||||||
|
"Failed to get frame of window $windowId, it may be hidden");
|
||||||
|
}
|
||||||
|
if (frame != null) {
|
||||||
|
ffi.cursorModel.moveLocal(0, 0);
|
||||||
|
final coords = RemoteWindowCoords(
|
||||||
|
frame,
|
||||||
|
CanvasCoords.fromCanvasModel(ffi.canvasModel),
|
||||||
|
CursorCoords.fromCursorModel(ffi.cursorModel),
|
||||||
|
displayRect);
|
||||||
|
returnValue = jsonEncode(coords.toJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (call.method == kWindowEventSetFullscreen) {
|
||||||
|
stateGlobal.setFullscreen(call.arguments == 'true');
|
||||||
|
}
|
||||||
|
_update_remote_count();
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:async';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
@@ -76,14 +77,20 @@ class _DesktopServerPageState extends State<DesktopServerPage>
|
|||||||
ChangeNotifierProvider.value(value: gFFI.chatModel),
|
ChangeNotifierProvider.value(value: gFFI.chatModel),
|
||||||
],
|
],
|
||||||
child: Consumer<ServerModel>(
|
child: Consumer<ServerModel>(
|
||||||
builder: (context, serverModel, child) => Container(
|
builder: (context, serverModel, child) {
|
||||||
decoration: BoxDecoration(
|
final body = Scaffold(
|
||||||
border: Border.all(color: MyTheme.color(context).border!)),
|
|
||||||
child: Scaffold(
|
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
body: ConnectionManager(),
|
body: ConnectionManager(),
|
||||||
),
|
);
|
||||||
),
|
return isLinux
|
||||||
|
? buildVirtualWindowFrame(context, body)
|
||||||
|
: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border:
|
||||||
|
Border.all(color: MyTheme.color(context).border!)),
|
||||||
|
child: body,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -157,7 +164,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
controller: serverModel.tabController,
|
controller: serverModel.tabController,
|
||||||
selectedBorderColor: MyTheme.accent,
|
selectedBorderColor: MyTheme.accent,
|
||||||
maxLabelWidth: 100,
|
maxLabelWidth: 100,
|
||||||
tail: buildScrollJumper(),
|
tail: null, //buildScrollJumper(),
|
||||||
selectedTabBackgroundColor:
|
selectedTabBackgroundColor:
|
||||||
Theme.of(context).hintColor.withOpacity(0),
|
Theme.of(context).hintColor.withOpacity(0),
|
||||||
tabBuilder: (key, icon, label, themeConf) {
|
tabBuilder: (key, icon, label, themeConf) {
|
||||||
@@ -282,9 +289,9 @@ class ConnectionManagerState extends State<ConnectionManager> {
|
|||||||
windowManager.close();
|
windowManager.close();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
|
||||||
final bool res;
|
final bool res;
|
||||||
if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
|
if (!option2bool(kOptionEnableConfirmClosingTabs,
|
||||||
|
bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) {
|
||||||
res = true;
|
res = true;
|
||||||
} else {
|
} else {
|
||||||
res = await closeConfirmDialog();
|
res = await closeConfirmDialog();
|
||||||
@@ -701,17 +708,86 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: !client.inVoiceCall,
|
offstage: !client.inVoiceCall,
|
||||||
child: buildButton(
|
child: Row(
|
||||||
context,
|
children: [
|
||||||
color: Colors.red,
|
Expanded(
|
||||||
onClick: () => closeVoiceCall(),
|
child: buildButton(context,
|
||||||
icon: Icon(
|
color: MyTheme.accent,
|
||||||
Icons.call_end_rounded,
|
onClick: null, onTapDown: (details) async {
|
||||||
color: Colors.white,
|
final devicesInfo = await AudioInput.getDevicesInfo();
|
||||||
size: 14,
|
List<String> devices = devicesInfo['devices'] as List<String>;
|
||||||
),
|
if (devices.isEmpty) {
|
||||||
text: "Stop voice call",
|
msgBox(
|
||||||
textColor: Colors.white,
|
gFFI.sessionId,
|
||||||
|
'custom-nocancel-info',
|
||||||
|
'Prompt',
|
||||||
|
'no_audio_input_device_tip',
|
||||||
|
'',
|
||||||
|
gFFI.dialogManager,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String currentDevice = devicesInfo['current'] as String;
|
||||||
|
final x = details.globalPosition.dx;
|
||||||
|
final y = details.globalPosition.dy;
|
||||||
|
final position = RelativeRect.fromLTRB(x, y, x, y);
|
||||||
|
showMenu(
|
||||||
|
context: context,
|
||||||
|
position: position,
|
||||||
|
items: devices
|
||||||
|
.map((d) => PopupMenuItem<String>(
|
||||||
|
value: d,
|
||||||
|
height: 18,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onTap: () => AudioInput.setDevice(d),
|
||||||
|
child: IgnorePointer(
|
||||||
|
child: RadioMenuButton(
|
||||||
|
value: d,
|
||||||
|
groupValue: currentDevice,
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) AudioInput.setDevice(v);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
child: Text(
|
||||||
|
d,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth:
|
||||||
|
kConnectionManagerWindowSizeClosedChat
|
||||||
|
.width -
|
||||||
|
80),
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.call_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
text: "Audio input",
|
||||||
|
textColor: Colors.white),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: buildButton(
|
||||||
|
context,
|
||||||
|
color: Colors.red,
|
||||||
|
onClick: () => closeVoiceCall(),
|
||||||
|
icon: Icon(
|
||||||
|
Icons.call_end_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 14,
|
||||||
|
),
|
||||||
|
text: "Stop voice call",
|
||||||
|
textColor: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Offstage(
|
Offstage(
|
||||||
@@ -872,12 +948,14 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
|
|
||||||
Widget buildButton(BuildContext context,
|
Widget buildButton(BuildContext context,
|
||||||
{required Color? color,
|
{required Color? color,
|
||||||
required Function() onClick,
|
GestureTapCallback? onClick,
|
||||||
Icon? icon,
|
Widget? icon,
|
||||||
BoxBorder? border,
|
BoxBorder? border,
|
||||||
required String text,
|
required String text,
|
||||||
required Color? textColor,
|
required Color? textColor,
|
||||||
String? tooltip}) {
|
String? tooltip,
|
||||||
|
GestureTapDownCallback? onTapDown}) {
|
||||||
|
assert(!(onClick == null && onTapDown == null));
|
||||||
Widget textWidget;
|
Widget textWidget;
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
textWidget = Text(
|
textWidget = Text(
|
||||||
@@ -901,7 +979,16 @@ class _CmControlPanel extends StatelessWidget {
|
|||||||
color: color, borderRadius: borderRadius, border: border),
|
color: color, borderRadius: borderRadius, border: border),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
onTap: () => checkClickTime(client.id, onClick),
|
onTap: () {
|
||||||
|
if (onClick == null) return;
|
||||||
|
checkClickTime(client.id, onClick);
|
||||||
|
},
|
||||||
|
onTapDown: (details) {
|
||||||
|
if (onTapDown == null) return;
|
||||||
|
checkClickTime(client.id, () {
|
||||||
|
onTapDown.call(details);
|
||||||
|
});
|
||||||
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class DesktopFileTransferScreen extends StatelessWidget {
|
|||||||
ChangeNotifierProvider.value(value: gFFI.canvasModel),
|
ChangeNotifierProvider.value(value: gFFI.canvasModel),
|
||||||
],
|
],
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
backgroundColor: isLinux ? Colors.transparent : null,
|
||||||
body: FileManagerTabPage(
|
body: FileManagerTabPage(
|
||||||
params: params,
|
params: params,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class DesktopPortForwardScreen extends StatelessWidget {
|
|||||||
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
ChangeNotifierProvider.value(value: gFFI.ffiModel),
|
||||||
],
|
],
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
backgroundColor: isLinux ? Colors.transparent : null,
|
||||||
body: PortForwardTabPage(
|
body: PortForwardTabPage(
|
||||||
params: params,
|
params: params,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -341,8 +341,9 @@ class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
|
|||||||
@protected
|
@protected
|
||||||
void handleTap() {
|
void handleTap() {
|
||||||
widget.onTap?.call();
|
widget.onTap?.call();
|
||||||
|
if (Navigator.canPop(context)) {
|
||||||
Navigator.pop<T>(context, widget.value);
|
Navigator.pop<T>(context, widget.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -445,9 +445,18 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
|||||||
dismissCallback: dismissCallback,
|
dismissCallback: dismissCallback,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
bool get isEnabled => enabled?.value ?? true;
|
||||||
|
|
||||||
RxBool get curOption;
|
RxBool get curOption;
|
||||||
Future<void> setOption(bool? option);
|
Future<void> setOption(bool? option);
|
||||||
|
|
||||||
|
tryPop(BuildContext context) {
|
||||||
|
if (dismissOnClicked && Navigator.canPop(context)) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
super.dismissCallback?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<mod_menu.PopupMenuEntry<T>> build(
|
List<mod_menu.PopupMenuEntry<T>> build(
|
||||||
BuildContext context, MenuConfig conf) {
|
BuildContext context, MenuConfig conf) {
|
||||||
@@ -481,44 +490,33 @@ abstract class MenuEntrySwitchBase<T> extends MenuEntryBase<T> {
|
|||||||
if (switchType == SwitchType.sswitch) {
|
if (switchType == SwitchType.sswitch) {
|
||||||
return Switch(
|
return Switch(
|
||||||
value: curOption.value,
|
value: curOption.value,
|
||||||
onChanged: (v) {
|
onChanged: isEnabled
|
||||||
if (super.dismissOnClicked &&
|
? (v) {
|
||||||
Navigator.canPop(context)) {
|
tryPop(context);
|
||||||
Navigator.pop(context);
|
setOption(v);
|
||||||
if (super.dismissCallback != null) {
|
}
|
||||||
super.dismissCallback!();
|
: null,
|
||||||
}
|
|
||||||
}
|
|
||||||
setOption(v);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return Checkbox(
|
return Checkbox(
|
||||||
value: curOption.value,
|
value: curOption.value,
|
||||||
onChanged: (v) {
|
onChanged: isEnabled
|
||||||
if (super.dismissOnClicked &&
|
? (v) {
|
||||||
Navigator.canPop(context)) {
|
tryPop(context);
|
||||||
Navigator.pop(context);
|
setOption(v);
|
||||||
if (super.dismissCallback != null) {
|
}
|
||||||
super.dismissCallback!();
|
: null,
|
||||||
}
|
|
||||||
}
|
|
||||||
setOption(v);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
))
|
))
|
||||||
])),
|
])),
|
||||||
onPressed: () {
|
onPressed: isEnabled
|
||||||
if (super.dismissOnClicked && Navigator.canPop(context)) {
|
? () {
|
||||||
Navigator.pop(context);
|
tryPop(context);
|
||||||
if (super.dismissCallback != null) {
|
setOption(!curOption.value);
|
||||||
super.dismissCallback!();
|
}
|
||||||
}
|
: null,
|
||||||
}
|
|
||||||
setOption(!curOption.value);
|
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hbb/common/widgets/audio_input.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
import 'package:flutter_hbb/common/widgets/toolbar.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_hbb/models/state_model.dart';
|
import 'package:flutter_hbb/models/state_model.dart';
|
||||||
import 'package:flutter_hbb/models/desktop_render_texture.dart';
|
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
import 'package:flutter_hbb/plugin/widgets/desc_ui.dart';
|
||||||
@@ -26,12 +26,11 @@ import './popup_menu.dart';
|
|||||||
import './kb_layout_type_chooser.dart';
|
import './kb_layout_type_chooser.dart';
|
||||||
|
|
||||||
class ToolbarState {
|
class ToolbarState {
|
||||||
final kStoreKey = 'remoteMenubarState';
|
|
||||||
late RxBool show;
|
late RxBool show;
|
||||||
late RxBool _pin;
|
late RxBool _pin;
|
||||||
|
|
||||||
ToolbarState() {
|
ToolbarState() {
|
||||||
final s = bind.getLocalFlutterOption(k: kStoreKey);
|
final s = bind.getLocalFlutterOption(k: kOptionRemoteMenubarState);
|
||||||
if (s.isEmpty) {
|
if (s.isEmpty) {
|
||||||
_initSet(false, false);
|
_initSet(false, false);
|
||||||
return;
|
return;
|
||||||
@@ -52,8 +51,8 @@ class ToolbarState {
|
|||||||
|
|
||||||
_initSet(bool s, bool p) {
|
_initSet(bool s, bool p) {
|
||||||
// Show remubar when connection is established.
|
// Show remubar when connection is established.
|
||||||
show =
|
show = RxBool(
|
||||||
RxBool(bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
|
bind.mainGetUserDefaultOption(key: kOptionCollapseToolbar) != 'Y');
|
||||||
_pin = RxBool(p);
|
_pin = RxBool(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +84,7 @@ class ToolbarState {
|
|||||||
|
|
||||||
_savePin() async {
|
_savePin() async {
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: kStoreKey, v: jsonEncode({'pin': _pin.value}));
|
k: kOptionRemoteMenubarState, v: jsonEncode({'pin': _pin.value}));
|
||||||
}
|
}
|
||||||
|
|
||||||
save() async {
|
save() async {
|
||||||
@@ -589,7 +588,7 @@ class _MobileActionMenu extends StatelessWidget {
|
|||||||
assetName: 'assets/actions_mobile.svg',
|
assetName: 'assets/actions_mobile.svg',
|
||||||
tooltip: 'Mobile Actions',
|
tooltip: 'Mobile Actions',
|
||||||
onPressed: () =>
|
onPressed: () =>
|
||||||
ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
|
ffi.dialogManager.mobileActionsOverlayVisible.toggle(),
|
||||||
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
|
||||||
? _ToolbarTheme.blueColor
|
? _ToolbarTheme.blueColor
|
||||||
: _ToolbarTheme.inactiveColor,
|
: _ToolbarTheme.inactiveColor,
|
||||||
@@ -615,14 +614,14 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
|
bind.mainGetUserDefaultOption(key: kKeyShowMonitorsToolbar) == 'Y';
|
||||||
|
|
||||||
bool get supportIndividualWindows =>
|
bool get supportIndividualWindows =>
|
||||||
useTextureRender && ffi.ffiModel.pi.isSupportMultiDisplay;
|
!isWeb && ffi.ffiModel.pi.isSupportMultiDisplay;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => showMonitorsToolbar
|
Widget build(BuildContext context) => showMonitorsToolbar
|
||||||
? buildMultiMonitorMenu()
|
? buildMultiMonitorMenu(context)
|
||||||
: Obx(() => buildMonitorMenu());
|
: Obx(() => buildMonitorMenu(context));
|
||||||
|
|
||||||
Widget buildMonitorMenu() {
|
Widget buildMonitorMenu(BuildContext context) {
|
||||||
final width = SimpleWrapper<double>(0);
|
final width = SimpleWrapper<double>(0);
|
||||||
final monitorsIcon =
|
final monitorsIcon =
|
||||||
globalMonitorsWidget(width, Colors.white, Colors.black38);
|
globalMonitorsWidget(width, Colors.white, Colors.black38);
|
||||||
@@ -636,20 +635,23 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
menuStyle: MenuStyle(
|
menuStyle: MenuStyle(
|
||||||
padding:
|
padding:
|
||||||
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))),
|
||||||
menuChildrenGetter: () => [buildMonitorSubmenuWidget()]);
|
menuChildrenGetter: () => [buildMonitorSubmenuWidget(context)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMultiMonitorMenu() {
|
Widget buildMultiMonitorMenu(BuildContext context) {
|
||||||
return Row(children: buildMonitorList(true));
|
return Row(children: buildMonitorList(context, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildMonitorSubmenuWidget() {
|
Widget buildMonitorSubmenuWidget(BuildContext context) {
|
||||||
|
final m = Provider.of<ImageModel>(context);
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Row(children: buildMonitorList(false)),
|
Row(children: buildMonitorList(context, false)),
|
||||||
supportIndividualWindows ? Divider() : Offstage(),
|
supportIndividualWindows && m.useTextureRender ? Divider() : Offstage(),
|
||||||
supportIndividualWindows ? chooseDisplayBehavior() : Offstage(),
|
supportIndividualWindows && m.useTextureRender
|
||||||
|
? chooseDisplayBehavior()
|
||||||
|
: Offstage(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -680,7 +682,7 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
List<Widget> buildMonitorList(bool isMulti) {
|
List<Widget> buildMonitorList(BuildContext context, bool isMulti) {
|
||||||
final List<Widget> monitorList = [];
|
final List<Widget> monitorList = [];
|
||||||
final pi = ffi.ffiModel.pi;
|
final pi = ffi.ffiModel.pi;
|
||||||
|
|
||||||
@@ -735,7 +737,10 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
for (int i = 0; i < pi.displays.length; i++) {
|
for (int i = 0; i < pi.displays.length; i++) {
|
||||||
monitorList.add(buildMonitorButton(i));
|
monitorList.add(buildMonitorButton(i));
|
||||||
}
|
}
|
||||||
if (supportIndividualWindows && pi.displays.length > 1) {
|
final m = Provider.of<ImageModel>(context);
|
||||||
|
if (supportIndividualWindows &&
|
||||||
|
m.useTextureRender &&
|
||||||
|
pi.displays.length > 1) {
|
||||||
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
monitorList.add(buildMonitorButton(kAllDisplayValue));
|
||||||
}
|
}
|
||||||
return monitorList;
|
return monitorList;
|
||||||
@@ -818,7 +823,12 @@ class _MonitorMenu extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
RxInt display = CurrentDisplayState.find(id);
|
RxInt display = CurrentDisplayState.find(id);
|
||||||
if (display.value != i) {
|
if (display.value != i) {
|
||||||
if (isChooseDisplayToOpenInNewWindow(pi, ffi.sessionId)) {
|
final isChooseDisplayToOpenInNewWindow = pi.isSupportMultiDisplay &&
|
||||||
|
bind.mainGetUseTextureRender() &&
|
||||||
|
bind.sessionGetDisplaysAsIndividualWindows(
|
||||||
|
sessionId: ffi.sessionId) ==
|
||||||
|
'Y';
|
||||||
|
if (isChooseDisplayToOpenInNewWindow) {
|
||||||
openMonitorInNewTabOrWindow(i, ffi.id, pi);
|
openMonitorInNewTabOrWindow(i, ffi.id, pi);
|
||||||
} else {
|
} else {
|
||||||
openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
|
openMonitorInTheSameTab(i, ffi, pi, updateCursorPos: !isMulti);
|
||||||
@@ -1045,7 +1055,6 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
_screenAdjustor.updateScreen();
|
_screenAdjustor.updateScreen();
|
||||||
|
|
||||||
menuChildrenGetter() {
|
menuChildrenGetter() {
|
||||||
final menuChildren = <Widget>[
|
final menuChildren = <Widget>[
|
||||||
_screenAdjustor.adjustWindow(context),
|
_screenAdjustor.adjustWindow(context),
|
||||||
@@ -1058,11 +1067,17 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
screenAdjustor: _screenAdjustor,
|
screenAdjustor: _screenAdjustor,
|
||||||
),
|
),
|
||||||
// We may add this feature if it is needed and we have an EV certificate.
|
if (pi.isRustDeskIdd)
|
||||||
// _VirtualDisplayMenu(
|
_RustDeskVirtualDisplayMenu(
|
||||||
// id: widget.id,
|
id: widget.id,
|
||||||
// ffi: widget.ffi,
|
ffi: widget.ffi,
|
||||||
// ),
|
),
|
||||||
|
if (pi.isAmyuniIdd)
|
||||||
|
_AmyuniVirtualDisplayMenu(
|
||||||
|
id: widget.id,
|
||||||
|
ffi: widget.ffi,
|
||||||
|
),
|
||||||
|
cursorToggles(),
|
||||||
Divider(),
|
Divider(),
|
||||||
toggles(),
|
toggles(),
|
||||||
];
|
];
|
||||||
@@ -1207,6 +1222,25 @@ class _DisplayMenuState extends State<_DisplayMenu> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cursorToggles() {
|
||||||
|
return futureBuilder(
|
||||||
|
future: toolbarCursor(context, id, ffi),
|
||||||
|
hasData: (data) {
|
||||||
|
final v = data as List<TToggleMenu>;
|
||||||
|
if (v.isEmpty) return Offstage();
|
||||||
|
return Column(children: [
|
||||||
|
Divider(),
|
||||||
|
...v
|
||||||
|
.map((e) => CkbMenuButton(
|
||||||
|
value: e.value,
|
||||||
|
onChanged: e.onChanged,
|
||||||
|
child: e.child,
|
||||||
|
ffi: ffi))
|
||||||
|
.toList(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
toggles() {
|
toggles() {
|
||||||
return futureBuilder(
|
return futureBuilder(
|
||||||
future: toolbarDisplayToggle(context, id, ffi),
|
future: toolbarDisplayToggle(context, id, ffi),
|
||||||
@@ -1540,21 +1574,23 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VirtualDisplayMenu extends StatefulWidget {
|
class _RustDeskVirtualDisplayMenu extends StatefulWidget {
|
||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
|
|
||||||
_VirtualDisplayMenu({
|
_RustDeskVirtualDisplayMenu({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.ffi,
|
required this.ffi,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_VirtualDisplayMenu> createState() => _VirtualDisplayMenuState();
|
State<_RustDeskVirtualDisplayMenu> createState() =>
|
||||||
|
_RustDeskVirtualDisplayMenuState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
class _RustDeskVirtualDisplayMenuState
|
||||||
|
extends State<_RustDeskVirtualDisplayMenu> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -1569,7 +1605,7 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
|||||||
return Offstage();
|
return Offstage();
|
||||||
}
|
}
|
||||||
|
|
||||||
final virtualDisplays = widget.ffi.ffiModel.pi.virtualDisplays;
|
final virtualDisplays = widget.ffi.ffiModel.pi.RustDeskVirtualDisplays;
|
||||||
final privacyModeState = PrivacyModeState.find(widget.id);
|
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||||
|
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
@@ -1611,6 +1647,82 @@ class _VirtualDisplayMenuState extends State<_VirtualDisplayMenu> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _AmyuniVirtualDisplayMenu extends StatefulWidget {
|
||||||
|
final String id;
|
||||||
|
final FFI ffi;
|
||||||
|
|
||||||
|
_AmyuniVirtualDisplayMenu({
|
||||||
|
Key? key,
|
||||||
|
required this.id,
|
||||||
|
required this.ffi,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_AmyuniVirtualDisplayMenu> createState() =>
|
||||||
|
_AmiyuniVirtualDisplayMenuState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AmiyuniVirtualDisplayMenuState extends State<_AmyuniVirtualDisplayMenu> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (widget.ffi.ffiModel.pi.platform != kPeerPlatformWindows) {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
if (!widget.ffi.ffiModel.pi.isInstalled) {
|
||||||
|
return Offstage();
|
||||||
|
}
|
||||||
|
|
||||||
|
final count = widget.ffi.ffiModel.pi.amyuniVirtualDisplayCount;
|
||||||
|
final privacyModeState = PrivacyModeState.find(widget.id);
|
||||||
|
|
||||||
|
final children = <Widget>[
|
||||||
|
Obx(() => Row(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||||
|
? null
|
||||||
|
: () => bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: widget.ffi.sessionId, index: 0, on: false),
|
||||||
|
child: Icon(Icons.remove),
|
||||||
|
),
|
||||||
|
Text(count.toString()),
|
||||||
|
TextButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 4
|
||||||
|
? null
|
||||||
|
: () => bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: widget.ffi.sessionId, index: 0, on: true),
|
||||||
|
child: Icon(Icons.add),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
Divider(),
|
||||||
|
Obx(() => MenuButton(
|
||||||
|
onPressed: privacyModeState.isNotEmpty || count == 0
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
bind.sessionToggleVirtualDisplay(
|
||||||
|
sessionId: widget.ffi.sessionId,
|
||||||
|
index: kAllVirtualDisplay,
|
||||||
|
on: false);
|
||||||
|
},
|
||||||
|
ffi: widget.ffi,
|
||||||
|
child: Text(translate('Plug out all')),
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return _SubmenuButton(
|
||||||
|
ffi: widget.ffi,
|
||||||
|
menuChildren: children,
|
||||||
|
child: Text(translate("Virtual display")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _KeyboardMenu extends StatelessWidget {
|
class _KeyboardMenu extends StatelessWidget {
|
||||||
final String id;
|
final String id;
|
||||||
final FFI ffi;
|
final FFI ffi;
|
||||||
@@ -1773,7 +1885,7 @@ class _KeyboardMenu extends StatelessWidget {
|
|||||||
? (value) async {
|
? (value) async {
|
||||||
if (value == null) return;
|
if (value == null) return;
|
||||||
await bind.sessionToggleOption(
|
await bind.sessionToggleOption(
|
||||||
sessionId: ffi.sessionId, value: kOptionViewOnly);
|
sessionId: ffi.sessionId, value: kOptionToggleViewOnly);
|
||||||
ffiModel.setViewOnly(id, value);
|
ffiModel.setViewOnly(id, value);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -1852,32 +1964,70 @@ class _VoiceCallMenu extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
menuChildrenGetter() {
|
||||||
|
final audioInput =
|
||||||
|
AudioInput(builder: (devices, currentDevice, setDevice) {
|
||||||
|
return Column(
|
||||||
|
children: devices
|
||||||
|
.map((d) => RdoMenuButton<String>(
|
||||||
|
child: Container(
|
||||||
|
child: Text(
|
||||||
|
d,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
constraints: BoxConstraints(maxWidth: 250),
|
||||||
|
),
|
||||||
|
value: d,
|
||||||
|
groupValue: currentDevice,
|
||||||
|
onChanged: (v) {
|
||||||
|
if (v != null) setDevice(v);
|
||||||
|
},
|
||||||
|
ffi: ffi,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return [
|
||||||
|
audioInput,
|
||||||
|
Divider(),
|
||||||
|
MenuButton(
|
||||||
|
child: Text(translate('End call')),
|
||||||
|
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||||
|
ffi: ffi,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return Obx(
|
return Obx(
|
||||||
() {
|
() {
|
||||||
final String tooltip;
|
|
||||||
final String icon;
|
|
||||||
switch (ffi.chatModel.voiceCallStatus.value) {
|
switch (ffi.chatModel.voiceCallStatus.value) {
|
||||||
case VoiceCallStatus.waitingForResponse:
|
case VoiceCallStatus.waitingForResponse:
|
||||||
tooltip = "Waiting";
|
return buildCallWaiting(context);
|
||||||
icon = "assets/call_wait.svg";
|
|
||||||
break;
|
|
||||||
case VoiceCallStatus.connected:
|
case VoiceCallStatus.connected:
|
||||||
tooltip = "Disconnect";
|
return _IconSubmenuButton(
|
||||||
icon = "assets/call_end.svg";
|
tooltip: 'Voice call',
|
||||||
break;
|
svg: 'assets/voice_call.svg',
|
||||||
|
color: _ToolbarTheme.blueColor,
|
||||||
|
hoverColor: _ToolbarTheme.hoverBlueColor,
|
||||||
|
menuChildrenGetter: menuChildrenGetter,
|
||||||
|
ffi: ffi,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
return Offstage();
|
return Offstage();
|
||||||
}
|
}
|
||||||
return _IconMenuButton(
|
|
||||||
assetName: icon,
|
|
||||||
tooltip: tooltip,
|
|
||||||
onPressed: () =>
|
|
||||||
bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
|
||||||
color: _ToolbarTheme.redColor,
|
|
||||||
hoverColor: _ToolbarTheme.hoverRedColor);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildCallWaiting(BuildContext context) {
|
||||||
|
return _IconMenuButton(
|
||||||
|
assetName: "assets/call_wait.svg",
|
||||||
|
tooltip: "Waiting",
|
||||||
|
onPressed: () => bind.sessionCloseVoiceCall(sessionId: ffi.sessionId),
|
||||||
|
color: _ToolbarTheme.redColor,
|
||||||
|
hoverColor: _ToolbarTheme.hoverRedColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecordMenu extends StatelessWidget {
|
class _RecordMenu extends StatelessWidget {
|
||||||
@@ -2014,7 +2164,7 @@ class _IconSubmenuButton extends StatefulWidget {
|
|||||||
final Color hoverColor;
|
final Color hoverColor;
|
||||||
final List<Widget> Function() menuChildrenGetter;
|
final List<Widget> Function() menuChildrenGetter;
|
||||||
final MenuStyle? menuStyle;
|
final MenuStyle? menuStyle;
|
||||||
final FFI ffi;
|
final FFI? ffi;
|
||||||
final double? width;
|
final double? width;
|
||||||
|
|
||||||
_IconSubmenuButton({
|
_IconSubmenuButton({
|
||||||
@@ -2025,7 +2175,7 @@ class _IconSubmenuButton extends StatefulWidget {
|
|||||||
required this.color,
|
required this.color,
|
||||||
required this.hoverColor,
|
required this.hoverColor,
|
||||||
required this.menuChildrenGetter,
|
required this.menuChildrenGetter,
|
||||||
required this.ffi,
|
this.ffi,
|
||||||
this.menuStyle,
|
this.menuStyle,
|
||||||
this.width,
|
this.width,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
@@ -2107,13 +2257,13 @@ class MenuButton extends StatelessWidget {
|
|||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
final Widget? trailingIcon;
|
final Widget? trailingIcon;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
final FFI ffi;
|
final FFI? ffi;
|
||||||
MenuButton(
|
MenuButton(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.trailingIcon,
|
this.trailingIcon,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.ffi})
|
this.ffi})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2122,7 +2272,9 @@ class MenuButton extends StatelessWidget {
|
|||||||
key: key,
|
key: key,
|
||||||
onPressed: onPressed != null
|
onPressed: onPressed != null
|
||||||
? () {
|
? () {
|
||||||
_menuDismissCallback(ffi);
|
if (ffi != null) {
|
||||||
|
_menuDismissCallback(ffi!);
|
||||||
|
}
|
||||||
onPressed?.call();
|
onPressed?.call();
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -2135,13 +2287,13 @@ class CkbMenuButton extends StatelessWidget {
|
|||||||
final bool? value;
|
final bool? value;
|
||||||
final ValueChanged<bool?>? onChanged;
|
final ValueChanged<bool?>? onChanged;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
final FFI ffi;
|
final FFI? ffi;
|
||||||
const CkbMenuButton(
|
const CkbMenuButton(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required this.value,
|
required this.value,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.ffi})
|
this.ffi})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -2152,7 +2304,9 @@ class CkbMenuButton extends StatelessWidget {
|
|||||||
child: child,
|
child: child,
|
||||||
onChanged: onChanged != null
|
onChanged: onChanged != null
|
||||||
? (bool? value) {
|
? (bool? value) {
|
||||||
_menuDismissCallback(ffi);
|
if (ffi != null) {
|
||||||
|
_menuDismissCallback(ffi!);
|
||||||
|
}
|
||||||
onChanged?.call(value);
|
onChanged?.call(value);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -2165,13 +2319,13 @@ class RdoMenuButton<T> extends StatelessWidget {
|
|||||||
final T? groupValue;
|
final T? groupValue;
|
||||||
final ValueChanged<T?>? onChanged;
|
final ValueChanged<T?>? onChanged;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
final FFI ffi;
|
final FFI? ffi;
|
||||||
const RdoMenuButton({
|
const RdoMenuButton({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.value,
|
required this.value,
|
||||||
required this.groupValue,
|
required this.groupValue,
|
||||||
required this.child,
|
required this.child,
|
||||||
required this.ffi,
|
this.ffi,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@@ -2183,7 +2337,9 @@ class RdoMenuButton<T> extends StatelessWidget {
|
|||||||
child: child,
|
child: child,
|
||||||
onChanged: onChanged != null
|
onChanged: onChanged != null
|
||||||
? (T? value) {
|
? (T? value) {
|
||||||
_menuDismissCallback(ffi);
|
if (ffi != null) {
|
||||||
|
_menuDismissCallback(ffi!);
|
||||||
|
}
|
||||||
onChanged?.call(value);
|
onChanged?.call(value);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -2227,18 +2383,18 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
final confLeft = double.tryParse(
|
final confLeft = double.tryParse(
|
||||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-left'));
|
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragLeft));
|
||||||
if (confLeft == null) {
|
if (confLeft == null) {
|
||||||
bind.mainSetLocalOption(
|
bind.mainSetLocalOption(
|
||||||
key: 'remote-menubar-drag-left', value: left.toString());
|
key: kOptionRemoteMenubarDragLeft, value: left.toString());
|
||||||
} else {
|
} else {
|
||||||
left = confLeft;
|
left = confLeft;
|
||||||
}
|
}
|
||||||
final confRight = double.tryParse(
|
final confRight = double.tryParse(
|
||||||
bind.mainGetLocalOption(key: 'remote-menubar-drag-right'));
|
bind.mainGetLocalOption(key: kOptionRemoteMenubarDragRight));
|
||||||
if (confRight == null) {
|
if (confRight == null) {
|
||||||
bind.mainSetLocalOption(
|
bind.mainSetLocalOption(
|
||||||
key: 'remote-menubar-drag-right', value: right.toString());
|
key: kOptionRemoteMenubarDragRight, value: right.toString());
|
||||||
} else {
|
} else {
|
||||||
right = confRight;
|
right = confRight;
|
||||||
}
|
}
|
||||||
@@ -2309,7 +2465,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
Obx(() => Offstage(
|
if (!isMacOS) Obx(() => Offstage(
|
||||||
offstage: isFullscreen.isFalse,
|
offstage: isFullscreen.isFalse,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () => widget.setMinimize(),
|
onPressed: () => widget.setMinimize(),
|
||||||
@@ -2370,10 +2526,11 @@ class InputModeMenu {
|
|||||||
|
|
||||||
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
|
_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos();
|
||||||
|
|
||||||
Widget _buildPointerTrackWidget(Widget child, FFI ffi) {
|
Widget _buildPointerTrackWidget(Widget child, FFI? ffi) {
|
||||||
return Listener(
|
return Listener(
|
||||||
onPointerHover: (PointerHoverEvent e) =>
|
onPointerHover: (PointerHoverEvent e) => {
|
||||||
ffi.inputModel.lastMousePos = e.position,
|
if (ffi != null) {ffi.inputModel.lastMousePos = e.position}
|
||||||
|
},
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import 'package:get/get.dart';
|
|||||||
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
|
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
|
||||||
import 'package:scroll_pos/scroll_pos.dart';
|
import 'package:scroll_pos/scroll_pos.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'package:visibility_detector/visibility_detector.dart';
|
||||||
|
|
||||||
import '../../utils/multi_window_manager.dart';
|
import '../../utils/multi_window_manager.dart';
|
||||||
|
|
||||||
@@ -256,6 +257,8 @@ class DesktopTab extends StatelessWidget {
|
|||||||
late final DesktopTabType tabType;
|
late final DesktopTabType tabType;
|
||||||
late final bool isMainWindow;
|
late final bool isMainWindow;
|
||||||
|
|
||||||
|
final RxList<String> invisibleTabKeys = RxList.empty();
|
||||||
|
|
||||||
DesktopTab({
|
DesktopTab({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
@@ -320,18 +323,18 @@ class DesktopTab extends StatelessWidget {
|
|||||||
return buildRemoteBlock(
|
return buildRemoteBlock(
|
||||||
child: child,
|
child: child,
|
||||||
use: () async {
|
use: () async {
|
||||||
var access_mode = await bind.mainGetOption(key: 'access-mode');
|
var access_mode = await bind.mainGetOption(key: kOptionAccessMode);
|
||||||
var option = option2bool(
|
var option = option2bool(
|
||||||
'allow-remote-config-modification',
|
kOptionAllowRemoteConfigModification,
|
||||||
await bind.mainGetOption(
|
await bind.mainGetOption(
|
||||||
key: 'allow-remote-config-modification'));
|
key: kOptionAllowRemoteConfigModification));
|
||||||
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
return access_mode == 'view' || (access_mode.isEmpty && !option);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _tabWidgets = [];
|
List<Widget> _tabWidgets = [];
|
||||||
Widget _buildPageView() {
|
Widget _buildPageView() {
|
||||||
return _buildBlock(
|
final child = _buildBlock(
|
||||||
child: Obx(() => PageView(
|
child: Obx(() => PageView(
|
||||||
controller: state.value.pageController,
|
controller: state.value.pageController,
|
||||||
physics: NeverScrollableScrollPhysics(),
|
physics: NeverScrollableScrollPhysics(),
|
||||||
@@ -355,6 +358,11 @@ class DesktopTab extends StatelessWidget {
|
|||||||
return newList;
|
return newList;
|
||||||
}
|
}
|
||||||
}())));
|
}())));
|
||||||
|
if (tabType == DesktopTabType.remoteScreen) {
|
||||||
|
return Container(color: kColorCanvas, child: child);
|
||||||
|
} else {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check whether to show ListView
|
/// Check whether to show ListView
|
||||||
@@ -388,6 +396,16 @@ class DesktopTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
onPanStart: (_) => startDragging(isMainWindow),
|
onPanStart: (_) => startDragging(isMainWindow),
|
||||||
|
onPanCancel: () {
|
||||||
|
if (isMacOS) {
|
||||||
|
setMovable(isMainWindow, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPanEnd: (_) {
|
||||||
|
if (isMacOS) {
|
||||||
|
setMovable(isMainWindow, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Offstage(
|
Offstage(
|
||||||
@@ -430,6 +448,7 @@ class DesktopTab extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: _ListView(
|
child: _ListView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
invisibleTabKeys: invisibleTabKeys,
|
||||||
tabBuilder: tabBuilder,
|
tabBuilder: tabBuilder,
|
||||||
tabMenuBuilder: tabMenuBuilder,
|
tabMenuBuilder: tabMenuBuilder,
|
||||||
labelGetter: labelGetter,
|
labelGetter: labelGetter,
|
||||||
@@ -448,12 +467,14 @@ class DesktopTab extends StatelessWidget {
|
|||||||
tabType: tabType,
|
tabType: tabType,
|
||||||
state: state,
|
state: state,
|
||||||
tabController: controller,
|
tabController: controller,
|
||||||
|
invisibleTabKeys: invisibleTabKeys,
|
||||||
tail: tail,
|
tail: tail,
|
||||||
showMinimize: showMinimize,
|
showMinimize: showMinimize,
|
||||||
showMaximize: showMaximize,
|
showMaximize: showMaximize,
|
||||||
showClose: showClose,
|
showClose: showClose,
|
||||||
onClose: onWindowCloseButton,
|
onClose: onWindowCloseButton,
|
||||||
)
|
labelGetter: labelGetter,
|
||||||
|
).paddingOnly(left: 10)
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -471,17 +492,22 @@ class WindowActionPanel extends StatefulWidget {
|
|||||||
final Widget? tail;
|
final Widget? tail;
|
||||||
final Future<bool> Function()? onClose;
|
final Future<bool> Function()? onClose;
|
||||||
|
|
||||||
|
final RxList<String> invisibleTabKeys;
|
||||||
|
final LabelGetter? labelGetter;
|
||||||
|
|
||||||
const WindowActionPanel(
|
const WindowActionPanel(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required this.isMainWindow,
|
required this.isMainWindow,
|
||||||
required this.tabType,
|
required this.tabType,
|
||||||
required this.state,
|
required this.state,
|
||||||
required this.tabController,
|
required this.tabController,
|
||||||
|
required this.invisibleTabKeys,
|
||||||
this.tail,
|
this.tail,
|
||||||
this.showMinimize = true,
|
this.showMinimize = true,
|
||||||
this.showMaximize = true,
|
this.showMaximize = true,
|
||||||
this.showClose = true,
|
this.showClose = true,
|
||||||
this.onClose})
|
this.onClose,
|
||||||
|
this.labelGetter})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -537,6 +563,16 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowFocus() {
|
||||||
|
stateGlobal.isFocused.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onWindowBlur() {
|
||||||
|
stateGlobal.isFocused.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onWindowMinimize() {
|
void onWindowMinimize() {
|
||||||
stateGlobal.setMinimized(true);
|
stateGlobal.setMinimized(true);
|
||||||
@@ -620,14 +656,12 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
}
|
}
|
||||||
// macOS specific workaround, the window is not hiding when in fullscreen.
|
// macOS specific workaround, the window is not hiding when in fullscreen.
|
||||||
if (isMacOS && await windowManager.isFullScreen()) {
|
if (isMacOS && await windowManager.isFullScreen()) {
|
||||||
stateGlobal.closeOnFullscreen ??= true;
|
|
||||||
await windowManager.setFullScreen(false);
|
await windowManager.setFullScreen(false);
|
||||||
await macOSWindowClose(
|
await macOSWindowClose(
|
||||||
() async => await windowManager.isFullScreen(),
|
() async => await windowManager.isFullScreen(),
|
||||||
mainWindowClose,
|
mainWindowClose,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
stateGlobal.closeOnFullscreen ??= false;
|
|
||||||
await mainWindowClose();
|
await mainWindowClose();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -639,7 +673,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
|
|
||||||
if (await widget.onClose?.call() ?? true) {
|
if (await widget.onClose?.call() ?? true) {
|
||||||
if (await controller.isFullScreen()) {
|
if (await controller.isFullScreen()) {
|
||||||
stateGlobal.closeOnFullscreen ??= true;
|
|
||||||
await controller.setFullscreen(false);
|
await controller.setFullscreen(false);
|
||||||
stateGlobal.setFullscreen(false, procWnd: false);
|
stateGlobal.setFullscreen(false, procWnd: false);
|
||||||
await macOSWindowClose(
|
await macOSWindowClose(
|
||||||
@@ -647,7 +680,6 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
() async => await notMainWindowClose(controller),
|
() async => await notMainWindowClose(controller),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
stateGlobal.closeOnFullscreen ??= false;
|
|
||||||
await notMainWindowClose(controller);
|
await notMainWindowClose(controller);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -658,11 +690,34 @@ class WindowActionPanelState extends State<WindowActionPanel>
|
|||||||
super.onWindowClose();
|
super.onWindowClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool showTabDowndown() {
|
||||||
|
return widget.tabController.state.value.tabs.length > 1 &&
|
||||||
|
(widget.tabController.tabType == DesktopTabType.remoteScreen ||
|
||||||
|
widget.tabController.tabType == DesktopTabType.fileTransfer ||
|
||||||
|
widget.tabController.tabType == DesktopTabType.portForward ||
|
||||||
|
widget.tabController.tabType == DesktopTabType.cm);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> existingInvisibleTab() {
|
||||||
|
return widget.invisibleTabKeys
|
||||||
|
.where((key) =>
|
||||||
|
widget.tabController.state.value.tabs.any((tab) => tab.key == key))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
|
Obx(() => Offstage(
|
||||||
|
offstage:
|
||||||
|
!(showTabDowndown() && existingInvisibleTab().isNotEmpty),
|
||||||
|
child: _TabDropDownButton(
|
||||||
|
controller: widget.tabController,
|
||||||
|
labelGetter: widget.labelGetter,
|
||||||
|
tabkeys: existingInvisibleTab()),
|
||||||
|
)),
|
||||||
Offstage(offstage: widget.tail == null, child: widget.tail),
|
Offstage(offstage: widget.tail == null, child: widget.tail),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage: kUseCompatibleUiMode,
|
offstage: kUseCompatibleUiMode,
|
||||||
@@ -741,6 +796,14 @@ void startDragging(bool isMainWindow) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setMovable(bool isMainWindow, bool movable) {
|
||||||
|
if (isMainWindow) {
|
||||||
|
windowManager.setMovable(movable);
|
||||||
|
} else {
|
||||||
|
WindowController.fromWindowId(kWindowId!).setMovable(movable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// return true -> window will be maximize
|
/// return true -> window will be maximize
|
||||||
/// return false -> window will be unmaximize
|
/// return false -> window will be unmaximize
|
||||||
Future<bool> toggleMaximize(bool isMainWindow) async {
|
Future<bool> toggleMaximize(bool isMainWindow) async {
|
||||||
@@ -768,9 +831,9 @@ Future<bool> closeConfirmDialog() async {
|
|||||||
var confirm = true;
|
var confirm = true;
|
||||||
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
|
||||||
submit() {
|
submit() {
|
||||||
final opt = "enable-confirm-closing-tabs";
|
String value = bool2option(kOptionEnableConfirmClosingTabs, confirm);
|
||||||
String value = bool2option(opt, confirm);
|
bind.mainSetLocalOption(
|
||||||
bind.mainSetLocalOption(key: opt, value: value);
|
key: kOptionEnableConfirmClosingTabs, value: value);
|
||||||
close(true);
|
close(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -814,6 +877,7 @@ Future<bool> closeConfirmDialog() async {
|
|||||||
|
|
||||||
class _ListView extends StatelessWidget {
|
class _ListView extends StatelessWidget {
|
||||||
final DesktopTabController controller;
|
final DesktopTabController controller;
|
||||||
|
final RxList<String> invisibleTabKeys;
|
||||||
|
|
||||||
final TabBuilder? tabBuilder;
|
final TabBuilder? tabBuilder;
|
||||||
final TabMenuBuilder? tabMenuBuilder;
|
final TabMenuBuilder? tabMenuBuilder;
|
||||||
@@ -825,8 +889,9 @@ class _ListView extends StatelessWidget {
|
|||||||
|
|
||||||
Rx<DesktopTabState> get state => controller.state;
|
Rx<DesktopTabState> get state => controller.state;
|
||||||
|
|
||||||
const _ListView({
|
_ListView({
|
||||||
required this.controller,
|
required this.controller,
|
||||||
|
required this.invisibleTabKeys,
|
||||||
this.tabBuilder,
|
this.tabBuilder,
|
||||||
this.tabMenuBuilder,
|
this.tabMenuBuilder,
|
||||||
this.labelGetter,
|
this.labelGetter,
|
||||||
@@ -846,6 +911,19 @@ class _ListView extends StatelessWidget {
|
|||||||
controller.tabType == DesktopTabType.install;
|
controller.tabType == DesktopTabType.install;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onVisibilityChanged(VisibilityInfo info) {
|
||||||
|
final key = (info.key as ValueKey).value;
|
||||||
|
if (info.visibleFraction < 0.75) {
|
||||||
|
if (!invisibleTabKeys.contains(key)) {
|
||||||
|
invisibleTabKeys.add(key);
|
||||||
|
}
|
||||||
|
invisibleTabKeys.removeWhere((key) =>
|
||||||
|
controller.state.value.tabs.where((e) => e.key == key).isEmpty);
|
||||||
|
} else {
|
||||||
|
invisibleTabKeys.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() => ListView(
|
return Obx(() => ListView(
|
||||||
@@ -858,35 +936,45 @@ class _ListView extends StatelessWidget {
|
|||||||
: state.value.tabs.asMap().entries.map((e) {
|
: state.value.tabs.asMap().entries.map((e) {
|
||||||
final index = e.key;
|
final index = e.key;
|
||||||
final tab = e.value;
|
final tab = e.value;
|
||||||
return _Tab(
|
final label = labelGetter == null
|
||||||
|
? Rx<String>(tab.label)
|
||||||
|
: labelGetter!(tab.label);
|
||||||
|
final child = VisibilityDetector(
|
||||||
key: ValueKey(tab.key),
|
key: ValueKey(tab.key),
|
||||||
index: index,
|
onVisibilityChanged: onVisibilityChanged,
|
||||||
tabInfoKey: tab.key,
|
child: _Tab(
|
||||||
label: labelGetter == null
|
key: ValueKey(tab.key),
|
||||||
? Rx<String>(tab.label)
|
index: index,
|
||||||
: labelGetter!(tab.label),
|
tabInfoKey: tab.key,
|
||||||
selectedIcon: tab.selectedIcon,
|
label: label,
|
||||||
unselectedIcon: tab.unselectedIcon,
|
tabType: controller.tabType,
|
||||||
closable: tab.closable,
|
selectedIcon: tab.selectedIcon,
|
||||||
selected: state.value.selected,
|
unselectedIcon: tab.unselectedIcon,
|
||||||
onClose: () {
|
closable: tab.closable,
|
||||||
if (tab.onTabCloseButton != null) {
|
selected: state.value.selected,
|
||||||
tab.onTabCloseButton!();
|
onClose: () {
|
||||||
} else {
|
if (tab.onTabCloseButton != null) {
|
||||||
controller.remove(index);
|
tab.onTabCloseButton!();
|
||||||
}
|
} else {
|
||||||
},
|
controller.remove(index);
|
||||||
onTap: () {
|
}
|
||||||
controller.jumpTo(index);
|
},
|
||||||
tab.onTap?.call();
|
onTap: () {
|
||||||
},
|
controller.jumpTo(index);
|
||||||
tabBuilder: tabBuilder,
|
tab.onTap?.call();
|
||||||
tabMenuBuilder: tabMenuBuilder,
|
},
|
||||||
maxLabelWidth: maxLabelWidth,
|
tabBuilder: tabBuilder,
|
||||||
selectedTabBackgroundColor: selectedTabBackgroundColor ??
|
tabMenuBuilder: tabMenuBuilder,
|
||||||
MyTheme.tabbar(context).selectedTabBackgroundColor,
|
maxLabelWidth: maxLabelWidth,
|
||||||
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
|
selectedTabBackgroundColor: selectedTabBackgroundColor ??
|
||||||
selectedBorderColor: selectedBorderColor,
|
MyTheme.tabbar(context).selectedTabBackgroundColor,
|
||||||
|
unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
|
||||||
|
selectedBorderColor: selectedBorderColor,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return GestureDetector(
|
||||||
|
onPanStart: (e) {},
|
||||||
|
child: child,
|
||||||
);
|
);
|
||||||
}).toList()));
|
}).toList()));
|
||||||
}
|
}
|
||||||
@@ -896,6 +984,7 @@ class _Tab extends StatefulWidget {
|
|||||||
final int index;
|
final int index;
|
||||||
final String tabInfoKey;
|
final String tabInfoKey;
|
||||||
final Rx<String> label;
|
final Rx<String> label;
|
||||||
|
final DesktopTabType tabType;
|
||||||
final IconData? selectedIcon;
|
final IconData? selectedIcon;
|
||||||
final IconData? unselectedIcon;
|
final IconData? unselectedIcon;
|
||||||
final bool closable;
|
final bool closable;
|
||||||
@@ -914,6 +1003,7 @@ class _Tab extends StatefulWidget {
|
|||||||
required this.index,
|
required this.index,
|
||||||
required this.tabInfoKey,
|
required this.tabInfoKey,
|
||||||
required this.label,
|
required this.label,
|
||||||
|
required this.tabType,
|
||||||
this.selectedIcon,
|
this.selectedIcon,
|
||||||
this.unselectedIcon,
|
this.unselectedIcon,
|
||||||
this.tabBuilder,
|
this.tabBuilder,
|
||||||
@@ -953,7 +1043,9 @@ class _TabState extends State<_Tab> with RestorationMixin {
|
|||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
|
constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200),
|
||||||
child: Tooltip(
|
child: Tooltip(
|
||||||
message: translate(widget.label.value),
|
message: widget.tabType == DesktopTabType.main
|
||||||
|
? ''
|
||||||
|
: translate(widget.label.value),
|
||||||
child: Text(
|
child: Text(
|
||||||
translate(widget.label.value),
|
translate(widget.label.value),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -1110,6 +1202,7 @@ class ActionIcon extends StatefulWidget {
|
|||||||
final String? message;
|
final String? message;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final GestureTapCallback? onTap;
|
final GestureTapCallback? onTap;
|
||||||
|
final GestureTapDownCallback? onTapDown;
|
||||||
final bool isClose;
|
final bool isClose;
|
||||||
final double iconSize;
|
final double iconSize;
|
||||||
final double boxSize;
|
final double boxSize;
|
||||||
@@ -1119,6 +1212,7 @@ class ActionIcon extends StatefulWidget {
|
|||||||
this.message,
|
this.message,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
|
this.onTapDown,
|
||||||
this.isClose = false,
|
this.isClose = false,
|
||||||
this.iconSize = _kActionIconSize,
|
this.iconSize = _kActionIconSize,
|
||||||
this.boxSize = _kTabBarHeight - 1})
|
this.boxSize = _kTabBarHeight - 1})
|
||||||
@@ -1148,6 +1242,7 @@ class _ActionIconState extends State<ActionIcon> {
|
|||||||
: MyTheme.tabbar(context).hoverColor,
|
: MyTheme.tabbar(context).hoverColor,
|
||||||
onHover: (value) => hover.value = value,
|
onHover: (value) => hover.value = value,
|
||||||
onTap: widget.onTap,
|
onTap: widget.onTap,
|
||||||
|
onTapDown: widget.onTapDown,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: widget.boxSize,
|
height: widget.boxSize,
|
||||||
width: widget.boxSize,
|
width: widget.boxSize,
|
||||||
@@ -1188,6 +1283,103 @@ class AddButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _TabDropDownButton extends StatefulWidget {
|
||||||
|
final DesktopTabController controller;
|
||||||
|
final List<String> tabkeys;
|
||||||
|
final LabelGetter? labelGetter;
|
||||||
|
|
||||||
|
const _TabDropDownButton(
|
||||||
|
{required this.controller, required this.tabkeys, this.labelGetter});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_TabDropDownButton> createState() => _TabDropDownButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabDropDownButtonState extends State<_TabDropDownButton> {
|
||||||
|
var position = RelativeRect.fromLTRB(0, 0, 0, 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
List<String> sortedKeys = widget.controller.state.value.tabs
|
||||||
|
.where((e) => widget.tabkeys.contains(e.key))
|
||||||
|
.map((e) => e.key)
|
||||||
|
.toList();
|
||||||
|
return ActionIcon(
|
||||||
|
onTapDown: (details) {
|
||||||
|
final x = details.globalPosition.dx;
|
||||||
|
final y = details.globalPosition.dy;
|
||||||
|
position = RelativeRect.fromLTRB(x, y, x, y);
|
||||||
|
},
|
||||||
|
icon: Icons.arrow_drop_down,
|
||||||
|
onTap: () {
|
||||||
|
showMenu(
|
||||||
|
context: context,
|
||||||
|
position: position,
|
||||||
|
items: sortedKeys.map((e) {
|
||||||
|
var label = e;
|
||||||
|
final tabInfo = widget.controller.state.value.tabs
|
||||||
|
.firstWhereOrNull((element) => element.key == e);
|
||||||
|
if (tabInfo != null) {
|
||||||
|
label = tabInfo.label;
|
||||||
|
}
|
||||||
|
if (widget.labelGetter != null) {
|
||||||
|
label = widget.labelGetter!(e).value;
|
||||||
|
}
|
||||||
|
var index = widget.controller.state.value.tabs
|
||||||
|
.indexWhere((t) => t.key == e);
|
||||||
|
label = '${index + 1}. $label';
|
||||||
|
final menuHover = false.obs;
|
||||||
|
final btnHover = false.obs;
|
||||||
|
return PopupMenuItem<String>(
|
||||||
|
value: e,
|
||||||
|
height: 32,
|
||||||
|
onTap: () {
|
||||||
|
widget.controller.jumpToByKey(e);
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: MouseRegion(
|
||||||
|
onHover: (event) => setState(() => menuHover.value = true),
|
||||||
|
onExit: (event) => setState(() => menuHover.value = false),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: InkWell(child: Text(label)),
|
||||||
|
),
|
||||||
|
Obx(
|
||||||
|
() => Offstage(
|
||||||
|
offstage: !(tabInfo?.onTabCloseButton != null &&
|
||||||
|
menuHover.value),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
tabInfo?.onTabCloseButton?.call();
|
||||||
|
if (Navigator.of(context).canPop()) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
onHover: (event) =>
|
||||||
|
setState(() => btnHover.value = true),
|
||||||
|
onExit: (event) =>
|
||||||
|
setState(() => btnHover.value = false),
|
||||||
|
child: Icon(Icons.close,
|
||||||
|
color:
|
||||||
|
btnHover.value ? Colors.red : null))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
class TabbarTheme extends ThemeExtension<TabbarTheme> {
|
||||||
final Color? selectedTabIconColor;
|
final Color? selectedTabIconColor;
|
||||||
final Color? unSelectedTabIconColor;
|
final Color? unSelectedTabIconColor;
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ Future<void> main(List<String> args) async {
|
|||||||
desktopType = DesktopType.main;
|
desktopType = DesktopType.main;
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
windowManager.setPreventClose(true);
|
windowManager.setPreventClose(true);
|
||||||
|
windowManager.setMovable(false);
|
||||||
runMainApp(true);
|
runMainApp(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,7 +145,8 @@ void runMainApp(bool startService) async {
|
|||||||
}
|
}
|
||||||
windowManager.setOpacity(1);
|
windowManager.setOpacity(1);
|
||||||
windowManager.setTitle(getWindowName());
|
windowManager.setTitle(getWindowName());
|
||||||
windowManager.setResizable(!bind.isIncomingOnly());
|
// Do not use `windowManager.setResizable()` here.
|
||||||
|
setResizable(!bind.isIncomingOnly());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +168,7 @@ void runMultiWindow(
|
|||||||
final title = getWindowName();
|
final title = getWindowName();
|
||||||
// set prevent close to true, we handle close event manually
|
// set prevent close to true, we handle close event manually
|
||||||
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
WindowController.fromWindowId(kWindowId!).setPreventClose(true);
|
||||||
|
WindowController.fromWindowId(kWindowId!).setMovable(false);
|
||||||
late Widget widget;
|
late Widget widget;
|
||||||
switch (appType) {
|
switch (appType) {
|
||||||
case kAppTypeDesktopRemote:
|
case kAppTypeDesktopRemote:
|
||||||
@@ -238,7 +241,7 @@ void runConnectionManagerScreen() async {
|
|||||||
} else {
|
} else {
|
||||||
await showCmWindow(isStartup: true);
|
await showCmWindow(isStartup: true);
|
||||||
}
|
}
|
||||||
windowManager.setResizable(false);
|
setResizable(false);
|
||||||
// Start the uni links handler and redirect links to Native, not for Flutter.
|
// Start the uni links handler and redirect links to Native, not for Flutter.
|
||||||
listenUniLinks(handleByFlutter: false);
|
listenUniLinks(handleByFlutter: false);
|
||||||
}
|
}
|
||||||
@@ -248,7 +251,7 @@ bool _isCmReadyToShow = false;
|
|||||||
showCmWindow({bool isStartup = false}) async {
|
showCmWindow({bool isStartup = false}) async {
|
||||||
if (isStartup) {
|
if (isStartup) {
|
||||||
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
WindowOptions windowOptions = getHiddenTitleBarWindowOptions(
|
||||||
size: kConnectionManagerWindowSizeClosedChat);
|
size: kConnectionManagerWindowSizeClosedChat, alwaysOnTop: true);
|
||||||
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
await windowManager.waitUntilReadyToShow(windowOptions, null);
|
||||||
bind.mainHideDocker();
|
bind.mainHideDocker();
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
@@ -337,12 +340,11 @@ void runInstallPage() async {
|
|||||||
windowManager.focus();
|
windowManager.focus();
|
||||||
windowManager.setOpacity(1);
|
windowManager.setOpacity(1);
|
||||||
windowManager.setAlignment(Alignment.center); // ensure
|
windowManager.setAlignment(Alignment.center); // ensure
|
||||||
windowManager.setTitle(getWindowName());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
WindowOptions getHiddenTitleBarWindowOptions(
|
WindowOptions getHiddenTitleBarWindowOptions(
|
||||||
{Size? size, bool center = false}) {
|
{Size? size, bool center = false, bool? alwaysOnTop}) {
|
||||||
var defaultTitleBarStyle = TitleBarStyle.hidden;
|
var defaultTitleBarStyle = TitleBarStyle.hidden;
|
||||||
// we do not hide titlebar on win7 because of the frame overflow.
|
// we do not hide titlebar on win7 because of the frame overflow.
|
||||||
if (kUseCompatibleUiMode) {
|
if (kUseCompatibleUiMode) {
|
||||||
@@ -354,6 +356,7 @@ WindowOptions getHiddenTitleBarWindowOptions(
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
skipTaskbar: false,
|
skipTaskbar: false,
|
||||||
titleBarStyle: defaultTitleBarStyle,
|
titleBarStyle: defaultTitleBarStyle,
|
||||||
|
alwaysOnTop: alwaysOnTop,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +443,9 @@ class _AppState extends State<App> {
|
|||||||
if (isDesktop && desktopType == DesktopType.main) {
|
if (isDesktop && desktopType == DesktopType.main) {
|
||||||
child = keyListenerBuilder(context, child);
|
child = keyListenerBuilder(context, child);
|
||||||
}
|
}
|
||||||
|
if (isLinux) {
|
||||||
|
child = buildVirtualWindowFrame(context, child);
|
||||||
|
}
|
||||||
return child;
|
return child;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -65,10 +65,12 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
Timer(const Duration(seconds: 1), () async {
|
if (!bind.isCustomClient()) {
|
||||||
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
Timer(const Duration(seconds: 1), () async {
|
||||||
if (_updateUrl.isNotEmpty) setState(() {});
|
_updateUrl = await bind.mainGetSoftwareUpdateUrl();
|
||||||
});
|
if (_updateUrl.isNotEmpty) setState(() {});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_idController.addListener(() {
|
_idController.addListener(() {
|
||||||
@@ -84,7 +86,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
|
|||||||
slivers: [
|
slivers: [
|
||||||
SliverList(
|
SliverList(
|
||||||
delegate: SliverChildListDelegate([
|
delegate: SliverChildListDelegate([
|
||||||
_buildUpdateUI(),
|
if (!bind.isCustomClient()) _buildUpdateUI(),
|
||||||
_buildRemoteIDTextField(),
|
_buildRemoteIDTextField(),
|
||||||
])),
|
])),
|
||||||
SliverFillRemaining(
|
SliverFillRemaining(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter_hbb/mobile/pages/settings_page.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../common.dart';
|
import '../../common.dart';
|
||||||
import '../../common/widgets/chat_page.dart';
|
import '../../common/widgets/chat_page.dart';
|
||||||
|
import '../../models/platform_model.dart';
|
||||||
import 'connection_page.dart';
|
import 'connection_page.dart';
|
||||||
|
|
||||||
abstract class PageShape extends Widget {
|
abstract class PageShape extends Widget {
|
||||||
@@ -25,8 +26,9 @@ class HomePageState extends State<HomePage> {
|
|||||||
var _selectedIndex = 0;
|
var _selectedIndex = 0;
|
||||||
int get selectedIndex => _selectedIndex;
|
int get selectedIndex => _selectedIndex;
|
||||||
final List<PageShape> _pages = [];
|
final List<PageShape> _pages = [];
|
||||||
|
int _chatPageTabIndex = -1;
|
||||||
bool get isChatPageCurrentTab => isAndroid
|
bool get isChatPageCurrentTab => isAndroid
|
||||||
? _selectedIndex == 1
|
? _selectedIndex == _chatPageTabIndex
|
||||||
: false; // change this when ios have chat page
|
: false; // change this when ios have chat page
|
||||||
|
|
||||||
void refreshPages() {
|
void refreshPages() {
|
||||||
@@ -43,8 +45,9 @@ class HomePageState extends State<HomePage> {
|
|||||||
|
|
||||||
void initPages() {
|
void initPages() {
|
||||||
_pages.clear();
|
_pages.clear();
|
||||||
_pages.add(ConnectionPage());
|
if (!bind.isIncomingOnly()) _pages.add(ConnectionPage());
|
||||||
if (isAndroid) {
|
if (isAndroid && !bind.isOutgoingOnly()) {
|
||||||
|
_chatPageTabIndex = _pages.length;
|
||||||
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
_pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
|
||||||
}
|
}
|
||||||
_pages.add(SettingsPage());
|
_pages.add(SettingsPage());
|
||||||
@@ -141,7 +144,7 @@ class HomePageState extends State<HomePage> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Text("RustDesk");
|
return Text(bind.mainGetAppNameSync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +157,7 @@ class WebHomePage extends StatelessWidget {
|
|||||||
// backgroundColor: MyTheme.grayBg,
|
// backgroundColor: MyTheme.grayBg,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
title: Text("RustDesk${isWeb ? " (Beta) " : ""}"),
|
title: Text(bind.mainGetAppNameSync()),
|
||||||
actions: connectionPage.appBarActions,
|
actions: connectionPage.appBarActions,
|
||||||
),
|
),
|
||||||
body: connectionPage,
|
body: connectionPage,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter_hbb/consts.dart';
|
|||||||
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
import 'package:flutter_hbb/mobile/widgets/gesture_help.dart';
|
||||||
import 'package:flutter_hbb/models/chat_model.dart';
|
import 'package:flutter_hbb/models/chat_model.dart';
|
||||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
@@ -68,7 +69,9 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.dialogManager
|
gFFI.dialogManager
|
||||||
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
.showLoading(translate('Connecting...'), onCancel: closeConnection);
|
||||||
});
|
});
|
||||||
WakelockPlus.enable();
|
if (!isWeb) {
|
||||||
|
WakelockPlus.enable();
|
||||||
|
}
|
||||||
_physicalFocusNode.requestFocus();
|
_physicalFocusNode.requestFocus();
|
||||||
gFFI.inputModel.listenToMouse(true);
|
gFFI.inputModel.listenToMouse(true);
|
||||||
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
|
||||||
@@ -77,7 +80,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
initSharedStates(widget.id);
|
initSharedStates(widget.id);
|
||||||
gFFI.chatModel
|
gFFI.chatModel
|
||||||
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
.changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
|
||||||
|
gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
_blockableOverlayState.applyFfi(gFFI);
|
_blockableOverlayState.applyFfi(gFFI);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +90,8 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
gFFI.dialogManager.hideMobileActionsOverlay();
|
gFFI.dialogManager.hideMobileActionsOverlay();
|
||||||
gFFI.inputModel.listenToMouse(false);
|
gFFI.inputModel.listenToMouse(false);
|
||||||
|
gFFI.imageModel.disposeImage();
|
||||||
|
gFFI.cursorModel.disposeImages();
|
||||||
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
await gFFI.invokeMethod("enable_soft_keyboard", true);
|
||||||
_mobileFocusNode.dispose();
|
_mobileFocusNode.dispose();
|
||||||
_physicalFocusNode.dispose();
|
_physicalFocusNode.dispose();
|
||||||
@@ -95,9 +100,16 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.dialogManager.dismissAll();
|
gFFI.dialogManager.dismissAll();
|
||||||
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
|
||||||
overlays: SystemUiOverlay.values);
|
overlays: SystemUiOverlay.values);
|
||||||
await WakelockPlus.disable();
|
if (!isWeb) {
|
||||||
|
await WakelockPlus.disable();
|
||||||
|
}
|
||||||
await keyboardSubscription.cancel();
|
await keyboardSubscription.cancel();
|
||||||
removeSharedStates(widget.id);
|
removeSharedStates(widget.id);
|
||||||
|
if (isAndroid) {
|
||||||
|
// Only one client is considered here for now.
|
||||||
|
// TODO: take into account the case where there are multiple clients
|
||||||
|
gFFI.invokeMethod("on_voice_call_closed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// to-do: It should be better to use transparent color instead of the bgColor.
|
// to-do: It should be better to use transparent color instead of the bgColor.
|
||||||
@@ -219,7 +231,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
_timer = Timer(kMobileDelaySoftKeyboard, () {
|
||||||
// show now, and sleep a while to requestFocus to
|
// show now, and sleep a while to requestFocus to
|
||||||
// make sure edit ready, so that keyboard wont show/hide/show/hide happen
|
// make sure edit ready, so that keyboard won't show/hide/show/hide happen
|
||||||
setState(() => _showEdit = true);
|
setState(() => _showEdit = true);
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
_timer = Timer(kMobileDelaySoftKeyboardFocus, () {
|
||||||
@@ -300,7 +312,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
initialEntries: [
|
initialEntries: [
|
||||||
OverlayEntry(builder: (context) {
|
OverlayEntry(builder: (context) {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.black,
|
color: kColorCanvas,
|
||||||
child: isWebDesktop
|
child: isWebDesktop
|
||||||
? getBodyForDesktopWithListener(keyboard)
|
? getBodyForDesktopWithListener(keyboard)
|
||||||
: SafeArea(
|
: SafeArea(
|
||||||
@@ -365,9 +377,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
clientClose(sessionId, gFFI.dialogManager);
|
clientClose(sessionId, gFFI.dialogManager);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
] +
|
|
||||||
<Widget>[
|
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(Icons.tv),
|
icon: Icon(Icons.tv),
|
||||||
@@ -411,12 +421,14 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
: <Widget>[
|
: <Widget>[
|
||||||
IconButton(
|
IconButton(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
icon: Icon(Icons.message),
|
icon: isAndroid
|
||||||
onPressed: () {
|
? SvgPicture.asset('assets/chat.svg',
|
||||||
gFFI.chatModel.changeCurrentKey(MessageKey(
|
colorFilter: ColorFilter.mode(
|
||||||
widget.id, ChatModel.clientModeID));
|
Colors.white, BlendMode.srcIn))
|
||||||
gFFI.chatModel.toggleChatOverlay();
|
: Icon(Icons.message),
|
||||||
},
|
onPressed: () => isAndroid
|
||||||
|
? showChatOptions(widget.id)
|
||||||
|
: onPressedTextChat(widget.id),
|
||||||
)
|
)
|
||||||
]) +
|
]) +
|
||||||
[
|
[
|
||||||
@@ -534,6 +546,88 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
}();
|
}();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPressedTextChat(String id) {
|
||||||
|
gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID));
|
||||||
|
gFFI.chatModel.toggleChatOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
showChatOptions(String id) async {
|
||||||
|
onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId);
|
||||||
|
onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId);
|
||||||
|
|
||||||
|
makeTextMenu(String label, Widget icon, VoidCallback onPressed,
|
||||||
|
{TextStyle? labelStyle}) =>
|
||||||
|
TTextMenu(
|
||||||
|
child: Text(translate(label), style: labelStyle),
|
||||||
|
trailingIcon: Transform.scale(
|
||||||
|
scale: (isDesktop || isWebDesktop) ? 0.8 : 1,
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: icon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: onPressed,
|
||||||
|
);
|
||||||
|
|
||||||
|
final isInVoice = [
|
||||||
|
VoiceCallStatus.waitingForResponse,
|
||||||
|
VoiceCallStatus.connected
|
||||||
|
].contains(gFFI.chatModel.voiceCallStatus.value);
|
||||||
|
final menus = [
|
||||||
|
makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent),
|
||||||
|
() => onPressedTextChat(widget.id)),
|
||||||
|
isInVoice
|
||||||
|
? makeTextMenu(
|
||||||
|
'End voice call',
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/call_wait.svg',
|
||||||
|
colorFilter:
|
||||||
|
ColorFilter.mode(Colors.redAccent, BlendMode.srcIn),
|
||||||
|
),
|
||||||
|
onPressEndVoiceCall,
|
||||||
|
labelStyle: TextStyle(color: Colors.redAccent))
|
||||||
|
: makeTextMenu(
|
||||||
|
'Voice call',
|
||||||
|
SvgPicture.asset(
|
||||||
|
'assets/call_wait.svg',
|
||||||
|
colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn),
|
||||||
|
),
|
||||||
|
onPressVoiceCall),
|
||||||
|
];
|
||||||
|
getChild(TTextMenu menu) {
|
||||||
|
if (menu.trailingIcon != null) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
menu.child,
|
||||||
|
menu.trailingIcon!,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return menu.child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final menuItems = menus
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) => PopupMenuItem<int>(child: getChild(e.value), value: e.key))
|
||||||
|
.toList();
|
||||||
|
Future.delayed(Duration.zero, () async {
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
final x = 120.0;
|
||||||
|
final y = size.height;
|
||||||
|
var index = await showMenu(
|
||||||
|
context: context,
|
||||||
|
position: RelativeRect.fromLTRB(x, y, x, y),
|
||||||
|
items: menuItems,
|
||||||
|
elevation: 8,
|
||||||
|
);
|
||||||
|
if (index != null && index < menus.length) {
|
||||||
|
menus[index].onPressed.call();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// aka changeTouchMode
|
/// aka changeTouchMode
|
||||||
BottomAppBar getGestureHelp() {
|
BottomAppBar getGestureHelp() {
|
||||||
return BottomAppBar(
|
return BottomAppBar(
|
||||||
@@ -546,7 +640,7 @@ class _RemotePageState extends State<RemotePage> {
|
|||||||
gFFI.ffiModel.toggleTouchMode();
|
gFFI.ffiModel.toggleTouchMode();
|
||||||
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
final v = gFFI.ffiModel.touchMode ? 'Y' : '';
|
||||||
bind.sessionPeerOption(
|
bind.sessionPeerOption(
|
||||||
sessionId: sessionId, name: "touch-mode", value: v);
|
sessionId: sessionId, name: kOptionTouchMode, value: v);
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,6 +926,7 @@ void showOptions(
|
|||||||
List<TRadioMenu<String>> imageQualityRadios =
|
List<TRadioMenu<String>> imageQualityRadios =
|
||||||
await toolbarImageQuality(context, id, gFFI);
|
await toolbarImageQuality(context, id, gFFI);
|
||||||
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
|
List<TRadioMenu<String>> codecRadios = await toolbarCodec(context, id, gFFI);
|
||||||
|
List<TToggleMenu> cursorToggles = await toolbarCursor(context, id, gFFI);
|
||||||
List<TToggleMenu> displayToggles =
|
List<TToggleMenu> displayToggles =
|
||||||
await toolbarDisplayToggle(context, id, gFFI);
|
await toolbarDisplayToggle(context, id, gFFI);
|
||||||
|
|
||||||
@@ -872,8 +967,23 @@ void showOptions(
|
|||||||
})),
|
})),
|
||||||
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border),
|
||||||
];
|
];
|
||||||
|
final rxCursorToggleValues = cursorToggles.map((e) => e.value.obs).toList();
|
||||||
|
final cursorTogglesList = cursorToggles
|
||||||
|
.asMap()
|
||||||
|
.entries
|
||||||
|
.map((e) => Obx(() => CheckboxListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
value: rxCursorToggleValues[e.key].value,
|
||||||
|
onChanged: (v) {
|
||||||
|
e.value.onChanged?.call(v);
|
||||||
|
if (v != null) rxCursorToggleValues[e.key].value = v;
|
||||||
|
},
|
||||||
|
title: e.value.child)))
|
||||||
|
.toList();
|
||||||
|
|
||||||
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
|
final rxToggleValues = displayToggles.map((e) => e.value.obs).toList();
|
||||||
final toggles = displayToggles
|
final displayTogglesList = displayToggles
|
||||||
.asMap()
|
.asMap()
|
||||||
.entries
|
.entries
|
||||||
.map((e) => Obx(() => CheckboxListTile(
|
.map((e) => Obx(() => CheckboxListTile(
|
||||||
@@ -886,6 +996,11 @@ void showOptions(
|
|||||||
},
|
},
|
||||||
title: e.value.child)))
|
title: e.value.child)))
|
||||||
.toList();
|
.toList();
|
||||||
|
final toggles = [
|
||||||
|
...cursorTogglesList,
|
||||||
|
if (cursorToggles.isNotEmpty) const Divider(color: MyTheme.border),
|
||||||
|
...displayTogglesList,
|
||||||
|
];
|
||||||
|
|
||||||
Widget privacyModeWidget = Offstage();
|
Widget privacyModeWidget = Offstage();
|
||||||
if (privacyModeList.length > 1) {
|
if (privacyModeList.length > 1) {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
final approveMode = gFFI.serverModel.approveMode;
|
final approveMode = gFFI.serverModel.approveMode;
|
||||||
final verificationMethod = gFFI.serverModel.verificationMethod;
|
final verificationMethod = gFFI.serverModel.verificationMethod;
|
||||||
final showPasswordOption = approveMode != 'click';
|
final showPasswordOption = approveMode != 'click';
|
||||||
|
final isApproveModeFixed = isOptionFixed(kOptionApproveMode);
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
enabled: gFFI.serverModel.connectStatus > 0,
|
enabled: gFFI.serverModel.connectStatus > 0,
|
||||||
@@ -50,16 +51,19 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
value: 'AcceptSessionsViaPassword',
|
value: 'AcceptSessionsViaPassword',
|
||||||
child: listTile(
|
child: listTile(
|
||||||
'Accept sessions via password', approveMode == 'password'),
|
'Accept sessions via password', approveMode == 'password'),
|
||||||
|
enabled: !isApproveModeFixed,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'AcceptSessionsViaClick',
|
value: 'AcceptSessionsViaClick',
|
||||||
child:
|
child:
|
||||||
listTile('Accept sessions via click', approveMode == 'click'),
|
listTile('Accept sessions via click', approveMode == 'click'),
|
||||||
|
enabled: !isApproveModeFixed,
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: "AcceptSessionsViaBoth",
|
value: "AcceptSessionsViaBoth",
|
||||||
child: listTile("Accept sessions via both",
|
child: listTile("Accept sessions via both",
|
||||||
approveMode != 'password' && approveMode != 'click'),
|
approveMode != 'password' && approveMode != 'click'),
|
||||||
|
enabled: !isApproveModeFixed,
|
||||||
),
|
),
|
||||||
if (showPasswordOption) const PopupMenuDivider(),
|
if (showPasswordOption) const PopupMenuDivider(),
|
||||||
if (showPasswordOption &&
|
if (showPasswordOption &&
|
||||||
@@ -107,7 +111,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
} else if (value == kUsePermanentPassword ||
|
} else if (value == kUsePermanentPassword ||
|
||||||
value == kUseTemporaryPassword ||
|
value == kUseTemporaryPassword ||
|
||||||
value == kUseBothPasswords) {
|
value == kUseBothPasswords) {
|
||||||
bind.mainSetOption(key: "verification-method", value: value);
|
bind.mainSetOption(key: kOptionVerificationMethod, value: value);
|
||||||
gFFI.serverModel.updatePasswordModel();
|
gFFI.serverModel.updatePasswordModel();
|
||||||
} else if (value.startsWith("AcceptSessionsVia")) {
|
} else if (value.startsWith("AcceptSessionsVia")) {
|
||||||
value = value.substring("AcceptSessionsVia".length);
|
value = value.substring("AcceptSessionsVia".length);
|
||||||
@@ -116,7 +120,7 @@ class ServerPage extends StatefulWidget implements PageShape {
|
|||||||
} else if (value == "Click") {
|
} else if (value == "Click") {
|
||||||
gFFI.serverModel.setApproveMode('click');
|
gFFI.serverModel.setApproveMode('click');
|
||||||
} else {
|
} else {
|
||||||
gFFI.serverModel.setApproveMode('');
|
gFFI.serverModel.setApproveMode(defaultOptionApproveMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -158,6 +162,7 @@ class _ServerPageState extends State<ServerPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
buildPresetPasswordWarning(),
|
||||||
gFFI.serverModel.isStart
|
gFFI.serverModel.isStart
|
||||||
? ServerInfo()
|
? ServerInfo()
|
||||||
: ServiceNotRunningNotification(),
|
: ServiceNotRunningNotification(),
|
||||||
@@ -226,7 +231,7 @@ class ScamWarningDialog extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ScamWarningDialogState extends State<ScamWarningDialog> {
|
class ScamWarningDialogState extends State<ScamWarningDialog> {
|
||||||
int _countdown = 12;
|
int _countdown = bind.isCustomClient() ? 0 : 12;
|
||||||
bool show_warning = false;
|
bool show_warning = false;
|
||||||
late Timer _timer;
|
late Timer _timer;
|
||||||
late ServerModel _serverModel;
|
late ServerModel _serverModel;
|
||||||
@@ -636,40 +641,94 @@ class ConnectionManager extends StatelessWidget {
|
|||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
).marginOnly(bottom: 5),
|
).marginOnly(bottom: 5),
|
||||||
client.authorized
|
client.authorized
|
||||||
? Container(
|
? _buildDisconnectButton(client)
|
||||||
alignment: Alignment.centerRight,
|
: _buildNewConnectionHint(serverModel, client),
|
||||||
child: ElevatedButton.icon(
|
if (client.incomingVoiceCall && !client.inVoiceCall)
|
||||||
style: ButtonStyle(
|
..._buildNewVoiceCallHint(context, serverModel, client),
|
||||||
backgroundColor:
|
|
||||||
MaterialStatePropertyAll(Colors.red)),
|
|
||||||
icon: const Icon(Icons.close),
|
|
||||||
onPressed: () {
|
|
||||||
bind.cmCloseConnection(connId: client.id);
|
|
||||||
gFFI.invokeMethod(
|
|
||||||
"cancel_notification", client.id);
|
|
||||||
},
|
|
||||||
label: Text(translate("Disconnect"))))
|
|
||||||
: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
TextButton(
|
|
||||||
child: Text(translate("Dismiss")),
|
|
||||||
onPressed: () {
|
|
||||||
serverModel.sendLoginResponse(
|
|
||||||
client, false);
|
|
||||||
}).marginOnly(right: 15),
|
|
||||||
if (serverModel.approveMode != 'password')
|
|
||||||
ElevatedButton.icon(
|
|
||||||
icon: const Icon(Icons.check),
|
|
||||||
label: Text(translate("Accept")),
|
|
||||||
onPressed: () {
|
|
||||||
serverModel.sendLoginResponse(
|
|
||||||
client, true);
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
])))
|
])))
|
||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDisconnectButton(Client client) {
|
||||||
|
final disconnectButton = ElevatedButton.icon(
|
||||||
|
style: ButtonStyle(backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () {
|
||||||
|
bind.cmCloseConnection(connId: client.id);
|
||||||
|
gFFI.invokeMethod("cancel_notification", client.id);
|
||||||
|
},
|
||||||
|
label: Text(translate("Disconnect")),
|
||||||
|
);
|
||||||
|
final buttons = [disconnectButton];
|
||||||
|
if (client.inVoiceCall) {
|
||||||
|
buttons.insert(
|
||||||
|
0,
|
||||||
|
ElevatedButton.icon(
|
||||||
|
style: ButtonStyle(
|
||||||
|
backgroundColor: MaterialStatePropertyAll(Colors.red)),
|
||||||
|
icon: const Icon(Icons.phone),
|
||||||
|
label: Text(translate("Stop")),
|
||||||
|
onPressed: () {
|
||||||
|
bind.cmCloseVoiceCall(id: client.id);
|
||||||
|
gFFI.invokeMethod("cancel_notification", client.id);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttons.length == 1) {
|
||||||
|
return Container(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: disconnectButton,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Row(
|
||||||
|
children: buttons,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNewConnectionHint(ServerModel serverModel, Client client) {
|
||||||
|
return Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||||
|
TextButton(
|
||||||
|
child: Text(translate("Dismiss")),
|
||||||
|
onPressed: () {
|
||||||
|
serverModel.sendLoginResponse(client, false);
|
||||||
|
}).marginOnly(right: 15),
|
||||||
|
if (serverModel.approveMode != 'password')
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
label: Text(translate("Accept")),
|
||||||
|
onPressed: () {
|
||||||
|
serverModel.sendLoginResponse(client, true);
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Widget> _buildNewVoiceCallHint(
|
||||||
|
BuildContext context, ServerModel serverModel, Client client) {
|
||||||
|
return [
|
||||||
|
Text(
|
||||||
|
translate("android_new_voice_call_tip"),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
).marginOnly(bottom: 5),
|
||||||
|
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
||||||
|
TextButton(
|
||||||
|
child: Text(translate("Dismiss")),
|
||||||
|
onPressed: () {
|
||||||
|
serverModel.handleVoiceCall(client, false);
|
||||||
|
}).marginOnly(right: 15),
|
||||||
|
if (serverModel.approveMode != 'password')
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
label: Text(translate("Accept")),
|
||||||
|
onPressed: () {
|
||||||
|
serverModel.handleVoiceCall(client, true);
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PaddingCard extends StatelessWidget {
|
class PaddingCard extends StatelessWidget {
|
||||||
@@ -786,6 +845,15 @@ void androidChannelInit() {
|
|||||||
gFFI.serverModel.stopService();
|
gFFI.serverModel.stopService();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "msgbox":
|
||||||
|
{
|
||||||
|
var type = arguments["type"] as String;
|
||||||
|
var title = arguments["title"] as String;
|
||||||
|
var text = arguments["text"] as String;
|
||||||
|
var link = (arguments["link"] ?? '') as String;
|
||||||
|
msgBox(gFFI.sessionId, type, title, text, link, gFFI.dialogManager);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrintStack(label: "MethodCallHandler err:$e");
|
debugPrintStack(label: "MethodCallHandler err:$e");
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class SettingsPage extends StatefulWidget implements PageShape {
|
|||||||
final icon = Icon(Icons.settings);
|
final icon = Icon(Icons.settings);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final appBarActions = [ScanButton()];
|
final appBarActions = bind.isDisableSettings() ? [] : [ScanButton()];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<SettingsPage> createState() => _SettingsState();
|
State<SettingsPage> createState() => _SettingsState();
|
||||||
@@ -44,6 +44,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
var _onlyWhiteList = false;
|
var _onlyWhiteList = false;
|
||||||
var _enableDirectIPAccess = false;
|
var _enableDirectIPAccess = false;
|
||||||
var _enableRecordSession = false;
|
var _enableRecordSession = false;
|
||||||
|
var _enableHardwareCodec = false;
|
||||||
var _autoRecordIncomingSession = false;
|
var _autoRecordIncomingSession = false;
|
||||||
var _allowAutoDisconnect = false;
|
var _allowAutoDisconnect = false;
|
||||||
var _localIP = "";
|
var _localIP = "";
|
||||||
@@ -86,7 +87,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final enableAbrRes = option2bool(
|
final enableAbrRes = option2bool(
|
||||||
"enable-abr", await bind.mainGetOption(key: "enable-abr"));
|
kOptionEnableAbr, await bind.mainGetOption(key: kOptionEnableAbr));
|
||||||
if (enableAbrRes != _enableAbr) {
|
if (enableAbrRes != _enableAbr) {
|
||||||
update = true;
|
update = true;
|
||||||
_enableAbr = enableAbrRes;
|
_enableAbr = enableAbrRes;
|
||||||
@@ -99,30 +100,37 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
_denyLANDiscovery = denyLanDiscovery;
|
_denyLANDiscovery = denyLanDiscovery;
|
||||||
}
|
}
|
||||||
|
|
||||||
final onlyWhiteList =
|
final onlyWhiteList = (await bind.mainGetOption(key: kOptionWhitelist)) !=
|
||||||
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
|
defaultOptionWhitelist;
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
if (onlyWhiteList != _onlyWhiteList) {
|
||||||
update = true;
|
update = true;
|
||||||
_onlyWhiteList = onlyWhiteList;
|
_onlyWhiteList = onlyWhiteList;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enableDirectIPAccess = option2bool(
|
final enableDirectIPAccess = option2bool(kOptionDirectServer,
|
||||||
'direct-server', await bind.mainGetOption(key: 'direct-server'));
|
await bind.mainGetOption(key: kOptionDirectServer));
|
||||||
if (enableDirectIPAccess != _enableDirectIPAccess) {
|
if (enableDirectIPAccess != _enableDirectIPAccess) {
|
||||||
update = true;
|
update = true;
|
||||||
_enableDirectIPAccess = enableDirectIPAccess;
|
_enableDirectIPAccess = enableDirectIPAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
final enableRecordSession = option2bool('enable-record-session',
|
final enableRecordSession = option2bool(kOptionEnableRecordSession,
|
||||||
await bind.mainGetOption(key: 'enable-record-session'));
|
await bind.mainGetOption(key: kOptionEnableRecordSession));
|
||||||
if (enableRecordSession != _enableRecordSession) {
|
if (enableRecordSession != _enableRecordSession) {
|
||||||
update = true;
|
update = true;
|
||||||
_enableRecordSession = enableRecordSession;
|
_enableRecordSession = enableRecordSession;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final enableHardwareCodec = option2bool(kOptionEnableHwcodec,
|
||||||
|
await bind.mainGetOption(key: kOptionEnableHwcodec));
|
||||||
|
if (_enableHardwareCodec != enableHardwareCodec) {
|
||||||
|
update = true;
|
||||||
|
_enableHardwareCodec = enableHardwareCodec;
|
||||||
|
}
|
||||||
|
|
||||||
final autoRecordIncomingSession = option2bool(
|
final autoRecordIncomingSession = option2bool(
|
||||||
'allow-auto-record-incoming',
|
kOptionAllowAutoRecordIncoming,
|
||||||
await bind.mainGetOption(key: 'allow-auto-record-incoming'));
|
await bind.mainGetOption(key: kOptionAllowAutoRecordIncoming));
|
||||||
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
|
if (autoRecordIncomingSession != _autoRecordIncomingSession) {
|
||||||
update = true;
|
update = true;
|
||||||
_autoRecordIncomingSession = autoRecordIncomingSession;
|
_autoRecordIncomingSession = autoRecordIncomingSession;
|
||||||
@@ -135,7 +143,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final directAccessPort =
|
final directAccessPort =
|
||||||
await bind.mainGetOption(key: 'direct-access-port');
|
await bind.mainGetOption(key: kOptionDirectAccessPort);
|
||||||
if (directAccessPort != _directAccessPort) {
|
if (directAccessPort != _directAccessPort) {
|
||||||
update = true;
|
update = true;
|
||||||
_directAccessPort = directAccessPort;
|
_directAccessPort = directAccessPort;
|
||||||
@@ -153,15 +161,15 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
_buildDate = buildDate;
|
_buildDate = buildDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
final allowAutoDisconnect = option2bool('allow-auto-disconnect',
|
final allowAutoDisconnect = option2bool(kOptionAllowAutoDisconnect,
|
||||||
await bind.mainGetOption(key: 'allow-auto-disconnect'));
|
await bind.mainGetOption(key: kOptionAllowAutoDisconnect));
|
||||||
if (allowAutoDisconnect != _allowAutoDisconnect) {
|
if (allowAutoDisconnect != _allowAutoDisconnect) {
|
||||||
update = true;
|
update = true;
|
||||||
_allowAutoDisconnect = allowAutoDisconnect;
|
_allowAutoDisconnect = allowAutoDisconnect;
|
||||||
}
|
}
|
||||||
|
|
||||||
final autoDisconnectTimeout =
|
final autoDisconnectTimeout =
|
||||||
await bind.mainGetOption(key: 'auto-disconnect-timeout');
|
await bind.mainGetOption(key: kOptionAutoDisconnectTimeout);
|
||||||
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
|
if (autoDisconnectTimeout != _autoDisconnectTimeout) {
|
||||||
update = true;
|
update = true;
|
||||||
_autoDisconnectTimeout = autoDisconnectTimeout;
|
_autoDisconnectTimeout = autoDisconnectTimeout;
|
||||||
@@ -218,6 +226,21 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Provider.of<FfiModel>(context);
|
Provider.of<FfiModel>(context);
|
||||||
|
final outgoingOnly = bind.isOutgoingOnly();
|
||||||
|
final customClientSection = CustomSettingsSection(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
if (bind.isCustomClient())
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: loadPowered(context),
|
||||||
|
),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: loadLogo(),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
));
|
||||||
final List<AbstractSettingsTile> enhancementsTiles = [];
|
final List<AbstractSettingsTile> enhancementsTiles = [];
|
||||||
final List<AbstractSettingsTile> shareScreenTiles = [
|
final List<AbstractSettingsTile> shareScreenTiles = [
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
@@ -234,16 +257,18 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Deny LAN discovery')),
|
title: Text(translate('Deny LAN discovery')),
|
||||||
initialValue: _denyLANDiscovery,
|
initialValue: _denyLANDiscovery,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableLanDiscovery)
|
||||||
await bind.mainSetOption(
|
? null
|
||||||
key: "enable-lan-discovery",
|
: (v) async {
|
||||||
value: bool2option("enable-lan-discovery", !v));
|
await bind.mainSetOption(
|
||||||
final newValue = !option2bool('enable-lan-discovery',
|
key: kOptionEnableLanDiscovery,
|
||||||
await bind.mainGetOption(key: 'enable-lan-discovery'));
|
value: bool2option(kOptionEnableLanDiscovery, !v));
|
||||||
setState(() {
|
final newValue = !option2bool(kOptionEnableLanDiscovery,
|
||||||
_denyLANDiscovery = newValue;
|
await bind.mainGetOption(key: kOptionEnableLanDiscovery));
|
||||||
});
|
setState(() {
|
||||||
},
|
_denyLANDiscovery = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Row(children: [
|
title: Row(children: [
|
||||||
@@ -258,7 +283,8 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
onToggle: (_) async {
|
onToggle: (_) async {
|
||||||
update() async {
|
update() async {
|
||||||
final onlyWhiteList =
|
final onlyWhiteList =
|
||||||
(await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
|
(await bind.mainGetOption(key: kOptionWhitelist)) !=
|
||||||
|
defaultOptionWhitelist;
|
||||||
if (onlyWhiteList != _onlyWhiteList) {
|
if (onlyWhiteList != _onlyWhiteList) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_onlyWhiteList = onlyWhiteList;
|
_onlyWhiteList = onlyWhiteList;
|
||||||
@@ -272,26 +298,34 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text('${translate('Adaptive bitrate')} (beta)'),
|
title: Text('${translate('Adaptive bitrate')} (beta)'),
|
||||||
initialValue: _enableAbr,
|
initialValue: _enableAbr,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableAbr)
|
||||||
await bind.mainSetOption(key: "enable-abr", value: v ? "" : "N");
|
? null
|
||||||
final newValue = await bind.mainGetOption(key: "enable-abr") != "N";
|
: (v) async {
|
||||||
setState(() {
|
await bind.mainSetOption(
|
||||||
_enableAbr = newValue;
|
key: kOptionEnableAbr, value: v ? defaultOptionYes : "N");
|
||||||
});
|
final newValue =
|
||||||
},
|
await bind.mainGetOption(key: kOptionEnableAbr) != "N";
|
||||||
|
setState(() {
|
||||||
|
_enableAbr = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Text(translate('Enable recording session')),
|
title: Text(translate('Enable recording session')),
|
||||||
initialValue: _enableRecordSession,
|
initialValue: _enableRecordSession,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionEnableRecordSession)
|
||||||
await bind.mainSetOption(
|
? null
|
||||||
key: "enable-record-session", value: v ? "" : "N");
|
: (v) async {
|
||||||
final newValue =
|
await bind.mainSetOption(
|
||||||
await bind.mainGetOption(key: "enable-record-session") != "N";
|
key: kOptionEnableRecordSession,
|
||||||
setState(() {
|
value: v ? defaultOptionYes : "N");
|
||||||
_enableRecordSession = newValue;
|
final newValue =
|
||||||
});
|
await bind.mainGetOption(key: kOptionEnableRecordSession) !=
|
||||||
},
|
"N";
|
||||||
|
setState(() {
|
||||||
|
_enableRecordSession = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
@@ -318,21 +352,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
Icons.edit,
|
Icons.edit,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: isOptionFixed(kOptionDirectAccessPort)
|
||||||
final port = await changeDirectAccessPort(
|
? null
|
||||||
_localIP, _directAccessPort);
|
: () async {
|
||||||
setState(() {
|
final port = await changeDirectAccessPort(
|
||||||
_directAccessPort = port;
|
_localIP, _directAccessPort);
|
||||||
});
|
setState(() {
|
||||||
}))
|
_directAccessPort = port;
|
||||||
|
});
|
||||||
|
}))
|
||||||
]),
|
]),
|
||||||
initialValue: _enableDirectIPAccess,
|
initialValue: _enableDirectIPAccess,
|
||||||
onToggle: (_) async {
|
onToggle: isOptionFixed(kOptionDirectServer)
|
||||||
_enableDirectIPAccess = !_enableDirectIPAccess;
|
? null
|
||||||
String value = bool2option('direct-server', _enableDirectIPAccess);
|
: (_) async {
|
||||||
await bind.mainSetOption(key: 'direct-server', value: value);
|
_enableDirectIPAccess = !_enableDirectIPAccess;
|
||||||
setState(() {});
|
String value =
|
||||||
},
|
bool2option(kOptionDirectServer, _enableDirectIPAccess);
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionDirectServer, value: value);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
SettingsTile.switchTile(
|
SettingsTile.switchTile(
|
||||||
title: Row(
|
title: Row(
|
||||||
@@ -359,22 +399,27 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
Icons.edit,
|
Icons.edit,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: isOptionFixed(kOptionAutoDisconnectTimeout)
|
||||||
final timeout = await changeAutoDisconnectTimeout(
|
? null
|
||||||
_autoDisconnectTimeout);
|
: () async {
|
||||||
setState(() {
|
final timeout = await changeAutoDisconnectTimeout(
|
||||||
_autoDisconnectTimeout = timeout;
|
_autoDisconnectTimeout);
|
||||||
});
|
setState(() {
|
||||||
}))
|
_autoDisconnectTimeout = timeout;
|
||||||
|
});
|
||||||
|
}))
|
||||||
]),
|
]),
|
||||||
initialValue: _allowAutoDisconnect,
|
initialValue: _allowAutoDisconnect,
|
||||||
onToggle: (_) async {
|
onToggle: isOptionFixed(kOptionAllowAutoDisconnect)
|
||||||
_allowAutoDisconnect = !_allowAutoDisconnect;
|
? null
|
||||||
String value =
|
: (_) async {
|
||||||
bool2option('allow-auto-disconnect', _allowAutoDisconnect);
|
_allowAutoDisconnect = !_allowAutoDisconnect;
|
||||||
await bind.mainSetOption(key: 'allow-auto-disconnect', value: value);
|
String value = bool2option(
|
||||||
setState(() {});
|
kOptionAllowAutoDisconnect, _allowAutoDisconnect);
|
||||||
},
|
await bind.mainSetOption(
|
||||||
|
key: kOptionAllowAutoDisconnect, value: value);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
if (_hasIgnoreBattery) {
|
if (_hasIgnoreBattery) {
|
||||||
@@ -448,33 +493,37 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return SettingsList(
|
final disabledSettings = bind.isDisableSettings();
|
||||||
|
final settings = SettingsList(
|
||||||
sections: [
|
sections: [
|
||||||
SettingsSection(
|
customClientSection,
|
||||||
title: Text(translate('Account')),
|
if (!bind.isDisableAccount())
|
||||||
tiles: [
|
SettingsSection(
|
||||||
SettingsTile(
|
title: Text(translate('Account')),
|
||||||
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
tiles: [
|
||||||
? translate('Login')
|
SettingsTile(
|
||||||
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
|
title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
|
||||||
leading: Icon(Icons.person),
|
? translate('Login')
|
||||||
onPressed: (context) {
|
: '${translate('Logout')} (${gFFI.userModel.userName.value})')),
|
||||||
if (gFFI.userModel.userName.value.isEmpty) {
|
leading: Icon(Icons.person),
|
||||||
loginDialog();
|
onPressed: (context) {
|
||||||
} else {
|
if (gFFI.userModel.userName.value.isEmpty) {
|
||||||
logOutConfirmDialog();
|
loginDialog();
|
||||||
}
|
} else {
|
||||||
},
|
logOutConfirmDialog();
|
||||||
),
|
}
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
SettingsSection(title: Text(translate("Settings")), tiles: [
|
SettingsSection(title: Text(translate("Settings")), tiles: [
|
||||||
SettingsTile(
|
if (!disabledSettings)
|
||||||
title: Text(translate('ID/Relay Server')),
|
SettingsTile(
|
||||||
leading: Icon(Icons.cloud),
|
title: Text(translate('ID/Relay Server')),
|
||||||
onPressed: (context) {
|
leading: Icon(Icons.cloud),
|
||||||
showServerSettings(gFFI.dialogManager);
|
onPressed: (context) {
|
||||||
}),
|
showServerSettings(gFFI.dialogManager);
|
||||||
|
}),
|
||||||
SettingsTile(
|
SettingsTile(
|
||||||
title: Text(translate('Language')),
|
title: Text(translate('Language')),
|
||||||
leading: Icon(Icons.translate),
|
leading: Icon(Icons.translate),
|
||||||
@@ -495,6 +544,26 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
if (isAndroid)
|
if (isAndroid)
|
||||||
|
SettingsSection(title: Text(translate('Hardware Codec')), tiles: [
|
||||||
|
SettingsTile.switchTile(
|
||||||
|
title: Text(translate('Enable hardware codec')),
|
||||||
|
initialValue: _enableHardwareCodec,
|
||||||
|
onToggle: isOptionFixed(kOptionEnableHwcodec)
|
||||||
|
? null
|
||||||
|
: (v) async {
|
||||||
|
await bind.mainSetOption(
|
||||||
|
key: kOptionEnableHwcodec,
|
||||||
|
value: v ? defaultOptionYes : "N");
|
||||||
|
final newValue =
|
||||||
|
await bind.mainGetOption(key: kOptionEnableHwcodec) !=
|
||||||
|
"N";
|
||||||
|
setState(() {
|
||||||
|
_enableHardwareCodec = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
if (isAndroid && !outgoingOnly)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Recording")),
|
title: Text(translate("Recording")),
|
||||||
tiles: [
|
tiles: [
|
||||||
@@ -506,30 +575,33 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
builder: (ctx, data) => Offstage(
|
builder: (ctx, data) => Offstage(
|
||||||
offstage: !data.hasData,
|
offstage: !data.hasData,
|
||||||
child: Text("${translate("Directory")}: ${data.data}")),
|
child: Text("${translate("Directory")}: ${data.data}")),
|
||||||
future: bind.mainDefaultVideoSaveDirectory()),
|
future: bind.mainVideoSaveDirectory(root: false)),
|
||||||
initialValue: _autoRecordIncomingSession,
|
initialValue: _autoRecordIncomingSession,
|
||||||
onToggle: (v) async {
|
onToggle: isOptionFixed(kOptionAllowAutoRecordIncoming)
|
||||||
await bind.mainSetOption(
|
? null
|
||||||
key: "allow-auto-record-incoming",
|
: (v) async {
|
||||||
value: bool2option("allow-auto-record-incoming", v));
|
await bind.mainSetOption(
|
||||||
final newValue = option2bool(
|
key: kOptionAllowAutoRecordIncoming,
|
||||||
'allow-auto-record-incoming',
|
value:
|
||||||
await bind.mainGetOption(
|
bool2option(kOptionAllowAutoRecordIncoming, v));
|
||||||
key: 'allow-auto-record-incoming'));
|
final newValue = option2bool(
|
||||||
setState(() {
|
kOptionAllowAutoRecordIncoming,
|
||||||
_autoRecordIncomingSession = newValue;
|
await bind.mainGetOption(
|
||||||
});
|
key: kOptionAllowAutoRecordIncoming));
|
||||||
},
|
setState(() {
|
||||||
|
_autoRecordIncomingSession = newValue;
|
||||||
|
});
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
if (isAndroid)
|
if (isAndroid && !disabledSettings && !outgoingOnly)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Share Screen")),
|
title: Text(translate("Share Screen")),
|
||||||
tiles: shareScreenTiles,
|
tiles: shareScreenTiles,
|
||||||
),
|
),
|
||||||
defaultDisplaySection(),
|
if (!bind.isIncomingOnly()) defaultDisplaySection(),
|
||||||
if (isAndroid)
|
if (isAndroid && !disabledSettings && !outgoingOnly)
|
||||||
SettingsSection(
|
SettingsSection(
|
||||||
title: Text(translate("Enhancements")),
|
title: Text(translate("Enhancements")),
|
||||||
tiles: enhancementsTiles,
|
tiles: enhancementsTiles,
|
||||||
@@ -578,6 +650,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> canStartOnBoot() async {
|
Future<bool> canStartOnBoot() async {
|
||||||
@@ -617,29 +690,32 @@ void showServerSettings(OverlayDialogManager dialogManager) async {
|
|||||||
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
void showLanguageSettings(OverlayDialogManager dialogManager) async {
|
||||||
try {
|
try {
|
||||||
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
final langs = json.decode(await bind.mainGetLangs()) as List<dynamic>;
|
||||||
var lang = bind.mainGetLocalOption(key: "lang");
|
var lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
|
||||||
dialogManager.show((setState, close, context) {
|
dialogManager.show((setState, close, context) {
|
||||||
setLang(v) async {
|
setLang(v) async {
|
||||||
if (lang != v) {
|
if (lang != v) {
|
||||||
setState(() {
|
setState(() {
|
||||||
lang = v;
|
lang = v;
|
||||||
});
|
});
|
||||||
await bind.mainSetLocalOption(key: "lang", value: v);
|
await bind.mainSetLocalOption(key: kCommConfKeyLang, value: v);
|
||||||
HomePage.homeKey.currentState?.refreshPages();
|
HomePage.homeKey.currentState?.refreshPages();
|
||||||
Future.delayed(Duration(milliseconds: 200), close);
|
Future.delayed(Duration(milliseconds: 200), close);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kCommConfKeyLang);
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(
|
content: Column(
|
||||||
children: [
|
children: [
|
||||||
getRadio(Text(translate('Default')), '', lang, setLang),
|
getRadio(Text(translate('Default')), defaultOptionLang, lang,
|
||||||
|
isOptFixed ? null : setLang),
|
||||||
Divider(color: MyTheme.border),
|
Divider(color: MyTheme.border),
|
||||||
] +
|
] +
|
||||||
langs.map((e) {
|
langs.map((e) {
|
||||||
final key = e[0] as String;
|
final key = e[0] as String;
|
||||||
final name = e[1] as String;
|
final name = e[1] as String;
|
||||||
return getRadio(Text(translate(name)), key, lang, setLang);
|
return getRadio(Text(translate(name)), key, lang,
|
||||||
|
isOptFixed ? null : setLang);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -663,13 +739,15 @@ void showThemeSettings(OverlayDialogManager dialogManager) async {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isOptFixed = isOptionFixed(kCommConfKeyTheme);
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Column(children: [
|
content: Column(children: [
|
||||||
getRadio(
|
getRadio(Text(translate('Light')), ThemeMode.light, themeMode,
|
||||||
Text(translate('Light')), ThemeMode.light, themeMode, setTheme),
|
isOptFixed ? null : setTheme),
|
||||||
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode, setTheme),
|
getRadio(Text(translate('Dark')), ThemeMode.dark, themeMode,
|
||||||
|
isOptFixed ? null : setTheme),
|
||||||
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
getRadio(Text(translate('Follow System')), ThemeMode.system, themeMode,
|
||||||
setTheme)
|
isOptFixed ? null : setTheme)
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}, backDismiss: true, clickMaskDismiss: true);
|
}, backDismiss: true, clickMaskDismiss: true);
|
||||||
@@ -757,11 +835,14 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
|
_RadioEntry('Scale original', kRemoteViewStyleOriginal),
|
||||||
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
|
_RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
|
||||||
],
|
],
|
||||||
getter: () => bind.mainGetUserDefaultOption(key: 'view_style'),
|
getter: () =>
|
||||||
asyncSetter: (value) async {
|
bind.mainGetUserDefaultOption(key: kOptionViewStyle),
|
||||||
await bind.mainSetUserDefaultOption(
|
asyncSetter: isOptionFixed(kOptionViewStyle)
|
||||||
key: 'view_style', value: value);
|
? null
|
||||||
},
|
: (value) async {
|
||||||
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: kOptionViewStyle, value: value);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_getPopupDialogRadioEntry(
|
_getPopupDialogRadioEntry(
|
||||||
title: 'Default Image Quality',
|
title: 'Default Image Quality',
|
||||||
@@ -772,16 +853,19 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
_RadioEntry('Custom', kRemoteImageQualityCustom),
|
_RadioEntry('Custom', kRemoteImageQualityCustom),
|
||||||
],
|
],
|
||||||
getter: () {
|
getter: () {
|
||||||
final v = bind.mainGetUserDefaultOption(key: 'image_quality');
|
final v =
|
||||||
|
bind.mainGetUserDefaultOption(key: kOptionImageQuality);
|
||||||
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
|
showCustomImageQuality.value = v == kRemoteImageQualityCustom;
|
||||||
return v;
|
return v;
|
||||||
},
|
},
|
||||||
asyncSetter: (value) async {
|
asyncSetter: isOptionFixed(kOptionImageQuality)
|
||||||
await bind.mainSetUserDefaultOption(
|
? null
|
||||||
key: 'image_quality', value: value);
|
: (value) async {
|
||||||
showCustomImageQuality.value =
|
await bind.mainSetUserDefaultOption(
|
||||||
value == kRemoteImageQualityCustom;
|
key: kOptionImageQuality, value: value);
|
||||||
},
|
showCustomImageQuality.value =
|
||||||
|
value == kRemoteImageQualityCustom;
|
||||||
|
},
|
||||||
tail: customImageQualitySetting(),
|
tail: customImageQualitySetting(),
|
||||||
showTail: showCustomImageQuality,
|
showTail: showCustomImageQuality,
|
||||||
notCloseValue: kRemoteImageQualityCustom,
|
notCloseValue: kRemoteImageQualityCustom,
|
||||||
@@ -790,11 +874,13 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
title: 'Default Codec',
|
title: 'Default Codec',
|
||||||
list: codecList,
|
list: codecList,
|
||||||
getter: () =>
|
getter: () =>
|
||||||
bind.mainGetUserDefaultOption(key: 'codec-preference'),
|
bind.mainGetUserDefaultOption(key: kOptionCodecPreference),
|
||||||
asyncSetter: (value) async {
|
asyncSetter: isOptionFixed(kOptionCodecPreference)
|
||||||
await bind.mainSetUserDefaultOption(
|
? null
|
||||||
key: 'codec-preference', value: value);
|
: (value) async {
|
||||||
},
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: kOptionCodecPreference, value: value);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -809,13 +895,17 @@ class __DisplayPageState extends State<_DisplayPage> {
|
|||||||
|
|
||||||
SettingsTile otherRow(String label, String key) {
|
SettingsTile otherRow(String label, String key) {
|
||||||
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
|
||||||
|
final isOptFixed = isOptionFixed(key);
|
||||||
return SettingsTile.switchTile(
|
return SettingsTile.switchTile(
|
||||||
initialValue: value,
|
initialValue: value,
|
||||||
title: Text(translate(label)),
|
title: Text(translate(label)),
|
||||||
onToggle: (b) async {
|
onToggle: isOptFixed
|
||||||
await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
|
? null
|
||||||
setState(() {});
|
: (b) async {
|
||||||
},
|
await bind.mainSetUserDefaultOption(
|
||||||
|
key: key, value: b ? 'Y' : defaultOptionNo);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -833,7 +923,7 @@ _getPopupDialogRadioEntry({
|
|||||||
required String title,
|
required String title,
|
||||||
required List<_RadioEntry> list,
|
required List<_RadioEntry> list,
|
||||||
required _RadioEntryGetter getter,
|
required _RadioEntryGetter getter,
|
||||||
required _RadioEntrySetter asyncSetter,
|
required _RadioEntrySetter? asyncSetter,
|
||||||
Widget? tail,
|
Widget? tail,
|
||||||
RxBool? showTail,
|
RxBool? showTail,
|
||||||
String? notCloseValue,
|
String? notCloseValue,
|
||||||
@@ -853,21 +943,23 @@ _getPopupDialogRadioEntry({
|
|||||||
|
|
||||||
void showDialog() async {
|
void showDialog() async {
|
||||||
gFFI.dialogManager.show((setState, close, context) {
|
gFFI.dialogManager.show((setState, close, context) {
|
||||||
onChanged(String? value) async {
|
final onChanged = asyncSetter == null
|
||||||
if (value == null) return;
|
? null
|
||||||
await asyncSetter(value);
|
: (String? value) async {
|
||||||
init();
|
if (value == null) return;
|
||||||
if (value != notCloseValue) {
|
await asyncSetter(value);
|
||||||
close();
|
init();
|
||||||
}
|
if (value != notCloseValue) {
|
||||||
}
|
close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
content: Obx(
|
content: Obx(
|
||||||
() => Column(children: [
|
() => Column(children: [
|
||||||
...list
|
...list
|
||||||
.map((e) => getRadio(Text(translate(e.label)), e.value,
|
.map((e) => getRadio(Text(translate(e.label)), e.value,
|
||||||
groupValue.value, (String? value) => onChanged(value)))
|
groupValue.value, onChanged))
|
||||||
.toList(),
|
.toList(),
|
||||||
Offstage(
|
Offstage(
|
||||||
offstage:
|
offstage:
|
||||||
|
|||||||
@@ -283,12 +283,3 @@ void setPrivacyModeDialog(
|
|||||||
);
|
);
|
||||||
}, backDismiss: true, clickMaskDismiss: true);
|
}, backDismiss: true, clickMaskDismiss: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> validateAsync(String value) async {
|
|
||||||
value = value.trim();
|
|
||||||
if (value.isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
final res = await bind.mainTestIfValidServer(server: value);
|
|
||||||
return res.isEmpty ? null : res;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import 'dart:io';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||||
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
import 'package:flutter_hbb/common/widgets/peers_view.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
|
import '../utils/http_service.dart' as http;
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
|
|
||||||
final syncAbOption = 'sync-ab-with-recent-sessions';
|
final syncAbOption = 'sync-ab-with-recent-sessions';
|
||||||
@@ -84,7 +85,7 @@ class AbModel {
|
|||||||
reset() async {
|
reset() async {
|
||||||
print("reset ab model");
|
print("reset ab model");
|
||||||
addressbooks.clear();
|
addressbooks.clear();
|
||||||
setCurrentName('');
|
_currentName.value = '';
|
||||||
await bind.mainClearAb();
|
await bind.mainClearAb();
|
||||||
listInitialized = false;
|
listInitialized = false;
|
||||||
}
|
}
|
||||||
@@ -509,7 +510,8 @@ class AbModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setShouldAsync(bool v) async {
|
void setShouldAsync(bool v) async {
|
||||||
await bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : '');
|
await bind.mainSetLocalOption(
|
||||||
|
key: syncAbOption, value: v ? 'Y' : defaultOptionNo);
|
||||||
_syncAllFromRecent = true;
|
_syncAllFromRecent = true;
|
||||||
_timerCounter = 0;
|
_timerCounter = 0;
|
||||||
}
|
}
|
||||||
@@ -548,7 +550,7 @@ class AbModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trySetCurrentToLast() {
|
trySetCurrentToLast() {
|
||||||
final name = bind.getLocalFlutterOption(k: 'current-ab-name');
|
final name = bind.getLocalFlutterOption(k: kOptionCurrentAbName);
|
||||||
if (addressbooks.containsKey(name)) {
|
if (addressbooks.containsKey(name)) {
|
||||||
_currentName.value = name;
|
_currentName.value = name;
|
||||||
}
|
}
|
||||||
@@ -647,6 +649,10 @@ class AbModel {
|
|||||||
return addressbooks.keys.toList();
|
return addressbooks.keys.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String personalAddressBookName() {
|
||||||
|
return _personalAddressBookName;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setCurrentName(String name) async {
|
Future<void> setCurrentName(String name) async {
|
||||||
final oldName = _currentName.value;
|
final oldName = _currentName.value;
|
||||||
if (addressbooks.containsKey(name)) {
|
if (addressbooks.containsKey(name)) {
|
||||||
|
|||||||
@@ -527,10 +527,16 @@ class ChatModel with ChangeNotifier {
|
|||||||
|
|
||||||
void onVoiceCallStarted() {
|
void onVoiceCallStarted() {
|
||||||
_voiceCallStatus.value = VoiceCallStatus.connected;
|
_voiceCallStatus.value = VoiceCallStatus.connected;
|
||||||
|
if (isAndroid) {
|
||||||
|
parent.target?.invokeMethod("on_voice_call_started");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onVoiceCallClosed(String reason) {
|
void onVoiceCallClosed(String reason) {
|
||||||
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
_voiceCallStatus.value = VoiceCallStatus.notStarted;
|
||||||
|
if (isAndroid) {
|
||||||
|
parent.target?.invokeMethod("on_voice_call_closed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onVoiceCallIncoming() {
|
void onVoiceCallIncoming() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart';
|
import 'package:flutter_gpu_texture_renderer/flutter_gpu_texture_renderer.dart';
|
||||||
|
import 'package:flutter_hbb/common/shared_state.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/model.dart';
|
import 'package:flutter_hbb/models/model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -10,15 +11,10 @@ import './platform_model.dart';
|
|||||||
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'
|
||||||
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
if (dart.library.html) 'package:flutter_hbb/web/texture_rgba_renderer.dart';
|
||||||
|
|
||||||
// Feature flutter_texture_render need to be enabled if feature gpucodec is enabled.
|
|
||||||
final useTextureRender = !isWeb &&
|
|
||||||
(bind.mainHasPixelbufferTextureRender() || bind.mainHasGpuTextureRender());
|
|
||||||
|
|
||||||
class _PixelbufferTexture {
|
class _PixelbufferTexture {
|
||||||
int _textureKey = -1;
|
int _textureKey = -1;
|
||||||
int _display = 0;
|
int _display = 0;
|
||||||
SessionID? _sessionId;
|
SessionID? _sessionId;
|
||||||
final support = bind.mainHasPixelbufferTextureRender();
|
|
||||||
bool _destroying = false;
|
bool _destroying = false;
|
||||||
int? _id;
|
int? _id;
|
||||||
|
|
||||||
@@ -27,26 +23,24 @@ class _PixelbufferTexture {
|
|||||||
int get display => _display;
|
int get display => _display;
|
||||||
|
|
||||||
create(int d, SessionID sessionId, FFI ffi) {
|
create(int d, SessionID sessionId, FFI ffi) {
|
||||||
if (support) {
|
_display = d;
|
||||||
_display = d;
|
_textureKey = bind.getNextTextureKey();
|
||||||
_textureKey = bind.getNextTextureKey();
|
_sessionId = sessionId;
|
||||||
_sessionId = sessionId;
|
|
||||||
|
|
||||||
textureRenderer.createTexture(_textureKey).then((id) async {
|
textureRenderer.createTexture(_textureKey).then((id) async {
|
||||||
_id = id;
|
_id = id;
|
||||||
if (id != -1) {
|
if (id != -1) {
|
||||||
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
ffi.textureModel.setRgbaTextureId(display: d, id: id);
|
||||||
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
final ptr = await textureRenderer.getTexturePtr(_textureKey);
|
||||||
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
platformFFI.registerPixelbufferTexture(sessionId, display, ptr);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id");
|
"create pixelbuffer texture: peerId: ${ffi.id} display:$_display, textureId:$id, texturePtr:$ptr");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(bool unregisterTexture, FFI ffi) async {
|
destroy(bool unregisterTexture, FFI ffi) async {
|
||||||
if (!_destroying && support && _textureKey != -1 && _sessionId != null) {
|
if (!_destroying && _textureKey != -1 && _sessionId != null) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
if (unregisterTexture) {
|
if (unregisterTexture) {
|
||||||
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
platformFFI.registerPixelbufferTexture(_sessionId!, display, 0);
|
||||||
@@ -101,13 +95,15 @@ class _GpuTexture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
destroy(FFI ffi) async {
|
destroy(bool unregisterTexture, FFI ffi) async {
|
||||||
// must stop texture render, render unregistered texture cause crash
|
// must stop texture render, render unregistered texture cause crash
|
||||||
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
if (!_destroying && support && _sessionId != null && _textureId != -1) {
|
||||||
_destroying = true;
|
_destroying = true;
|
||||||
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
if (unregisterTexture) {
|
||||||
// sleep for a while to avoid the texture is used after it's unregistered.
|
platformFFI.registerGpuTexture(_sessionId!, _display, 0);
|
||||||
await Future.delayed(Duration(milliseconds: 100));
|
// sleep for a while to avoid the texture is used after it's unregistered.
|
||||||
|
await Future.delayed(Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
await gpuTextureRenderer.unregisterTexture(_textureId);
|
await gpuTextureRenderer.unregisterTexture(_textureId);
|
||||||
_textureId = -1;
|
_textureId = -1;
|
||||||
_destroying = false;
|
_destroying = false;
|
||||||
@@ -152,40 +148,36 @@ class TextureModel {
|
|||||||
TextureModel(this.parent);
|
TextureModel(this.parent);
|
||||||
|
|
||||||
setTextureType({required int display, required bool gpuTexture}) {
|
setTextureType({required int display, required bool gpuTexture}) {
|
||||||
debugPrint("setTextureType: display:$display, isGpuTexture:$gpuTexture");
|
debugPrint("setTextureType: display=$display, isGpuTexture=$gpuTexture");
|
||||||
var texture = _control[display];
|
ensureControl(display);
|
||||||
if (texture == null) {
|
_control[display]?.setTextureType(gpuTexture: gpuTexture);
|
||||||
texture = _Control();
|
// For versions that do not support multiple displays, the display parameter is always 0, need set type of current display
|
||||||
_control[display] = texture;
|
final ffi = parent.target;
|
||||||
|
if (ffi == null) return;
|
||||||
|
if (!ffi.ffiModel.pi.isSupportMultiDisplay) {
|
||||||
|
final currentDisplay = CurrentDisplayState.find(ffi.id).value;
|
||||||
|
if (currentDisplay != display) {
|
||||||
|
debugPrint(
|
||||||
|
"setTextureType: currentDisplay=$currentDisplay, isGpuTexture=$gpuTexture");
|
||||||
|
ensureControl(currentDisplay);
|
||||||
|
_control[currentDisplay]?.setTextureType(gpuTexture: gpuTexture);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
texture.setTextureType(gpuTexture: gpuTexture);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setRgbaTextureId({required int display, required int id}) {
|
setRgbaTextureId({required int display, required int id}) {
|
||||||
var ctl = _control[display];
|
ensureControl(display);
|
||||||
if (ctl == null) {
|
_control[display]?.setRgbaTextureId(id);
|
||||||
ctl = _Control();
|
|
||||||
_control[display] = ctl;
|
|
||||||
}
|
|
||||||
ctl.setRgbaTextureId(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setGpuTextureId({required int display, required int id}) {
|
setGpuTextureId({required int display, required int id}) {
|
||||||
var ctl = _control[display];
|
ensureControl(display);
|
||||||
if (ctl == null) {
|
_control[display]?.setGpuTextureId(id);
|
||||||
ctl = _Control();
|
|
||||||
_control[display] = ctl;
|
|
||||||
}
|
|
||||||
ctl.setGpuTextureId(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RxInt getTextureId(int display) {
|
RxInt getTextureId(int display) {
|
||||||
var ctl = _control[display];
|
ensureControl(display);
|
||||||
if (ctl == null) {
|
return _control[display]!.textureID;
|
||||||
ctl = _Control();
|
|
||||||
_control[display] = ctl;
|
|
||||||
}
|
|
||||||
return ctl.textureID;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurrentDisplay(int curDisplay) {
|
updateCurrentDisplay(int curDisplay) {
|
||||||
@@ -211,7 +203,7 @@ class TextureModel {
|
|||||||
_pixelbufferRenderTextures.remove(idx);
|
_pixelbufferRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
if (_gpuRenderTextures.containsKey(idx)) {
|
if (_gpuRenderTextures.containsKey(idx)) {
|
||||||
_gpuRenderTextures[idx]!.destroy(ffi);
|
_gpuRenderTextures[idx]!.destroy(true, ffi);
|
||||||
_gpuRenderTextures.remove(idx);
|
_gpuRenderTextures.remove(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -238,7 +230,15 @@ class TextureModel {
|
|||||||
await texture.destroy(closeSession, ffi);
|
await texture.destroy(closeSession, ffi);
|
||||||
}
|
}
|
||||||
for (final texture in _gpuRenderTextures.values) {
|
for (final texture in _gpuRenderTextures.values) {
|
||||||
await texture.destroy(ffi);
|
await texture.destroy(closeSession, ffi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureControl(int display) {
|
||||||
|
var ctl = _control[display];
|
||||||
|
if (ctl == null) {
|
||||||
|
ctl = _Control();
|
||||||
|
_control[display] = ctl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:flutter_hbb/models/peer_model.dart';
|
|||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:http/http.dart' as http;
|
import '../utils/http_service.dart' as http;
|
||||||
|
|
||||||
class GroupModel {
|
class GroupModel {
|
||||||
final RxBool groupLoading = false.obs;
|
final RxBool groupLoading = false.obs;
|
||||||
@@ -26,6 +26,7 @@ class GroupModel {
|
|||||||
GroupModel(this.parent);
|
GroupModel(this.parent);
|
||||||
|
|
||||||
Future<void> pull({force = true, quiet = false}) async {
|
Future<void> pull({force = true, quiet = false}) async {
|
||||||
|
if (bind.isDisableGroupPanel()) return;
|
||||||
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
if (!gFFI.userModel.isLogin || groupLoading.value) return;
|
||||||
if (!force && initialized) return;
|
if (!force && initialized) return;
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import 'dart:io';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:flutter_hbb/utils/multi_window_manager.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../models/model.dart';
|
import '../../models/model.dart';
|
||||||
@@ -21,6 +24,128 @@ const _kMouseEventDown = 'mousedown';
|
|||||||
const _kMouseEventUp = 'mouseup';
|
const _kMouseEventUp = 'mouseup';
|
||||||
const _kMouseEventMove = 'mousemove';
|
const _kMouseEventMove = 'mousemove';
|
||||||
|
|
||||||
|
class CanvasCoords {
|
||||||
|
double x = 0;
|
||||||
|
double y = 0;
|
||||||
|
double scale = 1.0;
|
||||||
|
double scrollX = 0;
|
||||||
|
double scrollY = 0;
|
||||||
|
ScrollStyle scrollStyle = ScrollStyle.scrollauto;
|
||||||
|
Size size = Size.zero;
|
||||||
|
|
||||||
|
CanvasCoords();
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
'scale': scale,
|
||||||
|
'scrollX': scrollX,
|
||||||
|
'scrollY': scrollY,
|
||||||
|
'scrollStyle':
|
||||||
|
scrollStyle == ScrollStyle.scrollauto ? 'scrollauto' : 'scrollbar',
|
||||||
|
'size': {
|
||||||
|
'w': size.width,
|
||||||
|
'h': size.height,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static CanvasCoords fromJson(Map<String, dynamic> json) {
|
||||||
|
final model = CanvasCoords();
|
||||||
|
model.x = json['x'];
|
||||||
|
model.y = json['y'];
|
||||||
|
model.scale = json['scale'];
|
||||||
|
model.scrollX = json['scrollX'];
|
||||||
|
model.scrollY = json['scrollY'];
|
||||||
|
model.scrollStyle = json['scrollStyle'] == 'scrollauto'
|
||||||
|
? ScrollStyle.scrollauto
|
||||||
|
: ScrollStyle.scrollbar;
|
||||||
|
model.size = Size(json['size']['w'], json['size']['h']);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CanvasCoords fromCanvasModel(CanvasModel model) {
|
||||||
|
final coords = CanvasCoords();
|
||||||
|
coords.x = model.x;
|
||||||
|
coords.y = model.y;
|
||||||
|
coords.scale = model.scale;
|
||||||
|
coords.scrollX = model.scrollX;
|
||||||
|
coords.scrollY = model.scrollY;
|
||||||
|
coords.scrollStyle = model.scrollStyle;
|
||||||
|
coords.size = model.size;
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CursorCoords {
|
||||||
|
Offset offset = Offset.zero;
|
||||||
|
|
||||||
|
CursorCoords();
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'offset_x': offset.dx,
|
||||||
|
'offset_y': offset.dy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static CursorCoords fromJson(Map<String, dynamic> json) {
|
||||||
|
final model = CursorCoords();
|
||||||
|
model.offset = Offset(json['offset_x'], json['offset_y']);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CursorCoords fromCursorModel(CursorModel model) {
|
||||||
|
final coords = CursorCoords();
|
||||||
|
coords.offset = model.offset;
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteWindowCoords {
|
||||||
|
RemoteWindowCoords(
|
||||||
|
this.windowRect, this.canvas, this.cursor, this.remoteRect);
|
||||||
|
Rect windowRect;
|
||||||
|
CanvasCoords canvas;
|
||||||
|
CursorCoords cursor;
|
||||||
|
Rect remoteRect;
|
||||||
|
Offset relativeOffset = Offset.zero;
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'canvas': canvas.toJson(),
|
||||||
|
'cursor': cursor.toJson(),
|
||||||
|
'windowRect': rectToJson(windowRect),
|
||||||
|
'remoteRect': rectToJson(remoteRect),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, dynamic> rectToJson(Rect r) {
|
||||||
|
return {
|
||||||
|
'l': r.left,
|
||||||
|
't': r.top,
|
||||||
|
'w': r.width,
|
||||||
|
'h': r.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect rectFromJson(Map<String, dynamic> json) {
|
||||||
|
return Rect.fromLTWH(
|
||||||
|
json['l'],
|
||||||
|
json['t'],
|
||||||
|
json['w'],
|
||||||
|
json['h'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RemoteWindowCoords.fromJson(Map<String, dynamic> json)
|
||||||
|
: windowRect = rectFromJson(json['windowRect']),
|
||||||
|
canvas = CanvasCoords.fromJson(json['canvas']),
|
||||||
|
cursor = CursorCoords.fromJson(json['cursor']),
|
||||||
|
remoteRect = rectFromJson(json['remoteRect']);
|
||||||
|
}
|
||||||
|
|
||||||
extension ToString on MouseButtons {
|
extension ToString on MouseButtons {
|
||||||
String get value {
|
String get value {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
@@ -188,32 +313,29 @@ class InputModel {
|
|||||||
int _lastButtons = 0;
|
int _lastButtons = 0;
|
||||||
Offset lastMousePos = Offset.zero;
|
Offset lastMousePos = Offset.zero;
|
||||||
|
|
||||||
|
bool _queryOtherWindowCoords = false;
|
||||||
|
Rect? _windowRect;
|
||||||
|
List<RemoteWindowCoords> _remoteWindowCoords = [];
|
||||||
|
|
||||||
late final SessionID sessionId;
|
late final SessionID sessionId;
|
||||||
|
|
||||||
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
bool get keyboardPerm => parent.target!.ffiModel.keyboard;
|
||||||
String get id => parent.target?.id ?? '';
|
String get id => parent.target?.id ?? '';
|
||||||
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
String? get peerPlatform => parent.target?.ffiModel.pi.platform;
|
||||||
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
bool get isViewOnly => parent.target!.ffiModel.viewOnly;
|
||||||
|
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||||
|
|
||||||
InputModel(this.parent) {
|
InputModel(this.parent) {
|
||||||
sessionId = parent.target!.sessionId;
|
sessionId = parent.target!.sessionId;
|
||||||
|
|
||||||
// It is ok to call updateKeyboardMode() directly.
|
|
||||||
// Because `bind` is initialized in `PlatformFFI.init()` which is called very early.
|
|
||||||
// But we still wrap it in a Future.delayed() to make it more clear.
|
|
||||||
Future.delayed(Duration(milliseconds: 100), () {
|
|
||||||
updateKeyboardMode();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function must be called after the peer info is received.
|
||||||
|
// Because `sessionGetKeyboardMode` relies on the peer version.
|
||||||
updateKeyboardMode() async {
|
updateKeyboardMode() async {
|
||||||
// * Currently mobile does not enable map mode
|
// * Currently mobile does not enable map mode
|
||||||
if (isDesktop || isWebDesktop) {
|
if (isDesktop || isWebDesktop) {
|
||||||
if (keyboardMode.isEmpty) {
|
keyboardMode = await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
|
||||||
keyboardMode =
|
kKeyLegacyMode;
|
||||||
await bind.sessionGetKeyboardMode(sessionId: sessionId) ??
|
|
||||||
kKeyLegacyMode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,6 +738,9 @@ class InputModel {
|
|||||||
void onPointDownImage(PointerDownEvent e) {
|
void onPointDownImage(PointerDownEvent e) {
|
||||||
debugPrint("onPointDownImage ${e.kind}");
|
debugPrint("onPointDownImage ${e.kind}");
|
||||||
_stopFling = true;
|
_stopFling = true;
|
||||||
|
if (isDesktop) _queryOtherWindowCoords = true;
|
||||||
|
_remoteWindowCoords = [];
|
||||||
|
_windowRect = null;
|
||||||
if (isViewOnly) return;
|
if (isViewOnly) return;
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) {
|
if (e.kind != ui.PointerDeviceKind.mouse) {
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
@@ -628,6 +753,7 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void onPointUpImage(PointerUpEvent e) {
|
void onPointUpImage(PointerUpEvent e) {
|
||||||
|
if (isDesktop) _queryOtherWindowCoords = false;
|
||||||
if (isViewOnly) return;
|
if (isViewOnly) return;
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
@@ -638,11 +764,37 @@ class InputModel {
|
|||||||
void onPointMoveImage(PointerMoveEvent e) {
|
void onPointMoveImage(PointerMoveEvent e) {
|
||||||
if (isViewOnly) return;
|
if (isViewOnly) return;
|
||||||
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
if (e.kind != ui.PointerDeviceKind.mouse) return;
|
||||||
|
if (_queryOtherWindowCoords) {
|
||||||
|
Future.delayed(Duration.zero, () async {
|
||||||
|
_windowRect = await fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
|
||||||
|
});
|
||||||
|
_queryOtherWindowCoords = false;
|
||||||
|
}
|
||||||
if (isPhysicalMouse.value) {
|
if (isPhysicalMouse.value) {
|
||||||
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
|
handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<Rect?> fillRemoteCoordsAndGetCurFrame(
|
||||||
|
List<RemoteWindowCoords> remoteWindowCoords) async {
|
||||||
|
final coords =
|
||||||
|
await rustDeskWinManager.getOtherRemoteWindowCoordsFromMain();
|
||||||
|
final wc = WindowController.fromWindowId(kWindowId!);
|
||||||
|
try {
|
||||||
|
final frame = await wc.getFrame();
|
||||||
|
for (final c in coords) {
|
||||||
|
c.relativeOffset = Offset(
|
||||||
|
c.windowRect.left - frame.left, c.windowRect.top - frame.top);
|
||||||
|
remoteWindowCoords.add(c);
|
||||||
|
}
|
||||||
|
return frame;
|
||||||
|
} catch (e) {
|
||||||
|
// Unreachable code
|
||||||
|
debugPrint("Failed to get frame of window $kWindowId, it may be hidden");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
void onPointerSignalImage(PointerSignalEvent e) {
|
void onPointerSignalImage(PointerSignalEvent e) {
|
||||||
if (isViewOnly) return;
|
if (isViewOnly) return;
|
||||||
if (e is PointerScrollEvent) {
|
if (e is PointerScrollEvent) {
|
||||||
@@ -843,43 +995,107 @@ class InputModel {
|
|||||||
bool onExit = false,
|
bool onExit = false,
|
||||||
int buttons = kPrimaryMouseButton,
|
int buttons = kPrimaryMouseButton,
|
||||||
}) {
|
}) {
|
||||||
y -= CanvasModel.topToEdge;
|
|
||||||
x -= CanvasModel.leftToEdge;
|
|
||||||
final canvasModel = parent.target!.canvasModel;
|
|
||||||
final ffiModel = parent.target!.ffiModel;
|
final ffiModel = parent.target!.ffiModel;
|
||||||
|
CanvasCoords canvas =
|
||||||
|
CanvasCoords.fromCanvasModel(parent.target!.canvasModel);
|
||||||
|
Rect? rect = ffiModel.rect;
|
||||||
|
|
||||||
if (isMove) {
|
if (isMove) {
|
||||||
canvasModel.moveDesktopMouse(x, y);
|
if (_remoteWindowCoords.isNotEmpty &&
|
||||||
|
_windowRect != null &&
|
||||||
|
!_isInCurrentWindow(x, y)) {
|
||||||
|
final coords =
|
||||||
|
findRemoteCoords(x, y, _remoteWindowCoords, devicePixelRatio);
|
||||||
|
if (coords != null) {
|
||||||
|
isMove = false;
|
||||||
|
canvas = coords.canvas;
|
||||||
|
rect = coords.remoteRect;
|
||||||
|
x -= coords.relativeOffset.dx / devicePixelRatio;
|
||||||
|
y -= coords.relativeOffset.dy / devicePixelRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final nearThr = 3;
|
y -= CanvasModel.topToEdge;
|
||||||
var nearRight = (canvasModel.size.width - x) < nearThr;
|
x -= CanvasModel.leftToEdge;
|
||||||
var nearBottom = (canvasModel.size.height - y) < nearThr;
|
if (isMove) {
|
||||||
final rect = ffiModel.rect;
|
parent.target!.canvasModel.moveDesktopMouse(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _handlePointerDevicePos(
|
||||||
|
kind,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
isMove,
|
||||||
|
canvas,
|
||||||
|
rect,
|
||||||
|
evtType,
|
||||||
|
onExit: onExit,
|
||||||
|
buttons: buttons,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isInCurrentWindow(double x, double y) {
|
||||||
|
final w = _windowRect!.width / devicePixelRatio;
|
||||||
|
final h = _windowRect!.width / devicePixelRatio;
|
||||||
|
return x >= 0 && y >= 0 && x <= w && y <= h;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RemoteWindowCoords? findRemoteCoords(double x, double y,
|
||||||
|
List<RemoteWindowCoords> remoteWindowCoords, double devicePixelRatio) {
|
||||||
|
x *= devicePixelRatio;
|
||||||
|
y *= devicePixelRatio;
|
||||||
|
for (final c in remoteWindowCoords) {
|
||||||
|
if (x >= c.relativeOffset.dx &&
|
||||||
|
y >= c.relativeOffset.dy &&
|
||||||
|
x <= c.relativeOffset.dx + c.windowRect.width &&
|
||||||
|
y <= c.relativeOffset.dy + c.windowRect.height) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point? _handlePointerDevicePos(
|
||||||
|
String kind,
|
||||||
|
double x,
|
||||||
|
double y,
|
||||||
|
bool moveInCanvas,
|
||||||
|
CanvasCoords canvas,
|
||||||
|
Rect? rect,
|
||||||
|
String evtType, {
|
||||||
|
bool onExit = false,
|
||||||
|
int buttons = kPrimaryMouseButton,
|
||||||
|
}) {
|
||||||
if (rect == null) {
|
if (rect == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final imageWidth = rect.width * canvasModel.scale;
|
|
||||||
final imageHeight = rect.height * canvasModel.scale;
|
final nearThr = 3;
|
||||||
if (canvasModel.scrollStyle == ScrollStyle.scrollbar) {
|
var nearRight = (canvas.size.width - x) < nearThr;
|
||||||
x += imageWidth * canvasModel.scrollX;
|
var nearBottom = (canvas.size.height - y) < nearThr;
|
||||||
y += imageHeight * canvasModel.scrollY;
|
final imageWidth = rect.width * canvas.scale;
|
||||||
|
final imageHeight = rect.height * canvas.scale;
|
||||||
|
if (canvas.scrollStyle == ScrollStyle.scrollbar) {
|
||||||
|
x += imageWidth * canvas.scrollX;
|
||||||
|
y += imageHeight * canvas.scrollY;
|
||||||
|
|
||||||
// boxed size is a center widget
|
// boxed size is a center widget
|
||||||
if (canvasModel.size.width > imageWidth) {
|
if (canvas.size.width > imageWidth) {
|
||||||
x -= ((canvasModel.size.width - imageWidth) / 2);
|
x -= ((canvas.size.width - imageWidth) / 2);
|
||||||
}
|
}
|
||||||
if (canvasModel.size.height > imageHeight) {
|
if (canvas.size.height > imageHeight) {
|
||||||
y -= ((canvasModel.size.height - imageHeight) / 2);
|
y -= ((canvas.size.height - imageHeight) / 2);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
x -= canvasModel.x;
|
x -= canvas.x;
|
||||||
y -= canvasModel.y;
|
y -= canvas.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
x /= canvasModel.scale;
|
x /= canvas.scale;
|
||||||
y /= canvasModel.scale;
|
y /= canvas.scale;
|
||||||
if (canvasModel.scale > 0 && canvasModel.scale < 1) {
|
if (canvas.scale > 0 && canvas.scale < 1) {
|
||||||
final step = 1.0 / canvasModel.scale - 1;
|
final step = 1.0 / canvas.scale - 1;
|
||||||
if (nearRight) {
|
if (nearRight) {
|
||||||
x += step;
|
x += step;
|
||||||
}
|
}
|
||||||
@@ -902,8 +1118,7 @@ class InputModel {
|
|||||||
evtX = x.round();
|
evtX = x.round();
|
||||||
evtY = y.round();
|
evtY = y.round();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrintStack(
|
debugPrintStack(label: 'canvas.scale value ${canvas.scale}, $e');
|
||||||
label: 'canvasModel.scale value ${canvasModel.scale}, $e');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ class CachedPeerData {
|
|||||||
Map<String, dynamic> peerInfo = {};
|
Map<String, dynamic> peerInfo = {};
|
||||||
List<Map<String, dynamic>> cursorDataList = [];
|
List<Map<String, dynamic>> cursorDataList = [];
|
||||||
Map<String, dynamic> lastCursorId = {};
|
Map<String, dynamic> lastCursorId = {};
|
||||||
|
Map<String, bool> permissions = {};
|
||||||
|
|
||||||
bool secure = false;
|
bool secure = false;
|
||||||
bool direct = false;
|
bool direct = false;
|
||||||
|
|
||||||
@@ -62,6 +64,7 @@ class CachedPeerData {
|
|||||||
'peerInfo': peerInfo,
|
'peerInfo': peerInfo,
|
||||||
'cursorDataList': cursorDataList,
|
'cursorDataList': cursorDataList,
|
||||||
'lastCursorId': lastCursorId,
|
'lastCursorId': lastCursorId,
|
||||||
|
'permissions': permissions,
|
||||||
'secure': secure,
|
'secure': secure,
|
||||||
'direct': direct,
|
'direct': direct,
|
||||||
});
|
});
|
||||||
@@ -77,6 +80,9 @@ class CachedPeerData {
|
|||||||
data.cursorDataList.add(cursorData);
|
data.cursorDataList.add(cursorData);
|
||||||
}
|
}
|
||||||
data.lastCursorId = map['lastCursorId'];
|
data.lastCursorId = map['lastCursorId'];
|
||||||
|
map['permissions'].forEach((key, value) {
|
||||||
|
data.permissions[key] = value;
|
||||||
|
});
|
||||||
data.secure = map['secure'];
|
data.secure = map['secure'];
|
||||||
data.direct = map['direct'];
|
data.direct = map['direct'];
|
||||||
return data;
|
return data;
|
||||||
@@ -106,6 +112,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
RxBool waitForImageDialogShow = true.obs;
|
RxBool waitForImageDialogShow = true.obs;
|
||||||
Timer? waitForImageTimer;
|
Timer? waitForImageTimer;
|
||||||
RxBool waitForFirstImage = true.obs;
|
RxBool waitForFirstImage = true.obs;
|
||||||
|
bool isRefreshing = false;
|
||||||
|
|
||||||
Rect? get rect => _rect;
|
Rect? get rect => _rect;
|
||||||
bool get isOriginalResolutionSet =>
|
bool get isOriginalResolutionSet =>
|
||||||
@@ -116,6 +123,10 @@ class FfiModel with ChangeNotifier {
|
|||||||
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false;
|
_pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolution ?? false;
|
||||||
|
|
||||||
Map<String, bool> get permissions => _permissions;
|
Map<String, bool> get permissions => _permissions;
|
||||||
|
setPermissions(Map<String, bool> permissions) {
|
||||||
|
_permissions.clear();
|
||||||
|
_permissions.addAll(permissions);
|
||||||
|
}
|
||||||
|
|
||||||
bool? get secure => _secure;
|
bool? get secure => _secure;
|
||||||
|
|
||||||
@@ -138,6 +149,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
FfiModel(this.parent) {
|
FfiModel(this.parent) {
|
||||||
clear();
|
clear();
|
||||||
sessionId = parent.target!.sessionId;
|
sessionId = parent.target!.sessionId;
|
||||||
|
cachedPeerData.permissions = _permissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
|
Rect? globalDisplaysRect() => _getDisplaysRect(_pi.displays, true);
|
||||||
@@ -233,7 +245,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
handleMsgBox({
|
handleMsgBox({
|
||||||
'type': 'success',
|
'type': 'success',
|
||||||
'title': 'Successful',
|
'title': 'Successful',
|
||||||
'text': 'Connected, waiting for image...',
|
'text': kMsgboxTextWaitingForImage,
|
||||||
'link': '',
|
'link': '',
|
||||||
}, sessionId, peerId);
|
}, sessionId, peerId);
|
||||||
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
|
updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
|
||||||
@@ -367,16 +379,29 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
} else if (name == 'sync_peer_option') {
|
} else if (name == 'sync_peer_option') {
|
||||||
_handleSyncPeerOption(evt, peerId);
|
_handleSyncPeerOption(evt, peerId);
|
||||||
|
} else if (name == 'follow_current_display') {
|
||||||
|
handleFollowCurrentDisplay(evt, sessionId, peerId);
|
||||||
|
} else if (name == 'use_texture_render') {
|
||||||
|
_handleUseTextureRender(evt, sessionId, peerId);
|
||||||
} else {
|
} else {
|
||||||
debugPrint('Unknown event name: $name');
|
debugPrint('Unknown event name: $name');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_handleUseTextureRender(
|
||||||
|
Map<String, dynamic> evt, SessionID sessionId, String peerId) {
|
||||||
|
parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y');
|
||||||
|
waitForFirstImage.value = true;
|
||||||
|
isRefreshing = true;
|
||||||
|
showConnectedWaitingForImage(parent.target!.dialogManager, sessionId,
|
||||||
|
'success', 'Successful', kMsgboxTextWaitingForImage);
|
||||||
|
}
|
||||||
|
|
||||||
_handleSyncPeerOption(Map<String, dynamic> evt, String peer) {
|
_handleSyncPeerOption(Map<String, dynamic> evt, String peer) {
|
||||||
final k = evt['k'];
|
final k = evt['k'];
|
||||||
final v = evt['v'];
|
final v = evt['v'];
|
||||||
if (k == kOptionViewOnly) {
|
if (k == kOptionToggleViewOnly) {
|
||||||
setViewOnly(peer, v as bool);
|
setViewOnly(peer, v as bool);
|
||||||
} else if (k == 'keyboard_mode') {
|
} else if (k == 'keyboard_mode') {
|
||||||
parent.target?.inputModel.updateKeyboardMode();
|
parent.target?.inputModel.updateKeyboardMode();
|
||||||
@@ -440,7 +465,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCurDisplay(SessionID sessionId, {updateCursorPos = true}) {
|
updateCurDisplay(SessionID sessionId, {updateCursorPos = false}) {
|
||||||
final newRect = displaysRect();
|
final newRect = displaysRect();
|
||||||
if (newRect == null) {
|
if (newRect == null) {
|
||||||
return;
|
return;
|
||||||
@@ -559,10 +584,14 @@ class FfiModel with ChangeNotifier {
|
|||||||
showElevationError(sessionId, type, title, text, dialogManager);
|
showElevationError(sessionId, type, title, text, dialogManager);
|
||||||
} else if (type == 'relay-hint' || type == 'relay-hint2') {
|
} else if (type == 'relay-hint' || type == 'relay-hint2') {
|
||||||
showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
|
showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
|
||||||
} else if (text == 'Connected, waiting for image...') {
|
} else if (text == kMsgboxTextWaitingForImage) {
|
||||||
showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
|
showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
|
||||||
|
} else if (title == 'Privacy mode') {
|
||||||
|
final hasRetry = evt['hasRetry'] == 'true';
|
||||||
|
showPrivacyFailedDialog(
|
||||||
|
sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||||
} else {
|
} else {
|
||||||
var hasRetry = evt['hasRetry'] == 'true';
|
final hasRetry = evt['hasRetry'] == 'true';
|
||||||
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -650,13 +679,34 @@ class FfiModel with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
waitForImageDialogShow.value = true;
|
waitForImageDialogShow.value = true;
|
||||||
waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
|
waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
|
||||||
if (waitForFirstImage.isTrue) {
|
if (waitForFirstImage.isTrue && !isRefreshing) {
|
||||||
bind.sessionInputOsPassword(sessionId: sessionId, value: '');
|
bind.sessionInputOsPassword(sessionId: sessionId, value: '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId);
|
bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showPrivacyFailedDialog(
|
||||||
|
SessionID sessionId,
|
||||||
|
String type,
|
||||||
|
String title,
|
||||||
|
String text,
|
||||||
|
String link,
|
||||||
|
bool hasRetry,
|
||||||
|
OverlayDialogManager dialogManager) {
|
||||||
|
if (text == 'no_need_privacy_mode_no_physical_displays_tip' ||
|
||||||
|
text == 'Enter privacy mode') {
|
||||||
|
// There are display changes on the remote side,
|
||||||
|
// which will cause some messages to refresh the canvas and dismiss dialogs.
|
||||||
|
// So we add a delay here to ensure the dialog is displayed.
|
||||||
|
Future.delayed(Duration(milliseconds: 3000), () {
|
||||||
|
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_updateSessionWidthHeight(SessionID sessionId) {
|
_updateSessionWidthHeight(SessionID sessionId) {
|
||||||
if (_rect == null) return;
|
if (_rect == null) return;
|
||||||
if (_rect!.width <= 0 || _rect!.height <= 0) {
|
if (_rect!.width <= 0 || _rect!.height <= 0) {
|
||||||
@@ -687,9 +737,14 @@ class FfiModel with ChangeNotifier {
|
|||||||
|
|
||||||
/// Handle the peer info event based on [evt].
|
/// Handle the peer info event based on [evt].
|
||||||
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
|
handlePeerInfo(Map<String, dynamic> evt, String peerId, bool isCache) async {
|
||||||
|
// This call is to ensuer the keyboard mode is updated depending on the peer version.
|
||||||
|
parent.target?.inputModel.updateKeyboardMode();
|
||||||
|
|
||||||
// Map clone is required here, otherwise "evt" may be changed by other threads through the reference.
|
// Map clone is required here, otherwise "evt" may be changed by other threads through the reference.
|
||||||
// Because this function is asynchronous, there's an "await" in this function.
|
// Because this function is asynchronous, there's an "await" in this function.
|
||||||
cachedPeerData.peerInfo = {...evt};
|
cachedPeerData.peerInfo = {...evt};
|
||||||
|
// Do not cache resolutions, because a new display connection have different resolutions.
|
||||||
|
cachedPeerData.peerInfo.remove('resolutions');
|
||||||
|
|
||||||
// Recent peer is updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
|
// Recent peer is updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
|
||||||
bind.mainLoadRecentPeers();
|
bind.mainLoadRecentPeers();
|
||||||
@@ -722,7 +777,7 @@ class FfiModel with ChangeNotifier {
|
|||||||
_touchMode = true;
|
_touchMode = true;
|
||||||
} else {
|
} else {
|
||||||
_touchMode = await bind.sessionGetOption(
|
_touchMode = await bind.sessionGetOption(
|
||||||
sessionId: sessionId, arg: 'touch-mode') !=
|
sessionId: sessionId, arg: kOptionTouchMode) !=
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
if (connType == ConnType.fileTransfer) {
|
if (connType == ConnType.fileTransfer) {
|
||||||
@@ -742,17 +797,20 @@ class FfiModel with ChangeNotifier {
|
|||||||
if (displays.isNotEmpty) {
|
if (displays.isNotEmpty) {
|
||||||
_reconnects = 1;
|
_reconnects = 1;
|
||||||
waitForFirstImage.value = true;
|
waitForFirstImage.value = true;
|
||||||
|
isRefreshing = false;
|
||||||
}
|
}
|
||||||
Map<String, dynamic> features = json.decode(evt['features']);
|
Map<String, dynamic> features = json.decode(evt['features']);
|
||||||
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
_pi.features.privacyMode = features['privacy_mode'] == 1;
|
||||||
handleResolutions(peerId, evt["resolutions"]);
|
if (!isCache) {
|
||||||
|
handleResolutions(peerId, evt["resolutions"]);
|
||||||
|
}
|
||||||
parent.target?.elevationModel.onPeerInfo(_pi);
|
parent.target?.elevationModel.onPeerInfo(_pi);
|
||||||
}
|
}
|
||||||
if (connType == ConnType.defaultConn) {
|
if (connType == ConnType.defaultConn) {
|
||||||
setViewOnly(
|
setViewOnly(
|
||||||
peerId,
|
peerId,
|
||||||
bind.sessionGetToggleOptionSync(
|
bind.sessionGetToggleOptionSync(
|
||||||
sessionId: sessionId, arg: kOptionViewOnly));
|
sessionId: sessionId, arg: kOptionToggleViewOnly));
|
||||||
}
|
}
|
||||||
if (connType == ConnType.defaultConn) {
|
if (connType == ConnType.defaultConn) {
|
||||||
final platformAdditions = evt['platform_additions'];
|
final platformAdditions = evt['platform_additions'];
|
||||||
@@ -975,6 +1033,8 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
parent.target!.canvasModel
|
||||||
|
.tryUpdateScrollStyle(Duration(milliseconds: 300), null);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,15 +1046,21 @@ class FfiModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateData.isEmpty) {
|
if (updateData.isEmpty) {
|
||||||
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
|
_pi.platformAdditions.remove(kPlatformAdditionsRustDeskVirtualDisplays);
|
||||||
|
_pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
final updateJson = json.decode(updateData) as Map<String, dynamic>;
|
final updateJson = json.decode(updateData) as Map<String, dynamic>;
|
||||||
for (final key in updateJson.keys) {
|
for (final key in updateJson.keys) {
|
||||||
_pi.platformAdditions[key] = updateJson[key];
|
_pi.platformAdditions[key] = updateJson[key];
|
||||||
}
|
}
|
||||||
if (!updateJson.containsKey(kPlatformAdditionsVirtualDisplays)) {
|
if (!updateJson
|
||||||
_pi.platformAdditions.remove(kPlatformAdditionsVirtualDisplays);
|
.containsKey(kPlatformAdditionsRustDeskVirtualDisplays)) {
|
||||||
|
_pi.platformAdditions
|
||||||
|
.remove(kPlatformAdditionsRustDeskVirtualDisplays);
|
||||||
|
}
|
||||||
|
if (!updateJson.containsKey(kPlatformAdditionsAmyuniVirtualDisplays)) {
|
||||||
|
_pi.platformAdditions.remove(kPlatformAdditionsAmyuniVirtualDisplays);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to decode platformAdditions $e');
|
debugPrint('Failed to decode platformAdditions $e');
|
||||||
@@ -1005,9 +1071,30 @@ class FfiModel with ChangeNotifier {
|
|||||||
json.encode(_pi.platformAdditions);
|
json.encode(_pi.platformAdditions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleFollowCurrentDisplay(
|
||||||
|
Map<String, dynamic> evt, SessionID sessionId, String peerId) async {
|
||||||
|
if (evt['display_idx'] != null) {
|
||||||
|
if (pi.currentDisplay == kAllDisplayValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_pi.currentDisplay = int.parse(evt['display_idx']);
|
||||||
|
try {
|
||||||
|
CurrentDisplayState.find(peerId).value = _pi.currentDisplay;
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
bind.sessionSwitchDisplay(
|
||||||
|
isDesktop: isDesktop,
|
||||||
|
sessionId: sessionId,
|
||||||
|
value: Int32List.fromList([_pi.currentDisplay]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
// Directly switch to the new display without waiting for the response.
|
// Directly switch to the new display without waiting for the response.
|
||||||
switchToNewDisplay(int display, SessionID sessionId, String peerId,
|
switchToNewDisplay(int display, SessionID sessionId, String peerId,
|
||||||
{bool updateCursorPos = true}) {
|
{bool updateCursorPos = false}) {
|
||||||
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
|
// VideoHandler creation is upon when video frames are received, so either caching commands(don't know next width/height) or stopping recording when switching displays.
|
||||||
parent.target?.recordingModel.onClose();
|
parent.target?.recordingModel.onClose();
|
||||||
// no need to wait for the response
|
// no need to wait for the response
|
||||||
@@ -1082,6 +1169,8 @@ class ImageModel with ChangeNotifier {
|
|||||||
|
|
||||||
late final SessionID sessionId;
|
late final SessionID sessionId;
|
||||||
|
|
||||||
|
bool _useTextureRender = false;
|
||||||
|
|
||||||
WeakReference<FFI> parent;
|
WeakReference<FFI> parent;
|
||||||
|
|
||||||
final List<Function(String)> callbacksOnFirstImage = [];
|
final List<Function(String)> callbacksOnFirstImage = [];
|
||||||
@@ -1090,6 +1179,8 @@ class ImageModel with ChangeNotifier {
|
|||||||
sessionId = parent.target!.sessionId;
|
sessionId = parent.target!.sessionId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get useTextureRender => _useTextureRender;
|
||||||
|
|
||||||
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
|
||||||
|
|
||||||
onRgba(int display, Uint8List rgba) {
|
onRgba(int display, Uint8List rgba) {
|
||||||
@@ -1129,11 +1220,8 @@ class ImageModel with ChangeNotifier {
|
|||||||
if (parent.target != null) {
|
if (parent.target != null) {
|
||||||
await initializeCursorAndCanvas(parent.target!);
|
await initializeCursorAndCanvas(parent.target!);
|
||||||
}
|
}
|
||||||
if (parent.target?.ffiModel.isPeerAndroid ?? false) {
|
|
||||||
bind.sessionSetViewStyle(sessionId: sessionId, value: 'adaptive');
|
|
||||||
parent.target?.canvasModel.updateViewStyle();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
_image?.dispose();
|
||||||
_image = image;
|
_image = image;
|
||||||
if (image != null) notifyListeners();
|
if (image != null) notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -1157,6 +1245,24 @@ class ImageModel with ChangeNotifier {
|
|||||||
final yscale = size.height / _image!.height;
|
final yscale = size.height / _image!.height;
|
||||||
return min(xscale, yscale) / 1.5;
|
return min(xscale, yscale) / 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUserTextureRender() {
|
||||||
|
final preValue = _useTextureRender;
|
||||||
|
_useTextureRender = isDesktop && bind.mainGetUseTextureRender();
|
||||||
|
if (preValue != _useTextureRender) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setUseTextureRender(bool value) {
|
||||||
|
_useTextureRender = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void disposeImage() {
|
||||||
|
_image?.dispose();
|
||||||
|
_image = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ScrollStyle {
|
enum ScrollStyle {
|
||||||
@@ -1343,10 +1449,20 @@ class CanvasModel with ChangeNotifier {
|
|||||||
if (refreshMousePos) {
|
if (refreshMousePos) {
|
||||||
parent.target?.inputModel.refreshMousePos();
|
parent.target?.inputModel.refreshMousePos();
|
||||||
}
|
}
|
||||||
if (style == kRemoteViewStyleOriginal &&
|
tryUpdateScrollStyle(Duration.zero, style);
|
||||||
_scrollStyle == ScrollStyle.scrollbar) {
|
}
|
||||||
updateScrollPercent();
|
|
||||||
|
tryUpdateScrollStyle(Duration duration, String? style) async {
|
||||||
|
if (_scrollStyle != ScrollStyle.scrollbar) return;
|
||||||
|
style ??= await bind.sessionGetViewStyle(sessionId: sessionId);
|
||||||
|
if (style != kRemoteViewStyleOriginal) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resetScroll();
|
||||||
|
Future.delayed(duration, () async {
|
||||||
|
updateScrollPercent();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateScrollStyle() async {
|
updateScrollStyle() async {
|
||||||
@@ -1583,13 +1699,15 @@ const _forbiddenCursorPng =
|
|||||||
const _defaultCursorPng =
|
const _defaultCursorPng =
|
||||||
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAARzQklUCAgICHwIZIgAAAFmSURBVFiF7dWxSlxREMbx34QFDRowYBchZSxSCWlMCOwD5FGEFHap06UI7KPsAyyEEIQFqxRaCqYTsqCJFsKkuAeRXb17wrqV918dztw55zszc2fo6Oh47MR/e3zO1/iAHWmznHKGQwx9ip/LEbCfazbsoY8j/JLOhcC6sCW9wsjEwJf483AC9nPNc1+lFRwI13d+l3rYFS799rFGxJMqARv2pBXh+72XQ7gWvklPS7TmMl9Ak/M+DqrENvxAv/guKKApuKPWl0/TROK4+LbSqzhuB+OZ3fRSeFPWY+Fkyn56Y29hfgTSpnQ+s98cvorVey66uPlNFxKwZOYLCGfCs5n9NMYVrsp6mvXSoFqpqYFDvMBkStgJJe93dZOwVXxbqUnBENulydSReqUrDhcX0PT2EXarBYS3GNXMhboinBgIl9K71kg0L3+PvyYGdVpruT2MwrF0iotiXfIwus0Dj+OOjo6Of+e7ab74RkpgAAAAAElFTkSuQmCC';
|
||||||
|
|
||||||
|
const kPreForbiddenCursorId = -2;
|
||||||
final preForbiddenCursor = PredefinedCursor(
|
final preForbiddenCursor = PredefinedCursor(
|
||||||
png: _forbiddenCursorPng,
|
png: _forbiddenCursorPng,
|
||||||
id: -2,
|
id: kPreForbiddenCursorId,
|
||||||
);
|
);
|
||||||
|
const kPreDefaultCursorId = -1;
|
||||||
final preDefaultCursor = PredefinedCursor(
|
final preDefaultCursor = PredefinedCursor(
|
||||||
png: _defaultCursorPng,
|
png: _defaultCursorPng,
|
||||||
id: -1,
|
id: kPreDefaultCursorId,
|
||||||
hotxGetter: (double w) => w / 2,
|
hotxGetter: (double w) => w / 2,
|
||||||
hotyGetter: (double h) => h / 2,
|
hotyGetter: (double h) => h / 2,
|
||||||
);
|
);
|
||||||
@@ -1614,13 +1732,22 @@ class PredefinedCursor {
|
|||||||
init() {
|
init() {
|
||||||
_image2 = img2.decodePng(base64Decode(png));
|
_image2 = img2.decodePng(base64Decode(png));
|
||||||
if (_image2 != null) {
|
if (_image2 != null) {
|
||||||
|
// The png type of forbidden cursor image is `PngColorType.indexed`.
|
||||||
|
if (isWindows && id == kPreForbiddenCursorId) {
|
||||||
|
_image2 = _image2!.convert(format: img2.Format.uint8, numChannels: 4);
|
||||||
|
}
|
||||||
|
|
||||||
() async {
|
() async {
|
||||||
final defaultImg = _image2!;
|
final defaultImg = _image2!;
|
||||||
// This function is called only one time, no need to care about the performance.
|
// This function is called only one time, no need to care about the performance.
|
||||||
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
|
Uint8List data = defaultImg.getBytes(order: img2.ChannelOrder.rgba);
|
||||||
|
_image?.dispose();
|
||||||
_image = await img.decodeImageFromPixels(
|
_image = await img.decodeImageFromPixels(
|
||||||
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
|
data, defaultImg.width, defaultImg.height, ui.PixelFormat.rgba8888);
|
||||||
|
if (_image == null) {
|
||||||
|
print("decodeImageFromPixels failed, pre-defined cursor $id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
double scale = 1.0;
|
double scale = 1.0;
|
||||||
if (isWindows) {
|
if (isWindows) {
|
||||||
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
data = _image2!.getBytes(order: img2.ChannelOrder.bgra);
|
||||||
@@ -1660,6 +1787,8 @@ class CursorModel with ChangeNotifier {
|
|||||||
double _displayOriginX = 0;
|
double _displayOriginX = 0;
|
||||||
double _displayOriginY = 0;
|
double _displayOriginY = 0;
|
||||||
DateTime? _firstUpdateMouseTime;
|
DateTime? _firstUpdateMouseTime;
|
||||||
|
Rect? _windowRect;
|
||||||
|
List<RemoteWindowCoords> _remoteWindowCoords = [];
|
||||||
bool gotMouseControl = true;
|
bool gotMouseControl = true;
|
||||||
DateTime _lastPeerMouse = DateTime.now()
|
DateTime _lastPeerMouse = DateTime.now()
|
||||||
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
|
.subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec));
|
||||||
@@ -1672,6 +1801,8 @@ class CursorModel with ChangeNotifier {
|
|||||||
double get x => _x - _displayOriginX;
|
double get x => _x - _displayOriginX;
|
||||||
double get y => _y - _displayOriginY;
|
double get y => _y - _displayOriginY;
|
||||||
|
|
||||||
|
double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio;
|
||||||
|
|
||||||
Offset get offset => Offset(_x, _y);
|
Offset get offset => Offset(_x, _y);
|
||||||
|
|
||||||
double get hotx => _hotx;
|
double get hotx => _hotx;
|
||||||
@@ -1741,15 +1872,13 @@ class CursorModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePan(double dx, double dy, bool touchMode) {
|
updatePan(Offset delta, Offset localPosition, bool touchMode) {
|
||||||
if (touchMode) {
|
if (touchMode) {
|
||||||
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
_handleTouchMode(delta, localPosition);
|
||||||
_x += dx / scale;
|
|
||||||
_y += dy / scale;
|
|
||||||
parent.target?.inputModel.moveMouse(_x, _y);
|
|
||||||
notifyListeners();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
double dx = delta.dx;
|
||||||
|
double dy = delta.dy;
|
||||||
if (parent.target?.imageModel.image == null) return;
|
if (parent.target?.imageModel.image == null) return;
|
||||||
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
||||||
dx /= scale;
|
dx /= scale;
|
||||||
@@ -1816,6 +1945,46 @@ class CursorModel with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isInCurrentWindow(double x, double y) {
|
||||||
|
final w = _windowRect!.width / devicePixelRatio;
|
||||||
|
final h = _windowRect!.width / devicePixelRatio;
|
||||||
|
return x >= 0 && y >= 0 && x <= w && y <= h;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleTouchMode(Offset delta, Offset localPosition) {
|
||||||
|
bool isMoved = false;
|
||||||
|
if (_remoteWindowCoords.isNotEmpty &&
|
||||||
|
_windowRect != null &&
|
||||||
|
!_isInCurrentWindow(localPosition.dx, localPosition.dy)) {
|
||||||
|
final coords = InputModel.findRemoteCoords(localPosition.dx,
|
||||||
|
localPosition.dy, _remoteWindowCoords, devicePixelRatio);
|
||||||
|
if (coords != null) {
|
||||||
|
double x2 =
|
||||||
|
(localPosition.dx - coords.relativeOffset.dx / devicePixelRatio) /
|
||||||
|
coords.canvas.scale;
|
||||||
|
double y2 =
|
||||||
|
(localPosition.dy - coords.relativeOffset.dy / devicePixelRatio) /
|
||||||
|
coords.canvas.scale;
|
||||||
|
x2 += coords.cursor.offset.dx;
|
||||||
|
y2 += coords.cursor.offset.dy;
|
||||||
|
parent.target?.inputModel.moveMouse(x2, y2);
|
||||||
|
isMoved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isMoved) {
|
||||||
|
final scale = parent.target?.canvasModel.scale ?? 1.0;
|
||||||
|
_x += delta.dx / scale;
|
||||||
|
_y += delta.dy / scale;
|
||||||
|
parent.target?.inputModel.moveMouse(_x, _y);
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
disposeImages() {
|
||||||
|
_images.forEach((_, v) => v.item1.dispose());
|
||||||
|
_images.clear();
|
||||||
|
}
|
||||||
|
|
||||||
updateCursorData(Map<String, dynamic> evt) async {
|
updateCursorData(Map<String, dynamic> evt) async {
|
||||||
final id = int.parse(evt['id']);
|
final id = int.parse(evt['id']);
|
||||||
final hotx = double.parse(evt['hotx']);
|
final hotx = double.parse(evt['hotx']);
|
||||||
@@ -1826,7 +1995,11 @@ class CursorModel with ChangeNotifier {
|
|||||||
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
final rgba = Uint8List.fromList(colors.map((s) => s as int).toList());
|
||||||
final image = await img.decodeImageFromPixels(
|
final image = await img.decodeImageFromPixels(
|
||||||
rgba, width, height, ui.PixelFormat.rgba8888);
|
rgba, width, height, ui.PixelFormat.rgba8888);
|
||||||
|
if (image == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
|
if (await _updateCache(rgba, image, id, hotx, hoty, width, height)) {
|
||||||
|
_images[id]?.item1.dispose();
|
||||||
_images[id] = Tuple3(image, hotx, hoty);
|
_images[id] = Tuple3(image, hotx, hoty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1941,7 +2114,7 @@ class CursorModel with ChangeNotifier {
|
|||||||
_x = -10000;
|
_x = -10000;
|
||||||
_x = -10000;
|
_x = -10000;
|
||||||
_image = null;
|
_image = null;
|
||||||
_images.clear();
|
disposeImages();
|
||||||
|
|
||||||
_clearCache();
|
_clearCache();
|
||||||
_cache = null;
|
_cache = null;
|
||||||
@@ -1955,6 +2128,18 @@ class CursorModel with ChangeNotifier {
|
|||||||
deleteCustomCursor(k);
|
deleteCustomCursor(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trySetRemoteWindowCoords() {
|
||||||
|
Future.delayed(Duration.zero, () async {
|
||||||
|
_windowRect =
|
||||||
|
await InputModel.fillRemoteCoordsAndGetCurFrame(_remoteWindowCoords);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
clearRemoteWindowCoords() {
|
||||||
|
_windowRect = null;
|
||||||
|
_remoteWindowCoords.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class QualityMonitorData {
|
class QualityMonitorData {
|
||||||
@@ -2172,6 +2357,7 @@ class FFI {
|
|||||||
/// Mobile reuse FFI
|
/// Mobile reuse FFI
|
||||||
void mobileReset() {
|
void mobileReset() {
|
||||||
ffiModel.waitForFirstImage.value = true;
|
ffiModel.waitForFirstImage.value = true;
|
||||||
|
ffiModel.isRefreshing = false;
|
||||||
ffiModel.waitForImageDialogShow.value = true;
|
ffiModel.waitForImageDialogShow.value = true;
|
||||||
ffiModel.waitForImageTimer?.cancel();
|
ffiModel.waitForImageTimer?.cancel();
|
||||||
ffiModel.waitForImageTimer = null;
|
ffiModel.waitForImageTimer = null;
|
||||||
@@ -2238,7 +2424,7 @@ class FFI {
|
|||||||
sessionId: sessionId, displays: Int32List.fromList(displays));
|
sessionId: sessionId, displays: Int32List.fromList(displays));
|
||||||
ffiModel.pi.currentDisplay = display;
|
ffiModel.pi.currentDisplay = display;
|
||||||
}
|
}
|
||||||
if (connType == ConnType.defaultConn && useTextureRender) {
|
if (isDesktop && connType == ConnType.defaultConn) {
|
||||||
textureModel.updateCurrentDisplay(display ?? 0);
|
textureModel.updateCurrentDisplay(display ?? 0);
|
||||||
}
|
}
|
||||||
final stream = bind.sessionStart(sessionId: sessionId, id: id);
|
final stream = bind.sessionStart(sessionId: sessionId, id: id);
|
||||||
@@ -2260,9 +2446,8 @@ class FFI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final hasPixelBufferTextureRender = bind.mainHasPixelbufferTextureRender();
|
imageModel.updateUserTextureRender();
|
||||||
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
final hasGpuTextureRender = bind.mainHasGpuTextureRender();
|
||||||
|
|
||||||
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
|
||||||
// Preserved for the rgba data.
|
// Preserved for the rgba data.
|
||||||
stream.listen((message) {
|
stream.listen((message) {
|
||||||
@@ -2284,8 +2469,11 @@ class FFI {
|
|||||||
debugPrint('Unreachable, the cached data cannot be decoded.');
|
debugPrint('Unreachable, the cached data cannot be decoded.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ffiModel.setPermissions(data.permissions);
|
||||||
await ffiModel.handleCachedPeerData(data, id);
|
await ffiModel.handleCachedPeerData(data, id);
|
||||||
await sessionRefreshVideo(sessionId, ffiModel.pi);
|
await sessionRefreshVideo(sessionId, ffiModel.pi);
|
||||||
|
await bind.sessionRequestNewDisplayInitMsgs(
|
||||||
|
sessionId: sessionId, display: ffiModel.pi.currentDisplay);
|
||||||
});
|
});
|
||||||
isToNewWindowNotified.value = true;
|
isToNewWindowNotified.value = true;
|
||||||
}
|
}
|
||||||
@@ -2308,7 +2496,7 @@ class FFI {
|
|||||||
}
|
}
|
||||||
} else if (message is EventToUI_Rgba) {
|
} else if (message is EventToUI_Rgba) {
|
||||||
final display = message.field0;
|
final display = message.field0;
|
||||||
if (hasPixelBufferTextureRender) {
|
if (imageModel.useTextureRender) {
|
||||||
debugPrint("EventToUI_Rgba display:$display");
|
debugPrint("EventToUI_Rgba display:$display");
|
||||||
textureModel.setTextureType(display: display, gpuTexture: false);
|
textureModel.setTextureType(display: display, gpuTexture: false);
|
||||||
onEvent2UIRgba();
|
onEvent2UIRgba();
|
||||||
@@ -2490,14 +2678,21 @@ class PeerInfo with ChangeNotifier {
|
|||||||
bool get isInstalled =>
|
bool get isInstalled =>
|
||||||
platform != kPeerPlatformWindows ||
|
platform != kPeerPlatformWindows ||
|
||||||
platformAdditions[kPlatformAdditionsIsInstalled] == true;
|
platformAdditions[kPlatformAdditionsIsInstalled] == true;
|
||||||
List<int> get virtualDisplays => List<int>.from(
|
List<int> get RustDeskVirtualDisplays => List<int>.from(
|
||||||
platformAdditions[kPlatformAdditionsVirtualDisplays] ?? []);
|
platformAdditions[kPlatformAdditionsRustDeskVirtualDisplays] ?? []);
|
||||||
|
int get amyuniVirtualDisplayCount =>
|
||||||
|
platformAdditions[kPlatformAdditionsAmyuniVirtualDisplays] ?? 0;
|
||||||
|
|
||||||
bool get isSupportMultiDisplay =>
|
bool get isSupportMultiDisplay =>
|
||||||
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
|
(isDesktop || isWebDesktop) && isSupportMultiUiSession;
|
||||||
|
|
||||||
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
bool get cursorEmbedded => tryGetDisplay()?.cursorEmbedded ?? false;
|
||||||
|
|
||||||
|
bool get isRustDeskIdd =>
|
||||||
|
platformAdditions[kPlatformAdditionsIddImpl] == 'rustdesk_idd';
|
||||||
|
bool get isAmyuniIdd =>
|
||||||
|
platformAdditions[kPlatformAdditionsIddImpl] == 'amyuni_idd';
|
||||||
|
|
||||||
Display? tryGetDisplay() {
|
Display? tryGetDisplay() {
|
||||||
if (displays.isEmpty) {
|
if (displays.isEmpty) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -198,7 +198,10 @@ class PlatformFFI {
|
|||||||
await _ffiBind.mainDeviceId(id: id);
|
await _ffiBind.mainDeviceId(id: id);
|
||||||
await _ffiBind.mainDeviceName(name: name);
|
await _ffiBind.mainDeviceName(name: name);
|
||||||
await _ffiBind.mainSetHomeDir(home: _homeDir);
|
await _ffiBind.mainSetHomeDir(home: _homeDir);
|
||||||
await _ffiBind.mainInit(appDir: _dir);
|
await _ffiBind.mainInit(
|
||||||
|
appDir: _dir,
|
||||||
|
customClientConfig: '',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrintStack(label: 'initialize failed: $e');
|
debugPrintStack(label: 'initialize failed: $e');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/models/peer_model.dart';
|
import 'package:flutter_hbb/models/peer_model.dart';
|
||||||
import 'package:flutter_hbb/models/platform_model.dart';
|
import 'package:flutter_hbb/models/platform_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
@@ -22,9 +23,6 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
int get currentTab => _currentTab;
|
int get currentTab => _currentTab;
|
||||||
int _currentTab = 0; // index in tabNames
|
int _currentTab = 0; // index in tabNames
|
||||||
static const int maxTabCount = 5;
|
static const int maxTabCount = 5;
|
||||||
static const String kPeerTabIndex = 'peer-tab-index';
|
|
||||||
static const String kPeerTabOrder = 'peer-tab-order';
|
|
||||||
static const String kPeerTabVisible = 'peer-tab-visible';
|
|
||||||
static const List<String> tabNames = [
|
static const List<String> tabNames = [
|
||||||
'Recent sessions',
|
'Recent sessions',
|
||||||
'Favorites',
|
'Favorites',
|
||||||
@@ -44,7 +42,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
true,
|
true,
|
||||||
!isWeb,
|
!isWeb,
|
||||||
!(bind.isDisableAb() || bind.isDisableAccount()),
|
!(bind.isDisableAb() || bind.isDisableAccount()),
|
||||||
!bind.isDisableAccount(),
|
!(bind.isDisableGroupPanel() || bind.isDisableAccount()),
|
||||||
]);
|
]);
|
||||||
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
|
final List<bool> _isVisible = List.filled(maxTabCount, true, growable: false);
|
||||||
List<bool> get isVisibleEnabled => () {
|
List<bool> get isVisibleEnabled => () {
|
||||||
@@ -72,7 +70,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
PeerTabModel(this.parent) {
|
PeerTabModel(this.parent) {
|
||||||
// visible
|
// visible
|
||||||
try {
|
try {
|
||||||
final option = bind.getLocalFlutterOption(k: kPeerTabVisible);
|
final option = bind.getLocalFlutterOption(k: kOptionPeerTabVisible);
|
||||||
if (option.isNotEmpty) {
|
if (option.isNotEmpty) {
|
||||||
List<dynamic> decodeList = jsonDecode(option);
|
List<dynamic> decodeList = jsonDecode(option);
|
||||||
if (decodeList.length == _isVisible.length) {
|
if (decodeList.length == _isVisible.length) {
|
||||||
@@ -88,7 +86,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
// order
|
// order
|
||||||
try {
|
try {
|
||||||
final option = bind.getLocalFlutterOption(k: kPeerTabOrder);
|
final option = bind.getLocalFlutterOption(k: kOptionPeerTabOrder);
|
||||||
if (option.isNotEmpty) {
|
if (option.isNotEmpty) {
|
||||||
List<dynamic> decodeList = jsonDecode(option);
|
List<dynamic> decodeList = jsonDecode(option);
|
||||||
if (decodeList.length == maxTabCount) {
|
if (decodeList.length == maxTabCount) {
|
||||||
@@ -112,7 +110,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
// init currentTab
|
// init currentTab
|
||||||
_currentTab =
|
_currentTab =
|
||||||
int.tryParse(bind.getLocalFlutterOption(k: kPeerTabIndex)) ?? 0;
|
int.tryParse(bind.getLocalFlutterOption(k: kOptionPeerTabIndex)) ?? 0;
|
||||||
if (_currentTab < 0 || _currentTab >= maxTabCount) {
|
if (_currentTab < 0 || _currentTab >= maxTabCount) {
|
||||||
_currentTab = 0;
|
_currentTab = 0;
|
||||||
}
|
}
|
||||||
@@ -222,7 +220,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
bind.setLocalFlutterOption(
|
bind.setLocalFlutterOption(
|
||||||
k: kPeerTabVisible, v: jsonEncode(_isVisible));
|
k: kOptionPeerTabVisible, v: jsonEncode(_isVisible));
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -258,7 +256,7 @@ class PeerTabModel with ChangeNotifier {
|
|||||||
for (int i = 0; i < list.length; i++) {
|
for (int i = 0; i < list.length; i++) {
|
||||||
orders[i] = list[i];
|
orders[i] = list[i];
|
||||||
}
|
}
|
||||||
bind.setLocalFlutterOption(k: kPeerTabOrder, v: jsonEncode(orders));
|
bind.setLocalFlutterOption(k: kOptionPeerTabOrder, v: jsonEncode(orders));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
String get approveMode => _approveMode;
|
String get approveMode => _approveMode;
|
||||||
|
|
||||||
setVerificationMethod(String method) async {
|
setVerificationMethod(String method) async {
|
||||||
await bind.mainSetOption(key: "verification-method", value: method);
|
await bind.mainSetOption(key: kOptionVerificationMethod, value: method);
|
||||||
/*
|
/*
|
||||||
if (method != kUsePermanentPassword) {
|
if (method != kUsePermanentPassword) {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
@@ -99,7 +99,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setApproveMode(String mode) async {
|
setApproveMode(String mode) async {
|
||||||
await bind.mainSetOption(key: 'approve-mode', value: mode);
|
await bind.mainSetOption(key: kOptionApproveMode, value: mode);
|
||||||
/*
|
/*
|
||||||
if (mode != 'password') {
|
if (mode != 'password') {
|
||||||
await bind.mainSetOption(
|
await bind.mainSetOption(
|
||||||
@@ -125,8 +125,8 @@ class ServerModel with ChangeNotifier {
|
|||||||
/*
|
/*
|
||||||
// initital _hideCm at startup
|
// initital _hideCm at startup
|
||||||
final verificationMethod =
|
final verificationMethod =
|
||||||
bind.mainGetOptionSync(key: "verification-method");
|
bind.mainGetOptionSync(key: kOptionVerificationMethod);
|
||||||
final approveMode = bind.mainGetOptionSync(key: 'approve-mode');
|
final approveMode = bind.mainGetOptionSync(key: kOptionApproveMode);
|
||||||
_hideCm = option2bool(
|
_hideCm = option2bool(
|
||||||
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
|
'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
|
||||||
if (!(approveMode == 'password' &&
|
if (!(approveMode == 'password' &&
|
||||||
@@ -187,18 +187,19 @@ class ServerModel with ChangeNotifier {
|
|||||||
if (androidVersion < 30 ||
|
if (androidVersion < 30 ||
|
||||||
!await AndroidPermissionManager.check(kRecordAudio)) {
|
!await AndroidPermissionManager.check(kRecordAudio)) {
|
||||||
_audioOk = false;
|
_audioOk = false;
|
||||||
bind.mainSetOption(key: "enable-audio", value: "N");
|
bind.mainSetOption(key: kOptionEnableAudio, value: "N");
|
||||||
} else {
|
} else {
|
||||||
final audioOption = await bind.mainGetOption(key: 'enable-audio');
|
final audioOption = await bind.mainGetOption(key: kOptionEnableAudio);
|
||||||
_audioOk = audioOption.isEmpty;
|
_audioOk = audioOption.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// file
|
// file
|
||||||
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
_fileOk = false;
|
_fileOk = false;
|
||||||
bind.mainSetOption(key: "enable-file-transfer", value: "N");
|
bind.mainSetOption(key: kOptionEnableFileTransfer, value: "N");
|
||||||
} else {
|
} else {
|
||||||
final fileOption = await bind.mainGetOption(key: 'enable-file-transfer');
|
final fileOption =
|
||||||
|
await bind.mainGetOption(key: kOptionEnableFileTransfer);
|
||||||
_fileOk = fileOption.isEmpty;
|
_fileOk = fileOption.isEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,10 +210,10 @@ class ServerModel with ChangeNotifier {
|
|||||||
var update = false;
|
var update = false;
|
||||||
final temporaryPassword = await bind.mainGetTemporaryPassword();
|
final temporaryPassword = await bind.mainGetTemporaryPassword();
|
||||||
final verificationMethod =
|
final verificationMethod =
|
||||||
await bind.mainGetOption(key: "verification-method");
|
await bind.mainGetOption(key: kOptionVerificationMethod);
|
||||||
final temporaryPasswordLength =
|
final temporaryPasswordLength =
|
||||||
await bind.mainGetOption(key: "temporary-password-length");
|
await bind.mainGetOption(key: "temporary-password-length");
|
||||||
final approveMode = await bind.mainGetOption(key: 'approve-mode');
|
final approveMode = await bind.mainGetOption(key: kOptionApproveMode);
|
||||||
/*
|
/*
|
||||||
var hideCm = option2bool(
|
var hideCm = option2bool(
|
||||||
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
|
'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
|
||||||
@@ -283,7 +284,8 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_audioOk = !_audioOk;
|
_audioOk = !_audioOk;
|
||||||
bind.mainSetOption(key: "enable-audio", value: _audioOk ? '' : 'N');
|
bind.mainSetOption(
|
||||||
|
key: kOptionEnableAudio, value: _audioOk ? defaultOptionYes : 'N');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,7 +304,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fileOk = !_fileOk;
|
_fileOk = !_fileOk;
|
||||||
bind.mainSetOption(key: "enable-file-transfer", value: _fileOk ? '' : 'N');
|
bind.mainSetOption(
|
||||||
|
key: kOptionEnableFileTransfer,
|
||||||
|
value: _fileOk ? defaultOptionYes : 'N');
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +316,7 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
if (_inputOk) {
|
if (_inputOk) {
|
||||||
parent.target?.invokeMethod("stop_input");
|
parent.target?.invokeMethod("stop_input");
|
||||||
bind.mainSetOption(key: "enable-keyboard", value: 'N');
|
bind.mainSetOption(key: kOptionEnableKeyboard, value: 'N');
|
||||||
} else {
|
} else {
|
||||||
if (parent.target != null) {
|
if (parent.target != null) {
|
||||||
/// the result of toggle-on depends on user actions in the settings page.
|
/// the result of toggle-on depends on user actions in the settings page.
|
||||||
@@ -322,6 +326,20 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> checkRequestNotificationPermission() async {
|
||||||
|
debugPrint("androidVersion $androidVersion");
|
||||||
|
if (androidVersion < 33) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (await AndroidPermissionManager.check(kAndroid13Notification)) {
|
||||||
|
debugPrint("notification permission already granted");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var res = await AndroidPermissionManager.request(kAndroid13Notification);
|
||||||
|
debugPrint("notification permission request result: $res");
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
/// Toggle the screen sharing service.
|
/// Toggle the screen sharing service.
|
||||||
toggleService() async {
|
toggleService() async {
|
||||||
if (_isStart) {
|
if (_isStart) {
|
||||||
@@ -348,6 +366,10 @@ class ServerModel with ChangeNotifier {
|
|||||||
stopService();
|
stopService();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
await checkRequestNotificationPermission();
|
||||||
|
if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
|
||||||
|
await AndroidPermissionManager.request(kManageExternalStorage);
|
||||||
|
}
|
||||||
final res = await parent.target?.dialogManager
|
final res = await parent.target?.dialogManager
|
||||||
.show<bool>((setState, close, context) {
|
.show<bool>((setState, close, context) {
|
||||||
submit() => close(true);
|
submit() => close(true);
|
||||||
@@ -430,7 +452,9 @@ class ServerModel with ChangeNotifier {
|
|||||||
break;
|
break;
|
||||||
case "input":
|
case "input":
|
||||||
if (_inputOk != value) {
|
if (_inputOk != value) {
|
||||||
bind.mainSetOption(key: "enable-keyboard", value: value ? '' : 'N');
|
bind.mainSetOption(
|
||||||
|
key: kOptionEnableKeyboard,
|
||||||
|
value: value ? defaultOptionYes : 'N');
|
||||||
}
|
}
|
||||||
_inputOk = value;
|
_inputOk = value;
|
||||||
break;
|
break;
|
||||||
@@ -535,37 +559,60 @@ class ServerModel with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showLoginDialog(Client client) {
|
void showLoginDialog(Client client) {
|
||||||
|
showClientDialog(
|
||||||
|
client,
|
||||||
|
client.isFileTransfer ? "File Connection" : "Screen Connection",
|
||||||
|
'Do you accept?',
|
||||||
|
'android_new_connection_tip',
|
||||||
|
() => sendLoginResponse(client, false),
|
||||||
|
() => sendLoginResponse(client, true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleVoiceCall(Client client, bool accept) {
|
||||||
|
parent.target?.invokeMethod("cancel_notification", client.id);
|
||||||
|
bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
|
||||||
|
}
|
||||||
|
|
||||||
|
showVoiceCallDialog(Client client) {
|
||||||
|
showClientDialog(
|
||||||
|
client,
|
||||||
|
'Voice call',
|
||||||
|
'Do you accept?',
|
||||||
|
'android_new_voice_call_tip',
|
||||||
|
() => handleVoiceCall(client, false),
|
||||||
|
() => handleVoiceCall(client, true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showClientDialog(Client client, String title, String contentTitle,
|
||||||
|
String content, VoidCallback onCancel, VoidCallback onSubmit) {
|
||||||
parent.target?.dialogManager.show((setState, close, context) {
|
parent.target?.dialogManager.show((setState, close, context) {
|
||||||
cancel() {
|
cancel() {
|
||||||
sendLoginResponse(client, false);
|
onCancel();
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
sendLoginResponse(client, true);
|
onSubmit();
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return CustomAlertDialog(
|
return CustomAlertDialog(
|
||||||
title:
|
title:
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
Text(translate(
|
Text(translate(title)),
|
||||||
client.isFileTransfer ? "File Connection" : "Screen Connection")),
|
IconButton(onPressed: close, icon: const Icon(Icons.close))
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
close();
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.close))
|
|
||||||
]),
|
]),
|
||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(translate("Do you accept?")),
|
Text(translate(contentTitle)),
|
||||||
ClientInfo(client),
|
ClientInfo(client),
|
||||||
Text(
|
Text(
|
||||||
translate("android_new_connection_tip"),
|
translate(content),
|
||||||
style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium,
|
style: Theme.of(globalKey.currentContext!).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -661,10 +708,14 @@ class ServerModel with ChangeNotifier {
|
|||||||
_clients[index].inVoiceCall = client.inVoiceCall;
|
_clients[index].inVoiceCall = client.inVoiceCall;
|
||||||
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
|
_clients[index].incomingVoiceCall = client.incomingVoiceCall;
|
||||||
if (client.incomingVoiceCall) {
|
if (client.incomingVoiceCall) {
|
||||||
// Has incoming phone call, let's set the window on top.
|
if (isAndroid) {
|
||||||
Future.delayed(Duration.zero, () {
|
showVoiceCallDialog(client);
|
||||||
windowOnTop(null);
|
} else {
|
||||||
});
|
// Has incoming phone call, let's set the window on top.
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
windowOnTop(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ class StateGlobal {
|
|||||||
bool _isMinimized = false;
|
bool _isMinimized = false;
|
||||||
final RxBool isMaximized = false.obs;
|
final RxBool isMaximized = false.obs;
|
||||||
final RxBool _showTabBar = true.obs;
|
final RxBool _showTabBar = true.obs;
|
||||||
final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
|
final RxDouble _resizeEdgeSize = RxDouble(windowEdgeSize);
|
||||||
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
|
||||||
final RxBool showRemoteToolBar = false.obs;
|
final RxBool showRemoteToolBar = false.obs;
|
||||||
final svcStatus = SvcStatus.notReady.obs;
|
final svcStatus = SvcStatus.notReady.obs;
|
||||||
// Only used for macOS
|
final RxBool isFocused = false.obs;
|
||||||
bool? closeOnFullscreen;
|
|
||||||
|
|
||||||
String _inputSource = '';
|
String _inputSource = '';
|
||||||
|
|
||||||
@@ -55,8 +54,7 @@ class StateGlobal {
|
|||||||
if (!_fullscreen.isTrue) {
|
if (!_fullscreen.isTrue) {
|
||||||
if (isMaximized.value != v) {
|
if (isMaximized.value != v) {
|
||||||
isMaximized.value = v;
|
isMaximized.value = v;
|
||||||
_resizeEdgeSize.value =
|
refreshResizeEdgeSize();
|
||||||
isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
|
|
||||||
}
|
}
|
||||||
if (!isMacOS) {
|
if (!isMacOS) {
|
||||||
_windowBorderWidth.value = v ? 0 : kWindowBorderWidth;
|
_windowBorderWidth.value = v ? 0 : kWindowBorderWidth;
|
||||||
@@ -70,11 +68,7 @@ class StateGlobal {
|
|||||||
if (_fullscreen.value != v) {
|
if (_fullscreen.value != v) {
|
||||||
_fullscreen.value = v;
|
_fullscreen.value = v;
|
||||||
_showTabBar.value = !_fullscreen.value;
|
_showTabBar.value = !_fullscreen.value;
|
||||||
_resizeEdgeSize.value = fullscreen.isTrue
|
refreshResizeEdgeSize();
|
||||||
? kFullScreenEdgeSize
|
|
||||||
: isMaximized.isTrue
|
|
||||||
? kMaximizeEdgeSize
|
|
||||||
: kWindowEdgeSize;
|
|
||||||
print(
|
print(
|
||||||
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
"fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
|
||||||
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
|
_windowBorderWidth.value = fullscreen.isTrue ? 0 : kWindowBorderWidth;
|
||||||
@@ -95,6 +89,12 @@ class StateGlobal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
refreshResizeEdgeSize() => _resizeEdgeSize.value = fullscreen.isTrue
|
||||||
|
? kFullScreenEdgeSize
|
||||||
|
: isMaximized.isTrue
|
||||||
|
? kMaximizeEdgeSize
|
||||||
|
: windowEdgeSize;
|
||||||
|
|
||||||
String getInputSource({bool force = false}) {
|
String getInputSource({bool force = false}) {
|
||||||
if (force || _inputSource.isEmpty) {
|
if (force || _inputSource.isEmpty) {
|
||||||
_inputSource = bind.mainGetInputSource();
|
_inputSource = bind.mainGetInputSource();
|
||||||
@@ -112,4 +112,5 @@ class StateGlobal {
|
|||||||
static final StateGlobal instance = StateGlobal._();
|
static final StateGlobal instance = StateGlobal._();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This final variable is initialized when the first time it is accessed.
|
||||||
final stateGlobal = StateGlobal.instance;
|
final stateGlobal = StateGlobal.instance;
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
import 'package:flutter_hbb/common/hbbs/hbbs.dart';
|
||||||
import 'package:flutter_hbb/models/ab_model.dart';
|
import 'package:flutter_hbb/models/ab_model.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
|
|
||||||
import '../common.dart';
|
import '../common.dart';
|
||||||
|
import '../utils/http_service.dart' as http;
|
||||||
import 'model.dart';
|
import 'model.dart';
|
||||||
import 'platform_model.dart';
|
import 'platform_model.dart';
|
||||||
|
|
||||||
@@ -136,7 +136,6 @@ class UserModel {
|
|||||||
Future<LoginResponse> login(LoginRequest loginRequest) async {
|
Future<LoginResponse> login(LoginRequest loginRequest) async {
|
||||||
final url = await bind.mainGetApiServer();
|
final url = await bind.mainGetApiServer();
|
||||||
final resp = await http.post(Uri.parse('$url/api/login'),
|
final resp = await http.post(Uri.parse('$url/api/login'),
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: jsonEncode(loginRequest.toJson()));
|
body: jsonEncode(loginRequest.toJson()));
|
||||||
|
|
||||||
final Map<String, dynamic> body;
|
final Map<String, dynamic> body;
|
||||||
|
|||||||
115
flutter/lib/utils/http_service.dart
Normal file
115
flutter/lib/utils/http_service.dart
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import '../models/platform_model.dart';
|
||||||
|
export 'package:http/http.dart' show Response;
|
||||||
|
|
||||||
|
enum HttpMethod { get, post, put, delete }
|
||||||
|
|
||||||
|
class HttpService {
|
||||||
|
Future<http.Response> sendRequest(
|
||||||
|
Uri url,
|
||||||
|
HttpMethod method, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
dynamic body,
|
||||||
|
}) async {
|
||||||
|
headers ??= {'Content-Type': 'application/json'};
|
||||||
|
|
||||||
|
// Determine if there is currently a proxy setting, and if so, use FFI to call the Rust HTTP method.
|
||||||
|
final isProxy = await bind.mainGetProxyStatus();
|
||||||
|
|
||||||
|
if (!isProxy) {
|
||||||
|
return await _pollFultterHttp(url, method, headers: headers, body: body);
|
||||||
|
}
|
||||||
|
|
||||||
|
String headersJson = jsonEncode(headers);
|
||||||
|
String methodName = method.toString().split('.').last;
|
||||||
|
await bind.mainHttpRequest(
|
||||||
|
url: url.toString(),
|
||||||
|
method: methodName.toLowerCase(),
|
||||||
|
body: body,
|
||||||
|
header: headersJson);
|
||||||
|
|
||||||
|
var resJson = await _pollForResponse(url.toString());
|
||||||
|
return _parseHttpResponse(resJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> _pollFultterHttp(
|
||||||
|
Uri url,
|
||||||
|
HttpMethod method, {
|
||||||
|
Map<String, String>? headers,
|
||||||
|
dynamic body,
|
||||||
|
}) async {
|
||||||
|
var response = http.Response('', 400);
|
||||||
|
|
||||||
|
switch (method) {
|
||||||
|
case HttpMethod.get:
|
||||||
|
response = await http.get(url, headers: headers);
|
||||||
|
break;
|
||||||
|
case HttpMethod.post:
|
||||||
|
response = await http.post(url, headers: headers, body: body);
|
||||||
|
break;
|
||||||
|
case HttpMethod.put:
|
||||||
|
response = await http.put(url, headers: headers, body: body);
|
||||||
|
break;
|
||||||
|
case HttpMethod.delete:
|
||||||
|
response = await http.delete(url, headers: headers, body: body);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw Exception('Unsupported HTTP method');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _pollForResponse(String url) async {
|
||||||
|
String? responseJson = " ";
|
||||||
|
while (responseJson == " ") {
|
||||||
|
responseJson = await bind.mainGetHttpStatus(url: url);
|
||||||
|
if (responseJson == null) {
|
||||||
|
throw Exception('The HTTP request failed');
|
||||||
|
}
|
||||||
|
if (responseJson == " ") {
|
||||||
|
await Future.delayed(const Duration(milliseconds: 100));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return responseJson!;
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Response _parseHttpResponse(String responseJson) {
|
||||||
|
try {
|
||||||
|
var parsedJson = jsonDecode(responseJson);
|
||||||
|
String body = parsedJson['body'];
|
||||||
|
Map<String, String> headers = {};
|
||||||
|
for (var key in parsedJson['headers'].keys) {
|
||||||
|
headers[key] = parsedJson['headers'][key];
|
||||||
|
}
|
||||||
|
int statusCode = parsedJson['status_code'];
|
||||||
|
return http.Response(body, statusCode, headers: headers);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to parse response: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> get(Uri url, {Map<String, String>? headers}) async {
|
||||||
|
return await HttpService().sendRequest(url, HttpMethod.get, headers: headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> post(Uri url,
|
||||||
|
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||||
|
return await HttpService()
|
||||||
|
.sendRequest(url, HttpMethod.post, body: body, headers: headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> put(Uri url,
|
||||||
|
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||||
|
return await HttpService()
|
||||||
|
.sendRequest(url, HttpMethod.put, body: body, headers: headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> delete(Uri url,
|
||||||
|
{Map<String, String>? headers, Object? body, Encoding? encoding}) async {
|
||||||
|
return await HttpService()
|
||||||
|
.sendRequest(url, HttpMethod.delete, body: body, headers: headers);
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
|
||||||
Future<ui.Image> decodeImageFromPixels(
|
Future<ui.Image?> decodeImageFromPixels(
|
||||||
Uint8List pixels,
|
Uint8List pixels,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
@@ -18,36 +18,74 @@ Future<ui.Image> decodeImageFromPixels(
|
|||||||
}) async {
|
}) async {
|
||||||
if (targetWidth != null) {
|
if (targetWidth != null) {
|
||||||
assert(allowUpscaling || targetWidth <= width);
|
assert(allowUpscaling || targetWidth <= width);
|
||||||
|
if (!(allowUpscaling || targetWidth <= width)) {
|
||||||
|
print("not allow upscaling but targetWidth > width");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (targetHeight != null) {
|
if (targetHeight != null) {
|
||||||
assert(allowUpscaling || targetHeight <= height);
|
assert(allowUpscaling || targetHeight <= height);
|
||||||
}
|
if (!(allowUpscaling || targetHeight <= height)) {
|
||||||
|
print("not allow upscaling but targetHeight > height");
|
||||||
final ui.ImmutableBuffer buffer =
|
return null;
|
||||||
await ui.ImmutableBuffer.fromUint8List(pixels);
|
|
||||||
onPixelsCopied?.call();
|
|
||||||
final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw(
|
|
||||||
buffer,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
rowBytes: rowBytes,
|
|
||||||
pixelFormat: format,
|
|
||||||
);
|
|
||||||
if (!allowUpscaling) {
|
|
||||||
if (targetWidth != null && targetWidth > descriptor.width) {
|
|
||||||
targetWidth = descriptor.width;
|
|
||||||
}
|
|
||||||
if (targetHeight != null && targetHeight > descriptor.height) {
|
|
||||||
targetHeight = descriptor.height;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final ui.Codec codec = await descriptor.instantiateCodec(
|
final ui.ImmutableBuffer buffer;
|
||||||
targetWidth: targetWidth,
|
try {
|
||||||
targetHeight: targetHeight,
|
buffer = await ui.ImmutableBuffer.fromUint8List(pixels);
|
||||||
);
|
onPixelsCopied?.call();
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ui.ImageDescriptor descriptor;
|
||||||
|
try {
|
||||||
|
descriptor = ui.ImageDescriptor.raw(
|
||||||
|
buffer,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
rowBytes: rowBytes,
|
||||||
|
pixelFormat: format,
|
||||||
|
);
|
||||||
|
if (!allowUpscaling) {
|
||||||
|
if (targetWidth != null && targetWidth > descriptor.width) {
|
||||||
|
targetWidth = descriptor.width;
|
||||||
|
}
|
||||||
|
if (targetHeight != null && targetHeight > descriptor.height) {
|
||||||
|
targetHeight = descriptor.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("ImageDescriptor.raw failed: $e");
|
||||||
|
buffer.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ui.Codec codec;
|
||||||
|
try {
|
||||||
|
codec = await descriptor.instantiateCodec(
|
||||||
|
targetWidth: targetWidth,
|
||||||
|
targetHeight: targetHeight,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print("instantiateCodec failed: $e");
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ui.FrameInfo frameInfo;
|
||||||
|
try {
|
||||||
|
frameInfo = await codec.getNextFrame();
|
||||||
|
} catch (e) {
|
||||||
|
print("getNextFrame failed: $e");
|
||||||
|
codec.dispose();
|
||||||
|
buffer.dispose();
|
||||||
|
descriptor.dispose();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final ui.FrameInfo frameInfo = await codec.getNextFrame();
|
|
||||||
codec.dispose();
|
codec.dispose();
|
||||||
buffer.dispose();
|
buffer.dispose();
|
||||||
descriptor.dispose();
|
descriptor.dispose();
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hbb/consts.dart';
|
import 'package:flutter_hbb/consts.dart';
|
||||||
import 'package:flutter_hbb/common.dart';
|
import 'package:flutter_hbb/common.dart';
|
||||||
|
import 'package:flutter_hbb/main.dart';
|
||||||
|
import 'package:flutter_hbb/models/input_model.dart';
|
||||||
|
|
||||||
/// must keep the order
|
/// must keep the order
|
||||||
// ignore: constant_identifier_names
|
// ignore: constant_identifier_names
|
||||||
@@ -172,7 +174,9 @@ class RustDeskMultiWindowManager {
|
|||||||
windowId: windowId, peerId: remoteId);
|
windowId: windowId, peerId: remoteId);
|
||||||
}
|
}
|
||||||
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
|
await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
|
||||||
WindowController.fromWindowId(windowId).show();
|
if (methodName != kWindowEventNewRemoteDesktop) {
|
||||||
|
WindowController.fromWindowId(windowId).show();
|
||||||
|
}
|
||||||
registerActiveWindow(windowId);
|
registerActiveWindow(windowId);
|
||||||
return MultiWindowCallResult(windowId, null);
|
return MultiWindowCallResult(windowId, null);
|
||||||
}
|
}
|
||||||
@@ -332,10 +336,10 @@ class RustDeskMultiWindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeAllSubWindows() async {
|
Future<void> closeAllSubWindows() async {
|
||||||
await Future.wait(WindowType.values.map((e) => closeWindows(e)));
|
await Future.wait(WindowType.values.map((e) => _closeWindows(e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> closeWindows(WindowType type) async {
|
Future<void> _closeWindows(WindowType type) async {
|
||||||
if (type == WindowType.Main) {
|
if (type == WindowType.Main) {
|
||||||
// skip main window, use window manager instead
|
// skip main window, use window manager instead
|
||||||
return;
|
return;
|
||||||
@@ -343,7 +347,7 @@ class RustDeskMultiWindowManager {
|
|||||||
|
|
||||||
List<int> windows = [];
|
List<int> windows = [];
|
||||||
try {
|
try {
|
||||||
windows = await DesktopMultiWindow.getAllSubWindowIds();
|
windows = _findWindowsByType(type);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Failed to getAllSubWindowIds of $type, $e');
|
debugPrint('Failed to getAllSubWindowIds of $type, $e');
|
||||||
return;
|
return;
|
||||||
@@ -353,14 +357,9 @@ class RustDeskMultiWindowManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (final wId in windows) {
|
for (final wId in windows) {
|
||||||
debugPrint("closing multi window: ${type.toString()}");
|
debugPrint("closing multi window, type: ${type.toString()} id: $wId");
|
||||||
await saveWindowPosition(type, windowId: wId);
|
await saveWindowPosition(type, windowId: wId);
|
||||||
try {
|
try {
|
||||||
// final ids = await DesktopMultiWindow.getAllSubWindowIds();
|
|
||||||
// if (!ids.contains(wId)) {
|
|
||||||
// // no such window already
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
await WindowController.fromWindowId(wId).setPreventClose(false);
|
await WindowController.fromWindowId(wId).setPreventClose(false);
|
||||||
await WindowController.fromWindowId(wId).close();
|
await WindowController.fromWindowId(wId).close();
|
||||||
_activeWindows.remove(wId);
|
_activeWindows.remove(wId);
|
||||||
@@ -369,7 +368,6 @@ class RustDeskMultiWindowManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await _notifyActiveWindow();
|
|
||||||
clearWindowType(type);
|
clearWindowType(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,14 +400,6 @@ class RustDeskMultiWindowManager {
|
|||||||
await _notifyActiveWindow();
|
await _notifyActiveWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> destroyWindow(int windowId) async {
|
|
||||||
await WindowController.fromWindowId(windowId).setPreventClose(false);
|
|
||||||
await WindowController.fromWindowId(windowId).close();
|
|
||||||
_remoteDesktopWindows.remove(windowId);
|
|
||||||
_fileTransferWindows.remove(windowId);
|
|
||||||
_portForwardWindows.remove(windowId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove active window which has [`windowId`]
|
/// Remove active window which has [`windowId`]
|
||||||
///
|
///
|
||||||
/// [Availability]
|
/// [Availability]
|
||||||
@@ -431,6 +421,39 @@ class RustDeskMultiWindowManager {
|
|||||||
void unregisterActiveWindowListener(AsyncCallback callback) {
|
void unregisterActiveWindowListener(AsyncCallback callback) {
|
||||||
_windowActiveCallbacks.remove(callback);
|
_windowActiveCallbacks.remove(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This function is called from the main window.
|
||||||
|
// It will query the active remote windows to get their coords.
|
||||||
|
Future<List<String>> getOtherRemoteWindowCoords(int wId) async {
|
||||||
|
List<String> coords = [];
|
||||||
|
for (final windowId in _remoteDesktopWindows) {
|
||||||
|
if (windowId != wId) {
|
||||||
|
if (_activeWindows.contains(windowId)) {
|
||||||
|
final res = await DesktopMultiWindow.invokeMethod(
|
||||||
|
windowId, kWindowEventRemoteWindowCoords, '');
|
||||||
|
if (res != null) {
|
||||||
|
coords.add(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is called from one remote window.
|
||||||
|
// Only the main window knows `_remoteDesktopWindows` and `_activeWindows`.
|
||||||
|
// So we need to call the main window to get the other remote windows' coords.
|
||||||
|
Future<List<RemoteWindowCoords>> getOtherRemoteWindowCoordsFromMain() async {
|
||||||
|
List<RemoteWindowCoords> coords = [];
|
||||||
|
// Call the main window to get the coords of other remote windows.
|
||||||
|
String res = await DesktopMultiWindow.invokeMethod(
|
||||||
|
kMainWindowId, kWindowEventRemoteWindowCoords, kWindowId.toString());
|
||||||
|
List<dynamic> list = jsonDecode(res);
|
||||||
|
for (var item in list) {
|
||||||
|
coords.add(RemoteWindowCoords.fromJson(jsonDecode(item)));
|
||||||
|
}
|
||||||
|
return coords;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final rustDeskWinManager = RustDeskMultiWindowManager.instance;
|
final rustDeskWinManager = RustDeskMultiWindowManager.instance;
|
||||||
|
|||||||
@@ -203,12 +203,6 @@ class RustdeskImpl {
|
|||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> sessionGetFlutterOptionByPeerId(
|
|
||||||
{required String id, required String k, dynamic hint}) {
|
|
||||||
return Future(
|
|
||||||
() => js.context.callMethod('getByName', ['option:flutter:peer', k]));
|
|
||||||
}
|
|
||||||
|
|
||||||
int getNextTextureKey({dynamic hint}) {
|
int getNextTextureKey({dynamic hint}) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -346,6 +340,10 @@ class RustdeskImpl {
|
|||||||
return mode == kKeyLegacyMode;
|
return mode == kKeyLegacyMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool sessionIsMultiUiSession({required UuidValue sessionId, dynamic hint}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> sessionSetCustomImageQuality(
|
Future<void> sessionSetCustomImageQuality(
|
||||||
{required UuidValue sessionId, required int value, dynamic hint}) {
|
{required UuidValue sessionId, required int value, dynamic hint}) {
|
||||||
return Future(() => js.context.callMethod('setByName', [
|
return Future(() => js.context.callMethod('setByName', [
|
||||||
@@ -676,7 +674,8 @@ class RustdeskImpl {
|
|||||||
return Future(() => js.context.callMethod('setByName', ['options', json]));
|
return Future(() => js.context.callMethod('setByName', ['options', json]));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> mainTestIfValidServer({required String server, dynamic hint}) {
|
Future<String> mainTestIfValidServer(
|
||||||
|
{required String server, required bool testWithProxy, dynamic hint}) {
|
||||||
// TODO: implement
|
// TODO: implement
|
||||||
return Future.value('');
|
return Future.value('');
|
||||||
}
|
}
|
||||||
@@ -770,6 +769,24 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> mainGetProxyStatus({dynamic hint}) {
|
||||||
|
return Future(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> mainHttpRequest({
|
||||||
|
required String url,
|
||||||
|
required String method,
|
||||||
|
String? body,
|
||||||
|
required String header,
|
||||||
|
dynamic hint,
|
||||||
|
}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> mainGetHttpStatus({required String url, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
String mainGetLocalOption({required String key, dynamic hint}) {
|
String mainGetLocalOption({required String key, dynamic hint}) {
|
||||||
return js.context.callMethod('getByName', ['option:local', key]);
|
return js.context.callMethod('getByName', ['option:local', key]);
|
||||||
}
|
}
|
||||||
@@ -920,7 +937,7 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> mainDefaultVideoSaveDirectory({dynamic hint}) {
|
Future<String> mainVideoSaveDirectory({required bool root, dynamic hint}) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1052,7 +1069,7 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool mainHasGpucodec({dynamic hint}) {
|
bool mainHasVram({dynamic hint}) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1397,10 +1414,6 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool mainHasPixelbufferTextureRender({dynamic hint}) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool mainHasFileClipboard({dynamic hint}) {
|
bool mainHasFileClipboard({dynamic hint}) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1447,6 +1460,10 @@ class RustdeskImpl {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isDisableGroupPanel({dynamic hint}) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool isDisableAccount({dynamic hint}) {
|
bool isDisableAccount({dynamic hint}) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1573,5 +1590,27 @@ class RustdeskImpl {
|
|||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> mainCheckHwcodec({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sessionRequestNewDisplayInitMsgs(
|
||||||
|
{required UuidValue sessionId, required int display, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> mainHandleWaylandScreencastRestoreToken(
|
||||||
|
{required String key, required String value, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mainIsOptionFixed({required String key, dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool mainGetUseTextureRender({dynamic hint}) {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {}
|
void dispose() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,33 @@
|
|||||||
#include "my_application.h"
|
#include "my_application.h"
|
||||||
|
|
||||||
#define RUSTDESK_LIB_PATH "librustdesk.so"
|
#define RUSTDESK_LIB_PATH "librustdesk.so"
|
||||||
// #define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so"
|
|
||||||
typedef bool (*RustDeskCoreMain)();
|
typedef bool (*RustDeskCoreMain)();
|
||||||
bool gIsConnectionManager = false;
|
bool gIsConnectionManager = false;
|
||||||
|
|
||||||
|
void print_help_install_pkg(const char* so);
|
||||||
|
|
||||||
bool flutter_rustdesk_core_main() {
|
bool flutter_rustdesk_core_main() {
|
||||||
void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY);
|
void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY);
|
||||||
if (!librustdesk) {
|
if (!librustdesk) {
|
||||||
fprintf(stderr,"load librustdesk.so failed\n");
|
fprintf(stderr,"Failed to load \"librustdesk.so\"\n");
|
||||||
return true;
|
char* error;
|
||||||
|
if ((error = dlerror()) != nullptr) {
|
||||||
|
fprintf(stderr, "%s\n", error);
|
||||||
|
char* libmissed = strstr(error, ": cannot open shared object file: No such file or directory");
|
||||||
|
if (libmissed != nullptr) {
|
||||||
|
*libmissed = '\0';
|
||||||
|
char* so = strdup(error);
|
||||||
|
print_help_install_pkg(so);
|
||||||
|
free(so);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
auto core_main = (RustDeskCoreMain) dlsym(librustdesk,"rustdesk_core_main");
|
auto core_main = (RustDeskCoreMain) dlsym(librustdesk,"rustdesk_core_main");
|
||||||
char* error;
|
char* error;
|
||||||
if ((error = dlerror()) != nullptr) {
|
if ((error = dlerror()) != nullptr) {
|
||||||
fprintf(stderr, "error finding rustdesk_core_main: %s", error);
|
fprintf(stderr, "Program entry \"rustdesk_core_main\" is not found: %s\n", error);
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
return core_main();
|
return core_main();
|
||||||
}
|
}
|
||||||
@@ -33,3 +45,80 @@ int main(int argc, char** argv) {
|
|||||||
g_autoptr(MyApplication) app = my_application_new();
|
g_autoptr(MyApplication) app = my_application_new();
|
||||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char* mgr;
|
||||||
|
const char* search;
|
||||||
|
} PkgMgrSearch;
|
||||||
|
|
||||||
|
const PkgMgrSearch g_mgrs[] = {
|
||||||
|
{
|
||||||
|
"apt",
|
||||||
|
"apt-file search",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dnf",
|
||||||
|
"dnf provides",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"yum",
|
||||||
|
"yum provides",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"zypper",
|
||||||
|
"zypper wp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pacman",
|
||||||
|
"pacman -Qo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
int is_command_exists(const char* command) {
|
||||||
|
char* path = getenv("PATH");
|
||||||
|
char* path_copy = strdup(path);
|
||||||
|
char* dir = strtok(path_copy, ":");
|
||||||
|
|
||||||
|
while (dir != NULL) {
|
||||||
|
char command_path[256];
|
||||||
|
snprintf(command_path, sizeof(command_path), "%s/%s", dir, command);
|
||||||
|
if (access(command_path, X_OK) == 0) {
|
||||||
|
free(path_copy);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
dir = strtok(NULL, ":");
|
||||||
|
}
|
||||||
|
|
||||||
|
free(path_copy);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do not automatically search pkg
|
||||||
|
// as the search process can be time consuming and update may be required.
|
||||||
|
void print_help_install_pkg(const char* so)
|
||||||
|
{
|
||||||
|
if (strcmp(so, "libnsl.so.1") == 0) {
|
||||||
|
const char* mgr[] = {"yum", "dnf", NULL};
|
||||||
|
const char** m = mgr;
|
||||||
|
while (*m != NULL) {
|
||||||
|
if (is_command_exists(*m)) {
|
||||||
|
fprintf(stderr, "Please run \"%s install libnsl\" to install the required package.\n", *m);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
m++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PkgMgrSearch *mgr_search = g_mgrs;
|
||||||
|
while (mgr_search->mgr != NULL) {
|
||||||
|
if (is_command_exists(mgr_search->mgr) == 1) {
|
||||||
|
fprintf(stderr, "Please run \"%s %s\" to search and install the pkg.\n", mgr_search->search, so);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mgr_search++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ static void my_application_activate(GApplication* application) {
|
|||||||
MyApplication* self = MY_APPLICATION(application);
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
GtkWindow* window =
|
GtkWindow* window =
|
||||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
// we have custom window frame
|
|
||||||
gtk_window_set_decorated(window, FALSE);
|
gtk_window_set_decorated(window, FALSE);
|
||||||
// try setting icon for rustdesk, which uses the system cache
|
// try setting icon for rustdesk, which uses the system cache
|
||||||
GtkIconTheme* theme = gtk_icon_theme_get_default();
|
GtkIconTheme* theme = gtk_icon_theme_get_default();
|
||||||
@@ -75,12 +74,7 @@ static void my_application_activate(GApplication* application) {
|
|||||||
|
|
||||||
FlView* view = fl_view_new(project);
|
FlView* view = fl_view_new(project);
|
||||||
gtk_widget_show(GTK_WIDGET(view));
|
gtk_widget_show(GTK_WIDGET(view));
|
||||||
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||||
auto border_frame = gtk_frame_new(nullptr);
|
|
||||||
gtk_frame_set_shadow_type(GTK_FRAME(border_frame), GTK_SHADOW_ETCHED_IN);
|
|
||||||
gtk_container_add(GTK_CONTAINER(border_frame), GTK_WIDGET(view));
|
|
||||||
gtk_widget_show(GTK_WIDGET(border_frame));
|
|
||||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(border_frame));
|
|
||||||
|
|
||||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 0920;
|
LastSwiftUpdateCheck = 0920;
|
||||||
LastUpgradeCheck = 1430;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
33CC10EC2044A3C60003C045 = {
|
33CC10EC2044A3C60003C045 = {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter
|
cargo ndk --platform 21 --target armv7-linux-androideabi build --release --features flutter,hwcodec
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter
|
cargo ndk --platform 21 --target aarch64-linux-android build --release --features flutter,hwcodec
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: ef03db52a20a7899da135d694c071fa3866c8fb1
|
resolved-ref: 3535741662c5b7529e182227a277a8551aed3398
|
||||||
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"
|
||||||
@@ -849,18 +849,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.0"
|
version: "0.8.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
|
sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.0"
|
version: "1.11.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -921,10 +921,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.3"
|
version: "1.9.0"
|
||||||
path_parsing:
|
path_parsing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1526,10 +1526,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web
|
name: web
|
||||||
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
|
sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.0"
|
version: "0.4.2"
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
# 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
|
||||||
version: 1.2.4+39
|
version: 1.2.5+39
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '^3.1.0'
|
sdk: '^3.1.0'
|
||||||
|
|||||||
@@ -660,7 +660,7 @@ export default class Connection {
|
|||||||
const defaultToggleTrue = [
|
const defaultToggleTrue = [
|
||||||
'show-remote-cursor',
|
'show-remote-cursor',
|
||||||
'privacy-mode',
|
'privacy-mode',
|
||||||
'enable-file-transfer',
|
'enable-file-copy-paste',
|
||||||
'allow_swap_key',
|
'allow_swap_key',
|
||||||
];
|
];
|
||||||
return this._options[name] || (defaultToggleTrue.includes(name) ? true : false);
|
return this._options[name] || (defaultToggleTrue.includes(name) ? true : false);
|
||||||
@@ -906,7 +906,7 @@ export default class Connection {
|
|||||||
case "privacy-mode":
|
case "privacy-mode":
|
||||||
option.privacy_mode = v2;
|
option.privacy_mode = v2;
|
||||||
break;
|
break;
|
||||||
case "enable-file-transfer":
|
case "enable-file-copy-paste":
|
||||||
option.enable_file_transfer = v2;
|
option.enable_file_transfer = v2;
|
||||||
break;
|
break;
|
||||||
case "block-input":
|
case "block-input":
|
||||||
@@ -933,7 +933,7 @@ export default class Connection {
|
|||||||
option.show_remote_cursor = this.getToggleOption("show-remote-cursor")
|
option.show_remote_cursor = this.getToggleOption("show-remote-cursor")
|
||||||
? message.OptionMessage_BoolOption.Yes
|
? message.OptionMessage_BoolOption.Yes
|
||||||
: message.OptionMessage_BoolOption.No;
|
: message.OptionMessage_BoolOption.No;
|
||||||
option.enable_file_transfer = this.getToggleOption("enable-file-transfer")
|
option.enable_file_transfer = this.getToggleOption("enable-file-copy-paste")
|
||||||
? message.OptionMessage_BoolOption.Yes
|
? message.OptionMessage_BoolOption.Yes
|
||||||
: message.OptionMessage_BoolOption.No;
|
: message.OptionMessage_BoolOption.No;
|
||||||
option.lock_after_session_end = this.getToggleOption("lock-after-session-end")
|
option.lock_after_session_end = this.getToggleOption("lock-after-session-end")
|
||||||
|
|||||||
@@ -116,15 +116,27 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
|||||||
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, cmParam.size(), cmParam.c_str()) == 0) {
|
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, cmParam.size(), cmParam.c_str()) == 0) {
|
||||||
is_cm_page = true;
|
is_cm_page = true;
|
||||||
}
|
}
|
||||||
|
bool is_install_page = false;
|
||||||
|
auto installParam = std::string("--install");
|
||||||
|
if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, installParam.size(), installParam.c_str()) == 0) {
|
||||||
|
is_install_page = true;
|
||||||
|
}
|
||||||
|
|
||||||
command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end());
|
command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end());
|
||||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||||
|
|
||||||
FlutterWindow window(project);
|
FlutterWindow window(project);
|
||||||
Win32Window::Point origin(10, 10);
|
Win32Window::Point origin(10, 10);
|
||||||
Win32Window::Size size(800, 600);
|
Win32Window::Size size(800, 600);
|
||||||
if (!window.CreateAndShow(
|
std::wstring window_title;
|
||||||
is_cm_page ? app_name + L" - Connection Manager" : app_name, origin,
|
if (is_cm_page) {
|
||||||
size, !is_cm_page)) {
|
window_title = app_name + L" - Connection Manager";
|
||||||
|
} else if (is_install_page) {
|
||||||
|
window_title = app_name + L" - Install";
|
||||||
|
} else {
|
||||||
|
window_title = app_name;
|
||||||
|
}
|
||||||
|
if (!window.CreateAndShow(window_title, origin, size, !is_cm_page)) {
|
||||||
return EXIT_FAILURE;
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
window.SetQuitOnClose(true);
|
window.SetQuitOnClose(true);
|
||||||
|
|||||||
@@ -8,16 +8,15 @@ edition = "2018"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
flexi_logger = { version = "0.27", features = ["async"] }
|
flexi_logger = { version = "0.27", features = ["async"] }
|
||||||
protobuf = { version = "3.3", features = ["with-bytes"] }
|
protobuf = { version = "3.4", features = ["with-bytes"] }
|
||||||
tokio = { version = "1.36", features = ["full"] }
|
tokio = { version = "1.37", features = ["full"] }
|
||||||
tokio-util = { version = "0.7", features = ["full"] }
|
tokio-util = { version = "0.7", features = ["full"] }
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
bytes = { version = "1.4", features = ["serde"] }
|
bytes = { version = "1.6", features = ["serde"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.10"
|
env_logger = "0.10"
|
||||||
socket2 = { version = "0.3", features = ["reuseport"] }
|
socket2 = { version = "0.3", features = ["reuseport"] }
|
||||||
zstd = "0.13"
|
zstd = "0.13"
|
||||||
quinn = {version = "0.9", optional = true }
|
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
directories-next = "2.0"
|
directories-next = "2.0"
|
||||||
@@ -26,12 +25,12 @@ serde_derive = "1.0"
|
|||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
confy = { git = "https://github.com/open-trade/confy" }
|
confy = { git = "https://github.com/rustdesk-org/confy" }
|
||||||
dirs-next = "2.0"
|
dirs-next = "2.0"
|
||||||
filetime = "0.2"
|
filetime = "0.2"
|
||||||
sodiumoxide = "0.2"
|
sodiumoxide = "0.2"
|
||||||
regex = "1.8"
|
regex = "1.8"
|
||||||
tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
|
tokio-socks = { git = "https://github.com/rustdesk-org/tokio-socks" }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
@@ -40,17 +39,23 @@ toml = "0.7"
|
|||||||
uuid = { version = "1.3", features = ["v4"] }
|
uuid = { version = "1.3", features = ["v4"] }
|
||||||
# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052
|
# crash, versions >= 0.29.1 are affected by #GuillaumeGomez/sysinfo/1052
|
||||||
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" }
|
sysinfo = { git = "https://github.com/rustdesk-org/sysinfo" }
|
||||||
|
thiserror = "1.0"
|
||||||
|
httparse = "1.5"
|
||||||
|
base64 = "0.22"
|
||||||
|
url = "2.2"
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
mac_address = "1.1"
|
mac_address = "1.1"
|
||||||
machine-uid = { git = "https://github.com/21pages/machine-uid" }
|
machine-uid = { git = "https://github.com/21pages/machine-uid" }
|
||||||
|
[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
|
||||||
[features]
|
tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false }
|
||||||
quic = []
|
rustls-platform-verifier = "0.3.1"
|
||||||
flatpak = []
|
rustls-pki-types = "1.4"
|
||||||
|
[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies]
|
||||||
|
tokio-native-tls ="0.3"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
protobuf-codegen = { version = "3.3" }
|
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"] }
|
||||||
|
|||||||
@@ -504,6 +504,11 @@ message Resolution {
|
|||||||
int32 height = 2;
|
int32 height = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message DisplayResolution {
|
||||||
|
int32 display = 1;
|
||||||
|
Resolution resolution = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message SupportedResolutions { repeated Resolution resolutions = 1; }
|
message SupportedResolutions { repeated Resolution resolutions = 1; }
|
||||||
|
|
||||||
message SwitchDisplay {
|
message SwitchDisplay {
|
||||||
@@ -596,7 +601,10 @@ message OptionMessage {
|
|||||||
BoolOption disable_keyboard = 12;
|
BoolOption disable_keyboard = 12;
|
||||||
// Position 13 is used for Resolution. Remove later.
|
// Position 13 is used for Resolution. Remove later.
|
||||||
// Resolution custom_resolution = 13;
|
// Resolution custom_resolution = 13;
|
||||||
BoolOption support_windows_specific_session = 14;
|
// BoolOption support_windows_specific_session = 14;
|
||||||
|
// starting from 15 please, do not use removed fields
|
||||||
|
BoolOption follow_remote_cursor = 15;
|
||||||
|
BoolOption follow_remote_window = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
message TestDelay {
|
message TestDelay {
|
||||||
@@ -716,6 +724,13 @@ message WindowsSessions {
|
|||||||
uint32 current_sid = 2;
|
uint32 current_sid = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Query messages from peer.
|
||||||
|
message MessageQuery {
|
||||||
|
// The SwitchDisplay message of the target display.
|
||||||
|
// If the target display is not found, the message will be ignored.
|
||||||
|
int32 switch_display = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message Misc {
|
message Misc {
|
||||||
oneof union {
|
oneof union {
|
||||||
ChatMessage chat_message = 4;
|
ChatMessage chat_message = 4;
|
||||||
@@ -736,6 +751,8 @@ message Misc {
|
|||||||
bool portable_service_running = 20;
|
bool portable_service_running = 20;
|
||||||
SwitchSidesRequest switch_sides_request = 21;
|
SwitchSidesRequest switch_sides_request = 21;
|
||||||
SwitchBack switch_back = 22;
|
SwitchBack switch_back = 22;
|
||||||
|
// Deprecated since 1.2.4, use `change_display_resolution` (36) instead.
|
||||||
|
// But we must keep it for compatibility when peer version < 1.2.4.
|
||||||
Resolution change_resolution = 24;
|
Resolution change_resolution = 24;
|
||||||
PluginRequest plugin_request = 25;
|
PluginRequest plugin_request = 25;
|
||||||
PluginFailure plugin_failure = 26;
|
PluginFailure plugin_failure = 26;
|
||||||
@@ -748,6 +765,9 @@ message Misc {
|
|||||||
TogglePrivacyMode toggle_privacy_mode = 33;
|
TogglePrivacyMode toggle_privacy_mode = 33;
|
||||||
SupportedEncoding supported_encoding = 34;
|
SupportedEncoding supported_encoding = 34;
|
||||||
uint32 selected_sid = 35;
|
uint32 selected_sid = 35;
|
||||||
|
DisplayResolution change_display_resolution = 36;
|
||||||
|
MessageQuery message_query = 37;
|
||||||
|
int32 follow_current_display = 38;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,10 +40,6 @@ const SERIAL: i32 = 3;
|
|||||||
const PASSWORD_ENC_VERSION: &str = "00";
|
const PASSWORD_ENC_VERSION: &str = "00";
|
||||||
const ENCRYPT_MAX_LEN: usize = 128;
|
const ENCRYPT_MAX_LEN: usize = 128;
|
||||||
|
|
||||||
// config2 options
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub const CONFIG_OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
|
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
pub static ref ORG: RwLock<String> = RwLock::new("com.carriez".to_owned());
|
pub static ref ORG: RwLock<String> = RwLock::new("com.carriez".to_owned());
|
||||||
@@ -278,9 +274,13 @@ pub struct PeerConfig {
|
|||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub disable_clipboard: DisableClipboard,
|
pub disable_clipboard: DisableClipboard,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub enable_file_transfer: EnableFileTransfer,
|
pub enable_file_copy_paste: EnableFileCopyPaste,
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub show_quality_monitor: ShowQualityMonitor,
|
pub show_quality_monitor: ShowQualityMonitor,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub follow_remote_cursor: FollowRemoteCursor,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub follow_remote_window: FollowRemoteWindow,
|
||||||
#[serde(
|
#[serde(
|
||||||
default,
|
default,
|
||||||
deserialize_with = "deserialize_string",
|
deserialize_with = "deserialize_string",
|
||||||
@@ -351,8 +351,10 @@ impl Default for PeerConfig {
|
|||||||
direct_failures: Default::default(),
|
direct_failures: Default::default(),
|
||||||
disable_audio: Default::default(),
|
disable_audio: Default::default(),
|
||||||
disable_clipboard: Default::default(),
|
disable_clipboard: Default::default(),
|
||||||
enable_file_transfer: Default::default(),
|
enable_file_copy_paste: Default::default(),
|
||||||
show_quality_monitor: Default::default(),
|
show_quality_monitor: Default::default(),
|
||||||
|
follow_remote_cursor: Default::default(),
|
||||||
|
follow_remote_window: Default::default(),
|
||||||
keyboard_mode: Default::default(),
|
keyboard_mode: Default::default(),
|
||||||
view_only: Default::default(),
|
view_only: Default::default(),
|
||||||
reverse_mouse_wheel: Self::default_reverse_mouse_wheel(),
|
reverse_mouse_wheel: Self::default_reverse_mouse_wheel(),
|
||||||
@@ -409,9 +411,7 @@ fn patch(path: PathBuf) -> PathBuf {
|
|||||||
if let Ok(user) = crate::platform::linux::run_cmds_trim_newline("whoami") {
|
if let Ok(user) = crate::platform::linux::run_cmds_trim_newline("whoami") {
|
||||||
if user != "root" {
|
if user != "root" {
|
||||||
let cmd = format!("getent passwd '{}' | awk -F':' '{{print $6}}'", user);
|
let cmd = format!("getent passwd '{}' | awk -F':' '{{print $6}}'", user);
|
||||||
if let Ok(output) =
|
if let Ok(output) = crate::platform::linux::run_cmds_trim_newline(&cmd) {
|
||||||
crate::platform::linux::run_cmds_trim_newline(&cmd)
|
|
||||||
{
|
|
||||||
return output.into();
|
return output.into();
|
||||||
}
|
}
|
||||||
return format!("/home/{user}").into();
|
return format!("/home/{user}").into();
|
||||||
@@ -505,7 +505,7 @@ impl Config {
|
|||||||
fn store_<T: serde::Serialize>(config: &T, suffix: &str) {
|
fn store_<T: serde::Serialize>(config: &T, suffix: &str) {
|
||||||
let file = Self::file_(suffix);
|
let file = Self::file_(suffix);
|
||||||
if let Err(err) = store_path(file, config) {
|
if let Err(err) = store_path(file, config) {
|
||||||
log::error!("Failed to store config: {}", err);
|
log::error!("Failed to store {suffix} config: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -912,7 +912,7 @@ impl Config {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn purify_options(v: &mut HashMap<String, String>) {
|
fn purify_options(v: &mut HashMap<String, String>) {
|
||||||
v.retain(|k, v| is_option_can_save(&OVERWRITE_SETTINGS, &DEFAULT_SETTINGS, k, v));
|
v.retain(|k, _| is_option_can_save(&OVERWRITE_SETTINGS, k));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_options(mut v: HashMap<String, String>) {
|
pub fn set_options(mut v: HashMap<String, String>) {
|
||||||
@@ -936,7 +936,7 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_option(k: String, v: String) {
|
pub fn set_option(k: String, v: String) {
|
||||||
if !is_option_can_save(&OVERWRITE_SETTINGS, &DEFAULT_SETTINGS, &k, &v) {
|
if !is_option_can_save(&OVERWRITE_SETTINGS, &k) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut config = CONFIG2.write().unwrap();
|
let mut config = CONFIG2.write().unwrap();
|
||||||
@@ -1014,8 +1014,30 @@ impl Config {
|
|||||||
config.store();
|
config.store();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_socks_from_custom_client_advanced_settings(
|
||||||
|
settings: &HashMap<String, String>,
|
||||||
|
) -> Option<Socks5Server> {
|
||||||
|
let url = settings.get(keys::OPTION_PROXY_URL)?;
|
||||||
|
Some(Socks5Server {
|
||||||
|
proxy: url.to_owned(),
|
||||||
|
username: settings
|
||||||
|
.get(keys::OPTION_PROXY_USERNAME)
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
password: settings
|
||||||
|
.get(keys::OPTION_PROXY_PASSWORD)
|
||||||
|
.map(|x| x.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_socks() -> Option<Socks5Server> {
|
pub fn get_socks() -> Option<Socks5Server> {
|
||||||
CONFIG2.read().unwrap().socks.clone()
|
Self::get_socks_from_custom_client_advanced_settings(&OVERWRITE_SETTINGS.read().unwrap())
|
||||||
|
.or(CONFIG2.read().unwrap().socks.clone())
|
||||||
|
.or(Self::get_socks_from_custom_client_advanced_settings(
|
||||||
|
&DEFAULT_SETTINGS.read().unwrap(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -1024,10 +1046,26 @@ impl Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_network_type() -> NetworkType {
|
pub fn get_network_type() -> NetworkType {
|
||||||
match &CONFIG2.read().unwrap().socks {
|
if OVERWRITE_SETTINGS
|
||||||
None => NetworkType::Direct,
|
.read()
|
||||||
Some(_) => NetworkType::ProxySocks,
|
.unwrap()
|
||||||
|
.get(keys::OPTION_PROXY_URL)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return NetworkType::ProxySocks;
|
||||||
}
|
}
|
||||||
|
if CONFIG2.read().unwrap().socks.is_some() {
|
||||||
|
return NetworkType::ProxySocks;
|
||||||
|
}
|
||||||
|
if DEFAULT_SETTINGS
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.get(keys::OPTION_PROXY_URL)
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return NetworkType::ProxySocks;
|
||||||
|
}
|
||||||
|
NetworkType::Direct
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get() -> Config {
|
pub fn get() -> Config {
|
||||||
@@ -1190,36 +1228,36 @@ impl PeerConfig {
|
|||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_view_style,
|
default_view_style,
|
||||||
deserialize_view_style,
|
deserialize_view_style,
|
||||||
UserDefaultConfig::read("view_style")
|
UserDefaultConfig::read(keys::OPTION_VIEW_STYLE)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_scroll_style,
|
default_scroll_style,
|
||||||
deserialize_scroll_style,
|
deserialize_scroll_style,
|
||||||
UserDefaultConfig::read("scroll_style")
|
UserDefaultConfig::read(keys::OPTION_SCROLL_STYLE)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_image_quality,
|
default_image_quality,
|
||||||
deserialize_image_quality,
|
deserialize_image_quality,
|
||||||
UserDefaultConfig::read("image_quality")
|
UserDefaultConfig::read(keys::OPTION_IMAGE_QUALITY)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_reverse_mouse_wheel,
|
default_reverse_mouse_wheel,
|
||||||
deserialize_reverse_mouse_wheel,
|
deserialize_reverse_mouse_wheel,
|
||||||
UserDefaultConfig::read("reverse_mouse_wheel")
|
UserDefaultConfig::read(keys::OPTION_REVERSE_MOUSE_WHEEL)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_displays_as_individual_windows,
|
default_displays_as_individual_windows,
|
||||||
deserialize_displays_as_individual_windows,
|
deserialize_displays_as_individual_windows,
|
||||||
UserDefaultConfig::read("displays_as_individual_windows")
|
UserDefaultConfig::read(keys::OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS)
|
||||||
);
|
);
|
||||||
serde_field_string!(
|
serde_field_string!(
|
||||||
default_use_all_my_displays_for_the_remote_session,
|
default_use_all_my_displays_for_the_remote_session,
|
||||||
deserialize_use_all_my_displays_for_the_remote_session,
|
deserialize_use_all_my_displays_for_the_remote_session,
|
||||||
UserDefaultConfig::read("use_all_my_displays_for_the_remote_session")
|
UserDefaultConfig::read(keys::OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION)
|
||||||
);
|
);
|
||||||
|
|
||||||
fn default_custom_image_quality() -> Vec<i32> {
|
fn default_custom_image_quality() -> Vec<i32> {
|
||||||
let f: f64 = UserDefaultConfig::read("custom_image_quality")
|
let f: f64 = UserDefaultConfig::read(keys::OPTION_CUSTOM_IMAGE_QUALITY)
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(50.0);
|
.unwrap_or(50.0);
|
||||||
vec![f as _]
|
vec![f as _]
|
||||||
@@ -1240,12 +1278,12 @@ impl PeerConfig {
|
|||||||
fn default_options() -> HashMap<String, String> {
|
fn default_options() -> HashMap<String, String> {
|
||||||
let mut mp: HashMap<String, String> = Default::default();
|
let mut mp: HashMap<String, String> = Default::default();
|
||||||
[
|
[
|
||||||
"codec-preference",
|
keys::OPTION_CODEC_PREFERENCE,
|
||||||
"custom-fps",
|
keys::OPTION_CUSTOM_FPS,
|
||||||
"zoom-cursor",
|
keys::OPTION_ZOOM_CURSOR,
|
||||||
"touch-mode",
|
keys::OPTION_TOUCH_MODE,
|
||||||
"i444",
|
keys::OPTION_I444,
|
||||||
"swap-left-right-mouse",
|
keys::OPTION_SWAP_LEFT_RIGHT_MOUSE,
|
||||||
]
|
]
|
||||||
.map(|key| {
|
.map(|key| {
|
||||||
mp.insert(key.to_owned(), UserDefaultConfig::read(key));
|
mp.insert(key.to_owned(), UserDefaultConfig::read(key));
|
||||||
@@ -1260,6 +1298,19 @@ serde_field_bool!(
|
|||||||
default_show_remote_cursor,
|
default_show_remote_cursor,
|
||||||
"ShowRemoteCursor::default_show_remote_cursor"
|
"ShowRemoteCursor::default_show_remote_cursor"
|
||||||
);
|
);
|
||||||
|
serde_field_bool!(
|
||||||
|
FollowRemoteCursor,
|
||||||
|
"follow_remote_cursor",
|
||||||
|
default_follow_remote_cursor,
|
||||||
|
"FollowRemoteCursor::default_follow_remote_cursor"
|
||||||
|
);
|
||||||
|
|
||||||
|
serde_field_bool!(
|
||||||
|
FollowRemoteWindow,
|
||||||
|
"follow_remote_window",
|
||||||
|
default_follow_remote_window,
|
||||||
|
"FollowRemoteWindow::default_follow_remote_window"
|
||||||
|
);
|
||||||
serde_field_bool!(
|
serde_field_bool!(
|
||||||
ShowQualityMonitor,
|
ShowQualityMonitor,
|
||||||
"show_quality_monitor",
|
"show_quality_monitor",
|
||||||
@@ -1273,10 +1324,10 @@ serde_field_bool!(
|
|||||||
"DisableAudio::default_disable_audio"
|
"DisableAudio::default_disable_audio"
|
||||||
);
|
);
|
||||||
serde_field_bool!(
|
serde_field_bool!(
|
||||||
EnableFileTransfer,
|
EnableFileCopyPaste,
|
||||||
"enable_file_transfer",
|
"enable-file-copy-paste",
|
||||||
default_enable_file_transfer,
|
default_enable_file_copy_paste,
|
||||||
"EnableFileTransfer::default_enable_file_transfer"
|
"EnableFileCopyPaste::default_enable_file_copy_paste"
|
||||||
);
|
);
|
||||||
serde_field_bool!(
|
serde_field_bool!(
|
||||||
DisableClipboard,
|
DisableClipboard,
|
||||||
@@ -1398,10 +1449,17 @@ impl LocalConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_option(k: String, v: String) {
|
pub fn set_option(k: String, v: String) {
|
||||||
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &DEFAULT_LOCAL_SETTINGS, &k, &v) {
|
if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut config = LOCAL_CONFIG.write().unwrap();
|
let mut config = LOCAL_CONFIG.write().unwrap();
|
||||||
|
// The custom client will explictly set "default" as the default language.
|
||||||
|
let is_custom_client_default_lang = k == keys::OPTION_LANGUAGE && v == "default";
|
||||||
|
if is_custom_client_default_lang {
|
||||||
|
config.options.insert(k, "".to_owned());
|
||||||
|
config.store();
|
||||||
|
return;
|
||||||
|
}
|
||||||
let v2 = if v.is_empty() { None } else { Some(&v) };
|
let v2 = if v.is_empty() { None } else { Some(&v) };
|
||||||
if v2 != config.options.get(&k) {
|
if v2 != config.options.get(&k) {
|
||||||
if v2.is_none() {
|
if v2.is_none() {
|
||||||
@@ -1414,11 +1472,13 @@ impl LocalConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_flutter_option(k: &str) -> String {
|
pub fn get_flutter_option(k: &str) -> String {
|
||||||
if let Some(v) = LOCAL_CONFIG.read().unwrap().ui_flutter.get(k) {
|
get_or(
|
||||||
v.clone()
|
&OVERWRITE_LOCAL_SETTINGS,
|
||||||
} else {
|
&LOCAL_CONFIG.read().unwrap().ui_flutter,
|
||||||
"".to_owned()
|
&DEFAULT_LOCAL_SETTINGS,
|
||||||
}
|
k,
|
||||||
|
)
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_flutter_option(k: String, v: String) {
|
pub fn set_flutter_option(k: String, v: String) {
|
||||||
@@ -1495,8 +1555,10 @@ impl LanPeers {
|
|||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct HwCodecConfig {
|
pub struct HwCodecConfig {
|
||||||
#[serde(default, deserialize_with = "deserialize_hashmap_string_string")]
|
#[serde(default, deserialize_with = "deserialize_string")]
|
||||||
pub options: HashMap<String, String>,
|
pub ram: String,
|
||||||
|
#[serde(default, deserialize_with = "deserialize_string")]
|
||||||
|
pub vram: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HwCodecConfig {
|
impl HwCodecConfig {
|
||||||
@@ -1511,25 +1573,17 @@ impl HwCodecConfig {
|
|||||||
pub fn clear() {
|
pub fn clear() {
|
||||||
HwCodecConfig::default().store();
|
HwCodecConfig::default().store();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
pub fn clear_ram() {
|
||||||
pub struct GpucodecConfig {
|
let mut c = Self::load();
|
||||||
#[serde(default, deserialize_with = "deserialize_string")]
|
c.ram = Default::default();
|
||||||
pub available: String,
|
c.store();
|
||||||
}
|
|
||||||
|
|
||||||
impl GpucodecConfig {
|
|
||||||
pub fn load() -> GpucodecConfig {
|
|
||||||
Config::load_::<GpucodecConfig>("_gpucodec")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn store(&self) {
|
pub fn clear_vram() {
|
||||||
Config::store_(self, "_gpucodec");
|
let mut c = Self::load();
|
||||||
}
|
c.vram = Default::default();
|
||||||
|
c.store();
|
||||||
pub fn clear() {
|
|
||||||
GpucodecConfig::default().store();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1561,14 +1615,19 @@ impl UserDefaultConfig {
|
|||||||
|
|
||||||
pub fn get(&self, key: &str) -> String {
|
pub fn get(&self, key: &str) -> String {
|
||||||
match key {
|
match key {
|
||||||
"view_style" => self.get_string(key, "original", vec!["adaptive"]),
|
keys::OPTION_VIEW_STYLE => self.get_string(key, "original", vec!["adaptive"]),
|
||||||
"scroll_style" => self.get_string(key, "scrollauto", vec!["scrollbar"]),
|
keys::OPTION_SCROLL_STYLE => self.get_string(key, "scrollauto", vec!["scrollbar"]),
|
||||||
"image_quality" => self.get_string(key, "balanced", vec!["best", "low", "custom"]),
|
keys::OPTION_IMAGE_QUALITY => {
|
||||||
"codec-preference" => {
|
self.get_string(key, "balanced", vec!["best", "low", "custom"])
|
||||||
|
}
|
||||||
|
keys::OPTION_CODEC_PREFERENCE => {
|
||||||
self.get_string(key, "auto", vec!["vp8", "vp9", "av1", "h264", "h265"])
|
self.get_string(key, "auto", vec!["vp8", "vp9", "av1", "h264", "h265"])
|
||||||
}
|
}
|
||||||
"custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 0xFFF as f64),
|
keys::OPTION_CUSTOM_IMAGE_QUALITY => {
|
||||||
"custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0),
|
self.get_double_string(key, 50.0, 10.0, 0xFFF as f64)
|
||||||
|
}
|
||||||
|
keys::OPTION_CUSTOM_FPS => self.get_double_string(key, 30.0, 5.0, 120.0),
|
||||||
|
keys::OPTION_ENABLE_FILE_COPY_PASTE => self.get_string(key, "Y", vec!["", "N"]),
|
||||||
_ => self
|
_ => self
|
||||||
.get_after(key)
|
.get_after(key)
|
||||||
.map(|v| v.to_string())
|
.map(|v| v.to_string())
|
||||||
@@ -1577,12 +1636,7 @@ impl UserDefaultConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&mut self, key: String, value: String) {
|
pub fn set(&mut self, key: String, value: String) {
|
||||||
if !is_option_can_save(
|
if !is_option_can_save(&OVERWRITE_DISPLAY_SETTINGS, &key) {
|
||||||
&OVERWRITE_DISPLAY_SETTINGS,
|
|
||||||
&DEFAULT_DISPLAY_SETTINGS,
|
|
||||||
&key,
|
|
||||||
&value,
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if value.is_empty() {
|
if value.is_empty() {
|
||||||
@@ -1905,18 +1959,10 @@ fn get_or(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn is_option_can_save(
|
fn is_option_can_save(overwrite: &RwLock<HashMap<String, String>>, k: &str) -> bool {
|
||||||
overwrite: &RwLock<HashMap<String, String>>,
|
|
||||||
defaults: &RwLock<HashMap<String, String>>,
|
|
||||||
k: &str,
|
|
||||||
v: &str,
|
|
||||||
) -> bool {
|
|
||||||
if overwrite.read().unwrap().contains_key(k) {
|
if overwrite.read().unwrap().contains_key(k) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if defaults.read().unwrap().get(k).map_or(false, |x| x == v) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1972,6 +2018,170 @@ pub fn is_disable_installation() -> bool {
|
|||||||
is_some_hard_opton("disable-installation")
|
is_some_hard_opton("disable-installation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod keys {
|
||||||
|
pub const OPTION_VIEW_ONLY: &str = "view_only";
|
||||||
|
pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar";
|
||||||
|
pub const OPTION_COLLAPSE_TOOLBAR: &str = "collapse_toolbar";
|
||||||
|
pub const OPTION_SHOW_REMOTE_CURSOR: &str = "show_remote_cursor";
|
||||||
|
pub const OPTION_FOLLOW_REMOTE_CURSOR: &str = "follow_remote_cursor";
|
||||||
|
pub const OPTION_FOLLOW_REMOTE_WINDOW: &str = "follow_remote_window";
|
||||||
|
pub const OPTION_ZOOM_CURSOR: &str = "zoom-cursor";
|
||||||
|
pub const OPTION_SHOW_QUALITY_MONITOR: &str = "show_quality_monitor";
|
||||||
|
pub const OPTION_DISABLE_AUDIO: &str = "disable_audio";
|
||||||
|
pub const OPTION_ENABLE_FILE_COPY_PASTE: &str = "enable-file-copy-paste";
|
||||||
|
pub const OPTION_DISABLE_CLIPBOARD: &str = "disable_clipboard";
|
||||||
|
pub const OPTION_LOCK_AFTER_SESSION_END: &str = "lock_after_session_end";
|
||||||
|
pub const OPTION_PRIVACY_MODE: &str = "privacy_mode";
|
||||||
|
pub const OPTION_TOUCH_MODE: &str = "touch-mode";
|
||||||
|
pub const OPTION_I444: &str = "i444";
|
||||||
|
pub const OPTION_REVERSE_MOUSE_WHEEL: &str = "reverse_mouse_wheel";
|
||||||
|
pub const OPTION_SWAP_LEFT_RIGHT_MOUSE: &str = "swap-left-right-mouse";
|
||||||
|
pub const OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS: &str = "displays_as_individual_windows";
|
||||||
|
pub const OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION: &str =
|
||||||
|
"use_all_my_displays_for_the_remote_session";
|
||||||
|
pub const OPTION_VIEW_STYLE: &str = "view_style";
|
||||||
|
pub const OPTION_SCROLL_STYLE: &str = "scroll_style";
|
||||||
|
pub const OPTION_IMAGE_QUALITY: &str = "image_quality";
|
||||||
|
pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality";
|
||||||
|
pub const OPTION_CUSTOM_FPS: &str = "custom-fps";
|
||||||
|
pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference";
|
||||||
|
pub const OPTION_THEME: &str = "theme";
|
||||||
|
pub const OPTION_LANGUAGE: &str = "lang";
|
||||||
|
pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left";
|
||||||
|
pub const OPTION_REMOTE_MENUBAR_DRAG_RIGHT: &str = "remote-menubar-drag-right";
|
||||||
|
pub const OPTION_HIDE_AB_TAGS_PANEL: &str = "hideAbTagsPanel";
|
||||||
|
pub const OPTION_ENABLE_CONFIRM_CLOSING_TABS: &str = "enable-confirm-closing-tabs";
|
||||||
|
pub const OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS: &str =
|
||||||
|
"enable-open-new-connections-in-tabs";
|
||||||
|
pub const OPTION_TEXTURE_RENDER: &str = "use-texture-render";
|
||||||
|
pub const OPTION_ENABLE_CHECK_UPDATE: &str = "enable-check-update";
|
||||||
|
pub const OPTION_SYNC_AB_WITH_RECENT_SESSIONS: &str = "sync-ab-with-recent-sessions";
|
||||||
|
pub const OPTION_SYNC_AB_TAGS: &str = "sync-ab-tags";
|
||||||
|
pub const OPTION_FILTER_AB_BY_INTERSECTION: &str = "filter-ab-by-intersection";
|
||||||
|
pub const OPTION_ACCESS_MODE: &str = "access-mode";
|
||||||
|
pub const OPTION_ENABLE_KEYBOARD: &str = "enable-keyboard";
|
||||||
|
pub const OPTION_ENABLE_CLIPBOARD: &str = "enable-clipboard";
|
||||||
|
pub const OPTION_ENABLE_FILE_TRANSFER: &str = "enable-file-transfer";
|
||||||
|
pub const OPTION_ENABLE_AUDIO: &str = "enable-audio";
|
||||||
|
pub const OPTION_ENABLE_TUNNEL: &str = "enable-tunnel";
|
||||||
|
pub const OPTION_ENABLE_REMOTE_RESTART: &str = "enable-remote-restart";
|
||||||
|
pub const OPTION_ENABLE_RECORD_SESSION: &str = "enable-record-session";
|
||||||
|
pub const OPTION_ENABLE_BLOCK_INPUT: &str = "enable-block-input";
|
||||||
|
pub const OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION: &str = "allow-remote-config-modification";
|
||||||
|
pub const OPTION_ENABLE_LAN_DISCOVERY: &str = "enable-lan-discovery";
|
||||||
|
pub const OPTION_DIRECT_SERVER: &str = "direct-server";
|
||||||
|
pub const OPTION_DIRECT_ACCESS_PORT: &str = "direct-access-port";
|
||||||
|
pub const OPTION_WHITELIST: &str = "whitelist";
|
||||||
|
pub const OPTION_ALLOW_AUTO_DISCONNECT: &str = "allow-auto-disconnect";
|
||||||
|
pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout";
|
||||||
|
pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open";
|
||||||
|
pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming";
|
||||||
|
pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory";
|
||||||
|
pub const OPTION_ENABLE_ABR: &str = "enable-abr";
|
||||||
|
pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper";
|
||||||
|
pub const OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER: &str = "allow-always-software-render";
|
||||||
|
pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
|
||||||
|
pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec";
|
||||||
|
pub const OPTION_APPROVE_MODE: &str = "approve-mode";
|
||||||
|
|
||||||
|
// flutter local options
|
||||||
|
pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState";
|
||||||
|
pub const OPTION_FLUTTER_PEER_SORTING: &str = "peer-sorting";
|
||||||
|
pub const OPTION_FLUTTER_PEER_TAB_INDEX: &str = "peer-tab-index";
|
||||||
|
pub const OPTION_FLUTTER_PEER_TAB_ORDER: &str = "peer-tab-order";
|
||||||
|
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_CURRENT_AB_NAME: &str = "current-ab-name";
|
||||||
|
|
||||||
|
// proxy settings
|
||||||
|
// The following options are not real keys, they are just used for custom client advanced settings.
|
||||||
|
// The real keys are in Config2::socks.
|
||||||
|
pub const OPTION_PROXY_URL: &str = "proxy-url";
|
||||||
|
pub const OPTION_PROXY_USERNAME: &str = "proxy-username";
|
||||||
|
pub const OPTION_PROXY_PASSWORD: &str = "proxy-password";
|
||||||
|
|
||||||
|
// DEFAULT_DISPLAY_SETTINGS, OVERWRITE_DISPLAY_SETTINGS
|
||||||
|
pub const KEYS_DISPLAY_SETTINGS: &[&str] = &[
|
||||||
|
OPTION_VIEW_ONLY,
|
||||||
|
OPTION_SHOW_MONITORS_TOOLBAR,
|
||||||
|
OPTION_COLLAPSE_TOOLBAR,
|
||||||
|
OPTION_SHOW_REMOTE_CURSOR,
|
||||||
|
OPTION_FOLLOW_REMOTE_CURSOR,
|
||||||
|
OPTION_FOLLOW_REMOTE_WINDOW,
|
||||||
|
OPTION_ZOOM_CURSOR,
|
||||||
|
OPTION_SHOW_QUALITY_MONITOR,
|
||||||
|
OPTION_DISABLE_AUDIO,
|
||||||
|
OPTION_ENABLE_FILE_COPY_PASTE,
|
||||||
|
OPTION_DISABLE_CLIPBOARD,
|
||||||
|
OPTION_LOCK_AFTER_SESSION_END,
|
||||||
|
OPTION_PRIVACY_MODE,
|
||||||
|
OPTION_TOUCH_MODE,
|
||||||
|
OPTION_I444,
|
||||||
|
OPTION_REVERSE_MOUSE_WHEEL,
|
||||||
|
OPTION_SWAP_LEFT_RIGHT_MOUSE,
|
||||||
|
OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS,
|
||||||
|
OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION,
|
||||||
|
OPTION_VIEW_STYLE,
|
||||||
|
OPTION_SCROLL_STYLE,
|
||||||
|
OPTION_IMAGE_QUALITY,
|
||||||
|
OPTION_CUSTOM_IMAGE_QUALITY,
|
||||||
|
OPTION_CUSTOM_FPS,
|
||||||
|
OPTION_CODEC_PREFERENCE,
|
||||||
|
];
|
||||||
|
// DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS
|
||||||
|
pub const KEYS_LOCAL_SETTINGS: &[&str] = &[
|
||||||
|
OPTION_THEME,
|
||||||
|
OPTION_LANGUAGE,
|
||||||
|
OPTION_ENABLE_CONFIRM_CLOSING_TABS,
|
||||||
|
OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS,
|
||||||
|
OPTION_TEXTURE_RENDER,
|
||||||
|
OPTION_SYNC_AB_WITH_RECENT_SESSIONS,
|
||||||
|
OPTION_SYNC_AB_TAGS,
|
||||||
|
OPTION_FILTER_AB_BY_INTERSECTION,
|
||||||
|
OPTION_REMOTE_MENUBAR_DRAG_LEFT,
|
||||||
|
OPTION_REMOTE_MENUBAR_DRAG_RIGHT,
|
||||||
|
OPTION_HIDE_AB_TAGS_PANEL,
|
||||||
|
OPTION_FLUTTER_REMOTE_MENUBAR_STATE,
|
||||||
|
OPTION_FLUTTER_PEER_SORTING,
|
||||||
|
OPTION_FLUTTER_PEER_TAB_INDEX,
|
||||||
|
OPTION_FLUTTER_PEER_TAB_ORDER,
|
||||||
|
OPTION_FLUTTER_PEER_TAB_VISIBLE,
|
||||||
|
OPTION_FLUTTER_PEER_CARD_UI_TYLE,
|
||||||
|
OPTION_FLUTTER_CURRENT_AB_NAME,
|
||||||
|
];
|
||||||
|
// DEFAULT_SETTINGS, OVERWRITE_SETTINGS
|
||||||
|
pub const KEYS_SETTINGS: &[&str] = &[
|
||||||
|
OPTION_ACCESS_MODE,
|
||||||
|
OPTION_ENABLE_KEYBOARD,
|
||||||
|
OPTION_ENABLE_CLIPBOARD,
|
||||||
|
OPTION_ENABLE_FILE_TRANSFER,
|
||||||
|
OPTION_ENABLE_AUDIO,
|
||||||
|
OPTION_ENABLE_TUNNEL,
|
||||||
|
OPTION_ENABLE_REMOTE_RESTART,
|
||||||
|
OPTION_ENABLE_RECORD_SESSION,
|
||||||
|
OPTION_ENABLE_BLOCK_INPUT,
|
||||||
|
OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION,
|
||||||
|
OPTION_ENABLE_LAN_DISCOVERY,
|
||||||
|
OPTION_DIRECT_SERVER,
|
||||||
|
OPTION_DIRECT_ACCESS_PORT,
|
||||||
|
OPTION_WHITELIST,
|
||||||
|
OPTION_ALLOW_AUTO_DISCONNECT,
|
||||||
|
OPTION_AUTO_DISCONNECT_TIMEOUT,
|
||||||
|
OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN,
|
||||||
|
OPTION_ALLOW_AUTO_RECORD_INCOMING,
|
||||||
|
OPTION_VIDEO_SAVE_DIRECTORY,
|
||||||
|
OPTION_ENABLE_ABR,
|
||||||
|
OPTION_ALLOW_REMOVE_WALLPAPER,
|
||||||
|
OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER,
|
||||||
|
OPTION_ALLOW_LINUX_HEADLESS,
|
||||||
|
OPTION_ENABLE_HWCODEC,
|
||||||
|
OPTION_APPROVE_MODE,
|
||||||
|
OPTION_PROXY_URL,
|
||||||
|
OPTION_PROXY_USERNAME,
|
||||||
|
OPTION_PROXY_PASSWORD,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -2010,6 +2220,10 @@ mod tests {
|
|||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert("b".to_string(), "c".to_string());
|
.insert("b".to_string(), "c".to_string());
|
||||||
|
OVERWRITE_SETTINGS
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.insert("c".to_string(), "f".to_string());
|
||||||
OVERWRITE_SETTINGS
|
OVERWRITE_SETTINGS
|
||||||
.write()
|
.write()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -2030,7 +2244,7 @@ mod tests {
|
|||||||
res.insert("d".to_owned(), "c".to_string());
|
res.insert("d".to_owned(), "c".to_string());
|
||||||
res.insert("c".to_owned(), "a".to_string());
|
res.insert("c".to_owned(), "a".to_string());
|
||||||
res.insert("f".to_owned(), "a".to_string());
|
res.insert("f".to_owned(), "a".to_string());
|
||||||
res.insert("c".to_owned(), "d".to_string());
|
res.insert("e".to_owned(), "d".to_string());
|
||||||
Config::purify_options(&mut res);
|
Config::purify_options(&mut res);
|
||||||
assert!(res.len() == 2);
|
assert!(res.len() == 2);
|
||||||
res.insert("b".to_owned(), "c".to_string());
|
res.insert("b".to_owned(), "c".to_string());
|
||||||
@@ -2043,11 +2257,11 @@ mod tests {
|
|||||||
assert!(res.len() == 2);
|
assert!(res.len() == 2);
|
||||||
let res = Config::get_options();
|
let res = Config::get_options();
|
||||||
assert!(res["a"] == "b");
|
assert!(res["a"] == "b");
|
||||||
assert!(res["c"] == "a");
|
assert!(res["c"] == "f");
|
||||||
assert!(res["b"] == "c");
|
assert!(res["b"] == "c");
|
||||||
assert!(res["d"] == "c");
|
assert!(res["d"] == "c");
|
||||||
assert!(Config::get_option("a") == "b");
|
assert!(Config::get_option("a") == "b");
|
||||||
assert!(Config::get_option("c") == "a");
|
assert!(Config::get_option("c") == "f");
|
||||||
assert!(Config::get_option("b") == "c");
|
assert!(Config::get_option("b") == "c");
|
||||||
assert!(Config::get_option("d") == "c");
|
assert!(Config::get_option("d") == "c");
|
||||||
DEFAULT_SETTINGS.write().unwrap().clear();
|
DEFAULT_SETTINGS.write().unwrap().clear();
|
||||||
|
|||||||
@@ -16,14 +16,13 @@ use std::{
|
|||||||
};
|
};
|
||||||
pub use tokio;
|
pub use tokio;
|
||||||
pub use tokio_util;
|
pub use tokio_util;
|
||||||
|
pub mod proxy;
|
||||||
pub mod socket_client;
|
pub mod socket_client;
|
||||||
pub mod tcp;
|
pub mod tcp;
|
||||||
pub mod udp;
|
pub mod udp;
|
||||||
pub use env_logger;
|
pub use env_logger;
|
||||||
pub use log;
|
pub use log;
|
||||||
pub mod bytes_codec;
|
pub mod bytes_codec;
|
||||||
#[cfg(feature = "quic")]
|
|
||||||
pub mod quic;
|
|
||||||
pub use anyhow::{self, bail};
|
pub use anyhow::{self, bail};
|
||||||
pub use futures_util;
|
pub use futures_util;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
@@ -42,17 +41,18 @@ pub use chrono;
|
|||||||
pub use directories_next;
|
pub use directories_next;
|
||||||
pub use libc;
|
pub use libc;
|
||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
|
pub use base64;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub use dlopen;
|
pub use dlopen;
|
||||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||||
pub use machine_uid;
|
pub use machine_uid;
|
||||||
|
pub use serde_derive;
|
||||||
|
pub use serde_json;
|
||||||
pub use sysinfo;
|
pub use sysinfo;
|
||||||
|
pub use thiserror;
|
||||||
pub use toml;
|
pub use toml;
|
||||||
pub use uuid;
|
pub use uuid;
|
||||||
|
|
||||||
#[cfg(feature = "quic")]
|
|
||||||
pub type Stream = quic::Connection;
|
|
||||||
#[cfg(not(feature = "quic"))]
|
|
||||||
pub type Stream = tcp::FramedStream;
|
pub type Stream = tcp::FramedStream;
|
||||||
pub type SessionID = uuid::Uuid;
|
pub type SessionID = uuid::Uuid;
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHand
|
|||||||
.rotate(
|
.rotate(
|
||||||
Criterion::Age(Age::Day),
|
Criterion::Age(Age::Day),
|
||||||
Naming::Timestamps,
|
Naming::Timestamps,
|
||||||
Cleanup::KeepLogFiles(6),
|
Cleanup::KeepLogFiles(31),
|
||||||
)
|
)
|
||||||
.start()
|
.start()
|
||||||
.ok();
|
.ok();
|
||||||
|
|||||||
@@ -49,6 +49,16 @@ pub fn is_x11_or_headless() -> bool {
|
|||||||
const INVALID_SESSION: &str = "4294967295";
|
const INVALID_SESSION: &str = "4294967295";
|
||||||
|
|
||||||
pub fn get_display_server() -> String {
|
pub fn get_display_server() -> String {
|
||||||
|
// Check for forced display server environment variable first
|
||||||
|
if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") {
|
||||||
|
return forced_display;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if `loginctl` can be called successfully
|
||||||
|
if run_loginctl(None).is_err() {
|
||||||
|
return DISPLAY_SERVER_X11.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
let mut session = get_values_of_seat0(&[0])[0].clone();
|
let mut session = get_values_of_seat0(&[0])[0].clone();
|
||||||
if session.is_empty() {
|
if session.is_empty() {
|
||||||
// loginctl has not given the expected output. try something else.
|
// loginctl has not given the expected output. try something else.
|
||||||
@@ -64,7 +74,7 @@ pub fn get_display_server() -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if session.is_empty() {
|
if session.is_empty() {
|
||||||
"".to_owned()
|
std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned())
|
||||||
} else {
|
} else {
|
||||||
get_display_server_of_session(&session)
|
get_display_server_of_session(&session)
|
||||||
}
|
}
|
||||||
@@ -132,9 +142,17 @@ pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec<String> {
|
|||||||
_get_values_of_seat0(indices, false)
|
_get_values_of_seat0(indices, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore "3 sessions listed."
|
||||||
|
fn ignore_loginctl_line(line: &str) -> bool {
|
||||||
|
line.contains("sessions") || line.split(" ").count() < 4
|
||||||
|
}
|
||||||
|
|
||||||
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
|
fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<String> {
|
||||||
if let Ok(output) = run_loginctl(None) {
|
if let Ok(output) = run_loginctl(None) {
|
||||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||||
|
if ignore_loginctl_line(line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if line.contains("seat0") {
|
if line.contains("seat0") {
|
||||||
if let Some(sid) = line.split_whitespace().next() {
|
if let Some(sid) = line.split_whitespace().next() {
|
||||||
if is_active(sid) {
|
if is_active(sid) {
|
||||||
@@ -153,6 +171,9 @@ fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec<Stri
|
|||||||
|
|
||||||
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
// some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73
|
||||||
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
for line in String::from_utf8_lossy(&output.stdout).lines() {
|
||||||
|
if ignore_loginctl_line(line) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(sid) = line.split_whitespace().next() {
|
if let Some(sid) = line.split_whitespace().next() {
|
||||||
if is_active(sid) {
|
if is_active(sid) {
|
||||||
let d = get_display_server_of_session(sid);
|
let d = get_display_server_of_session(sid);
|
||||||
@@ -213,8 +234,19 @@ pub fn run_cmds_trim_newline(cmds: &str) -> ResultType<String> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "flatpak"))]
|
|
||||||
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
|
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
|
||||||
|
if std::env::var("FLATPAK_ID").is_ok() {
|
||||||
|
let mut l_args = String::from("loginctl");
|
||||||
|
if let Some(a) = args.as_ref() {
|
||||||
|
l_args = format!("{} {}", l_args, a.join(" "));
|
||||||
|
}
|
||||||
|
let res = std::process::Command::new("flatpak-spawn")
|
||||||
|
.args(vec![String::from("--host"), l_args])
|
||||||
|
.output();
|
||||||
|
if res.is_ok() {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut cmd = std::process::Command::new("loginctl");
|
let mut cmd = std::process::Command::new("loginctl");
|
||||||
if let Some(a) = args {
|
if let Some(a) = args {
|
||||||
return cmd.args(a).output();
|
return cmd.args(a).output();
|
||||||
@@ -222,17 +254,6 @@ fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output
|
|||||||
cmd.output()
|
cmd.output()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "flatpak")]
|
|
||||||
fn run_loginctl(args: Option<Vec<&str>>) -> std::io::Result<std::process::Output> {
|
|
||||||
let mut l_args = String::from("loginctl");
|
|
||||||
if let Some(a) = args {
|
|
||||||
l_args = format!("{} {}", l_args, a.join(" "));
|
|
||||||
}
|
|
||||||
std::process::Command::new("flatpak-spawn")
|
|
||||||
.args(vec![String::from("--host"), l_args])
|
|
||||||
.output()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// forever: may not work
|
/// forever: may not work
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> {
|
pub fn system_message(title: &str, msg: &str, forever: bool) -> ResultType<()> {
|
||||||
@@ -280,6 +301,9 @@ mod tests {
|
|||||||
fn test_run_cmds_trim_newline() {
|
fn test_run_cmds_trim_newline() {
|
||||||
assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123");
|
assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123");
|
||||||
assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123");
|
assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123");
|
||||||
assert_eq!(run_cmds_trim_newline("whoami").unwrap() + "\n", run_cmds("whoami").unwrap());
|
assert_eq!(
|
||||||
|
run_cmds_trim_newline("whoami").unwrap() + "\n",
|
||||||
|
run_cmds("whoami").unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
561
libs/hbb_common/src/proxy.rs
Normal file
561
libs/hbb_common/src/proxy.rs
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
use std::{
|
||||||
|
io::Error as IoError,
|
||||||
|
net::{SocketAddr, ToSocketAddrs},
|
||||||
|
};
|
||||||
|
|
||||||
|
use base64::{engine::general_purpose, Engine};
|
||||||
|
use httparse::{Error as HttpParseError, Response, EMPTY_HEADER};
|
||||||
|
use log::info;
|
||||||
|
use thiserror::Error as ThisError;
|
||||||
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream};
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
|
use tokio_native_tls::{native_tls, TlsConnector, TlsStream};
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
|
use tokio_rustls::{client::TlsStream, TlsConnector};
|
||||||
|
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr};
|
||||||
|
use tokio_util::codec::Framed;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
bytes_codec::BytesCodec,
|
||||||
|
config::Socks5Server,
|
||||||
|
tcp::{DynTcpStream, FramedStream},
|
||||||
|
ResultType,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, ThisError)]
|
||||||
|
pub enum ProxyError {
|
||||||
|
#[error("IO Error: {0}")]
|
||||||
|
IoError(#[from] IoError),
|
||||||
|
#[error("Target parse error: {0}")]
|
||||||
|
TargetParseError(String),
|
||||||
|
#[error("HTTP parse error: {0}")]
|
||||||
|
HttpParseError(#[from] HttpParseError),
|
||||||
|
#[error("The maximum response header length is exceeded: {0}")]
|
||||||
|
MaximumResponseHeaderLengthExceeded(usize),
|
||||||
|
#[error("The end of file is reached")]
|
||||||
|
EndOfFile,
|
||||||
|
#[error("The url is error: {0}")]
|
||||||
|
UrlBadScheme(String),
|
||||||
|
#[error("The url parse error: {0}")]
|
||||||
|
UrlParseScheme(#[from] url::ParseError),
|
||||||
|
#[error("No HTTP code was found in the response")]
|
||||||
|
NoHttpCode,
|
||||||
|
#[error("The HTTP code is not equal 200: {0}")]
|
||||||
|
HttpCode200(u16),
|
||||||
|
#[error("The proxy address resolution failed: {0}")]
|
||||||
|
AddressResolutionFailed(String),
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
|
#[error("The native tls error: {0}")]
|
||||||
|
NativeTlsError(#[from] tokio_native_tls::native_tls::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096;
|
||||||
|
/// The maximum HTTP Headers, which can be parsed.
|
||||||
|
const MAXIMUM_RESPONSE_HEADERS: usize = 16;
|
||||||
|
const DEFINE_TIME_OUT: u64 = 600;
|
||||||
|
|
||||||
|
pub trait IntoUrl {
|
||||||
|
|
||||||
|
// Besides parsing as a valid `Url`, the `Url` must be a valid
|
||||||
|
// `http::Uri`, in that it makes sense to use in a network request.
|
||||||
|
fn into_url(self) -> Result<Url, ProxyError>;
|
||||||
|
|
||||||
|
fn as_str(&self) -> &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoUrl for Url {
|
||||||
|
fn into_url(self) -> Result<Url, ProxyError> {
|
||||||
|
if self.has_host() {
|
||||||
|
Ok(self)
|
||||||
|
} else {
|
||||||
|
Err(ProxyError::UrlBadScheme(self.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_str(&self) -> &str {
|
||||||
|
self.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoUrl for &'a str {
|
||||||
|
fn into_url(self) -> Result<Url, ProxyError> {
|
||||||
|
Url::parse(self)
|
||||||
|
.map_err(ProxyError::UrlParseScheme)?
|
||||||
|
.into_url()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_str(&self) -> &str {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoUrl for &'a String {
|
||||||
|
fn into_url(self) -> Result<Url, ProxyError> {
|
||||||
|
(&**self).into_url()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_str(&self) -> &str {
|
||||||
|
self.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoUrl for String {
|
||||||
|
fn into_url(self) -> Result<Url, ProxyError> {
|
||||||
|
(&*self).into_url()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_str(&self) -> &str {
|
||||||
|
self.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Auth {
|
||||||
|
user_name: String,
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Auth {
|
||||||
|
fn get_proxy_authorization(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"Proxy-Authorization: Basic {}\r\n",
|
||||||
|
self.get_basic_authorization()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_basic_authorization(&self) -> String {
|
||||||
|
let authorization = format!("{}:{}", &self.user_name, &self.password);
|
||||||
|
general_purpose::STANDARD.encode(authorization.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ProxyScheme {
|
||||||
|
Http {
|
||||||
|
auth: Option<Auth>,
|
||||||
|
host: String,
|
||||||
|
},
|
||||||
|
Https {
|
||||||
|
auth: Option<Auth>,
|
||||||
|
host: String,
|
||||||
|
},
|
||||||
|
Socks5 {
|
||||||
|
addr: SocketAddr,
|
||||||
|
auth: Option<Auth>,
|
||||||
|
remote_dns: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProxyScheme {
|
||||||
|
pub fn maybe_auth(&self) -> Option<&Auth> {
|
||||||
|
match self {
|
||||||
|
ProxyScheme::Http { auth, .. }
|
||||||
|
| ProxyScheme::Https { auth, .. }
|
||||||
|
| ProxyScheme::Socks5 { auth, .. } => auth.as_ref(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn socks5(addr: SocketAddr) -> Result<Self, ProxyError> {
|
||||||
|
Ok(ProxyScheme::Socks5 {
|
||||||
|
addr,
|
||||||
|
auth: None,
|
||||||
|
remote_dns: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn http(host: &str) -> Result<Self, ProxyError> {
|
||||||
|
Ok(ProxyScheme::Http {
|
||||||
|
auth: None,
|
||||||
|
host: host.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn https(host: &str) -> Result<Self, ProxyError> {
|
||||||
|
Ok(ProxyScheme::Https {
|
||||||
|
auth: None,
|
||||||
|
host: host.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_basic_auth<T: Into<String>, U: Into<String>>(&mut self, username: T, password: U) {
|
||||||
|
let auth = Auth {
|
||||||
|
user_name: username.into(),
|
||||||
|
password: password.into(),
|
||||||
|
};
|
||||||
|
match self {
|
||||||
|
ProxyScheme::Http { auth: a, .. } => *a = Some(auth),
|
||||||
|
ProxyScheme::Https { auth: a, .. } => *a = Some(auth),
|
||||||
|
ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse(url: Url) -> Result<Self, ProxyError> {
|
||||||
|
use url::Position;
|
||||||
|
|
||||||
|
// Resolve URL to a host and port
|
||||||
|
let to_addr = || {
|
||||||
|
let addrs = url.socket_addrs(|| match url.scheme() {
|
||||||
|
"socks5" => Some(1080),
|
||||||
|
_ => None,
|
||||||
|
})?;
|
||||||
|
addrs
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost))
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut scheme: Self = match url.scheme() {
|
||||||
|
"http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?,
|
||||||
|
"https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?,
|
||||||
|
"socks5" => Self::socks5(to_addr()?)?,
|
||||||
|
e => return Err(ProxyError::UrlBadScheme(e.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(pwd) = url.password() {
|
||||||
|
let username = url.username();
|
||||||
|
scheme.set_basic_auth(username, pwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(scheme)
|
||||||
|
}
|
||||||
|
pub async fn socket_addrs(&self) -> Result<SocketAddr, ProxyError> {
|
||||||
|
info!("Resolving socket address");
|
||||||
|
match self {
|
||||||
|
ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await,
|
||||||
|
ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await,
|
||||||
|
ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_host(&self, host: &str, default_port: u16) -> Result<SocketAddr, ProxyError> {
|
||||||
|
let (host_str, port) = match host.split_once(':') {
|
||||||
|
Some((h, p)) => (h, p.parse::<u16>().ok()),
|
||||||
|
None => (host, None),
|
||||||
|
};
|
||||||
|
let addr = (host_str, port.unwrap_or(default_port))
|
||||||
|
.to_socket_addrs()?
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?;
|
||||||
|
Ok(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_domain(&self) -> Result<String, ProxyError> {
|
||||||
|
match self {
|
||||||
|
ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => {
|
||||||
|
let domain = host
|
||||||
|
.split(':')
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?;
|
||||||
|
Ok(domain.to_string())
|
||||||
|
}
|
||||||
|
ProxyScheme::Socks5 { addr, .. } => match addr {
|
||||||
|
SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()),
|
||||||
|
SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_host_and_port(&self) -> Result<String, ProxyError> {
|
||||||
|
match self {
|
||||||
|
ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)),
|
||||||
|
ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)),
|
||||||
|
ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn append_default_port(&self, host: &str, default_port: u16) -> String {
|
||||||
|
if host.contains(':') {
|
||||||
|
host.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}:{}", host, default_port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait IntoProxyScheme {
|
||||||
|
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: IntoUrl> IntoProxyScheme for S {
|
||||||
|
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
|
||||||
|
// validate the URL
|
||||||
|
let url = match self.as_str().into_url() {
|
||||||
|
Ok(ok) => ok,
|
||||||
|
Err(e) => {
|
||||||
|
match e {
|
||||||
|
// If the string does not contain protocol headers, try to parse it using the socks5 protocol
|
||||||
|
ProxyError::UrlParseScheme(_source) => {
|
||||||
|
let try_this = format!("socks5://{}", self.as_str());
|
||||||
|
try_this.into_url()?
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ProxyScheme::parse(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoProxyScheme for ProxyScheme {
|
||||||
|
fn into_proxy_scheme(self) -> Result<ProxyScheme, ProxyError> {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Proxy {
|
||||||
|
pub intercept: ProxyScheme,
|
||||||
|
ms_timeout: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Proxy {
|
||||||
|
pub fn new<U: IntoProxyScheme>(proxy_scheme: U, ms_timeout: u64) -> Result<Self, ProxyError> {
|
||||||
|
Ok(Self {
|
||||||
|
intercept: proxy_scheme.into_proxy_scheme()?,
|
||||||
|
ms_timeout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_http_or_https(&self) -> bool {
|
||||||
|
return match self.intercept {
|
||||||
|
ProxyScheme::Socks5 { .. } => false,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_conf(conf: &Socks5Server, ms_timeout: Option<u64>) -> Result<Self, ProxyError> {
|
||||||
|
let mut proxy;
|
||||||
|
match ms_timeout {
|
||||||
|
None => {
|
||||||
|
proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?;
|
||||||
|
}
|
||||||
|
Some(time_out) => {
|
||||||
|
proxy = Self::new(&conf.proxy, time_out)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !conf.password.is_empty() && !conf.username.is_empty() {
|
||||||
|
proxy = proxy.basic_auth(&conf.username, &conf.password);
|
||||||
|
}
|
||||||
|
Ok(proxy)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn proxy_addrs(&self) -> Result<SocketAddr, ProxyError> {
|
||||||
|
self.intercept.socket_addrs().await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn basic_auth(mut self, username: &str, password: &str) -> Proxy {
|
||||||
|
self.intercept.set_basic_auth(username, password);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect<'t, T>(
|
||||||
|
self,
|
||||||
|
target: T,
|
||||||
|
local_addr: Option<SocketAddr>,
|
||||||
|
) -> ResultType<FramedStream>
|
||||||
|
where
|
||||||
|
T: IntoTargetAddr<'t>,
|
||||||
|
{
|
||||||
|
info!("Connect to proxy server");
|
||||||
|
let proxy = self.proxy_addrs().await?;
|
||||||
|
|
||||||
|
let local = if let Some(addr) = local_addr {
|
||||||
|
addr
|
||||||
|
} else {
|
||||||
|
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
|
||||||
|
};
|
||||||
|
|
||||||
|
let stream = super::timeout(
|
||||||
|
self.ms_timeout,
|
||||||
|
crate::tcp::new_socket(local, true)?.connect(proxy),
|
||||||
|
)
|
||||||
|
.await??;
|
||||||
|
stream.set_nodelay(true).ok();
|
||||||
|
|
||||||
|
let addr = stream.local_addr()?;
|
||||||
|
|
||||||
|
return match self.intercept {
|
||||||
|
ProxyScheme::Http { .. } => {
|
||||||
|
info!("Connect to remote http proxy server: {}", proxy);
|
||||||
|
let stream =
|
||||||
|
super::timeout(self.ms_timeout, self.http_connect(stream, target)).await??;
|
||||||
|
Ok(FramedStream(
|
||||||
|
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||||
|
addr,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
ProxyScheme::Https { .. } => {
|
||||||
|
info!("Connect to remote https proxy server: {}", proxy);
|
||||||
|
let stream =
|
||||||
|
super::timeout(self.ms_timeout, self.https_connect(stream, target)).await??;
|
||||||
|
Ok(FramedStream(
|
||||||
|
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||||
|
addr,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
ProxyScheme::Socks5 { .. } => {
|
||||||
|
info!("Connect to remote socket5 proxy server: {}", proxy);
|
||||||
|
let stream = if let Some(auth) = self.intercept.maybe_auth() {
|
||||||
|
super::timeout(
|
||||||
|
self.ms_timeout,
|
||||||
|
Socks5Stream::connect_with_password_and_socket(
|
||||||
|
stream,
|
||||||
|
target,
|
||||||
|
&auth.user_name,
|
||||||
|
&auth.password,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await??
|
||||||
|
} else {
|
||||||
|
super::timeout(
|
||||||
|
self.ms_timeout,
|
||||||
|
Socks5Stream::connect_with_socket(stream, target),
|
||||||
|
)
|
||||||
|
.await??
|
||||||
|
};
|
||||||
|
Ok(FramedStream(
|
||||||
|
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
||||||
|
addr,
|
||||||
|
None,
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||||
|
pub async fn https_connect<'a, Input, T>(
|
||||||
|
self,
|
||||||
|
io: Input,
|
||||||
|
target: T,
|
||||||
|
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
|
||||||
|
where
|
||||||
|
Input: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
T: IntoTargetAddr<'a>,
|
||||||
|
{
|
||||||
|
let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?);
|
||||||
|
let stream = tls_connector
|
||||||
|
.connect(&self.intercept.get_domain()?, io)
|
||||||
|
.await?;
|
||||||
|
self.http_connect(stream, target).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||||
|
pub async fn https_connect<'a, Input, T>(
|
||||||
|
self,
|
||||||
|
io: Input,
|
||||||
|
target: T,
|
||||||
|
) -> Result<BufStream<TlsStream<Input>>, ProxyError>
|
||||||
|
where
|
||||||
|
Input: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
T: IntoTargetAddr<'a>,
|
||||||
|
{
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
let verifier = rustls_platform_verifier::tls_config();
|
||||||
|
let url_domain = self.intercept.get_domain()?;
|
||||||
|
|
||||||
|
let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str())
|
||||||
|
.map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))?
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let tls_connector = TlsConnector::from(std::sync::Arc::new(verifier));
|
||||||
|
let stream = tls_connector.connect(domain, io).await?;
|
||||||
|
self.http_connect(stream, target).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn http_connect<'a, Input, T>(
|
||||||
|
self,
|
||||||
|
io: Input,
|
||||||
|
target: T,
|
||||||
|
) -> Result<BufStream<Input>, ProxyError>
|
||||||
|
where
|
||||||
|
Input: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
T: IntoTargetAddr<'a>,
|
||||||
|
{
|
||||||
|
let mut stream = BufStream::new(io);
|
||||||
|
let (domain, port) = get_domain_and_port(target)?;
|
||||||
|
|
||||||
|
let request = self.make_request(&domain, port);
|
||||||
|
stream.write_all(request.as_bytes()).await?;
|
||||||
|
stream.flush().await?;
|
||||||
|
recv_and_check_response(&mut stream).await?;
|
||||||
|
Ok(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_request(&self, host: &str, port: u16) -> String {
|
||||||
|
let mut request = format!(
|
||||||
|
"CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n",
|
||||||
|
host = host,
|
||||||
|
port = port
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(auth) = self.intercept.maybe_auth() {
|
||||||
|
request = format!("{}{}", request, auth.get_proxy_authorization());
|
||||||
|
}
|
||||||
|
|
||||||
|
request.push_str("\r\n");
|
||||||
|
request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_domain_and_port<'a, T: IntoTargetAddr<'a>>(target: T) -> Result<(String, u16), ProxyError> {
|
||||||
|
let target_addr = target
|
||||||
|
.into_target_addr()
|
||||||
|
.map_err(|e| ProxyError::TargetParseError(e.to_string()))?;
|
||||||
|
match target_addr {
|
||||||
|
tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())),
|
||||||
|
tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), port)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_response<IO>(stream: &mut BufStream<IO>) -> Result<String, ProxyError>
|
||||||
|
where
|
||||||
|
IO: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
use tokio::io::AsyncBufReadExt;
|
||||||
|
let mut response = String::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if stream.read_line(&mut response).await? == 0 {
|
||||||
|
return Err(ProxyError::EndOfFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() {
|
||||||
|
return Err(ProxyError::MaximumResponseHeaderLengthExceeded(
|
||||||
|
response.len(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.ends_with("\r\n\r\n") {
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn recv_and_check_response<IO>(stream: &mut BufStream<IO>) -> Result<(), ProxyError>
|
||||||
|
where
|
||||||
|
IO: AsyncRead + AsyncWrite + Unpin,
|
||||||
|
{
|
||||||
|
let response_string = get_response(stream).await?;
|
||||||
|
|
||||||
|
let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS];
|
||||||
|
let mut response = Response::new(&mut response_headers);
|
||||||
|
let response_bytes = response_string.into_bytes();
|
||||||
|
response.parse(&response_bytes)?;
|
||||||
|
|
||||||
|
return match response.code {
|
||||||
|
Some(code) => {
|
||||||
|
if code == 200 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ProxyError::HttpCode200(code))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(ProxyError::NoHttpCode),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
use crate::{allow_err, anyhow::anyhow, ResultType};
|
|
||||||
use protobuf::Message;
|
|
||||||
use std::{net::SocketAddr, sync::Arc};
|
|
||||||
use tokio::{self, stream::StreamExt, sync::mpsc};
|
|
||||||
|
|
||||||
const QUIC_HBB: &[&[u8]] = &[b"hbb"];
|
|
||||||
const SERVER_NAME: &str = "hbb";
|
|
||||||
|
|
||||||
type Sender = mpsc::UnboundedSender<Value>;
|
|
||||||
type Receiver = mpsc::UnboundedReceiver<Value>;
|
|
||||||
|
|
||||||
pub fn new_server(socket: std::net::UdpSocket) -> ResultType<(Server, SocketAddr)> {
|
|
||||||
let mut transport_config = quinn::TransportConfig::default();
|
|
||||||
transport_config.stream_window_uni(0);
|
|
||||||
let mut server_config = quinn::ServerConfig::default();
|
|
||||||
server_config.transport = Arc::new(transport_config);
|
|
||||||
let mut server_config = quinn::ServerConfigBuilder::new(server_config);
|
|
||||||
server_config.protocols(QUIC_HBB);
|
|
||||||
// server_config.enable_keylog();
|
|
||||||
// server_config.use_stateless_retry(true);
|
|
||||||
let mut endpoint = quinn::Endpoint::builder();
|
|
||||||
endpoint.listen(server_config.build());
|
|
||||||
let (end, incoming) = endpoint.with_socket(socket)?;
|
|
||||||
Ok((Server { incoming }, end.local_addr()?))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn new_client(local_addr: &SocketAddr, peer: &SocketAddr) -> ResultType<Connection> {
|
|
||||||
let mut endpoint = quinn::Endpoint::builder();
|
|
||||||
let mut client_config = quinn::ClientConfigBuilder::default();
|
|
||||||
client_config.protocols(QUIC_HBB);
|
|
||||||
//client_config.enable_keylog();
|
|
||||||
endpoint.default_client_config(client_config.build());
|
|
||||||
let (endpoint, _) = endpoint.bind(local_addr)?;
|
|
||||||
let new_conn = endpoint.connect(peer, SERVER_NAME)?.await?;
|
|
||||||
Connection::new_for_client(new_conn.connection).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Server {
|
|
||||||
incoming: quinn::Incoming,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Server {
|
|
||||||
#[inline]
|
|
||||||
pub async fn next(&mut self) -> ResultType<Option<Connection>> {
|
|
||||||
Connection::new_for_server(&mut self.incoming).await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Connection {
|
|
||||||
conn: quinn::Connection,
|
|
||||||
tx: quinn::SendStream,
|
|
||||||
rx: Receiver,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Value = ResultType<Vec<u8>>;
|
|
||||||
|
|
||||||
impl Connection {
|
|
||||||
async fn new_for_server(incoming: &mut quinn::Incoming) -> ResultType<Option<Self>> {
|
|
||||||
if let Some(conn) = incoming.next().await {
|
|
||||||
let quinn::NewConnection {
|
|
||||||
connection: conn,
|
|
||||||
// uni_streams,
|
|
||||||
mut bi_streams,
|
|
||||||
..
|
|
||||||
} = conn.await?;
|
|
||||||
let (tx, rx) = mpsc::unbounded_channel::<Value>();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let stream = bi_streams.next().await;
|
|
||||||
if let Some(stream) = stream {
|
|
||||||
let stream = match stream {
|
|
||||||
Err(e) => {
|
|
||||||
tx.send(Err(e.into())).ok();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Ok(s) => s,
|
|
||||||
};
|
|
||||||
let cloned = tx.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
allow_err!(handle_request(stream.1, cloned).await);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tx.send(Err(anyhow!("Reset by the peer"))).ok();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log::info!("Exit connection outer loop");
|
|
||||||
});
|
|
||||||
let tx = conn.open_uni().await?;
|
|
||||||
Ok(Some(Self { conn, tx, rx }))
|
|
||||||
} else {
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn new_for_client(conn: quinn::Connection) -> ResultType<Self> {
|
|
||||||
let (tx, rx_quic) = conn.open_bi().await?;
|
|
||||||
let (tx_mpsc, rx) = mpsc::unbounded_channel::<Value>();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
allow_err!(handle_request(rx_quic, tx_mpsc).await);
|
|
||||||
});
|
|
||||||
Ok(Self { conn, tx, rx })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn next(&mut self) -> Option<Value> {
|
|
||||||
// None is returned when all Sender halves have dropped,
|
|
||||||
// indicating that no further values can be sent on the channel.
|
|
||||||
self.rx.recv().await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn remote_address(&self) -> SocketAddr {
|
|
||||||
self.conn.remote_address()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn send_raw(&mut self, bytes: &[u8]) -> ResultType<()> {
|
|
||||||
self.tx.write_all(bytes).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub async fn send(&mut self, msg: &dyn Message) -> ResultType<()> {
|
|
||||||
match msg.write_to_bytes() {
|
|
||||||
Ok(bytes) => self.send_raw(&bytes).await?,
|
|
||||||
err => allow_err!(err),
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_request(rx: quinn::RecvStream, tx: Sender) -> ResultType<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -49,19 +49,27 @@ pub fn increase_port<T: std::string::ToString>(host: T, offset: i32) -> String {
|
|||||||
host
|
host
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_if_valid_server(host: &str) -> String {
|
pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String {
|
||||||
let host = check_port(host, 0);
|
let host = check_port(host, 0);
|
||||||
|
|
||||||
use std::net::ToSocketAddrs;
|
use std::net::ToSocketAddrs;
|
||||||
match Config::get_network_type() {
|
|
||||||
NetworkType::Direct => match host.to_socket_addrs() {
|
if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() {
|
||||||
|
test_if_valid_server_for_proxy_(&host)
|
||||||
|
} else {
|
||||||
|
match host.to_socket_addrs() {
|
||||||
Err(err) => err.to_string(),
|
Err(err) => err.to_string(),
|
||||||
Ok(_) => "".to_owned(),
|
Ok(_) => "".to_owned(),
|
||||||
},
|
}
|
||||||
NetworkType::ProxySocks => match &host.into_target_addr() {
|
}
|
||||||
Err(err) => err.to_string(),
|
}
|
||||||
Ok(_) => "".to_owned(),
|
|
||||||
},
|
#[inline]
|
||||||
|
pub fn test_if_valid_server_for_proxy_(host: &str) -> String {
|
||||||
|
// `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing,
|
||||||
|
// it can be used for both `socks` and `http` proxy.
|
||||||
|
match &host.into_target_addr() {
|
||||||
|
Err(err) => err.to_string(),
|
||||||
|
Ok(_) => "".to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,15 +115,7 @@ pub async fn connect_tcp_local<
|
|||||||
ms_timeout: u64,
|
ms_timeout: u64,
|
||||||
) -> ResultType<FramedStream> {
|
) -> ResultType<FramedStream> {
|
||||||
if let Some(conf) = Config::get_socks() {
|
if let Some(conf) = Config::get_socks() {
|
||||||
return FramedStream::connect(
|
return FramedStream::connect(target, local, &conf, ms_timeout).await;
|
||||||
conf.proxy.as_str(),
|
|
||||||
target,
|
|
||||||
local,
|
|
||||||
conf.username.as_str(),
|
|
||||||
conf.password.as_str(),
|
|
||||||
ms_timeout,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
if let Some(target) = target.resolve() {
|
if let Some(target) = target.resolve() {
|
||||||
if let Some(local) = local {
|
if let Some(local) = local {
|
||||||
@@ -255,10 +255,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_test_if_valid_server() {
|
fn test_test_if_valid_server() {
|
||||||
assert!(!test_if_valid_server("a").is_empty());
|
assert!(!test_if_valid_server("a", false).is_empty());
|
||||||
// on Linux, "1" is resolved to "0.0.0.1"
|
// on Linux, "1" is resolved to "0.0.0.1"
|
||||||
assert!(test_if_valid_server("1.1.1.1").is_empty());
|
assert!(test_if_valid_server("1.1.1.1", false).is_empty());
|
||||||
assert!(test_if_valid_server("1.1.1.1:1").is_empty());
|
assert!(test_if_valid_server("1.1.1.1:1", false).is_empty());
|
||||||
|
assert!(test_if_valid_server("microsoft.com", false).is_empty());
|
||||||
|
assert!(test_if_valid_server("microsoft.com:1", false).is_empty());
|
||||||
|
|
||||||
|
// with proxy
|
||||||
|
// `:0` indicates `let host = check_port(host, 0);` is called.
|
||||||
|
assert!(test_if_valid_server_for_proxy_("a:0").is_empty());
|
||||||
|
assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty());
|
||||||
|
assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty());
|
||||||
|
assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty());
|
||||||
|
assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::{bail, bytes_codec::BytesCodec, ResultType};
|
use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy};
|
||||||
use anyhow::Context as AnyhowCtx;
|
use anyhow::Context as AnyhowCtx;
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
use bytes::{BufMut, Bytes, BytesMut};
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
@@ -18,20 +18,20 @@ use tokio::{
|
|||||||
io::{AsyncRead, AsyncWrite, ReadBuf},
|
io::{AsyncRead, AsyncWrite, ReadBuf},
|
||||||
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
|
net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs},
|
||||||
};
|
};
|
||||||
use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr, ToProxyAddrs};
|
use tokio_socks::IntoTargetAddr;
|
||||||
use tokio_util::codec::Framed;
|
use tokio_util::codec::Framed;
|
||||||
|
|
||||||
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
|
pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {}
|
||||||
pub struct DynTcpStream(Box<dyn TcpStreamTrait + Send + Sync>);
|
pub struct DynTcpStream(pub(crate) Box<dyn TcpStreamTrait + Send + Sync>);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Encrypt(Key, u64, u64);
|
pub struct Encrypt(Key, u64, u64);
|
||||||
|
|
||||||
pub struct FramedStream(
|
pub struct FramedStream(
|
||||||
Framed<DynTcpStream, BytesCodec>,
|
pub(crate) Framed<DynTcpStream, BytesCodec>,
|
||||||
SocketAddr,
|
pub(crate) SocketAddr,
|
||||||
Option<Encrypt>,
|
pub(crate) Option<Encrypt>,
|
||||||
u64,
|
pub(crate) u64,
|
||||||
);
|
);
|
||||||
|
|
||||||
impl Deref for FramedStream {
|
impl Deref for FramedStream {
|
||||||
@@ -62,7 +62,7 @@ impl DerefMut for DynTcpStream {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
|
pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result<TcpSocket, std::io::Error> {
|
||||||
let socket = match addr {
|
let socket = match addr {
|
||||||
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
|
std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?,
|
||||||
std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?,
|
std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?,
|
||||||
@@ -109,51 +109,17 @@ impl FramedStream {
|
|||||||
bail!(format!("Failed to connect to {remote_addr}"));
|
bail!(format!("Failed to connect to {remote_addr}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect<'a, 't, P, T>(
|
pub async fn connect<'t, T>(
|
||||||
proxy: P,
|
|
||||||
target: T,
|
target: T,
|
||||||
local_addr: Option<SocketAddr>,
|
local_addr: Option<SocketAddr>,
|
||||||
username: &'a str,
|
proxy_conf: &Socks5Server,
|
||||||
password: &'a str,
|
|
||||||
ms_timeout: u64,
|
ms_timeout: u64,
|
||||||
) -> ResultType<Self>
|
) -> ResultType<Self>
|
||||||
where
|
where
|
||||||
P: ToProxyAddrs,
|
|
||||||
T: IntoTargetAddr<'t>,
|
T: IntoTargetAddr<'t>,
|
||||||
{
|
{
|
||||||
if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await {
|
let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?;
|
||||||
let local = if let Some(addr) = local_addr {
|
proxy.connect::<T>(target, local_addr).await
|
||||||
addr
|
|
||||||
} else {
|
|
||||||
crate::config::Config::get_any_listen_addr(proxy.is_ipv4())
|
|
||||||
};
|
|
||||||
let stream =
|
|
||||||
super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??;
|
|
||||||
stream.set_nodelay(true).ok();
|
|
||||||
let stream = if username.trim().is_empty() {
|
|
||||||
super::timeout(
|
|
||||||
ms_timeout,
|
|
||||||
Socks5Stream::connect_with_socket(stream, target),
|
|
||||||
)
|
|
||||||
.await??
|
|
||||||
} else {
|
|
||||||
super::timeout(
|
|
||||||
ms_timeout,
|
|
||||||
Socks5Stream::connect_with_password_and_socket(
|
|
||||||
stream, target, username, password,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await??
|
|
||||||
};
|
|
||||||
let addr = stream.local_addr()?;
|
|
||||||
return Ok(Self(
|
|
||||||
Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()),
|
|
||||||
addr,
|
|
||||||
None,
|
|
||||||
0,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
bail!("could not resolve to any address");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn local_addr(&self) -> SocketAddr {
|
pub fn local_addr(&self) -> SocketAddr {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustdesk-portable-packer"
|
name = "rustdesk-portable-packer"
|
||||||
version = "1.2.4"
|
version = "1.2.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "RustDesk Remote Desktop"
|
description = "RustDesk Remote Desktop"
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import os
|
|||||||
import optparse
|
import optparse
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
import brotli
|
import brotli
|
||||||
|
import datetime
|
||||||
|
|
||||||
# 4GB maximum
|
# 4GB maximum
|
||||||
length_count = 4
|
length_count = 4
|
||||||
@@ -22,7 +23,7 @@ def generate_md5_table(folder: str, level) -> dict:
|
|||||||
for f in files:
|
for f in files:
|
||||||
md5_generator = md5()
|
md5_generator = md5()
|
||||||
full_path = os.path.join(root, f)
|
full_path = os.path.join(root, f)
|
||||||
print(f"processing {full_path}...")
|
print(f"Processing {full_path}...")
|
||||||
f = open(full_path, "rb")
|
f = open(full_path, "rb")
|
||||||
content = f.read()
|
content = f.read()
|
||||||
content_compressed = brotli.compress(
|
content_compressed = brotli.compress(
|
||||||
@@ -34,7 +35,7 @@ def generate_md5_table(folder: str, level) -> dict:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def write_metadata(md5_table: dict, output_folder: str, exe: str):
|
def write_package_metadata(md5_table: dict, output_folder: str, exe: str):
|
||||||
output_path = os.path.join(output_folder, "data.bin")
|
output_path = os.path.join(output_folder, "data.bin")
|
||||||
with open(output_path, "wb") as f:
|
with open(output_path, "wb") as f:
|
||||||
f.write("rustdesk".encode(encoding=encoding))
|
f.write("rustdesk".encode(encoding=encoding))
|
||||||
@@ -55,8 +56,13 @@ def write_metadata(md5_table: dict, output_folder: str, exe: str):
|
|||||||
f.write("rustdesk".encode(encoding=encoding))
|
f.write("rustdesk".encode(encoding=encoding))
|
||||||
# executable
|
# executable
|
||||||
f.write(exe.encode(encoding='utf-8'))
|
f.write(exe.encode(encoding='utf-8'))
|
||||||
print(f"metadata had written to {output_path}")
|
print(f"Metadata has been written to {output_path}")
|
||||||
|
|
||||||
|
def write_app_metadata(output_folder: str):
|
||||||
|
output_path = os.path.join(output_folder, "app_metadata.toml")
|
||||||
|
with open(output_path, "w") as f:
|
||||||
|
f.write(f"timestamp = {int(datetime.datetime.now().timestamp() * 1000)}\n")
|
||||||
|
print(f"App metadata has been written to {output_path}")
|
||||||
|
|
||||||
def build_portable(output_folder: str, target: str):
|
def build_portable(output_folder: str, target: str):
|
||||||
os.chdir(output_folder)
|
os.chdir(output_folder)
|
||||||
@@ -91,11 +97,12 @@ if __name__ == '__main__':
|
|||||||
options.executable = folder + '/' + options.executable
|
options.executable = folder + '/' + options.executable
|
||||||
exe: str = os.path.abspath(options.executable)
|
exe: str = os.path.abspath(options.executable)
|
||||||
if not exe.startswith(os.path.abspath(folder)):
|
if not exe.startswith(os.path.abspath(folder)):
|
||||||
print("the executable must locate in source folder")
|
print("The executable must locate in source folder")
|
||||||
exit(-1)
|
exit(-1)
|
||||||
exe = '.' + exe[len(os.path.abspath(folder)):]
|
exe = '.' + exe[len(os.path.abspath(folder)):]
|
||||||
print("executable path: " + exe)
|
print("Executable path: " + exe)
|
||||||
print("compression level: " + str(options.level))
|
print("Compression level: " + str(options.level))
|
||||||
md5_table = generate_md5_table(folder, options.level)
|
md5_table = generate_md5_table(folder, options.level)
|
||||||
write_metadata(md5_table, output_folder, exe)
|
write_package_metadata(md5_table, output_folder, exe)
|
||||||
|
write_app_metadata(output_folder)
|
||||||
build_portable(output_folder, options.target)
|
build_portable(output_folder, options.target)
|
||||||
|
|||||||
@@ -9,9 +9,52 @@ use bin_reader::BinaryReader;
|
|||||||
|
|
||||||
pub mod bin_reader;
|
pub mod bin_reader;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml");
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
const APP_METADATA: &[u8] = &[];
|
||||||
|
const APP_METADATA_CONFIG: &str = "meta.toml";
|
||||||
|
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";
|
||||||
|
|
||||||
|
fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool {
|
||||||
|
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
for line in app_metadata.lines() {
|
||||||
|
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
|
||||||
|
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
|
||||||
|
*ts = stored_ts;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if *ts == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(content) = std::fs::read_to_string(dir.join(APP_METADATA_CONFIG)) {
|
||||||
|
for line in content.lines() {
|
||||||
|
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
|
||||||
|
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
|
||||||
|
return *ts == stored_ts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_meta(dir: &PathBuf, ts: u64) {
|
||||||
|
let meta_file = dir.join(APP_METADATA_CONFIG);
|
||||||
|
if ts != 0 {
|
||||||
|
let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts);
|
||||||
|
// Ignore is ok here
|
||||||
|
let _ = std::fs::write(meta_file, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<PathBuf> {
|
fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<PathBuf> {
|
||||||
let dir = if let Some(dir) = dir {
|
let dir = if let Some(dir) = dir {
|
||||||
dir
|
dir
|
||||||
@@ -24,12 +67,15 @@ fn setup(reader: BinaryReader, dir: Option<PathBuf>, clear: bool) -> Option<Path
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if clear {
|
|
||||||
|
let mut ts = 0;
|
||||||
|
if clear || !is_timestamp_matches(&dir, &mut ts) {
|
||||||
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() {
|
||||||
file.write_to_file(&dir);
|
file.write_to_file(&dir);
|
||||||
}
|
}
|
||||||
|
write_meta(&dir, ts);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
windows::copy_runtime_broker(&dir);
|
windows::copy_runtime_broker(&dir);
|
||||||
#[cfg(linux)]
|
#[cfg(linux)]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user