diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 000000000..32a440b28
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,50 @@
+FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04
+ENV HOME=/home/vscode
+ENV WORKDIR=$HOME/rustdesk
+
+WORKDIR $HOME
+RUN sudo apt update -y && sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+WORKDIR /
+
+RUN git clone https://github.com/microsoft/vcpkg
+WORKDIR vcpkg
+RUN git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82
+RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics
+ENV VCPKG_ROOT=/vcpkg
+RUN $VCPKG_ROOT/vcpkg --disable-metrics install libvpx libyuv opus
+
+WORKDIR /
+RUN wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz && tar xzf dep.tar.gz
+
+
+USER vscode
+WORKDIR $HOME
+RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
+RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh
+RUN chmod +x rustup.sh
+RUN $HOME/rustup.sh -y
+RUN $HOME/.cargo/bin/rustup target add aarch64-linux-android
+RUN $HOME/.cargo/bin/cargo install cargo-ndk
+
+# Install Flutter
+RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.3-stable.tar.xz
+RUN tar xf flutter_linux_3.7.3-stable.tar.xz && rm flutter_linux_3.7.3-stable.tar.xz
+ENV PATH="$PATH:$HOME/flutter/bin"
+RUN dart pub global activate ffigen 5.0.1
+
+
+# Install packages
+RUN sudo apt-get install -y libclang-dev
+RUN sudo apt install -y gcc-multilib
+
+WORKDIR $WORKDIR
+ENV ANDROID_NDK_HOME=/opt/android/ndk/22.1.7171670
+
+# Somehow try to automate flutter pub get
+# https://rustdesk.com/docs/en/dev/build/android/
+# Put below steps in entrypoint.sh
+# cd flutter
+# wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz
+# tar xzf so.tar.gz
+
+# own /opt/android
diff --git a/.devcontainer/build.sh b/.devcontainer/build.sh
new file mode 100755
index 000000000..df87aace7
--- /dev/null
+++ b/.devcontainer/build.sh
@@ -0,0 +1,75 @@
+#!/bin/bash
+
+set -e
+
+MODE=${1:---debug}
+TYPE=${2:-linux}
+MODE=${MODE/*-/}
+
+
+build(){
+ pwd
+ $WORKDIR/entrypoint $1
+}
+
+build_arm64(){
+ CWD=$(pwd)
+ cd $WORKDIR/flutter
+ flutter pub get
+ cd $WORKDIR
+ $WORKDIR/flutter/ndk_arm64.sh
+ cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
+ cd $CWD
+}
+
+build_apk(){
+ cd $WORKDIR/flutter
+ MODE=$1 $WORKDIR/flutter/build_android.sh
+ cd $WORKDIR
+}
+
+key_gen(){
+ if [ ! -f $WORKDIR/flutter/android/key.properties ]
+ then
+ if [ ! -f $HOME/upload-keystore.jks ]
+ then
+ $WORKDIR/.devcontainer/setup.sh key
+ fi
+ read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password
+ echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties
+ else
+ echo "Believing storeFile is created ref: $WORKDIR/flutter/android/key.properties"
+ fi
+}
+
+android_build(){
+ if [ ! -d $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a ]
+ then
+ $WORKDIR/.devcontainer/setup.sh android
+ fi
+ build_arm64
+ case $1 in
+ debug)
+ build_apk debug
+ ;;
+ release)
+ key_gen
+ build_apk release
+ ;;
+ esac
+}
+
+case "$MODE:$TYPE" in
+ "debug:linux")
+ build
+ ;;
+ "release:linux")
+ build --release
+ ;;
+ "debug:android")
+ android_build debug
+ ;;
+ "release:android")
+ android_build release
+ ;;
+esac
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..cd82c75e3
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,34 @@
+{
+ "name": "rustdesk",
+ "build": {
+ "dockerfile": "./Dockerfile",
+ "context": "."
+ },
+ "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache",
+ "workspaceFolder": "/home/vscode/rustdesk",
+ "postStartCommand": ".devcontainer/build.sh",
+ "features": {
+ "ghcr.io/devcontainers/features/java:1": {},
+ "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": {
+ "PACKAGES": "platform-tools,ndk;22.1.7171670"
+ }
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "vadimcn.vscode-lldb",
+ "mutantdino.resourcemonitor",
+ "rust-lang.rust-analyzer",
+ "tamasfe.even-better-toml",
+ "serayuzgur.crates",
+ "mhutchie.git-graph",
+ "eamodio.gitlens"
+ ],
+ "settings": {
+ "files.watcherExclude": {
+ "**/target/**": true
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh
new file mode 100755
index 000000000..c972f47b2
--- /dev/null
+++ b/.devcontainer/setup.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+set -e
+case $1 in
+ android)
+ # install deps
+ cd $WORKDIR/flutter
+ flutter pub get
+ wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz
+ tar xzf so.tar.gz
+ rm so.tar.gz
+ sudo chown -R $(whoami) $ANDROID_HOME
+ echo "Setup is Done."
+ ;;
+ linux)
+ echo "Linux Setup"
+ ;;
+ key)
+ echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!"
+ keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
+ ;;
+esac
+
+
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
deleted file mode 100644
index 5ba29c8b6..000000000
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ /dev/null
@@ -1,32 +0,0 @@
----
-name: Bug Report
-about: Report a bug (English only, Please).
-title: ""
-labels: bug
-assignees: ''
-
----
-
-
-
-**Describe the bug you encountered:**
-
-...
-
-**What did you expect to happen instead?**
-
-...
-
-
-**How did you install `RustDesk`?**
-
-
-
----
-
-**RustDesk version and environment**
-
-
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
new file mode 100644
index 000000000..fea1a3672
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -0,0 +1,54 @@
+name: 🐞 Bug report
+description: Thanks for taking the time to fill out this bug report! Please fill the form in **English**
+body:
+ - type: textarea
+ id: desc
+ attributes:
+ label: Bug Description
+ description: A clear and concise description of what the bug is
+ validations:
+ required: true
+ - type: textarea
+ id: reproduce
+ attributes:
+ label: How to Reproduce
+ description: What steps can we take to reproduce this behavior?
+ validations:
+ required: true
+ - type: textarea
+ id: expected
+ attributes:
+ label: Expected Behavior
+ description: A clear and concise description of what you expected to happen
+ validations:
+ required: true
+ - type: input
+ id: os
+ attributes:
+ label: Operating system(s) on local side and remote side
+ description: What operating system(s) do you see this bug on? local side -> remote side.
+ placeholder: |
+ Windows 10 -> osx
+ validations:
+ required: true
+ - type: input
+ id: version
+ attributes:
+ label: RustDesk Version(s) on local side and remote side
+ description: What RustDesk version(s) do you see this bug on? local side -> remote side.
+ placeholder: |
+ 1.1.9 -> 1.1.8
+ validations:
+ required: true
+ - type: textarea
+ id: screenshots
+ attributes:
+ label: Screenshots
+ description: Please add screenshots to help explain your problem, if applicable, please upload video.
+ validations:
+ required: true
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional Context
+ description: Add any additonal context about the problem here
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 01de3b330..2da6bbaf1 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -1,4 +1,4 @@
-blank_issues_enabled: true
+blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://github.com/rustdesk/rustdesk/discussions/category_choices
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
deleted file mode 100644
index 0d21f017d..000000000
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Feature Request
-about: Suggest an idea for this project ((English only, Please).
-title: ''
-labels: enhancement
-assignees: ''
-
----
-
-
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml
new file mode 100644
index 000000000..29b0d0e0f
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yaml
@@ -0,0 +1,24 @@
+name: 🛠️ Feature request
+description: Suggest an idea for RustDesk
+body:
+ - type: textarea
+ id: desc
+ attributes:
+ label: Description
+ description: Describe your suggested feature and the main use cases
+ validations:
+ required: true
+
+ - type: textarea
+ id: users
+ attributes:
+ label: Impact
+ description: What types of users can benefit from using the suggested feature?
+ validations:
+ required: true
+
+ - type: textarea
+ id: context
+ attributes:
+ label: Additional Context
+ description: Add any additonal context about the feature here
diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml
new file mode 100644
index 000000000..a1ff080c5
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/task.yaml
@@ -0,0 +1,20 @@
+name: 📝 Task
+description: Create a task for the team to work on
+title: "[Task]: "
+labels: [Task]
+body:
+- type: checkboxes
+ attributes:
+ label: Is there an existing issue for this?
+ description: Please search to see if an issue related to this already exists.
+ options:
+ - label: I have searched the existing issues
+ required: true
+- type: textarea
+ attributes:
+ label: SubTasks
+ placeholder: |
+ - Sub Task 1
+ - Sub Task 2
+ validations:
+ required: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2e1702a60..bba114315 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,6 +7,9 @@ name: CI
on:
workflow_dispatch:
pull_request:
+ paths-ignore:
+ - "docs/**"
+ - "README.md"
push:
branches:
- master
@@ -14,6 +17,8 @@ on:
- '*'
paths-ignore:
- ".github/**"
+ - "docs/**"
+ - "README.md"
jobs:
# ensure_cargo_fmt:
diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml
index 7825286bd..cae5b82c7 100644
--- a/.github/workflows/flutter-ci.yml
+++ b/.github/workflows/flutter-ci.yml
@@ -1,8 +1,11 @@
-name: Flutter CI
+name: Full Flutter CI
on:
workflow_dispatch:
pull_request:
+ paths-ignore:
+ - "docs/**"
+ - "README.md"
push:
branches:
- master
@@ -10,43 +13,110 @@ on:
- '*'
paths-ignore:
- ".github/**"
+ - "docs/**"
+ - "README.md"
+
+env:
+ LLVM_VERSION: "15.0.6"
+ FLUTTER_VERSION: "3.7.5"
+ # vcpkg version: 2022.05.10
+ # for multiarch gcc compatibility
+ VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44"
+ VERSION: "1.2.0"
jobs:
- build:
+ build-for-windows:
name: ${{ matrix.job.target }} (${{ matrix.job.os }})
runs-on: ${{ matrix.job.os }}
strategy:
- fail-fast: false
+ fail-fast: true
matrix:
job:
- # - { 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-musleabihf, os: ubuntu-20.04, use-cross: true }
# - { target: i686-pc-windows-msvc , os: windows-2019 }
- # - { 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: x86_64-apple-darwin , os: macos-10.15 }
# - { target: x86_64-pc-windows-gnu , os: windows-2019 }
- # - { target: x86_64-pc-windows-msvc , os: windows-2019 }
- - { 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-pc-windows-msvc, os: windows-2019 }
steps:
- name: Checkout source code
uses: actions/checkout@v3
- - name: Install prerequisites
- shell: bash
- run: |
- case ${{ matrix.job.target }} in
- x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;;
- # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;;
- # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;;
- esac
+ - name: Install LLVM and Clang
+ uses: KyleMayes/install-llvm-action@v1
+ with:
+ version: ${{ env.LLVM_VERSION }}
- name: Install flutter
uses: subosito/flutter-action@v2
with:
- channel: 'stable'
+ channel: "stable"
+ flutter-version: ${{ env.FLUTTER_VERSION }}
+ cache: true
+
+ - name: Replace engine with rustdesk custom flutter engine
+ run: |
+ flutter doctor -v
+ flutter precache --windows
+ Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip
+ Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
+ mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: "1.62"
+ target: ${{ matrix.job.target }}
+ override: true
+ components: rustfmt
+ profile: minimal # minimal component installation (ie, no documentation)
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ prefix-key: ${{ matrix.job.os }}
+
+ - name: Install flutter rust bridge deps
+ run: |
+ cargo install flutter_rust_bridge_codegen
+ Push-Location flutter ; flutter pub get ; Pop-Location
+ ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
+
+ - name: Restore from cache and install vcpkg
+ uses: lukka/run-vcpkg@v7
+ with:
+ setupOnly: true
+ vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
+
+ - name: Install vcpkg dependencies
+ run: |
+ $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
+ shell: bash
+
+ - name: Build rustdesk
+ run: python3 .\build.py --portable --hwcodec --flutter
+
+ build-for-macOS:
+ name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}]
+ runs-on: ${{ matrix.job.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ job:
+ - {
+ target: x86_64-apple-darwin,
+ os: macos-latest,
+ extra-build-args: "",
+ }
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v3
+
+ - name: Install build runtime
+ run: |
+ brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config
+
+ - name: Install flutter
+ uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+ flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
@@ -56,14 +126,14 @@ jobs:
override: true
profile: minimal # minimal component installation (ie, no documentation)
- - uses: Swatinem/rust-cache@v1
+ - uses: Swatinem/rust-cache@v2
+ with:
+ prefix-key: ${{ matrix.job.os }}
- name: Install flutter rust bridge deps
+ shell: bash
run: |
- dart pub global activate ffigen --version 5.0.1
- # flutter_rust_bridge
- pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd
- pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd
+ cargo install flutter_rust_bridge_codegen
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
@@ -71,29 +141,835 @@ jobs:
uses: lukka/run-vcpkg@v7
with:
setupOnly: true
- vcpkgGitCommitId: '1d4128f08e30cec31b94500840c7eca8ebc579cb'
+ vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
run: |
$VCPKG_ROOT/vcpkg install libvpx libyuv opus
- shell: bash
- - name: Show version information (Rust, cargo, GCC)
+ - name: Show version information (Rust, cargo, Clang)
shell: bash
run: |
- gcc --version || true
+ clang --version || true
rustup -V
rustup toolchain list
rustup default
cargo -V
rustc -V
- - name: Build rustdesk ffi lib
- run: cargo build --features flutter --lib
-
- - name: Build Flutter
+ - name: Build rustdesk
run: |
+ # --hwcodec not supported on macos yet
+ ./build.py --flutter ${{ matrix.job.extra-build-args }}
+
+ build-vcpkg-deps-linux:
+ runs-on: ${{ matrix.job.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ job:
+ # - { arch: armv7, os: ubuntu-20.04 }
+ - { arch: x86_64, os: ubuntu-20.04 }
+ - { arch: aarch64, os: ubuntu-20.04 }
+ steps:
+ - name: Create vcpkg artifacts folder
+ run: mkdir -p /opt/artifacts
+
+ - name: Cache Vcpkg
+ id: cache-vcpkg
+ uses: actions/cache@v3
+ with:
+ path: /opt/artifacts
+ key: vcpkg-${{ matrix.job.arch }}
+
+ - uses: Kingtous/run-on-arch-action@amd64-support
+ name: Run vcpkg install on ${{ matrix.job.arch }}
+ id: vcpkg
+ with:
+ arch: ${{ matrix.job.arch }}
+ distro: ubuntu18.04
+ githubToken: ${{ github.token }}
+ setup: |
+ ls -l "/opt/artifacts"
+ dockerRunArgs: |
+ --volume "/opt/artifacts:/artifacts"
+ shell: /bin/bash
+ install: |
+ apt update -y
+ case "${{ matrix.job.arch }}" in
+ x86_64)
+ # CMake 3.15+
+ apt install -y gpg wget ca-certificates
+ echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null
+ wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
+ apt update -y
+ apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev
+ ;;
+ aarch64|armv7)
+ apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool
+ esac
+ cmake --version
+ gcc -v
+ run: |
+ # disable git safe.directory
+ git config --global --add safe.directory "*"
+ case "${{ matrix.job.arch }}" in
+ x86_64)
+ export VCPKG_FORCE_SYSTEM_BINARIES=1
+ pushd /artifacts
+ git clone https://github.com/microsoft/vcpkg.git || true
+ pushd vcpkg
+ git reset --hard ${{ env.VCPKG_COMMIT_ID }}
+ ./bootstrap-vcpkg.sh
+ ./vcpkg install libvpx libyuv opus
+ ;;
+ aarch64|armv7)
+ pushd /artifacts
+ # libyuv
+ git clone https://chromium.googlesource.com/libyuv/libyuv || true
+ pushd libyuv
+ git pull
+ mkdir -p build
+ pushd build
+ mkdir -p /artifacts/vcpkg/installed
+ cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed
+ make -j4 && make install
+ popd
+ popd
+ # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC
+ wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz
+ tar -zxvf opus.tar.gz; ls -l
+ pushd opus-1.1.2
+ ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed
+ make -j4; make install
+ ;;
+ esac
+ - name: Upload artifacts
+ uses: actions/upload-artifact@master
+ with:
+ name: vcpkg-artifact-${{ matrix.job.arch }}
+ path: |
+ /opt/artifacts/vcpkg/installed
+
+ generate-bridge-linux:
+ name: generate bridge
+ runs-on: ${{ matrix.job.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ job:
+ - {
+ target: x86_64-unknown-linux-gnu,
+ os: ubuntu-20.04,
+ extra-build-args: "",
+ }
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v3
+
+ - name: Install prerequisites
+ run: |
+ sudo apt update -y
+ sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ target: ${{ matrix.job.target }}
+ override: true
+ profile: minimal # minimal component installation (ie, no documentation)
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ prefix-key: bridge-${{ matrix.job.os }}
+ workspace: "/tmp/flutter_rust_bridge/frb_codegen"
+
+ - name: Cache Bridge
+ id: cache-bridge
+ uses: actions/cache@v3
+ with:
+ path: /tmp/flutter_rust_bridge
+ key: vcpkg-${{ matrix.job.arch }}
+
+ - name: Install flutter
+ uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+ flutter-version: ${{ env.FLUTTER_VERSION }}
+ cache: true
+
+ - name: Install flutter rust bridge deps
+ shell: bash
+ run: |
+ cargo install flutter_rust_bridge_codegen
+ pushd flutter && flutter pub get && popd
+
+ - name: Run flutter rust bridge
+ run: |
+ ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
+
+ - name: Upload Artifact
+ uses: actions/upload-artifact@master
+ with:
+ name: bridge-artifact
+ path: |
+ ./src/bridge_generated.rs
+ ./src/bridge_generated.io.rs
+ ./flutter/lib/generated_bridge.dart
+ ./flutter/lib/generated_bridge.freezed.dart
+
+ build-rustdesk-android-arm64:
+ needs: [generate-bridge-linux]
+ name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
+ runs-on: ${{ matrix.job.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ job:
+ - {
+ arch: x86_64,
+ target: aarch64-linux-android,
+ os: ubuntu-20.04,
+ extra-build-features: "",
+ }
+ # - {
+ # arch: x86_64,
+ # target: armv7-linux-androideabi,
+ # os: ubuntu-20.04,
+ # extra-build-features: "",
+ # }
+ steps:
+ - name: Install dependencies
+ run: |
+ sudo apt update
+ sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless
+ - name: Checkout source code
+ uses: actions/checkout@v3
+ - name: Install flutter
+ uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+ flutter-version: ${{ env.FLUTTER_VERSION }}
+ - uses: nttld/setup-ndk@v1
+ id: setup-ndk
+ with:
+ ndk-version: r22b
+ add-to-path: true
+
+ - name: Download deps
+ shell: bash
+ run: |
+ pushd /opt
+ wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz
+ tar xzf dep.tar.gz
+
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ profile: minimal # minimal component installation (ie, no documentation)
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ prefix-key: rustdesk-lib-cache
+ key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
+
+ - name: Disable rust bridge build
+ run: |
+ sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs
+
+ - name: Build rustdesk lib
+ env:
+ ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
+ ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }}
+ VCPKG_ROOT: /opt/vcpkg
+ run: |
+ rustup target add ${{ matrix.job.target }}
+ cargo install cargo-ndk
+ case ${{ matrix.job.target }} in
+ aarch64-linux-android)
+ ./flutter/ndk_arm64.sh
+ mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
+ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
+ ;;
+ armv7-linux-androideabi)
+ ./flutter/ndk_arm.sh
+ mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
+ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
+ ;;
+ esac
+
+ - name: Build rustdesk
+ shell: bash
+ env:
+ JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64
+ run: |
+ export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH
+ # download so
pushd flutter
- flutter pub get
- flutter build linux --debug -v
+ wget -O so.tar.gz https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz
+ tar xzvf so.tar.gz
popd
+ # temporary use debug sign config
+ sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle
+ case ${{ matrix.job.target }} in
+ aarch64-linux-android)
+ mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a
+ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so
+ # build flutter
+ pushd flutter
+ flutter build apk --release --target-platform android-arm64 --split-per-abi
+ mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk
+ ;;
+ armv7-linux-androideabi)
+ mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a
+ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so
+ # build flutter
+ pushd flutter
+ flutter build apk --release --target-platform android-arm --split-per-abi
+ mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk
+ ;;
+ esac
+ popd
+ mkdir -p signed-apk; pushd signed-apk
+ mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk .
+
+ build-rustdesk-lib-linux-amd64:
+ needs: [generate-bridge-linux, build-vcpkg-deps-linux]
+ name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
+ runs-on: ${{ matrix.job.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ # use a high level qemu-user-static
+ job:
+ # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
+ # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
+ - {
+ arch: x86_64,
+ target: x86_64-unknown-linux-gnu,
+ os: ubuntu-20.04,
+ extra-build-features: "",
+ }
+ - {
+ arch: x86_64,
+ target: x86_64-unknown-linux-gnu,
+ os: ubuntu-20.04,
+ extra-build-features: "flatpak",
+ }
+ - {
+ arch: x86_64,
+ target: x86_64-unknown-linux-gnu,
+ os: ubuntu-20.04,
+ extra-build-features: "appimage",
+ }
+ # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
+ steps:
+ - name: Maximize build space
+ run: |
+ sudo rm -rf /opt/ghc
+ sudo rm -rf /usr/local/lib/android
+ sudo rm -rf /usr/share/dotnet
+ sudo apt update -y
+ sudo apt install qemu-user-static
+
+ - name: Checkout source code
+ uses: actions/checkout@v3
+
+ - name: Set Swap Space
+ uses: pierotofy/set-swap-space@master
+ with:
+ swap-size-gb: 12
+
+ - name: Free Space
+ run: |
+ df
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ target: ${{ matrix.job.target }}
+ override: true
+ profile: minimal # minimal component installation (ie, no documentation)
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ prefix-key: rustdesk-lib-cache
+ key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
+ cache-directories: "/opt/rust-registry"
+
+ - name: Install local registry
+ run: |
+ mkdir -p /opt/rust-registry
+ cargo install cargo-local-registry
+
+ - name: Build local registry
+ uses: nick-fields/retry@v2
+ id: build-local-registry
+ continue-on-error: true
+ with:
+ max_attempts: 3
+ timeout_minutes: 15
+ retry_on: error
+ command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry
+
+ - name: Disable rust bridge build
+ run: |
+ sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs
+ # only build cdylib
+ sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
+
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
+
+ - name: Restore vcpkg files
+ uses: actions/download-artifact@master
+ with:
+ name: vcpkg-artifact-${{ matrix.job.arch }}
+ path: /opt/artifacts/vcpkg/installed
+
+ - uses: Kingtous/run-on-arch-action@amd64-support
+ name: Build rustdesk library for ${{ matrix.job.arch }}
+ id: vcpkg
+ with:
+ arch: ${{ matrix.job.arch }}
+ distro: ubuntu18.04
+ # not ready yet
+ # distro: ubuntu18.04-rustdesk
+ githubToken: ${{ github.token }}
+ setup: |
+ ls -l "${PWD}"
+ ls -l /opt/artifacts/vcpkg/installed
+ dockerRunArgs: |
+ --volume "${PWD}:/workspace"
+ --volume "/opt/artifacts:/opt/artifacts"
+ --volume "/opt/rust-registry:/opt/rust-registry"
+ shell: /bin/bash
+ install: |
+ apt update -y
+ echo -e "installing deps"
+ apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null
+ # we have libopus compiled by us.
+ apt remove -y libopus-dev || true
+ # output devs
+ ls -l ./
+ tree -L 3 /opt/artifacts/vcpkg/installed
+ run: |
+ # disable git safe.directory
+ git config --global --add safe.directory "*"
+ # rust
+ pushd /opt
+ wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz
+ tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
+ cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh
+ rm -rf rust-1.64.0-${{ matrix.job.target }}
+ # edit config
+ mkdir -p ~/.cargo/
+ echo """
+ [source.crates-io]
+ registry = 'https://github.com/rust-lang/crates.io-index'
+ replace-with = 'local-registry'
+
+ [source.local-registry]
+ local-registry = '/opt/rust-registry/'
+ """ > ~/.cargo/config
+ cat ~/.cargo/config
+ # start build
+ pushd /workspace
+ # mock
+ case "${{ matrix.job.arch }}" in
+ x86_64)
+ # no need mock on x86_64
+ export VCPKG_ROOT=/opt/artifacts/vcpkg
+ cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
+ ;;
+ esac
+
+ - name: Upload Artifacts
+ uses: actions/upload-artifact@master
+ with:
+ name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so
+ path: target/release/liblibrustdesk.so
+
+ build-rustdesk-lib-linux-arm:
+ needs: [generate-bridge-linux, build-vcpkg-deps-linux]
+ name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
+ runs-on: ${{ matrix.job.os }}
+ strategy:
+ fail-fast: true
+ matrix:
+ # use a high level qemu-user-static
+ job:
+ - {
+ arch: aarch64,
+ target: aarch64-unknown-linux-gnu,
+ os: ubuntu-20.04,
+ use-cross: true,
+ extra-build-features: "",
+ }
+ - {
+ arch: aarch64,
+ target: aarch64-unknown-linux-gnu,
+ os: ubuntu-18.04, # just for naming package, not running host
+ use-cross: true,
+ extra-build-features: "appimage",
+ }
+ # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" }
+ # - {
+ # arch: armv7,
+ # target: arm-unknown-linux-gnueabihf,
+ # os: ubuntu-20.04,
+ # use-cross: true,
+ # extra-build-features: "",
+ # }
+ # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" }
+ # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
+ steps:
+ - name: Maximize build space
+ run: |
+ sudo rm -rf /opt/ghc
+ sudo rm -rf /usr/local/lib/android
+ sudo rm -rf /usr/share/dotnet
+ sudo apt update -y
+ sudo apt install qemu-user-static
+
+ - name: Checkout source code
+ uses: actions/checkout@v3
+
+ - name: Set Swap Space
+ uses: pierotofy/set-swap-space@master
+ with:
+ swap-size-gb: 12
+
+ - name: Free Space
+ run: |
+ df
+
+ - name: Install Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ target: ${{ matrix.job.target }}
+ override: true
+ profile: minimal # minimal component installation (ie, no documentation)
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ prefix-key: rustdesk-lib-cache
+ key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
+ cache-directories: "/opt/rust-registry"
+
+ - name: Install local registry
+ run: |
+ mkdir -p /opt/rust-registry
+ cargo install cargo-local-registry
+
+ - name: Build local registry
+ uses: nick-fields/retry@v2
+ id: build-local-registry
+ continue-on-error: true
+ with:
+ max_attempts: 3
+ timeout_minutes: 15
+ retry_on: error
+ command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry
+
+ - name: Disable rust bridge build
+ run: |
+ sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs
+ # only build cdylib
+ sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
+
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
+
+ - name: Restore vcpkg files
+ uses: actions/download-artifact@master
+ with:
+ name: vcpkg-artifact-${{ matrix.job.arch }}
+ path: /opt/artifacts/vcpkg/installed
+
+ - uses: Kingtous/run-on-arch-action@amd64-support
+ name: Build rustdesk library for ${{ matrix.job.arch }}
+ id: vcpkg
+ with:
+ arch: ${{ matrix.job.arch }}
+ distro: ubuntu18.04-rustdesk
+ githubToken: ${{ github.token }}
+ setup: |
+ ls -l "${PWD}"
+ ls -l /opt/artifacts/vcpkg/installed
+ dockerRunArgs: |
+ --volume "${PWD}:/workspace"
+ --volume "/opt/artifacts:/opt/artifacts"
+ --volume "/opt/rust-registry:/opt/rust-registry"
+ shell: /bin/bash
+ install: |
+ apt update -y
+ echo -e "installing deps"
+ apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null
+ # we have libopus compiled by us.
+ apt remove -y libopus-dev || true
+ # output devs
+ ls -l ./
+ tree -L 3 /opt/artifacts/vcpkg/installed
+ run: |
+ # disable git safe.directory
+ git config --global --add safe.directory "*"
+ # rust
+ pushd /opt
+ wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz
+ tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
+ cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh
+ rm -rf rust-1.64.0-${{ matrix.job.target }}
+ # edit config
+ mkdir -p ~/.cargo/
+ echo """
+ [source.crates-io]
+ registry = 'https://github.com/rust-lang/crates.io-index'
+ replace-with = 'local-registry'
+
+ [source.local-registry]
+ local-registry = '/opt/rust-registry/'
+ """ > ~/.cargo/config
+ cat ~/.cargo/config
+ # start build
+ pushd /workspace
+ # mock
+ case "${{ matrix.job.arch }}" in
+ aarch64)
+ cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/
+ cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/
+ ls -l /opt/artifacts/vcpkg/installed/lib/
+ mkdir -p /vcpkg/installed/arm64-linux
+ ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib
+ ln -s /usr/include /vcpkg/installed/arm64-linux/include
+ export VCPKG_ROOT=/vcpkg
+ # disable hwcodec for compilation
+ cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
+ ;;
+ armv7)
+ cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/
+ cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/
+ mkdir -p /vcpkg/installed/arm-linux
+ ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib
+ ln -s /usr/include /vcpkg/installed/arm-linux/include
+ export VCPKG_ROOT=/vcpkg
+ # disable hwcodec for compilation
+ cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release
+ ;;
+ esac
+
+ - name: Upload Artifacts
+ uses: actions/upload-artifact@master
+ with:
+ name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so
+ path: target/release/liblibrustdesk.so
+
+ build-rustdesk-linux-arm:
+ needs: [build-rustdesk-lib-linux-arm]
+ name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
+ runs-on: ubuntu-20.04 # 20.04 has more performance on arm build
+ strategy:
+ fail-fast: true
+ matrix:
+ job:
+ - {
+ arch: aarch64,
+ target: aarch64-unknown-linux-gnu,
+ os: ubuntu-18.04, # just for naming package, not running host
+ use-cross: true,
+ extra-build-features: "",
+ }
+ - {
+ arch: aarch64,
+ target: aarch64-unknown-linux-gnu,
+ os: ubuntu-18.04, # just for naming package, not running host
+ use-cross: true,
+ extra-build-features: "appimage",
+ }
+ # - {
+ # arch: aarch64,
+ # target: aarch64-unknown-linux-gnu,
+ # os: ubuntu-18.04, # just for naming package, not running host
+ # use-cross: true,
+ # extra-build-features: "flatpak",
+ # }
+ # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" }
+ # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" }
+ # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v3
+
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
+
+ - name: Prepare env
+ run: |
+ sudo apt update -y
+ sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools
+ mkdir -p ./target/release/
+
+ - name: Restore the rustdesk lib file
+ uses: actions/download-artifact@master
+ with:
+ name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so
+ path: ./target/release/
+
+ - name: Download Flutter
+ shell: bash
+ run: |
+ # disable git safe.directory
+ git config --global --add safe.directory "*"
+ pushd /opt
+ # clone repo and reset to flutter 3.7.0
+ git clone https://github.com/sony/flutter-elinux.git || true
+ pushd flutter-elinux
+ # reset to flutter 3.7.0
+ git fetch
+ git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5
+ popd
+
+ - uses: Kingtous/run-on-arch-action@amd64-support
+ name: Build rustdesk binary for ${{ matrix.job.arch }}
+ id: vcpkg
+ with:
+ arch: ${{ matrix.job.arch }}
+ distro: ubuntu18.04-rustdesk
+ githubToken: ${{ github.token }}
+ setup: |
+ ls -l "${PWD}"
+ dockerRunArgs: |
+ --volume "${PWD}:/workspace"
+ --volume "/opt/artifacts:/opt/artifacts"
+ --volume "/opt/flutter-elinux:/opt/flutter-elinux"
+ shell: /bin/bash
+ install: |
+ apt update -y
+ apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm
+ run: |
+ # disable git safe.directory
+ git config --global --add safe.directory "*"
+ pushd /workspace
+ # we use flutter-elinux to build our rustdesk
+ export PATH=/opt/flutter-elinux/bin:$PATH
+ sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py
+ # Setup flutter-elinux. Run doctor to check if issues here.
+ flutter-elinux doctor -v
+ # Patch arm64 engine for flutter 3.6.0+
+ flutter-elinux precache --linux
+ pushd /tmp
+ curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz
+ tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib
+ cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64
+ popd
+ case ${{ matrix.job.arch }} in
+ aarch64)
+ sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py
+ sed -i "s/x64\/release/arm64\/release/g" ./build.py
+ ;;
+ armv7)
+ sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py
+ sed -i "s/x64\/release/arm\/release/g" ./build.py
+ ;;
+ esac
+ python3 ./build.py --flutter --hwcodec --skip-cargo
+
+ build-rustdesk-linux-amd64:
+ needs: [build-rustdesk-lib-linux-amd64]
+ name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
+ runs-on: ubuntu-20.04
+ strategy:
+ fail-fast: true
+ matrix:
+ job:
+ # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true }
+ # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
+ - {
+ arch: x86_64,
+ target: x86_64-unknown-linux-gnu,
+ os: ubuntu-20.04,
+ extra-build-features: "",
+ }
+ - {
+ arch: x86_64,
+ target: x86_64-unknown-linux-gnu,
+ os: ubuntu-20.04,
+ extra-build-features: "flatpak",
+ }
+ - {
+ arch: x86_64,
+ target: x86_64-unknown-linux-gnu,
+ os: ubuntu-20.04,
+ extra-build-features: "appimage",
+ }
+ # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true }
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v3
+
+ - name: Restore bridge files
+ uses: actions/download-artifact@master
+ with:
+ name: bridge-artifact
+ path: ./
+
+ - name: Prepare env
+ run: |
+ sudo apt update -y
+ sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools
+ mkdir -p ./target/release/
+
+ - name: Restore the rustdesk lib file
+ uses: actions/download-artifact@master
+ with:
+ name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so
+ path: ./target/release/
+
+ - uses: Kingtous/run-on-arch-action@amd64-support
+ name: Build rustdesk binary for ${{ matrix.job.arch }}
+ id: vcpkg
+ with:
+ arch: ${{ matrix.job.arch }}
+ distro: ubuntu18.04
+ githubToken: ${{ github.token }}
+ setup: |
+ ls -l "${PWD}"
+ dockerRunArgs: |
+ --volume "${PWD}:/workspace"
+ --volume "/opt/artifacts:/opt/artifacts"
+ shell: /bin/bash
+ install: |
+ apt update -y
+ apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm
+ run: |
+ # disable git safe.directory
+ git config --global --add safe.directory "*"
+ # Setup Flutter
+ pushd /opt
+ wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
+ tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
+ ls -l .
+ export PATH=/opt/flutter/bin:$PATH
+ flutter doctor -v
+ pushd /workspace
+ python3 ./build.py --flutter --hwcodec --skip-cargo
diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml
index eb13edf15..ffcadd18b 100644
--- a/.github/workflows/flutter-nightly.yml
+++ b/.github/workflows/flutter-nightly.yml
@@ -7,14 +7,19 @@ on:
workflow_dispatch:
env:
- LLVM_VERSION: "10.0"
- # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first.
- FLUTTER_VERSION: "3.0.5"
+ LLVM_VERSION: "15.0.6"
+ FLUTTER_VERSION: "3.7.0"
TAG_NAME: "nightly"
# vcpkg version: 2022.05.10
# for multiarch gcc compatibility
VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44"
VERSION: "1.2.0"
+ #signing keys env variable checks
+ ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
+ MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}'
+ # To make a custom build with your own servers set the below secret values
+ RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}'
+ RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}'
jobs:
build-for-windows:
@@ -47,17 +52,16 @@ jobs:
run: |
flutter doctor -v
flutter precache --windows
- Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
+ Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip
Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
- mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
+ mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
- toolchain: "1.62"
+ toolchain: stable
target: ${{ matrix.job.target }}
override: true
- components: rustfmt
profile: minimal # minimal component installation (ie, no documentation)
- uses: Swatinem/rust-cache@v2
@@ -66,12 +70,7 @@ jobs:
- name: Install flutter rust bridge deps
run: |
- dart pub global activate ffigen --version 5.0.1
- $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
- Push-Location ..
- git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
- Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
- Pop-Location
+ cargo install flutter_rust_bridge_codegen
Push-Location flutter ; flutter pub get ; Pop-Location
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
@@ -150,6 +149,7 @@ jobs:
uses: actions/checkout@v3
- name: Import the codesign cert
+ if: env.MACOS_P12_BASE64 != null
uses: apple-actions/import-codesign-certs@v1
with:
p12-file-base64: ${{ secrets.MACOS_P12_BASE64 }}
@@ -157,11 +157,13 @@ jobs:
keychain: rustdesk
- name: Check sign and import sign key
+ if: env.MACOS_P12_BASE64 != null
run: |
security default-keychain -s rustdesk.keychain
security find-identity -v
- name: Import notarize key
+ if: env.MACOS_P12_BASE64 != null
uses: timheuer/base64-to-file@v1.2
with:
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling
@@ -170,6 +172,7 @@ jobs:
encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }}
- name: Install rcodesign tool
+ if: env.MACOS_P12_BASE64 != null
shell: bash
run: |
pushd /tmp
@@ -180,7 +183,7 @@ jobs:
- name: Install build runtime
run: |
- brew install llvm create-dmg nasm yasm cmake gcc wget ninja
+ brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config
- name: Install flutter
uses: subosito/flutter-action@v2
@@ -203,14 +206,7 @@ jobs:
- name: Install flutter rust bridge deps
shell: bash
run: |
- dart pub global activate ffigen --version 5.0.1
- # flutter_rust_bridge
- pushd /tmp
- wget https://github.com/Kingtous/flutter_rust_bridge/releases/download/1.32.0-rustdesk/flutter_rust_bridge_codegen-x86_64-darwin.tgz
- tar -zxvf flutter_rust_bridge_codegen-x86_64-darwin.tgz
- mkdir -p ~/.cargo/bin
- mv flutter_rust_bridge_codegen ~/.cargo/bin; chmod +x ~/.cargo/bin/flutter_rust_bridge_codegen
- popd
+ cargo install flutter_rust_bridge_codegen
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
@@ -240,21 +236,22 @@ jobs:
./build.py --flutter ${{ matrix.job.extra-build-args }}
- name: Codesign app and create signed dmg
+ if: env.MACOS_P12_BASE64 != null
run: |
security default-keychain -s rustdesk.keychain
security unlock-keychain -p ${{ secrets.MACOS_P12_PASSWORD }} rustdesk.keychain
# start sign the rustdesk.app and dmg
rm rustdesk-${{ env.VERSION }}.dmg || true
- codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/rustdesk.app -v
- create-dmg --icon "rustdesk.app" 200 190 --hide-extension "rustdesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/rustdesk.app
- codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v
+ codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
+ create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
+ codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg
- name: Rename rustdesk
run: |
for name in rustdesk*??.dmg; do
- mv "$name" "${name%%.dmg}-untested-${{ matrix.job.target }}.dmg"
+ mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg"
done
- name: Publish DMG package
@@ -402,15 +399,10 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- - name: Install ffigen
- run: |
- dart pub global activate ffigen --version 5.0.1
-
- name: Install flutter rust bridge deps
shell: bash
run: |
- pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd
- pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd
+ cargo install flutter_rust_bridge_codegen
pushd flutter && flutter pub get && popd
- name: Run flutter rust bridge
@@ -423,6 +415,7 @@ jobs:
name: bridge-artifact
path: |
./src/bridge_generated.rs
+ ./src/bridge_generated.io.rs
./flutter/lib/generated_bridge.dart
./flutter/lib/generated_bridge.freezed.dart
@@ -551,6 +544,7 @@ jobs:
- uses: r0adkll/sign-android-release@v1
name: Sign app APK
+ if: env.ANDROID_SIGNING_KEY != null
id: sign-rustdesk
with:
releaseDirectory: ./signed-apk
@@ -563,12 +557,14 @@ jobs:
BUILD_TOOLS_VERSION: "30.0.2"
- name: Upload Artifacts
+ if: env.ANDROID_SIGNING_KEY != null
uses: actions/upload-artifact@master
with:
name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk
path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}}
- - name: Publish apk package
+ - name: Publish signed apk package
+ if: env.ANDROID_SIGNING_KEY != null
uses: softprops/action-gh-release@v1
with:
prerelease: true
@@ -576,6 +572,15 @@ jobs:
files: |
${{steps.sign-rustdesk.outputs.signedReleaseFile}}
+ - name: Publish unsigned apk package
+ if: env.ANDROID_SIGNING_KEY == null
+ uses: softprops/action-gh-release@v1
+ with:
+ prerelease: true
+ tag_name: ${{ env.TAG_NAME }}
+ files: |
+ signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk
+
build-rustdesk-lib-linux-amd64:
needs: [generate-bridge-linux, build-vcpkg-deps-linux]
name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
@@ -641,14 +646,6 @@ jobs:
key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
cache-directories: "/opt/rust-registry"
- - name: Install Rust toolchain
- uses: actions-rs/toolchain@v1
- with:
- toolchain: stable
- target: ${{ matrix.job.target }}
- override: true
- profile: minimal # minimal component installation (ie, no documentation)
-
- name: Install local registry
run: |
mkdir -p /opt/rust-registry
@@ -761,6 +758,13 @@ jobs:
use-cross: true,
extra-build-features: "",
}
+ - {
+ arch: aarch64,
+ target: aarch64-unknown-linux-gnu,
+ os: ubuntu-18.04, # just for naming package, not running host
+ use-cross: true,
+ extra-build-features: "appimage",
+ }
# - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" }
# - {
# arch: armv7,
@@ -800,14 +804,6 @@ jobs:
override: true
profile: minimal # minimal component installation (ie, no documentation)
- - name: Install Rust toolchain
- uses: actions-rs/toolchain@v1
- with:
- toolchain: stable
- target: ${{ matrix.job.target }}
- override: true
- profile: minimal # minimal component installation (ie, no documentation)
-
- uses: Swatinem/rust-cache@v2
with:
prefix-key: rustdesk-lib-cache
@@ -939,6 +935,13 @@ jobs:
use-cross: true,
extra-build-features: "",
}
+ - {
+ arch: aarch64,
+ target: aarch64-unknown-linux-gnu,
+ os: ubuntu-18.04, # just for naming package, not running host
+ use-cross: true,
+ extra-build-features: "appimage",
+ }
# - {
# arch: aarch64,
# target: aarch64-unknown-linux-gnu,
@@ -962,7 +965,7 @@ jobs:
- name: Prepare env
run: |
sudo apt update -y
- sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev
+ sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools
mkdir -p ./target/release/
- name: Restore the rustdesk lib file
@@ -977,12 +980,12 @@ jobs:
# disable git safe.directory
git config --global --add safe.directory "*"
pushd /opt
- # clone repo and reset to flutter 3.0.5
+ # clone repo and reset to flutter 3.7.0
git clone https://github.com/sony/flutter-elinux.git || true
pushd flutter-elinux
- # reset to flutter 3.0.5
+ # reset to flutter 3.7.0
git fetch
- git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8
+ git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5
popd
- uses: Kingtous/run-on-arch-action@amd64-support
@@ -1007,10 +1010,17 @@ jobs:
git config --global --add safe.directory "*"
pushd /workspace
# we use flutter-elinux to build our rustdesk
- sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py
- # Setup flutter-elinux
export PATH=/opt/flutter-elinux/bin:$PATH
+ sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py
+ # Setup flutter-elinux. Run doctor to check if issues here.
flutter-elinux doctor -v
+ # Patch arm64 engine for flutter 3.6.0+
+ flutter-elinux precache --linux
+ pushd /tmp
+ curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz
+ tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib
+ cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64
+ popd
# edit to corresponding arch
case ${{ matrix.job.arch }} in
aarch64)
@@ -1024,7 +1034,7 @@ jobs:
esac
python3 ./build.py --flutter --hwcodec --skip-cargo
# rpm package
- echo -e "start packaging"
+ echo -e "start packaging fedora package"
pushd /workspace
case ${{ matrix.job.arch }} in
armv7)
@@ -1041,12 +1051,30 @@ jobs:
for name in rustdesk*??.rpm; do
mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm"
done
+ # rpm suse package
+ echo -e "start packaging suse package"
+ pushd /workspace
+ case ${{ matrix.job.arch }} in
+ armv7)
+ sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec
+ sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter-suse.spec
+ ;;
+ aarch64)
+ sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec
+ ;;
+ esac
+ HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb
+ pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }}
+ mkdir -p /opt/artifacts/rpm
+ for name in rustdesk*??.rpm; do
+ mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm"
+ done
- name: Rename rustdesk
shell: bash
run: |
for name in rustdesk*??.deb; do
- mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb"
+ cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb"
done
- name: Publish debian package
@@ -1057,6 +1085,29 @@ jobs:
tag_name: ${{ env.TAG_NAME }}
files: |
rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb
+
+ - name: Build appimage package
+ if: ${{ matrix.job.extra-build-features == 'appimage' }}
+ shell: bash
+ run: |
+ # set-up appimage-builder
+ pushd /tmp
+ 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
+ chmod +x appimage-builder-x86_64.AppImage
+ 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
+ if: ${{ matrix.job.extra-build-features == 'appimage' }}
+ uses: softprops/action-gh-release@v1
+ with:
+ prerelease: true
+ tag_name: ${{ env.TAG_NAME }}
+ files: |
+ ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
- name: Upload Artifact
uses: actions/upload-artifact@master
@@ -1224,6 +1275,19 @@ jobs:
for name in rustdesk*??.rpm; do
mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm"
done
+ # rpm suse package
+ pushd /workspace
+ case ${{ matrix.job.arch }} in
+ armv7)
+ sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec
+ ;;
+ esac
+ HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb
+ pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }}
+ mkdir -p /opt/artifacts/rpm
+ for name in rustdesk*??.rpm; do
+ mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm"
+ done
- name: Rename rustdesk
shell: bash
@@ -1310,7 +1374,7 @@ jobs:
popd
# run appimage-builder
pushd appimage
- sudo appimage-builder --skip-tests
+ sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-x86_64.yml
- name: Publish appimage package
if: ${{ matrix.job.extra-build-features == 'appimage' }}
diff --git a/.gitignore b/.gitignore
index 1ecea7af8..a71c71a4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ cert.pfx
sciter.dll
**pdb
src/bridge_generated.rs
+src/bridge_generated.io.rs
*deb
rustdesk
*.cache
@@ -39,3 +40,8 @@ flatpak/.flatpak-builder/debian-binary
flatpak/build/**
# bridge file
lib/generated_bridge.dart
+# vscode devcontainer
+.gitconfig
+.vscode-server/
+.ssh
+.devcontainer/.*
diff --git a/Cargo.lock b/Cargo.lock
index 659702704..8f8895bd5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -45,11 +45,13 @@ dependencies = [
[[package]]
name = "allo-isolate"
-version = "0.1.13"
+version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccb993621e6bf1b67591005b0adad126159a0ab31af379743906158aed5330d0"
+checksum = "8ed55848be9f41d44c79df6045b680a74a78bc579e0813f7f196cd7928e22fb1"
dependencies = [
+ "anyhow",
"atomic",
+ "chrono",
]
[[package]]
@@ -151,7 +153,7 @@ checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb"
dependencies = [
"clipboard-win",
"core-graphics 0.22.3",
- "image",
+ "image 0.23.14",
"log",
"objc",
"objc-foundation",
@@ -252,9 +254,9 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -269,31 +271,31 @@ version = "0.1.59"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
name = "atk"
-version = "0.15.1"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd"
+checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf"
dependencies = [
"atk-sys",
"bitflags",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
]
[[package]]
name = "atk-sys"
-version = "0.15.1"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6"
+checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148"
dependencies = [
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
@@ -375,8 +377,8 @@ dependencies = [
"lazycell",
"log",
"peeking_take_while",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"regex",
"rustc-hash",
"shlex",
@@ -395,14 +397,20 @@ dependencies = [
"lazy_static",
"lazycell",
"peeking_take_while",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"regex",
"rustc-hash",
"shlex",
- "syn",
+ "syn 1.0.105",
]
+[[package]]
+name = "bit_field"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
+
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -471,6 +479,12 @@ dependencies = [
"alloc-stdlib",
]
+[[package]]
+name = "build-target"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b"
+
[[package]]
name = "bumpalo"
version = "3.11.1"
@@ -500,24 +514,25 @@ dependencies = [
[[package]]
name = "cairo-rs"
-version = "0.15.12"
+version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc"
+checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d"
dependencies = [
"bitflags",
"cairo-sys-rs",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
+ "once_cell",
"thiserror",
]
[[package]]
name = "cairo-sys-rs"
-version = "0.15.1"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
+checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421"
dependencies = [
- "glib-sys 0.15.10",
+ "glib-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
@@ -565,19 +580,19 @@ dependencies = [
[[package]]
name = "cbindgen"
-version = "0.23.0"
+version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b6d248e3ca02f3fbfabcb9284464c596baec223a26d91bbf44a5a62ddb0d900"
+checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb"
dependencies = [
"clap 3.2.23",
"heck 0.4.0",
"indexmap",
"log",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"serde 1.0.149",
"serde_json 1.0.89",
- "syn",
+ "syn 1.0.105",
"tempfile",
"toml",
]
@@ -706,9 +721,9 @@ checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
dependencies = [
"heck 0.4.0",
"proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -838,6 +853,16 @@ dependencies = [
"toml",
]
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if 1.0.0",
+ "wasm-bindgen",
+]
+
[[package]]
name = "convert_case"
version = "0.5.0"
@@ -954,7 +979,7 @@ dependencies = [
"alsa",
"core-foundation-sys 0.8.3",
"coreaudio-rs",
- "jni",
+ "jni 0.19.0",
"js-sys",
"lazy_static",
"libc",
@@ -1041,6 +1066,12 @@ dependencies = [
"cfg-if 1.0.0",
]
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
+
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -1088,10 +1119,10 @@ dependencies = [
"cc",
"codespan-reporting",
"once_cell",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"scratch",
- "syn",
+ "syn 1.0.105",
]
[[package]]
@@ -1106,20 +1137,20 @@ version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
name = "dark-light"
-version = "0.2.3"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a"
+checksum = "a62007a65515b3cd88c733dd3464431f05d2ad066999a824259d8edc3cf6f645"
dependencies = [
"dconf_rs",
"detect-desktop-environment",
- "dirs",
+ "dirs 4.0.0",
"objc",
"rust-ini",
"web-sys",
@@ -1128,14 +1159,38 @@ dependencies = [
"zvariant",
]
+[[package]]
+name = "darling"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
+dependencies = [
+ "darling_core 0.10.2",
+ "darling_macro 0.10.2",
+]
+
[[package]]
name = "darling"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
- "darling_core",
- "darling_macro",
+ "darling_core 0.13.4",
+ "darling_macro 0.13.4",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "strsim 0.9.3",
+ "syn 1.0.105",
]
[[package]]
@@ -1146,10 +1201,21 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
dependencies = [
"fnv",
"ident_case",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"strsim 0.10.0",
- "syn",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
+dependencies = [
+ "darling_core 0.10.2",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1158,9 +1224,9 @@ version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
- "darling_core",
- "quote",
- "syn",
+ "darling_core 0.13.4",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1316,8 +1382,9 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e"
[[package]]
name = "default-net"
-version = "0.11.0"
-source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14e349ed1e06fb344a7dd8b5a676375cf671b31e8900075dd2be816efc063a63"
dependencies = [
"libc",
"memalloc",
@@ -1335,15 +1402,38 @@ dependencies = [
"byteorder",
]
+[[package]]
+name = "delegate"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "derive_setters"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1cf41b4580a37cca5ef2ada2cc43cf5d6be3983f4522e83010d67ab6925e84b"
+dependencies = [
+ "darling 0.10.2",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1372,6 +1462,16 @@ dependencies = [
"dirs-sys-next",
]
+[[package]]
+name = "dirs"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
+dependencies = [
+ "cfg-if 0.1.10",
+ "dirs-sys",
+]
+
[[package]]
name = "dirs"
version = "4.0.0"
@@ -1428,6 +1528,29 @@ dependencies = [
"libloading",
]
+[[package]]
+name = "dlopen"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937"
+dependencies = [
+ "dlopen_derive",
+ "lazy_static",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "dlopen_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581"
+dependencies = [
+ "libc",
+ "quote 0.6.13",
+ "syn 0.15.44",
+]
+
[[package]]
name = "dlv-list"
version = "0.3.0"
@@ -1513,7 +1636,6 @@ version = "0.0.14"
dependencies = [
"core-graphics 0.22.3",
"hbb_common",
- "libc",
"log",
"objc",
"pkg-config",
@@ -1540,9 +1662,9 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1552,9 +1674,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb"
dependencies = [
"once_cell",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1573,9 +1695,9 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1618,10 +1740,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e"
dependencies = [
"proc-macro-error",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"rustversion",
- "syn",
+ "syn 1.0.105",
"synstructure",
]
@@ -1672,6 +1794,34 @@ version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+[[package]]
+name = "exr"
+version = "1.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8af5ef47e2ed89d23d0ecbc1b681b30390069de70260937877514377fc24feb"
+dependencies = [
+ "bit_field",
+ "flume",
+ "half",
+ "lebe",
+ "miniz_oxide 0.6.2",
+ "smallvec",
+ "threadpool",
+ "zune-inflate",
+]
+
+[[package]]
+name = "extend"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c5216e387a76eebaaf11f6d871ec8a4aae0b25f05456ee21f228e024b1b3610"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
[[package]]
name = "failure"
version = "0.1.8"
@@ -1742,48 +1892,78 @@ dependencies = [
"time 0.3.9",
]
+[[package]]
+name = "flume"
+version = "0.10.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "nanorand",
+ "pin-project",
+ "spin 0.9.5",
+]
+
[[package]]
name = "flutter_rust_bridge"
-version = "1.32.0"
-source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1"
+version = "1.61.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8079119bbe8fb63d7ebb731fa2aa68c6c8375f4ac95ca26d5868e64c0f4b9244"
dependencies = [
"allo-isolate",
"anyhow",
+ "build-target",
+ "bytemuck",
+ "cc",
+ "chrono",
+ "console_error_panic_hook",
"flutter_rust_bridge_macros",
+ "js-sys",
"lazy_static",
+ "libc",
+ "log",
"parking_lot 0.12.1",
"threadpool",
+ "wasm-bindgen",
+ "web-sys",
]
[[package]]
name = "flutter_rust_bridge_codegen"
-version = "1.32.0"
-source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1"
+version = "1.61.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efd7396bc479eae8aa24243e4c0e3d3dbda1909134f8de6bde4f080d262c9a0d"
dependencies = [
"anyhow",
"cargo_metadata",
"cbindgen",
+ "clap 3.2.23",
"convert_case",
+ "delegate",
"enum_dispatch",
"env_logger 0.9.3",
+ "extend",
+ "itertools 0.10.5",
"lazy_static",
"log",
"pathdiff",
- "quote",
+ "quote 1.0.21",
"regex",
"serde 1.0.149",
"serde_yaml",
- "structopt",
- "syn",
+ "syn 1.0.105",
"tempfile",
"thiserror",
"toml",
+ "topological-sort",
]
[[package]]
name = "flutter_rust_bridge_macros"
-version = "1.32.0"
-source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1"
+version = "1.61.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d5cd827645690ef378be57a890d0581e17c28d07b712872af7d744f454fd27d"
[[package]]
name = "fnv"
@@ -1815,6 +1995,19 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fruitbasket"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "898289b8e0528c84fb9b88f15ac9d5109bcaf23e0e49bb6f64deee0d86b6a351"
+dependencies = [
+ "dirs 2.0.2",
+ "objc",
+ "objc-foundation",
+ "objc_id",
+ "time 0.1.45",
+]
+
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
@@ -1912,9 +2105,9 @@ version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -1958,63 +2151,90 @@ dependencies = [
[[package]]
name = "gdk"
-version = "0.15.4"
+version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8"
+checksum = "aa9cb33da481c6c040404a11f8212d193889e9b435db2c14fd86987f630d3ce1"
dependencies = [
"bitflags",
"cairo-rs",
"gdk-pixbuf",
"gdk-sys",
"gio",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
"pango",
]
[[package]]
name = "gdk-pixbuf"
-version = "0.15.11"
+version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a"
+checksum = "c3578c60dee9d029ad86593ed88cb40f35c1b83360e12498d055022385dd9a05"
dependencies = [
"bitflags",
"gdk-pixbuf-sys",
"gio",
- "glib 0.15.12",
+ "glib 0.16.5",
"libc",
]
[[package]]
name = "gdk-pixbuf-sys"
-version = "0.15.10"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7"
+checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016"
dependencies = [
- "gio-sys 0.15.10",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "gio-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
[[package]]
name = "gdk-sys"
-version = "0.15.1"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88"
+checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
- "gio-sys 0.15.10",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "gio-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"pango-sys",
"pkg-config",
"system-deps 6.0.3",
]
+[[package]]
+name = "gdkwayland-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba"
+dependencies = [
+ "gdk-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
+ "libc",
+ "pkg-config",
+ "system-deps 6.0.3",
+]
+
+[[package]]
+name = "gdkx11-sys"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af"
+dependencies = [
+ "gdk-sys",
+ "glib-sys 0.16.3",
+ "libc",
+ "system-deps 6.0.3",
+ "x11 2.20.1",
+]
+
[[package]]
name = "generic-array"
version = "0.14.6"
@@ -2042,8 +2262,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
+ "js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gif"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06"
+dependencies = [
+ "color_quant",
+ "weezl",
]
[[package]]
@@ -2054,34 +2286,24 @@ checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
[[package]]
name = "gio"
-version = "0.15.12"
+version = "0.16.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b"
+checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"futures-io",
- "gio-sys 0.15.10",
- "glib 0.15.12",
+ "futures-util",
+ "gio-sys",
+ "glib 0.16.5",
"libc",
"once_cell",
+ "pin-project-lite",
+ "smallvec",
"thiserror",
]
-[[package]]
-name = "gio-sys"
-version = "0.15.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d"
-dependencies = [
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
- "libc",
- "system-deps 6.0.3",
- "winapi 0.3.9",
-]
-
[[package]]
name = "gio-sys"
version = "0.16.3"
@@ -2114,26 +2336,6 @@ dependencies = [
"once_cell",
]
-[[package]]
-name = "glib"
-version = "0.15.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d"
-dependencies = [
- "bitflags",
- "futures-channel",
- "futures-core",
- "futures-executor",
- "futures-task",
- "glib-macros 0.15.11",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
- "libc",
- "once_cell",
- "smallvec",
- "thiserror",
-]
-
[[package]]
name = "glib"
version = "0.16.5"
@@ -2146,7 +2348,7 @@ dependencies = [
"futures-executor",
"futures-task",
"futures-util",
- "gio-sys 0.16.3",
+ "gio-sys",
"glib-macros 0.16.3",
"glib-sys 0.16.3",
"gobject-sys 0.16.3",
@@ -2164,27 +2366,12 @@ checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039"
dependencies = [
"anyhow",
"heck 0.3.3",
- "itertools",
+ "itertools 0.9.0",
"proc-macro-crate 0.1.5",
"proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "glib-macros"
-version = "0.15.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64"
-dependencies = [
- "anyhow",
- "heck 0.4.0",
- "proc-macro-crate 1.2.1",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -2197,9 +2384,9 @@ dependencies = [
"heck 0.4.0",
"proc-macro-crate 1.2.1",
"proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -2212,16 +2399,6 @@ dependencies = [
"system-deps 1.3.2",
]
-[[package]]
-name = "glib-sys"
-version = "0.15.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
-dependencies = [
- "libc",
- "system-deps 6.0.3",
-]
-
[[package]]
name = "glib-sys"
version = "0.16.3"
@@ -2249,17 +2426,6 @@ dependencies = [
"system-deps 1.3.2",
]
-[[package]]
-name = "gobject-sys"
-version = "0.15.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
-dependencies = [
- "glib-sys 0.15.10",
- "libc",
- "system-deps 6.0.3",
-]
-
[[package]]
name = "gobject-sys"
version = "0.16.3"
@@ -2288,7 +2454,7 @@ dependencies = [
"gstreamer-sys",
"libc",
"muldiv",
- "num-rational",
+ "num-rational 0.3.2",
"once_cell",
"paste",
"pretty-hex",
@@ -2406,9 +2572,9 @@ dependencies = [
[[package]]
name = "gtk"
-version = "0.15.5"
+version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0"
+checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6"
dependencies = [
"atk",
"bitflags",
@@ -2418,7 +2584,7 @@ dependencies = [
"gdk",
"gdk-pixbuf",
"gio",
- "glib 0.15.12",
+ "glib 0.16.5",
"gtk-sys",
"gtk3-macros",
"libc",
@@ -2429,17 +2595,17 @@ dependencies = [
[[package]]
name = "gtk-sys"
-version = "0.15.3"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84"
+checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3"
dependencies = [
"atk-sys",
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gdk-sys",
- "gio-sys 0.15.10",
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "gio-sys",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"pango-sys",
"system-deps 6.0.3",
@@ -2447,16 +2613,16 @@ dependencies = [
[[package]]
name = "gtk3-macros"
-version = "0.15.4"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9"
+checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff"
dependencies = [
"anyhow",
"proc-macro-crate 1.2.1",
"proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -2478,6 +2644,15 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "half"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0"
+dependencies = [
+ "crunchy",
+]
+
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -2492,6 +2667,7 @@ name = "hbb_common"
version = "0.1.0"
dependencies = [
"anyhow",
+ "backtrace",
"bytes",
"chrono",
"confy",
@@ -2502,9 +2678,11 @@ dependencies = [
"futures",
"futures-util",
"lazy_static",
+ "libc",
"log",
"mac_address",
"machine-uid",
+ "osascript",
"protobuf",
"protobuf-codegen",
"quinn",
@@ -2515,6 +2693,7 @@ dependencies = [
"serde_json 1.0.89",
"socket2 0.3.19",
"sodiumoxide",
+ "sysinfo",
"tokio",
"tokio-socks",
"tokio-util",
@@ -2602,7 +2781,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hwcodec"
version = "0.1.0"
-source = "git+https://github.com/21pages/hwcodec#e819484c4c010199f2a0977bdf306b4edbeafbae"
+source = "git+https://github.com/21pages/hwcodec#64f885b3787694b16dfcff08256750b0376b2eba"
dependencies = [
"bindgen 0.59.2",
"cc",
@@ -2699,10 +2878,29 @@ dependencies = [
"byteorder",
"color_quant",
"num-iter",
- "num-rational",
+ "num-rational 0.3.2",
"num-traits 0.2.15",
- "png",
- "tiff",
+ "png 0.16.8",
+ "tiff 0.6.1",
+]
+
+[[package]]
+name = "image"
+version = "0.24.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945"
+dependencies = [
+ "bytemuck",
+ "byteorder",
+ "color_quant",
+ "exr",
+ "gif",
+ "jpeg-decoder 0.3.0",
+ "num-rational 0.4.1",
+ "num-traits 0.2.15",
+ "png 0.17.7",
+ "scoped_threadpool",
+ "tiff 0.8.1",
]
[[package]]
@@ -2728,8 +2926,8 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
dependencies = [
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
]
[[package]]
@@ -2798,6 +2996,15 @@ dependencies = [
"either",
]
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
[[package]]
name = "itoa"
version = "0.3.4"
@@ -2824,6 +3031,20 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "jni"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
+dependencies = [
+ "cesu8",
+ "combine",
+ "jni-sys",
+ "log",
+ "thiserror",
+ "walkdir",
+]
+
[[package]]
name = "jni-sys"
version = "0.3.0"
@@ -2845,6 +3066,15 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
+[[package]]
+name = "jpeg-decoder"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e"
+dependencies = [
+ "rayon",
+]
+
[[package]]
name = "js-sys"
version = "0.3.60"
@@ -2864,6 +3094,17 @@ dependencies = [
"winapi-build",
]
+[[package]]
+name = "keyboard-types"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68"
+dependencies = [
+ "bitflags",
+ "serde 1.0.149",
+ "unicode-segmentation",
+]
+
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -2877,12 +3118,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
-name = "libappindicator"
-version = "0.7.1"
+name = "lebe"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8"
+checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
+
+[[package]]
+name = "libappindicator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89e1edfdc9b0853358306c6dfb4b77c79c779174256fe93d80c0b5ebca451a2f"
dependencies = [
- "glib 0.15.12",
+ "glib 0.16.5",
"gtk",
"gtk-sys",
"libappindicator-sys",
@@ -2891,9 +3138,9 @@ dependencies = [
[[package]]
name = "libappindicator-sys"
-version = "0.7.3"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa"
+checksum = "08fcb2bea89cee9613982501ec83eaa2d09256b24540ae463c52a28906163918"
dependencies = [
"gtk-sys",
"libloading",
@@ -2994,6 +3241,25 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "libxdo"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db"
+dependencies = [
+ "libxdo-sys",
+]
+
+[[package]]
+name = "libxdo-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212"
+dependencies = [
+ "libc",
+ "x11 2.20.1",
+]
+
[[package]]
name = "link-cplusplus"
version = "1.0.7"
@@ -3249,12 +3515,41 @@ dependencies = [
"glob",
]
+[[package]]
+name = "muda"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66365a21dc5e322c6b6ba25c735d00153c57dd2eb377926aa50e3caf547b6f6"
+dependencies = [
+ "cocoa",
+ "crossbeam-channel",
+ "gdk",
+ "gdk-pixbuf",
+ "gtk",
+ "keyboard-types",
+ "libxdo",
+ "objc",
+ "once_cell",
+ "png 0.17.7",
+ "thiserror",
+ "windows-sys 0.45.0",
+]
+
[[package]]
name = "muldiv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204"
+[[package]]
+name = "nanorand"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
+dependencies = [
+ "getrandom",
+]
+
[[package]]
name = "ndk"
version = "0.5.0"
@@ -3337,11 +3632,11 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
dependencies = [
- "darling",
+ "darling 0.13.4",
"proc-macro-crate 1.2.1",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -3488,9 +3783,9 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -3525,6 +3820,17 @@ dependencies = [
"num-traits 0.2.15",
]
+[[package]]
+name = "num-rational"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0"
+dependencies = [
+ "autocfg 1.1.0",
+ "num-integer",
+ "num-traits 0.2.15",
+]
+
[[package]]
name = "num-traits"
version = "0.1.43"
@@ -3569,9 +3875,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
dependencies = [
"proc-macro-crate 1.2.1",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -3590,6 +3896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
+ "objc_exception",
]
[[package]]
@@ -3603,6 +3910,15 @@ dependencies = [
"objc_id",
]
+[[package]]
+name = "objc_exception"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
+dependencies = [
+ "cc",
+]
+
[[package]]
name = "objc_id"
version = "0.1.1"
@@ -3627,7 +3943,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1"
dependencies = [
- "jni",
+ "jni 0.19.0",
"ndk 0.6.0",
"ndk-context",
"num-derive",
@@ -3646,9 +3962,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.16.0"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
[[package]]
name = "openssl-probe"
@@ -3683,19 +3999,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee"
[[package]]
-name = "padlock"
-version = "0.2.0"
+name = "osascript"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f"
+checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc"
+dependencies = [
+ "serde 1.0.149",
+ "serde_derive",
+ "serde_json 1.0.89",
+]
[[package]]
name = "pango"
-version = "0.15.10"
+version = "0.16.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f"
+checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94"
dependencies = [
"bitflags",
- "glib 0.15.12",
+ "gio",
+ "glib 0.16.5",
"libc",
"once_cell",
"pango-sys",
@@ -3703,12 +4025,12 @@ dependencies = [
[[package]]
name = "pango-sys"
-version = "0.15.10"
+version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa"
+checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f"
dependencies = [
- "glib-sys 0.15.10",
- "gobject-sys 0.15.10",
+ "glib-sys 0.16.3",
+ "gobject-sys 0.16.3",
"libc",
"system-deps 6.0.3",
]
@@ -3869,9 +4191,9 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -3904,6 +4226,18 @@ dependencies = [
"miniz_oxide 0.3.7",
]
+[[package]]
+name = "png"
+version = "0.17.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "flate2",
+ "miniz_oxide 0.6.2",
+]
+
[[package]]
name = "polling"
version = "2.5.1"
@@ -3966,9 +4300,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
"version_check",
]
@@ -3978,11 +4312,20 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"version_check",
]
+[[package]]
+name = "proc-macro2"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
+dependencies = [
+ "unicode-xid 0.1.0",
+]
+
[[package]]
name = "proc-macro2"
version = "1.0.47"
@@ -4110,13 +4453,22 @@ dependencies = [
"tracing",
]
+[[package]]
+name = "quote"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
+dependencies = [
+ "proc-macro2 0.4.30",
+]
+
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
- "proc-macro2",
+ "proc-macro2 1.0.47",
]
[[package]]
@@ -4304,12 +4656,13 @@ dependencies = [
[[package]]
name = "rdev"
version = "0.5.0-2"
-source = "git+https://github.com/fufesou/rdev#1be26c7e8ed0d43cebdd8331d467bb61130a2e6e"
+source = "git+https://github.com/fufesou/rdev#25a99ce71ab42843ad253dd51e6a35e83e87a8a4"
dependencies = [
"cocoa",
"core-foundation 0.9.3",
"core-foundation-sys 0.8.3",
"core-graphics 0.22.3",
+ "dispatch",
"enum-map",
"epoll",
"inotify",
@@ -4446,7 +4799,7 @@ dependencies = [
"cc",
"libc",
"once_cell",
- "spin",
+ "spin 0.5.2",
"untrusted",
"web-sys",
"winapi 0.3.9",
@@ -4562,7 +4915,6 @@ dependencies = [
"arboard",
"async-process",
"async-trait",
- "backtrace",
"base64",
"bytes",
"cc",
@@ -4582,22 +4934,21 @@ dependencies = [
"dbus-crossroads",
"default-net",
"dispatch",
+ "dlopen",
"enigo",
"errno",
"evdev",
"flexi_logger",
"flutter_rust_bridge",
"flutter_rust_bridge_codegen",
- "glib 0.16.5",
- "gtk",
+ "fruitbasket",
"hbb_common",
"hound",
+ "image 0.24.5",
"impersonate_system",
"include_dir",
- "jni",
+ "jni 0.19.0",
"lazy_static",
- "libappindicator",
- "libc",
"libpulse-binding",
"libpulse-simple-binding",
"mac_address",
@@ -4606,6 +4957,7 @@ dependencies = [
"mouce",
"num_cpus",
"objc",
+ "objc_id",
"parity-tokio-ipc",
"rdev",
"repng",
@@ -4625,9 +4977,9 @@ dependencies = [
"shutdown_hooks",
"simple_rc",
"sys-locale",
- "sysinfo",
"system_shutdown",
- "tray-item",
+ "tao",
+ "tray-icon",
"trayicon",
"url",
"uuid",
@@ -4639,6 +4991,7 @@ dependencies = [
"winreg 0.10.1",
"winres",
"wol-rs",
+ "xrandr-parser",
]
[[package]]
@@ -4646,7 +4999,7 @@ name = "rustdesk-portable-packer"
version = "0.1.0"
dependencies = [
"brotli",
- "dirs",
+ "dirs 4.0.0",
"embed-resource",
"md5",
]
@@ -4765,6 +5118,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
+[[package]]
+name = "scoped_threadpool"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8"
+
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -4786,9 +5145,8 @@ dependencies = [
"gstreamer-video",
"hbb_common",
"hwcodec",
- "jni",
+ "jni 0.19.0",
"lazy_static",
- "libc",
"log",
"ndk 0.7.0",
"num_cpus",
@@ -4889,9 +5247,9 @@ version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -4923,9 +5281,9 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5024,6 +5382,12 @@ version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
+[[package]]
+name = "simd-adler32"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18"
+
[[package]]
name = "simple_rc"
version = "0.1.0"
@@ -5114,6 +5478,15 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+[[package]]
+name = "spin"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc"
+dependencies = [
+ "lock_api",
+]
+
[[package]]
name = "static_assertions"
version = "1.1.0"
@@ -5144,36 +5517,18 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+[[package]]
+name = "strsim"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
+
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
-[[package]]
-name = "structopt"
-version = "0.3.26"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
-dependencies = [
- "clap 2.34.0",
- "lazy_static",
- "structopt-derive",
-]
-
-[[package]]
-name = "structopt-derive"
-version = "0.4.18"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
-dependencies = [
- "heck 0.3.3",
- "proc-macro-error",
- "proc-macro2",
- "quote",
- "syn",
-]
-
[[package]]
name = "strum"
version = "0.18.0"
@@ -5193,9 +5548,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c"
dependencies = [
"heck 0.3.3",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5205,10 +5560,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck 0.4.0",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"rustversion",
- "syn",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "syn"
+version = "0.15.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
+dependencies = [
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "unicode-xid 0.1.0",
]
[[package]]
@@ -5217,8 +5583,8 @@ version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
dependencies = [
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"unicode-ident",
]
@@ -5228,10 +5594,10 @@ version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "unicode-xid",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+ "unicode-xid 0.2.4",
]
[[package]]
@@ -5320,6 +5686,61 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "tao"
+version = "0.17.0"
+source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf"
+dependencies = [
+ "bitflags",
+ "cairo-rs",
+ "cc",
+ "cocoa",
+ "core-foundation 0.9.3",
+ "core-graphics 0.22.3",
+ "crossbeam-channel",
+ "dispatch",
+ "gdk",
+ "gdk-pixbuf",
+ "gdk-sys",
+ "gdkwayland-sys",
+ "gdkx11-sys",
+ "gio",
+ "glib 0.16.5",
+ "glib-sys 0.16.3",
+ "gtk",
+ "image 0.24.5",
+ "instant",
+ "jni 0.20.0",
+ "lazy_static",
+ "libc",
+ "log",
+ "ndk 0.6.0",
+ "ndk-context",
+ "ndk-sys 0.3.0",
+ "objc",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "png 0.17.7",
+ "raw-window-handle 0.5.0",
+ "scopeguard",
+ "tao-macros",
+ "unicode-segmentation",
+ "uuid",
+ "windows 0.44.0",
+ "windows-implement",
+ "x11-dl",
+]
+
+[[package]]
+name = "tao-macros"
+version = "0.1.0"
+source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
[[package]]
name = "tap"
version = "1.0.1"
@@ -5387,7 +5808,7 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
[[package]]
name = "tfc"
version = "0.6.1"
-source = "git+https://github.com/fufesou/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a"
+source = "git+https://github.com/fufesou/The-Fat-Controller#a5f13e6ef80327eb8d860aeb26b0af93eb5aee2b"
dependencies = [
"core-graphics 0.22.3",
"unicode-segmentation",
@@ -5410,9 +5831,9 @@ version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5430,11 +5851,22 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
dependencies = [
- "jpeg-decoder",
+ "jpeg-decoder 0.1.22",
"miniz_oxide 0.4.4",
"weezl",
]
+[[package]]
+name = "tiff"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
+dependencies = [
+ "flate2",
+ "jpeg-decoder 0.3.0",
+ "weezl",
+]
+
[[package]]
name = "time"
version = "0.1.45"
@@ -5505,9 +5937,9 @@ version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5564,6 +5996,12 @@ dependencies = [
"serde 1.0.149",
]
+[[package]]
+name = "topological-sort"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d"
+
[[package]]
name = "tower-service"
version = "0.3.2"
@@ -5588,9 +6026,9 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
[[package]]
@@ -5613,21 +6051,22 @@ dependencies = [
]
[[package]]
-name = "tray-item"
-version = "0.7.1"
+name = "tray-icon"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0914b62e00e8f51241806cb9f9c4ea6b10c75d94cae02c89278de6f4b98c7d0f"
+checksum = "d62801a4da61bb100b8d3174a5a46fed7b6ea03cc2ae93ee7340793b09a94ce3"
dependencies = [
"cocoa",
"core-graphics 0.22.3",
- "gtk",
+ "crossbeam-channel",
+ "dirs-next",
"libappindicator",
- "libc",
+ "muda",
"objc",
- "objc-foundation",
- "objc_id",
- "padlock",
- "winapi 0.3.9",
+ "once_cell",
+ "png 0.17.7",
+ "thiserror",
+ "windows-sys 0.45.0",
]
[[package]]
@@ -5700,6 +6139,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+[[package]]
+name = "unicode-xid"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
+
[[package]]
name = "unicode-xid"
version = "0.2.4"
@@ -5726,9 +6171,9 @@ dependencies = [
[[package]]
name = "uuid"
-version = "1.2.2"
+version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
+checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
dependencies = [
"getrandom",
]
@@ -5844,9 +6289,9 @@ dependencies = [
"bumpalo",
"log",
"once_cell",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
"wasm-bindgen-shared",
]
@@ -5868,7 +6313,7 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
dependencies = [
- "quote",
+ "quote 1.0.21",
"wasm-bindgen-macro-support",
]
@@ -5878,9 +6323,9 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -5948,8 +6393,8 @@ version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53"
dependencies = [
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"xml-rs",
]
@@ -6157,6 +6602,39 @@ dependencies = [
"windows_x86_64_msvc 0.34.0",
]
+[[package]]
+name = "windows"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f"
+dependencies = [
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
+]
+
[[package]]
name = "windows-service"
version = "0.4.0"
@@ -6202,19 +6680,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
- "windows_aarch64_msvc 0.42.0",
- "windows_i686_gnu 0.42.0",
- "windows_i686_msvc 0.42.0",
- "windows_x86_64_gnu 0.42.0",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm",
- "windows_x86_64_msvc 0.42.0",
+ "windows_x86_64_msvc 0.42.1",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc 0.42.1",
+ "windows_i686_gnu 0.42.1",
+ "windows_i686_msvc 0.42.1",
+ "windows_x86_64_gnu 0.42.1",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc 0.42.1",
]
[[package]]
name = "windows_aarch64_gnullvm"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_msvc"
@@ -6242,9 +6744,9 @@ checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
+checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_i686_gnu"
@@ -6272,9 +6774,9 @@ checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
+checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_msvc"
@@ -6302,9 +6804,9 @@ checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
+checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_x86_64_gnu"
@@ -6332,15 +6834,15 @@ checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnullvm"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
+checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_msvc"
@@ -6368,9 +6870,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
-version = "0.42.0"
+version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
+checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "winit"
@@ -6481,12 +6983,12 @@ dependencies = [
[[package]]
name = "x11-dl"
-version = "2.20.1"
+version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0"
+checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
dependencies = [
- "lazy_static",
"libc",
+ "once_cell",
"pkg-config",
]
@@ -6517,6 +7019,16 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
+[[package]]
+name = "xrandr-parser"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5af43ba661cee58bd86b9f81a899e45a15ac7f42fa4401340f73c0c2950030c1"
+dependencies = [
+ "derive_setters",
+ "serde 1.0.149",
+]
+
[[package]]
name = "yaml-rust"
version = "0.4.5"
@@ -6542,7 +7054,7 @@ dependencies = [
"async-trait",
"byteorder",
"derivative",
- "dirs",
+ "dirs 4.0.0",
"enumflags2",
"event-listener",
"futures-core",
@@ -6572,10 +7084,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e"
dependencies = [
"proc-macro-crate 1.2.1",
- "proc-macro2",
- "quote",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
"regex",
- "syn",
+ "syn 1.0.105",
]
[[package]]
@@ -6618,6 +7130,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "zune-inflate"
+version = "0.2.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c473377c11c4a3ac6a2758f944cd336678e9c977aa0abf54f6450cf77e902d6d"
+dependencies = [
+ "simd-adler32",
+]
+
[[package]]
name = "zvariant"
version = "3.9.0"
@@ -6639,7 +7160,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81"
dependencies = [
"proc-macro-crate 1.2.1",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 1.0.47",
+ "quote 1.0.21",
+ "syn 1.0.105",
]
diff --git a/Cargo.toml b/Cargo.toml
index 2713df11d..b53615c4e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ inline = []
hbbs = []
cli = []
with_rc = ["simple_rc"]
+flutter_texture_render = []
appimage = []
flatpak = []
use_samplerate = ["samplerate"]
@@ -43,7 +44,6 @@ cfg-if = "1.0"
lazy_static = "1.4"
sha2 = "0.10"
repng = "0.2"
-libc = "0.2"
parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
runas = "0.2"
@@ -56,15 +56,15 @@ uuid = { version = "1.0", features = ["v4"] }
clap = "3.0"
rpassword = "7.0"
base64 = "0.13"
-sysinfo = "0.24"
num_cpus = "1.13"
bytes = { version = "1.2", features = ["serde"] }
-default-net = { git = "https://github.com/Kingtous/default-net" }
+default-net = "0.12.0"
wol-rs = "0.9.1"
-flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true }
+flutter_rust_bridge = { version = "1.61.1", optional = true }
errno = "0.2.8"
rdev = { git = "https://github.com/fufesou/rdev" }
url = { version = "2.1", features = ["serde"] }
+dlopen = "0.1"
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false }
chrono = "0.4.23"
@@ -86,7 +86,6 @@ arboard = "2.0"
system_shutdown = "3.0.0"
[target.'cfg(target_os = "windows")'.dependencies]
-#systray = { git = "https://github.com/open-trade/systray-rs" }
trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] }
winit = "0.26"
winapi = { version = "0.3", features = ["winuser"] }
@@ -104,8 +103,14 @@ dispatch = "0.2"
core-foundation = "0.9"
core-graphics = "0.22"
include_dir = "0.7.2"
-tray-item = "0.7" # looks better than trayicon
-dark-light = "0.2"
+dark-light = "1.0"
+fruitbasket = "0.10.0"
+objc_id = "0.1.1"
+
+[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
+tray-icon = "0.4"
+tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" }
+image = "0.24"
[target.'cfg(target_os = "linux")'.dependencies]
psimple = { package = "libpulse-simple-binding", version = "2.25" }
@@ -116,20 +121,18 @@ mouce = { git="https://github.com/fufesou/mouce.git" }
evdev = { git="https://github.com/fufesou/evdev" }
dbus = "0.9"
dbus-crossroads = "0.5"
-gtk = "0.15"
-libappindicator = "0.7"
-glib = "0.16.5"
-backtrace = "0.3"
+xrandr-parser = "0.3.0"
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.11"
jni = "0.19"
[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]
-flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" }
+flutter_rust_bridge = "1.61.1"
[workspace]
members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"]
+exclude = ["vdi/host"]
[package.metadata.winres]
LegalCopyright = "Copyright © 2022 Purslane, Inc."
@@ -144,7 +147,7 @@ winapi = { version = "0.3", features = [ "winnt" ] }
cc = "1.0"
hbb_common = { path = "libs/hbb_common" }
simple_rc = { path = "libs/simple_rc", optional = true }
-flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" }
+flutter_rust_bridge_codegen = "1.61.1"
[dev-dependencies]
hound = "3.5"
@@ -155,7 +158,6 @@ identifier = "com.carriez.rustdesk"
icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"]
deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libvdpau1", "libva2"]
osx_minimum_system_version = "10.14"
-resources = ["res/mac-tray-light.png","res/mac-tray-dark.png"]
#https://github.com/johnthagen/min-sized-rust
[profile.release]
diff --git a/README.md b/README.md
index 79255e455..8af79915b 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-
+
Servers •
Build •
Docker •
@@ -19,7 +19,7 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c
RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started.
-[**How does RustDesk work?**](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)
@@ -31,7 +31,7 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/
## Free Public Servers
-Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow.
+Below are the servers you are using for free, they may change over time. If you are not close to one of these, your network may be slow.
| Location | Vendor | Specification |
| --------- | ------------- | ------------------ |
| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
@@ -41,6 +41,14 @@ Below are the servers you are using for free, it may change along the time. If y
| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
| Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM |
+## Dev Container
+
+[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
+
+If you already have VS Code and Docker installed, you can click the badge above to get started. Clicking will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
+
+Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
+
## Dependencies
Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only.
@@ -198,7 +206,7 @@ Please ensure that you are running these commands from the root of the RustDesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
-- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client
## Snapshot
diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml
new file mode 100644
index 000000000..f3cd8f568
--- /dev/null
+++ b/appimage/AppImageBuilder-aarch64.yml
@@ -0,0 +1,85 @@
+# appimage-builder recipe see https://appimage-builder.readthedocs.io for details
+version: 1
+script:
+ - rm -rf ./AppDir || true
+ - bsdtar -zxvf ../rustdesk-1.2.0.deb
+ - tar -xvf ./data.tar.xz
+ - mkdir ./AppDir
+ - mv ./usr ./AppDir/usr
+ # 32x32 icon
+ - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done
+ # desktop file
+ # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop
+ - rm -rf ./AppDir/usr/share/applications
+AppDir:
+ path: ./AppDir
+ app_info:
+ id: rustdesk
+ name: rustdesk
+ icon: rustdesk
+ version: 1.2.0
+ exec: usr/lib/rustdesk/rustdesk
+ exec_args: $@
+ apt:
+ arch:
+ - arm64
+ allow_unauthenticated: true
+ sources:
+ - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse
+ key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
+ - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse
+ key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
+ - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted
+ universe multiverse
+ key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32'
+ include:
+ - libc6
+ - libgtk-3-0
+ - libxcb-randr0
+ - libxdo3
+ - libxfixes3
+ - libxcb-shape0
+ - libxcb-xfixes0
+ - libasound2
+ - libsystemd0
+ - curl
+ - libva-drm2
+ - libva-x11-2
+ - libvdpau1
+ - libgstreamer-plugins-base1.0-0
+ exclude:
+ - humanity-icon-theme
+ - hicolor-icon-theme
+ - adwaita-icon-theme
+ - ubuntu-mono
+ files:
+ include: []
+ exclude:
+ - usr/share/man
+ - usr/share/doc/*/README.*
+ - usr/share/doc/*/changelog.*
+ - usr/share/doc/*/NEWS.*
+ - usr/share/doc/*/TODO.*
+ runtime:
+ env:
+ GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
+ GDK_BACKEND: x11
+ test:
+ fedora-30:
+ image: appimagecrafters/tests-env:fedora-30
+ command: ./AppRun
+ debian-stable:
+ image: appimagecrafters/tests-env:debian-stable
+ command: ./AppRun
+ archlinux-latest:
+ image: appimagecrafters/tests-env:archlinux-latest
+ command: ./AppRun
+ centos-7:
+ image: appimagecrafters/tests-env:centos-7
+ command: ./AppRun
+ ubuntu-xenial:
+ image: appimagecrafters/tests-env:ubuntu-xenial
+ command: ./AppRun
+AppImage:
+ arch: aarch64
+ update-information: guess
diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder-x86_64.yml
similarity index 99%
rename from appimage/AppImageBuilder.yml
rename to appimage/AppImageBuilder-x86_64.yml
index ae95fd2ce..59dd5164f 100644
--- a/appimage/AppImageBuilder.yml
+++ b/appimage/AppImageBuilder-x86_64.yml
@@ -66,6 +66,7 @@ AppDir:
runtime:
env:
GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/
+ GDK_BACKEND: x11
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30
diff --git a/build.py b/build.py
index 6b107ff4b..45fe1b132 100755
--- a/build.py
+++ b/build.py
@@ -18,14 +18,11 @@ exe_path = 'target/release/' + hbb_name
flutter_win_target_dir = 'flutter/build/windows/runner/Release/'
skip_cargo = False
-def custom_os_system(cmd):
- err = os._system(cmd)
+def system2(cmd):
+ err = os.system(cmd)
if err != 0:
print(f"Error occurred when executing: {cmd}. Exiting.")
sys.exit(-1)
-# replace prebuilt os.system
-os._system = os.system
-os.system = custom_os_system
def get_version():
with open("Cargo.toml", encoding="utf-8") as fh:
@@ -144,8 +141,8 @@ def generate_build_script_for_docker():
# build rustdesk
./build.py --flutter --hwcodec
''')
- os.system("chmod +x /tmp/build.sh")
- os.system("bash /tmp/build.sh")
+ system2("chmod +x /tmp/build.sh")
+ system2("bash /tmp/build.sh")
def download_extract_features(features, res_dir):
@@ -239,6 +236,7 @@ def get_features(args):
features.append('hwcodec')
if args.flutter:
features.append('flutter')
+ features.append('flutter_texture_render')
if args.flatpak:
features.append('flatpak')
if args.appimage:
@@ -249,7 +247,7 @@ def get_features(args):
def generate_control_file(version):
control_file_path = "../res/DEBIAN/control"
- os.system('/bin/rm -rf %s' % control_file_path)
+ system2('/bin/rm -rf %s' % control_file_path)
content = """Package: rustdesk
Version: %s
@@ -267,45 +265,45 @@ Description: A remote control software.
def ffi_bindgen_function_refactor():
# workaround ffigen
- os.system(
+ system2(
'sed -i "s/ffi.NativeFunction> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
- os.system('mkdir -p tmpdeb/DEBIAN')
+ system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version)
- os.system('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
+ system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
- os.system('dpkg-deb -b tmpdeb rustdesk.deb;')
+ system2('dpkg-deb -b tmpdeb rustdesk.deb;')
- os.system('/bin/rm -rf tmpdeb/')
- os.system('/bin/rm -rf ../res/DEBIAN/control')
+ system2('/bin/rm -rf tmpdeb/')
+ system2('/bin/rm -rf ../res/DEBIAN/control')
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
os.chdir("..")
@@ -313,46 +311,43 @@ def build_flutter_deb(version, features):
def build_flutter_dmg(version, features):
if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
- os.system(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
+ system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release')
# copy dylib
- os.system(
+ system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
- # ffi_bindgen_function_refactor()
- # limitations from flutter rust bridge
- os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h')
os.chdir('flutter')
- os.system('flutter build macos --release')
- os.system(
- "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/rustdesk.app")
+ system2('flutter build macos --release')
+ system2(
+ "create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
os.chdir("..")
def build_flutter_arch_manjaro(version, features):
if not skip_cargo:
- os.system(f'cargo build --features {features} --lib --release')
+ system2(f'cargo build --features {features} --lib --release')
ffi_bindgen_function_refactor()
os.chdir('flutter')
- os.system('flutter build linux --release')
- os.system('strip build/linux/x64/release/bundle/lib/librustdesk.so')
+ system2('flutter build linux --release')
+ system2('strip build/linux/x64/release/bundle/lib/librustdesk.so')
os.chdir('../res')
- os.system('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
+ system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
def build_flutter_windows(version, features):
if not skip_cargo:
- os.system(f'cargo build --features {features} --lib --release')
+ system2(f'cargo build --features {features} --lib --release')
if not os.path.exists("target/release/librustdesk.dll"):
print("cargo build failed, please check rust source code.")
exit(-1)
os.chdir('flutter')
- os.system('flutter build windows --release')
+ system2('flutter build windows --release')
os.chdir('..')
shutil.copy2('target/release/deps/dylib_virtual_display.dll',
flutter_win_target_dir)
os.chdir('libs/portable')
- os.system('pip3 install -r requirements.txt')
- os.system(
+ system2('pip3 install -r requirements.txt')
+ system2(
f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe')
os.chdir('../..')
if os.path.exists('./rustdesk_portable.exe'):
@@ -373,22 +368,15 @@ def main():
parser = make_parser()
args = parser.parse_args()
- shutil.copy2('Cargo.toml', 'Cargo.toml.bk')
- shutil.copy2('src/main.rs', 'src/main.rs.bk')
- if windows:
- txt = open('src/main.rs', encoding='utf8').read()
- with open('src/main.rs', 'wt', encoding='utf8') as fh:
- fh.write(txt.replace(
- '//#![windows_subsystem', '#![windows_subsystem'))
if os.path.exists(exe_path):
os.unlink(exe_path)
if os.path.isfile('/usr/bin/pacman'):
- os.system('git checkout src/ui/common.tis')
+ system2('git checkout src/ui/common.tis')
version = get_version()
features = ','.join(get_features(args))
flutter = args.flutter
if not flutter:
- os.system('python3 res/inline-sciter.py')
+ system2('python3 res/inline-sciter.py')
print(args.skip_cargo)
if args.skip_cargo:
skip_cargo = True
@@ -396,55 +384,55 @@ def main():
if windows:
# build virtual display dynamic library
os.chdir('libs/virtual_display/dylib')
- os.system('cargo build --release')
+ system2('cargo build --release')
os.chdir('../../..')
if flutter:
build_flutter_windows(version, features)
return
- os.system('cargo build --release --features ' + features)
- # os.system('upx.exe target/release/rustdesk.exe')
- os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe')
+ system2('cargo build --release --features ' + features)
+ # system2('upx.exe target/release/rustdesk.exe')
+ system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
pa = os.environ.get('P')
if pa:
- os.system(
+ system2(
f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com '
'target\\release\\rustdesk.exe')
else:
print('Not signed')
- os.system(
+ system2(
f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe')
elif os.path.isfile('/usr/bin/pacman'):
# pacman -S -needed base-devel
- os.system("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)
+ system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)
if flutter:
build_flutter_arch_manjaro(version, features)
else:
- os.system('cargo build --release --features ' + features)
- os.system('git checkout src/ui/common.tis')
- os.system('strip target/release/rustdesk')
- os.system('ln -s res/pacman_install && ln -s res/PKGBUILD')
- os.system('HBB=`pwd` makepkg -f')
- os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (
+ system2('cargo build --release --features ' + features)
+ system2('git checkout src/ui/common.tis')
+ system2('strip target/release/rustdesk')
+ system2('ln -s res/pacman_install && ln -s res/PKGBUILD')
+ system2('HBB=`pwd` makepkg -f')
+ system2('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (
version, version))
# pacman -U ./rustdesk.pkg.tar.zst
elif os.path.isfile('/usr/bin/yum'):
- os.system('cargo build --release --features ' + features)
- os.system('strip target/release/rustdesk')
- os.system(
+ system2('cargo build --release --features ' + features)
+ system2('strip target/release/rustdesk')
+ system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version)
- os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec')
- os.system(
+ system2('HBB=`pwd` rpmbuild -ba res/rpm.spec')
+ system2(
'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % (
version, version))
# yum localinstall rustdesk.rpm
elif os.path.isfile('/usr/bin/zypper'):
- os.system('cargo build --release --features ' + features)
- os.system('strip target/release/rustdesk')
- os.system(
+ system2('cargo build --release --features ' + features)
+ system2('strip target/release/rustdesk')
+ system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version)
- os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec')
- os.system(
+ system2('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec')
+ system2(
'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % (
version, version))
# yum localinstall rustdesk.rpm
@@ -454,18 +442,18 @@ def main():
build_flutter_dmg(version, features)
pass
else:
- # os.system(
+ # system2(
# 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb')
build_flutter_deb(version, features)
else:
- os.system('cargo bundle --release --features ' + features)
+ system2('cargo bundle --release --features ' + features)
if osx:
- os.system(
+ system2(
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')
- os.system(
+ system2(
'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
# https://github.com/sindresorhus/create-dmg
- os.system('/bin/rm -rf *.dmg')
+ system2('/bin/rm -rf *.dmg')
plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist"
txt = open(plist).read()
with open(plist, "wt") as fh:
@@ -475,7 +463,7 @@ def main():
"""))
pa = os.environ.get('P')
if pa:
- os.system('''
+ system2('''
# buggy: rcodesign sign ... path/*, have to sign one by one
# install rcodesign via cargo install apple-codesign
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk
@@ -485,11 +473,11 @@ def main():
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/*
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app
'''.format(pa))
- os.system('create-dmg target/release/bundle/osx/RustDesk.app')
+ system2('create-dmg target/release/bundle/osx/RustDesk.app')
os.rename('RustDesk %s.dmg' %
version, 'rustdesk-%s.dmg' % version)
if pa:
- os.system('''
+ system2('''
# https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html
# https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html
# https://developer.apple.com/developer-id/
@@ -506,34 +494,32 @@ def main():
print('Not signed')
else:
# buid deb package
- os.system(
+ system2(
'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
- os.system('dpkg-deb -R rustdesk.deb tmpdeb')
- os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
- os.system(
+ system2('dpkg-deb -R rustdesk.deb tmpdeb')
+ system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
+ system2(
'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
- os.system(
+ system2(
'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png')
- os.system(
+ system2(
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
- os.system(
+ system2(
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
- os.system('cp -a res/DEBIAN/* tmpdeb/DEBIAN/')
- os.system('strip tmpdeb/usr/bin/rustdesk')
- os.system('mkdir -p tmpdeb/usr/lib/rustdesk')
- os.system('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
- os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
+ system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/')
+ system2('strip tmpdeb/usr/bin/rustdesk')
+ system2('mkdir -p tmpdeb/usr/lib/rustdesk')
+ system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/')
+ system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/')
md5_file('usr/share/rustdesk/files/systemd/rustdesk.service')
md5_file('usr/lib/rustdesk/libsciter-gtk.so')
- os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
+ system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
- os.system("mv Cargo.toml.bk Cargo.toml")
- os.system("mv src/main.rs.bk src/main.rs")
def md5_file(fn):
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest()
- os.system('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
+ system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
if __name__ == "__main__":
diff --git a/build.rs b/build.rs
index ade63f0bc..d15f27424 100644
--- a/build.rs
+++ b/build.rs
@@ -85,26 +85,35 @@ fn install_oboe() {
#[cfg(feature = "flutter")]
fn gen_flutter_rust_bridge() {
+ use lib_flutter_rust_bridge_codegen::{
+ config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts,
+ };
let llvm_path = match std::env::var("LLVM_HOME") {
Ok(path) => Some(vec![path]),
Err(_) => None,
};
// Tell Cargo that if the given file changes, to rerun this build script.
println!("cargo:rerun-if-changed=src/flutter_ffi.rs");
- // settings for fbr_codegen
- let opts = lib_flutter_rust_bridge_codegen::Opts {
+ // Options for frb_codegen
+ let raw_opts = RawOpts {
// Path of input Rust code
- rust_input: "src/flutter_ffi.rs".to_string(),
+ rust_input: vec!["src/flutter_ffi.rs".to_string()],
// Path of output generated Dart code
- dart_output: "flutter/lib/generated_bridge.dart".to_string(),
+ dart_output: vec!["flutter/lib/generated_bridge.dart".to_string()],
// Path of output generated C header
c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]),
- // for other options lets use default
+ /// Path to the installed LLVM
llvm_path,
+ // for other options use defaults
..Default::default()
};
- // run fbr_codegen
- lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap();
+ // get opts from raw opts
+ let configs = config_parse(raw_opts);
+ // generation of rust api for ffi
+ let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap();
+ for config in configs.iter() {
+ frb_codegen(config, &all_symbols).unwrap();
+ }
}
fn main() {
diff --git a/docs/CONTRIBUTING-DE.md b/docs/CONTRIBUTING-DE.md
new file mode 100644
index 000000000..6258a9a7a
--- /dev/null
+++ b/docs/CONTRIBUTING-DE.md
@@ -0,0 +1,50 @@
+# Beitrge zu RustDesk
+
+RustDesk begrt Beitrge von jedem. Hier sind die Richtlinien, wenn Sie uns
+helfen mchten:
+
+## Beitrge
+
+Beitrge zu RustDesk oder seinen Abhngigkeiten sollten in Form von Pull
+Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur
+(jemand mit der Erlaubnis, Korrekturen einzubringen) geprft und entweder in den
+Hauptbaum eingefgt oder Feedback fr notwendige nderungen gegeben. Alle
+Beitrge sollten diesem Format folgen, auch die von Hauptakteuren.
+
+Wenn Sie an einem Problem arbeiten mchten, melden Sie es bitte zuerst an, indem
+Sie auf GitHub erklren, dass Sie daran arbeiten mchten. Damit soll verhindert
+werden, dass Beitrge zum gleichen Thema doppelt bearbeitet werden.
+
+## Checkliste fr Pull Requests
+
+- Verzweigen Sie sich vom Master-Branch und, falls ntig, wechseln Sie zum
+ aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das
+ Zusammenfhren mit dem Master nicht reibungslos funktioniert, werden Sie
+ mglicherweise aufgefordert, Ihre nderungen zu berarbeiten.
+
+- Commits sollten so klein wie mglich sein und gleichzeitig sicherstellen, dass
+ jeder Commit unabhngig voneinander korrekt ist (d. h., jeder Commit sollte
+ sich bersetzen lassen und Tests bestehen).
+
+- Commits sollten von einem "Herkunftszertifikat fr Entwickler"
+ (https://developercertificate.org) begleitet werden, das besagt, dass Sie (und
+ ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE)
+ einverstanden sind. In Git ist dies die Option `-s` fr `git commit`.
+
+- Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur
+ Begutachtung bentigen, knnen Sie einem Gutachter mit @ antworten und um eine
+ Begutachtung des Pull Requests oder einen Kommentar bitten. Sie knnen auch
+ per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten.
+
+- Fgen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue
+ Funktion beziehen.
+
+Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow).
+
+## Verhalten
+
+https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md
+
+## Kommunikation
+
+RustDesk-Mitarbeiter arbeiten hufig im [Discord](https://discord.gg/nDceKgxnkV).
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index f3165a684..31fd632e6 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -35,7 +35,7 @@ efforts from contributors on the same issue.
- Add tests relevant to the fixed bug or new feature.
-For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow).
+For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
## Conduct
diff --git a/docs/DEVCONTAINER-DE.md b/docs/DEVCONTAINER-DE.md
new file mode 100644
index 000000000..2a0d73f17
--- /dev/null
+++ b/docs/DEVCONTAINER-DE.md
@@ -0,0 +1,14 @@
+
+Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Binrprogramm im Debug-Modus erstellt.
+
+Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an.
+
+Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgefhrt werden mssen, um bestimmte Builds zu erstellen.
+
+Kommando|Build-Typ|Modus
+-|-|-|
+`.devcontainer/build.sh --debug linux`|Linux|debug
+`.devcontainer/build.sh --release linux`|Linux|release
+`.devcontainer/build.sh --debug android`|android-arm64|debug
+`.devcontainer/build.sh --release android`|android-arm64|release
+
diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md
new file mode 100644
index 000000000..067e0ecf9
--- /dev/null
+++ b/docs/DEVCONTAINER.md
@@ -0,0 +1,14 @@
+
+After the start of devcontainer in docker container, a linux binary in debug mode is created.
+
+Currently devcontainer offers linux and android builds in both debug and release mode.
+
+Below is the table on commands to run from root of the project for creating specific builds.
+
+Command|Build Type|Mode
+-|-|-|
+`.devcontainer/build.sh --debug linux`|Linux|debug
+`.devcontainer/build.sh --release linux`|Linux|release
+`.devcontainer/build.sh --debug android`|android-arm64|debug
+`.devcontainer/build.sh --release android`|android-arm64|debug
+
diff --git a/docs/README-DE.md b/docs/README-DE.md
index 0b51d8fdd..dd2aa8609 100644
--- a/docs/README-DE.md
+++ b/docs/README-DE.md
@@ -1,63 +1,92 @@
- Server •
- Kompilieren •
+ Server •
+ Kompilieren •
Docker •
Dateistruktur •
Screenshots
- [English ] | [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ]
- Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren
+ [English ] | [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [فارسی ] | [Français ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ] | [Dansk ]
+ Wir brauchen deine Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in deine Muttersprache zu übersetzen.
-Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
[](https://ko-fi.com/I2I04VU09)
-Das hier ist ein Programm was, man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/server) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
+RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Du hast die volle Kontrolle über deine Daten und musst dir keine Sorgen um die Sicherheit machen. Du kannst unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo).
-RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start.
+
-[**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
+RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [CONTRIBUTING-DE.md](CONTRIBUTING-DE.md) an, wenn du Unterstützung beim Start brauchst.
-## Kostenlose öffentliche Server
+[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
-Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird.
+[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases)
-| Standort | Serverart | Spezifikationen | Kommentare |
-| --------- | ------------- | ------------------ | ---------- |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | |
-| Germany | Codext | 2 vCPU / 4GB RAM |
-| Germany | Hetzner | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
+[**Nächtliche Erstellung**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+
+[ ](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+
+## Freie öffentliche Server
+
+Nachfolgend sind die Server gelistet, die du kostenlos nutzen kannst. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird.
+| Standort | Anbieter | Spezifikation |
+| --------- | ------------- | ------------------ |
+| Südkorea (Seoul) | AWS lightsail | 1 vCPU / 0,5 GB RAM |
+| Deutschland | Hetzner | 2 vCPU / 4 GB RAM |
+| Deutschland | Codext | 4 vCPU / 8 GB RAM |
+| Finnland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM |
+| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8 GB RAM |
+| Ukraine (Kiew) | dc.volia (2VM) | 2 vCPU / 4 GB RAM |
+
+## Dev-Container
+
+[](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
+
+Wenn du VS Code und Docker bereits installiert hast, kannst du auf das Abzeichen oben klicken, um loszulegen. Wenn du darauf klickst, wird VS Code automatisch die Dev-Container-Erweiterung installieren, den Quellcode in ein Container-Volume klonen und einen Dev-Container für die Verwendung aufsetzen.
+
+Weitere Informationen findest du in [DEVCONTAINER-DE.md](DEVCONTAINER-DE.md).
## Abhängigkeiten
-Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek selbst herunter.
+Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter.
+
+Bitte lade die dynamische Bibliothek Sciter selbst herunter.
[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)
+[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
-## Die groben Schritte zum Kompilieren
+## Grobe Schritte zum Kompilieren
-- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor
+- Bereite deine Rust-Entwicklungsumgebung und C++-Build-Umgebung vor
-- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu
+- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die Systemumgebungsvariable `VCPKG_ROOT` hinzu
- Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
- - Linux/MacOS: `vcpkg install libvpx libyuv opus`
+ - Linux/macOS: `vcpkg install libvpx libyuv opus`
- Nutze `cargo run`
+## [Erstellen](https://rustdesk.com/docs/de/dev/build/)
+
## Kompilieren auf Linux
### Ubuntu 18 (Debian 10)
```sh
-sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake
+sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \
+ libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \
+ libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
+### openSUSE Tumbleweed
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
+```
### Fedora 28 (CentOS 8)
```sh
@@ -82,7 +111,7 @@ export VCPKG_ROOT=$HOME/vcpkg
vcpkg/vcpkg install libvpx libyuv opus
```
-### libvpx reparieren (Für Fedora)
+### libvpx reparieren (für Fedora)
```sh
cd vcpkg/buildtrees/libvpx/src
@@ -105,16 +134,40 @@ cd rustdesk
mkdir -p target/debug
wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
mv libsciter-gtk.so target/debug
-cargo run
+VCPKG_ROOT=$HOME/vcpkg cargo run
```
-### Ändere Wayland zu X11 (Xorg)
+### Wayland zu X11 (Xorg) ändern
-RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen.
+RustDesk unterstützt Wayland nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard-GNOME-Sitzung zu nutzen.
-## Auf Docker Kompilieren
+## Wayland-Unterstützung
-Beginne damit das Repository zu klonen und den Docker Container zu bauen:
+Wayland scheint keine API für das Senden von Tastatureingaben an andere Fenster zu bieten. Daher verwendet RustDesk eine API von einer niedrigeren Ebene, nämlich dem Gerät `/dev/uinput` (Linux-Kernelebene).
+
+Wenn Wayland die kontrollierte Seite ist, müssen Sie wie folgt vorgehen:
+```bash
+# Dienst uinput starten
+$ sudo rustdesk --service
+$ rustdesk
+```
+**Hinweis**: Die Wayland-Bildschirmaufnahme verwendet verschiedene Schnittstellen. RustDesk unterstützt derzeit nur org.freedesktop.portal.ScreenCast.
+```bash
+$ dbus-send --session --print-reply \
+ --dest=org.freedesktop.portal.Desktop \
+ /org/freedesktop/portal/desktop \
+ org.freedesktop.DBus.Properties.Get \
+ string:org.freedesktop.portal.ScreenCast string:version
+# Keine Unterstützung
+Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
+# Unterstützung
+method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
+ variant uint32 4
+```
+
+## Auf Docker kompilieren
+
+Beginne damit, das Repository zu klonen und den Docker-Container zu bauen:
```sh
git clone https://github.com/rustdesk/rustdesk
@@ -122,13 +175,13 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
-Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl:
+Führe jedes Mal, wenn du das Programm kompilieren musst, folgenden Befehl aus:
```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
```
-Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Darauf folgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen:
+Bedenke, dass das erste Kompilieren länger dauern kann, bis die Abhängigkeiten zwischengespeichert sind. Nachfolgende Kompiliervorgänge sind schneller. Wenn du verschiedene Argumente für den Kompilierbefehl angeben musst, kannst du dies am Ende des Befehls an der Position `` tun. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du `--release` am Ende des Befehls anhängen. Das daraus entstehende Programm findest du im Zielordner auf deinem System. Du kannst es mit folgendem Befehl ausführen:
```sh
target/debug/rustdesk
@@ -140,18 +193,20 @@ Oder, wenn du eine Releaseversion benutzt:
target/release/rustdesk
```
-Bitte gehe sicher, dass du diese Befehle vom Stammverzeichnis vom RustDesk Repository nutzt, sonst kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass Unterbefehle von Cargo, wie z. B. `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System.
+Bitte stelle sicher, dass du diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzt. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenke auch, dass andere Cargo-Unterbefehle wie `install` oder `run` aktuell noch nicht unterstützt werden, da sie das Programm innerhalb des Containers starten oder installieren würden, anstatt auf deinem eigentlichen System.
## Dateistruktur
-- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer, und ein paar andere nützliche Funktionen
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme
-- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus und Tastatur Steuerung
+- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatursteuerung
- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
-- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerkverbindungen
- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung
-- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, für Verbindung von außen warten, direkt (TCP hole punching) oder weitergeleitet
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient
## Screenshots
diff --git a/docs/README-FA.md b/docs/README-FA.md
index d86c82836..496e81849 100644
--- a/docs/README-FA.md
+++ b/docs/README-FA.md
@@ -1,60 +1,60 @@
-
+
- اسنپ شات •
- ساختار •
- داکر •
- ساخت •
- سرور
- [English ] | [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ]
- برای ترجمه این RustDesk UI ،README و Doc به زبان مادری شما به کمکتون نیاز داریم
+ تصاویر محیط نرمافزار •
+ ساختار •
+ داکر •
+ ساخت •
+ سرور
+[English ] | [Українська ] | [česky ] | [中文 ] | [Magyar ] | [Español ] | [Français ] | [Deutsch ] | [Polski ] | [Indonesian ] | [Suomi ] | [മലയാളം ] | [日本語 ] | [Nederlands ] | [Italiano ] | [Русский ] | [Português (Brasil) ] | [Esperanto ] | [한국어 ] | [العربي ] | [Tiếng Việt ]
+برای ترجمه این سند (README)، رابط کاربری RustDesk ، و مستندات آن به زبان مادری شما به کمکتان نیازمندیم.
-با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
+با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV)
[](https://ko-fi.com/I2I04VU09)
-یک نرم افزار دیگر کنترل دسکتاپ از راه دور، که با Rust نوشته شده است. راه اندازی سریع وبدون نیاز به تنظیمات. شما کنترل کاملی بر داده های خود دارید، بدون هیچ گونه نگرانی امنیتی.
+راستدسک (RustDesk) نرمافزاری برای کارکردن با رایانهی رومیزی از راه دور است و با زبان برنامهنویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آنها کنترل کامل داشته باشید.
+
میتوانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راهاندازی کنید](https://rustdesk.com/server) یا
[ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk).
-راست دسک (RustDesk) از مشارکت همه استقبال می کند. برای راهنمایی جهت مشارکت به [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید.
+ما از مشارکت همه استقبال می کنیم. برای راهنمایی جهت مشارکت به[`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید.
-[راست دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
+[راستدسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F)
-[دانلود باینری](https://github.com/rustdesk/rustdesk/releases)
+[دریافت نرمافزار](https://github.com/rustdesk/rustdesk/releases)
## سرورهای عمومی رایگان
-سرورهایی زیر را به صورت رایگان میتوانید استفاده می کنید. این لیست ممکن است در طول زمان تغییر کند. اگر به این سرورها نزدیک نیستید، ممکن است سرویس شما کند شود.
+شما ميتوانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر میکند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد.
| موقعیت | سرویس دهنده | مشخصات |
| --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
-| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
+| کرهی جنوبی، سئول | AWS lightsail | 1 vCPU / 0.5GB RAM |
+| آلمان | Hetzner | 2 vCPU / 4GB RAM |
+| آلمان | Codext | 4 vCPU / 8GB RAM |
+| فنلاند، هلسینکی | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
+| ایالات متحده، اَشبرن | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
## وابستگی ها
-نسخههای دسکتاپ از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده میکنند، لطفا کتابخانه پویا sciter را خودتان دانلود کنید.
+نسخههای رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده میکنند. خواهشمندیم کتابخانهی پویای sciter را خودتان دانلود کنید از این منابع دریافت کنید.
-[ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) |
-[لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) |
-[مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
+- [ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll)
+- [لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so)
+- [مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib)
-نسخه های موبایل از Flutter استفاده می کنند. بعداً نسخه دسکتاپ را از Sciter به Flutter منتقل خواهیم کرد.
+نسخه های همراه از Flutter استفاده می کنند. نسخهی رومیزی را هم از Sciter به Flutter منتقل خواهیم کرد.
-## مراحل بنیادین برای ساخت
+## نیازمندیهای ساخت
-- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید
+- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید
-- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید:
-
- - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
- - Linux/MacOS: `vcpkg install libvpx libyuv opus`
-
-- run `cargo run`
+- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید.
+- بستههای vcpkg مورد نیاز را نصب کنید:
+ - ویندوز: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static`
+ - مک و لینوکس: `vcpkg install libvpx libyuv opus`
+- این دستور را اجرا کنید: `cargo run`
## [ساخت](https://rustdesk.com/docs/en/dev/build/)
@@ -118,11 +118,11 @@ VCPKG_ROOT=$HOME/vcpkg cargo run
### تغییر Wayland به (X11 (Xorg
-راست دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیشفرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید.
+راستدسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیشفرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید.
## نحوه ساخت با داکر
-این مخزن گیت را کلون کنید و کانتینر را به روش زیر بسازید
+این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید
```sh
git clone https://github.com/rustdesk/rustdesk
@@ -130,13 +130,13 @@ cd rustdesk
docker build -t "rustdesk-builder" .
```
-سپس، هر بار که نیاز به ساخت اپلیکیشن داشتید، دستور زیر را اجرا کنید:
+سپس، هر بار که نیاز به ساخت نرمافزار داشتید، دستور زیر را اجرا کنید:
```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
```
-توجه داشته باشید که ساخت اول ممکن است قبل از کش شدن وابستگی ها بیشتر طول بکشد، دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور:
+توجه داشته باشید که نخستین ساخت ممکن است به دلیل محلی نبودن وابستگیها بیشتر طول بکشد. اما دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور:
```sh
target/debug/rustdesk
@@ -163,7 +163,7 @@ target/release/rustdesk
- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile
- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client
-## اسکرین شات ها
+## تصاویر محیط نرمافزار

diff --git a/docs/README-JP.md b/docs/README-JP.md
index 6d3b6d380..36c74dfed 100644
--- a/docs/README-JP.md
+++ b/docs/README-JP.md
@@ -14,7 +14,7 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt
[](https://ko-fi.com/I2I04VU09)
-Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます。](https://github.com/rustdesk/rustdesk-server-demo).
+Rustで書かれた、設定不要ですぐに使えるリモートデスクトップソフトウェアです。自分のデータを完全にコントロールでき、セキュリティの心配もありません。私たちのランデブー/リレーサーバを使うことも、[自分で設定する](https://rustdesk.com/server) ことも、 [自分でランデブー/リレーサーバを書くこともできます](https://github.com/rustdesk/rustdesk-server-demo)。

@@ -58,7 +58,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON
-## [Build](https://rustdesk.com/docs/en/dev/build/)
+## [ビルド](https://rustdesk.com/docs/en/dev/build/)
## Linuxでのビルド手順
@@ -105,7 +105,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
cd
```
-### Build
+### ビルド
```sh
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
@@ -154,7 +154,7 @@ target/release/rustdesk
これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。
-## File Structure
+## ファイル構造
- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数
- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ
@@ -165,7 +165,7 @@ target/release/rustdesk
- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。
- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード
-## Snapshot
+## スナップショット

diff --git a/docs/SECURITY.md b/docs/SECURITY.md
index f1114f913..c595885f2 100644
--- a/docs/SECURITY.md
+++ b/docs/SECURITY.md
@@ -1,13 +1,9 @@
# Security Policy
-## Supported Versions
-
-| Version | Supported |
-| --------- | ------------------ |
-| 1.1.x | :white_check_mark: |
-| 1.x | :white_check_mark: |
-| Below 1.0 | :x: |
-
## Reporting a Vulnerability
-Here we should write what to do in case of a security vulnerability
+We value security for the project very highly. We encourage all users to report any vulnerabilities they discover to us.
+If you find a security vulnerability in the RustDesk project, please report it responsibly by sending an email to info@rustdesk.com.
+
+At this juncture, we don't have a bug bounty program. We are a small team trying to solve a big problem. We urge you to report any vulnerabilities responsibly
+so that we can continue building a secure application for the entire community.
diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt
index 1f35ef92d..f78b3a20b 100644
--- a/fastlane/metadata/android/en-US/full_description.txt
+++ b/fastlane/metadata/android/en-US/full_description.txt
@@ -2,7 +2,7 @@ An open-source remote desktop application, the open source TeamViewer alternativ
Source code: https://github.com/rustdesk/rustdesk
Doc: https://rustdesk.com/docs/en/manual/mobile/
-In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Addroid remote control.
+In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Android remote control.
In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk.
diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml
index 04b2ccc9a..b3c655917 100644
--- a/flutter/android/app/src/main/AndroidManifest.xml
+++ b/flutter/android/app/src/main/AndroidManifest.xml
@@ -11,19 +11,25 @@
-
+
+
+ android:requestLegacyExternalStorage="true"
+ android:roundIcon="@mipmap/ic_launcher"
+ android:supportsRtl="true">
+ android:enabled="true"
+ android:exported="true">
+
+
+
@@ -50,8 +56,6 @@
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
-
-
@@ -59,6 +63,11 @@
+
+
-
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt
index 328701567..71bbba754 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt
@@ -1,21 +1,45 @@
package com.carriez.flutter_hbb
+import android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
+import android.Manifest.permission.SYSTEM_ALERT_WINDOW
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
+import android.util.Log
import android.widget.Toast
+import com.hjq.permissions.XXPermissions
+import io.flutter.embedding.android.FlutterActivity
+
+const val DEBUG_BOOT_COMPLETED = "com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED"
class BootReceiver : BroadcastReceiver() {
+ private val logTag = "tagBootReceiver"
+
override fun onReceive(context: Context, intent: Intent) {
- if ("android.intent.action.BOOT_COMPLETED" == intent.action){
- val it = Intent(context,MainService::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ Log.d(logTag, "onReceive ${intent.action}")
+
+ if (Intent.ACTION_BOOT_COMPLETED == intent.action || DEBUG_BOOT_COMPLETED == intent.action) {
+ // check SharedPreferences config
+ val prefs = context.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
+ if (!prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) {
+ Log.d(logTag, "KEY_START_ON_BOOT_OPT is false")
+ return
}
- Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show();
+ // check pre-permission
+ if (!XXPermissions.isGranted(context, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, SYSTEM_ALERT_WINDOW)){
+ Log.d(logTag, "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS or SYSTEM_ALERT_WINDOW is not granted")
+ return
+ }
+
+ val it = Intent(context, MainService::class.java).apply {
+ action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE
+ putExtra(EXT_INIT_FROM_BOOT, true)
+ }
+ Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(it)
- }else{
+ } else {
context.startService(it)
}
}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
index fd340f7ed..52a5ff75e 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt
@@ -7,35 +7,29 @@ package com.carriez.flutter_hbb
* Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG
*/
-import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
-import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.IBinder
-import android.provider.Settings
import android.util.Log
import android.view.WindowManager
-import androidx.annotation.RequiresApi
+import com.hjq.permissions.XXPermissions
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
-const val MEDIA_REQUEST_CODE = 42
class MainActivity : FlutterActivity() {
companion object {
- lateinit var flutterMethodChannel: MethodChannel
+ var flutterMethodChannel: MethodChannel? = null
}
private val channelTag = "mChannel"
private val logTag = "mMainActivity"
- private var mediaProjectionResultIntent: Intent? = null
private var mainService: MainService? = null
- @RequiresApi(Build.VERSION_CODES.M)
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
if (MainService.isReady) {
@@ -46,169 +40,32 @@ class MainActivity : FlutterActivity() {
flutterMethodChannel = MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
channelTag
- ).apply {
- // make sure result is set, otherwise flutter will await forever
- setMethodCallHandler { call, result ->
- when (call.method) {
- "init_service" -> {
- Intent(activity, MainService::class.java).also {
- bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
- }
- if (MainService.isReady) {
- result.success(false)
- return@setMethodCallHandler
- }
- getMediaProjection()
- result.success(true)
- }
- "start_capture" -> {
- mainService?.let {
- result.success(it.startCapture())
- } ?: let {
- result.success(false)
- }
- }
- "stop_service" -> {
- Log.d(logTag, "Stop service")
- mainService?.let {
- it.destroy()
- result.success(true)
- } ?: let {
- result.success(false)
- }
- }
- "check_permission" -> {
- if (call.arguments is String) {
- result.success(checkPermission(context, call.arguments as String))
- } else {
- result.success(false)
- }
- }
- "request_permission" -> {
- if (call.arguments is String) {
- requestPermission(context, call.arguments as String)
- result.success(true)
- } else {
- result.success(false)
- }
- }
- "check_video_permission" -> {
- mainService?.let {
- result.success(it.checkMediaPermission())
- } ?: let {
- result.success(false)
- }
- }
- "check_service" -> {
- flutterMethodChannel.invokeMethod(
- "on_state_changed",
- mapOf("name" to "input", "value" to InputService.isOpen.toString())
- )
- flutterMethodChannel.invokeMethod(
- "on_state_changed",
- mapOf("name" to "media", "value" to MainService.isReady.toString())
- )
- result.success(true)
- }
- "init_input" -> {
- initInput()
- result.success(true)
- }
- "stop_input" -> {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- InputService.ctx?.disableSelf()
- }
- InputService.ctx = null
- flutterMethodChannel.invokeMethod(
- "on_state_changed",
- mapOf("name" to "input", "value" to InputService.isOpen.toString())
- )
- result.success(true)
- }
- "cancel_notification" -> {
- try {
- val id = call.arguments as Int
- mainService?.cancelNotification(id)
- } finally {
- result.success(true)
- }
- }
- "enable_soft_keyboard" -> {
- // https://blog.csdn.net/hanye2020/article/details/105553780
- try {
- if (call.arguments as Boolean) {
- window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
- } else {
- window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
- }
- } finally {
- result.success(true)
- }
- }
- else -> {
- result.error("-1", "No such method", null)
- }
- }
- }
- }
- }
-
- private fun getMediaProjection() {
- val mMediaProjectionManager =
- getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
- val mIntent = mMediaProjectionManager.createScreenCaptureIntent()
- startActivityForResult(mIntent, MEDIA_REQUEST_CODE)
- }
-
- private fun initService() {
- if (mediaProjectionResultIntent == null) {
- Log.w(logTag, "initService fail,mediaProjectionResultIntent is null")
- return
- }
- Log.d(logTag, "Init service")
- val serviceIntent = Intent(this, MainService::class.java)
- serviceIntent.action = INIT_SERVICE
- serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent)
-
- launchMainService(serviceIntent)
- }
-
- private fun launchMainService(intent: Intent) {
- // TEST api < O
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- startForegroundService(intent)
- } else {
- startService(intent)
- }
- }
-
- private fun initInput() {
- val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
- if (intent.resolveActivity(packageManager) != null) {
- startActivity(intent)
- }
+ )
+ initFlutterChannel(flutterMethodChannel!!)
}
override fun onResume() {
super.onResume()
val inputPer = InputService.isOpen
activity.runOnUiThread {
- flutterMethodChannel.invokeMethod(
+ flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to inputPer.toString())
)
}
}
+ private fun requestMediaProjection() {
+ val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
+ action = ACT_REQUEST_MEDIA_PROJECTION
+ }
+ startActivityForResult(intent, REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION)
+ }
+
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == MEDIA_REQUEST_CODE) {
- if (resultCode == Activity.RESULT_OK && data != null) {
- mediaProjectionResultIntent = data
- initService()
- } else {
- flutterMethodChannel.invokeMethod("on_media_projection_canceled", null)
- }
+ if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) {
+ flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null)
}
}
@@ -232,4 +89,138 @@ class MainActivity : FlutterActivity() {
mainService = null
}
}
+
+ private fun initFlutterChannel(flutterMethodChannel: MethodChannel) {
+ flutterMethodChannel.setMethodCallHandler { call, result ->
+ // make sure result will be invoked, otherwise flutter will await forever
+ when (call.method) {
+ "init_service" -> {
+ Intent(activity, MainService::class.java).also {
+ bindService(it, serviceConnection, Context.BIND_AUTO_CREATE)
+ }
+ if (MainService.isReady) {
+ result.success(false)
+ return@setMethodCallHandler
+ }
+ requestMediaProjection()
+ result.success(true)
+ }
+ "start_capture" -> {
+ mainService?.let {
+ result.success(it.startCapture())
+ } ?: let {
+ result.success(false)
+ }
+ }
+ "stop_service" -> {
+ Log.d(logTag, "Stop service")
+ mainService?.let {
+ it.destroy()
+ result.success(true)
+ } ?: let {
+ result.success(false)
+ }
+ }
+ "check_permission" -> {
+ if (call.arguments is String) {
+ result.success(XXPermissions.isGranted(context, call.arguments as String))
+ } else {
+ result.success(false)
+ }
+ }
+ "request_permission" -> {
+ if (call.arguments is String) {
+ requestPermission(context, call.arguments as String)
+ result.success(true)
+ } else {
+ result.success(false)
+ }
+ }
+ START_ACTION -> {
+ if (call.arguments is String) {
+ startAction(context, call.arguments as String)
+ result.success(true)
+ } else {
+ result.success(false)
+ }
+ }
+ "check_video_permission" -> {
+ mainService?.let {
+ result.success(it.checkMediaPermission())
+ } ?: let {
+ result.success(false)
+ }
+ }
+ "check_service" -> {
+ Companion.flutterMethodChannel?.invokeMethod(
+ "on_state_changed",
+ mapOf("name" to "input", "value" to InputService.isOpen.toString())
+ )
+ Companion.flutterMethodChannel?.invokeMethod(
+ "on_state_changed",
+ mapOf("name" to "media", "value" to MainService.isReady.toString())
+ )
+ result.success(true)
+ }
+ "stop_input" -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ InputService.ctx?.disableSelf()
+ }
+ InputService.ctx = null
+ Companion.flutterMethodChannel?.invokeMethod(
+ "on_state_changed",
+ mapOf("name" to "input", "value" to InputService.isOpen.toString())
+ )
+ result.success(true)
+ }
+ "cancel_notification" -> {
+ if (call.arguments is Int) {
+ val id = call.arguments as Int
+ mainService?.cancelNotification(id)
+ } else {
+ result.success(true)
+ }
+ }
+ "enable_soft_keyboard" -> {
+ // https://blog.csdn.net/hanye2020/article/details/105553780
+ if (call.arguments as Boolean) {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
+ } else {
+ window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
+ }
+ result.success(true)
+
+ }
+ GET_START_ON_BOOT_OPT -> {
+ val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
+ result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false))
+ }
+ SET_START_ON_BOOT_OPT -> {
+ if (call.arguments is Boolean) {
+ val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
+ val edit = prefs.edit()
+ edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean)
+ edit.apply()
+ result.success(true)
+ } else {
+ result.success(false)
+ }
+ }
+ SYNC_APP_DIR_CONFIG_PATH -> {
+ if (call.arguments is String) {
+ val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE)
+ val edit = prefs.edit()
+ edit.putString(KEY_APP_DIR_CONFIG_PATH, call.arguments as String)
+ edit.apply()
+ result.success(true)
+ } else {
+ result.success(false)
+ }
+ }
+ else -> {
+ result.error("-1", "No such method", null)
+ }
+ }
+ }
+ }
}
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
index ac736ffdc..fa7440c8d 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
@@ -35,6 +35,7 @@ import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
+import io.flutter.embedding.android.FlutterActivity
import java.util.concurrent.Executors
import kotlin.concurrent.thread
import org.json.JSONException
@@ -43,10 +44,6 @@ import java.nio.ByteBuffer
import kotlin.math.max
import kotlin.math.min
-const val EXTRA_MP_DATA = "mp_intent"
-const val INIT_SERVICE = "init_service"
-const val ACTION_LOGIN_REQ_NOTIFY = "ACTION_LOGIN_REQ_NOTIFY"
-const val EXTRA_LOGIN_REQ_NOTIFY = "EXTRA_LOGIN_REQ_NOTIFY"
const val DEFAULT_NOTIFY_TITLE = "RustDesk"
const val DEFAULT_NOTIFY_TEXT = "Service is running"
@@ -147,7 +144,11 @@ class MainService : Service() {
// jvm call rust
private external fun init(ctx: Context)
- private external fun startServer()
+
+ /// 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
@@ -195,6 +196,7 @@ class MainService : Service() {
override fun onCreate() {
super.onCreate()
+ Log.d(logTag,"MainService onCreate")
HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply {
start()
serviceLooper = looper
@@ -202,7 +204,13 @@ class MainService : Service() {
}
updateScreenInfo(resources.configuration.orientation)
initNotification()
- startServer()
+
+ // keep the config dir same with flutter
+ val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE)
+ val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: ""
+ startServer(configPath)
+
+ createForegroundNotification()
}
override fun onDestroy() {
@@ -277,22 +285,30 @@ class MainService : Service() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- Log.d("whichService", "this service:${Thread.currentThread()}")
+ Log.d("whichService", "this service: ${Thread.currentThread()}")
super.onStartCommand(intent, flags, startId)
- if (intent?.action == INIT_SERVICE) {
- Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}")
+ if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) {
createForegroundNotification()
- val mMediaProjectionManager =
+
+ if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) {
+ startService()
+ }
+ Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}")
+ val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
- intent.getParcelableExtra(EXTRA_MP_DATA)?.let {
+
+ intent.getParcelableExtra(EXT_MEDIA_PROJECTION_RES_INTENT)?.let {
mediaProjection =
- mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
+ mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it)
checkMediaPermission()
init(this)
_isReady = true
+ } ?: let {
+ Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection")
+ requestMediaProjection()
}
}
- return START_NOT_STICKY // don't use sticky (auto restart),the new service (from auto restart) will lose control
+ return START_NOT_STICKY // don't use sticky (auto restart), the new service (from auto restart) will lose control
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -300,6 +316,14 @@ class MainService : Service() {
updateScreenInfo(newConfig.orientation)
}
+ private fun requestMediaProjection() {
+ val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply {
+ action = ACT_REQUEST_MEDIA_PROJECTION
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ startActivity(intent)
+ }
+
@SuppressLint("WrongConstant")
private fun createSurface(): Surface? {
return if (useVP9) {
@@ -400,13 +424,13 @@ class MainService : Service() {
fun checkMediaPermission(): Boolean {
Handler(Looper.getMainLooper()).post {
- MainActivity.flutterMethodChannel.invokeMethod(
+ MainActivity.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "media", "value" to isReady.toString())
)
}
Handler(Looper.getMainLooper()).post {
- MainActivity.flutterMethodChannel.invokeMethod(
+ MainActivity.flutterMethodChannel?.invokeMethod(
"on_state_changed",
mapOf("name" to "input", "value" to InputService.isOpen.toString())
)
@@ -594,7 +618,7 @@ class MainService : Service() {
}
val notification = notificationBuilder
.setOngoing(true)
- .setSmallIcon(R.mipmap.ic_launcher)
+ .setSmallIcon(R.mipmap.ic_stat_logo)
.setDefaults(Notification.DEFAULT_ALL)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
@@ -653,8 +677,8 @@ class MainService : Service() {
@SuppressLint("UnspecifiedImmutableFlag")
private fun genLoginRequestPendingIntent(res: Boolean): PendingIntent {
val intent = Intent(this, MainService::class.java).apply {
- action = ACTION_LOGIN_REQ_NOTIFY
- putExtra(EXTRA_LOGIN_REQ_NOTIFY, res)
+ action = ACT_LOGIN_REQ_NOTIFY
+ putExtra(EXT_LOGIN_REQ_NOTIFY, res)
}
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE)
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt
new file mode 100644
index 000000000..3beb7ec6b
--- /dev/null
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt
@@ -0,0 +1,54 @@
+package com.carriez.flutter_hbb
+
+import android.app.Activity
+import android.content.Intent
+import android.media.projection.MediaProjectionManager
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+
+class PermissionRequestTransparentActivity: Activity() {
+ private val logTag = "permissionRequest"
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(logTag, "onCreate PermissionRequestTransparentActivity: intent.action: ${intent.action}")
+
+ when (intent.action) {
+ ACT_REQUEST_MEDIA_PROJECTION -> {
+ val mediaProjectionManager =
+ getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
+ val intent = mediaProjectionManager.createScreenCaptureIntent()
+ startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION)
+ }
+ else -> finish()
+ }
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (requestCode == REQ_REQUEST_MEDIA_PROJECTION) {
+ if (resultCode == RESULT_OK && data != null) {
+ launchService(data)
+ } else {
+ setResult(RES_FAILED)
+ }
+ }
+
+ finish()
+ }
+
+ private fun launchService(mediaProjectionResultIntent: Intent) {
+ Log.d(logTag, "Launch MainService")
+ val serviceIntent = Intent(this, MainService::class.java)
+ serviceIntent.action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE
+ serviceIntent.putExtra(EXT_MEDIA_PROJECTION_RES_INTENT, mediaProjectionResultIntent)
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ startForegroundService(serviceIntent)
+ } else {
+ startService(serviceIntent)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt
index 4bf244a06..f8ef07fd1 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt
@@ -1,5 +1,6 @@
package com.carriez.flutter_hbb
+import android.Manifest.permission.*
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
@@ -12,8 +13,8 @@ import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
-import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS
-import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
+import android.provider.Settings
+import android.provider.Settings.*
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService
import com.hjq.permissions.Permission
@@ -22,6 +23,31 @@ import java.nio.ByteBuffer
import java.util.*
+// intent action, extra
+const val ACT_REQUEST_MEDIA_PROJECTION = "REQUEST_MEDIA_PROJECTION"
+const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SERVICE"
+const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
+const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT"
+const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT"
+const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY"
+
+// Activity requestCode
+const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101
+const val REQ_REQUEST_MEDIA_PROJECTION = 201
+
+// Activity responseCode
+const val RES_FAILED = -100
+
+// Flutter channel
+const val START_ACTION = "start_action"
+const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt"
+const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt"
+const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir"
+
+const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES"
+const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT"
+const val KEY_APP_DIR_CONFIG_PATH = "KEY_APP_DIR_CONFIG_PATH"
+
@SuppressLint("ConstantLocale")
val LOCAL_NAME = Locale.getDefault().toString()
val SCREEN_INFO = Info(0, 0, 1, 200)
@@ -30,61 +56,13 @@ data class Info(
var width: Int, var height: Int, var scale: Int, var dpi: Int
)
-@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-fun testVP9Support(): Boolean {
- return true
- val res = MediaCodecList(MediaCodecList.ALL_CODECS)
- .findEncoderForFormat(
- MediaFormat.createVideoFormat(
- MediaFormat.MIMETYPE_VIDEO_VP9,
- SCREEN_INFO.width,
- SCREEN_INFO.width
- )
- )
- return res != null
-}
-
-@RequiresApi(Build.VERSION_CODES.M)
fun requestPermission(context: Context, type: String) {
- val permission = when (type) {
- "ignore_battery_optimizations" -> {
- try {
- context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
- data = Uri.parse("package:" + context.packageName)
- })
- } catch (e:Exception) {
- e.printStackTrace()
- }
- return
- }
- "application_details_settings" -> {
- try {
- context.startActivity(Intent().apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- action = "android.settings.APPLICATION_DETAILS_SETTINGS"
- data = Uri.parse("package:" + context.packageName)
- })
- } catch (e:Exception) {
- e.printStackTrace()
- }
- return
- }
- "audio" -> {
- Permission.RECORD_AUDIO
- }
- "file" -> {
- Permission.MANAGE_EXTERNAL_STORAGE
- }
- else -> {
- return
- }
- }
XXPermissions.with(context)
- .permission(permission)
+ .permission(type)
.request { _, all ->
if (all) {
Handler(Looper.getMainLooper()).post {
- MainActivity.flutterMethodChannel.invokeMethod(
+ MainActivity.flutterMethodChannel?.invokeMethod(
"on_android_permission_result",
mapOf("type" to type, "result" to all)
)
@@ -93,24 +71,18 @@ fun requestPermission(context: Context, type: String) {
}
}
-@RequiresApi(Build.VERSION_CODES.M)
-fun checkPermission(context: Context, type: String): Boolean {
- val permission = when (type) {
- "ignore_battery_optimizations" -> {
- val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager
- return pw.isIgnoringBatteryOptimizations(context.packageName)
- }
- "audio" -> {
- Permission.RECORD_AUDIO
- }
- "file" -> {
- Permission.MANAGE_EXTERNAL_STORAGE
- }
- else -> {
- return false
- }
+fun startAction(context: Context, action: String) {
+ try {
+ context.startActivity(Intent(action).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ // don't pass package name when launch ACTION_ACCESSIBILITY_SETTINGS
+ if (ACTION_ACCESSIBILITY_SETTINGS != action) {
+ data = Uri.parse("package:" + context.packageName)
+ }
+ })
+ } catch (e: Exception) {
+ e.printStackTrace()
}
- return XXPermissions.isGranted(context, permission)
}
class AudioReader(val bufSize: Int, private val maxFrames: Int) {
diff --git a/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..65291b96e
--- /dev/null
+++ b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 000000000..65291b96e
--- /dev/null
+++ b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index d5d2c49c8..d05404d3a 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..3742f241f
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 000000000..964c5faa0
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png
new file mode 100644
index 000000000..79a814f59
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png
new file mode 100644
index 000000000..814ba4549
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index e30cc5019..f16b3d61d 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..de17ccbda
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 000000000..2136a2f3c
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png
new file mode 100644
index 000000000..c179bf053
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 41ccba607..d9bd8fdfe 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..f8ced45f1
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..415eca622
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png
new file mode 100644
index 000000000..d82d1a81b
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index c10349d71..eba179347 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..0f46fafaf
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..87889c953
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png
new file mode 100644
index 000000000..2cbe6eaf1
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 52fde7830..a8d80d2a2 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 000000000..88eafe8dd
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 000000000..00709a815
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png
new file mode 100644
index 000000000..209c5f977
Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/values/ic_launcher_background.xml b/flutter/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 000000000..ab9832824
--- /dev/null
+++ b/flutter/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #ffffff
+
\ No newline at end of file
diff --git a/flutter/android/app/src/main/res/values/styles.xml b/flutter/android/app/src/main/res/values/styles.xml
index d74aa35c2..146267c91 100644
--- a/flutter/android/app/src/main/res/values/styles.xml
+++ b/flutter/android/app/src/main/res/values/styles.xml
@@ -15,4 +15,12 @@
+
diff --git a/flutter/assets/GitHub.svg b/flutter/assets/GitHub.svg
new file mode 100644
index 000000000..ef0bb12a7
--- /dev/null
+++ b/flutter/assets/GitHub.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/Github.svg b/flutter/assets/Github.svg
deleted file mode 100644
index a5bd1de81..000000000
--- a/flutter/assets/Github.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg
index b7bb2f42f..df394a84f 100644
--- a/flutter/assets/Google.svg
+++ b/flutter/assets/Google.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg
index 0fa45b93d..931e72844 100644
--- a/flutter/assets/Okta.svg
+++ b/flutter/assets/Okta.svg
@@ -1,30 +1 @@
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg
new file mode 100644
index 000000000..3049f3b89
--- /dev/null
+++ b/flutter/assets/actions.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/actions_mobile.svg b/flutter/assets/actions_mobile.svg
new file mode 100644
index 000000000..4185945e1
--- /dev/null
+++ b/flutter/assets/actions_mobile.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/android.svg b/flutter/assets/android.svg
index e46dab11e..6fd89c9ab 100644
--- a/flutter/assets/android.svg
+++ b/flutter/assets/android.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/arrow.svg b/flutter/assets/arrow.svg
new file mode 100644
index 000000000..d0f032bc2
--- /dev/null
+++ b/flutter/assets/arrow.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/call_end.svg b/flutter/assets/call_end.svg
new file mode 100644
index 000000000..7c07ee25d
--- /dev/null
+++ b/flutter/assets/call_end.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/call_wait.svg b/flutter/assets/call_wait.svg
new file mode 100644
index 000000000..530f12a97
--- /dev/null
+++ b/flutter/assets/call_wait.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/chat.svg b/flutter/assets/chat.svg
new file mode 100644
index 000000000..c4ab3c92d
--- /dev/null
+++ b/flutter/assets/chat.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/close.svg b/flutter/assets/close.svg
new file mode 100644
index 000000000..fb18eabd2
--- /dev/null
+++ b/flutter/assets/close.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/display.svg b/flutter/assets/display.svg
new file mode 100644
index 000000000..9d107d699
--- /dev/null
+++ b/flutter/assets/display.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/dots.svg b/flutter/assets/dots.svg
new file mode 100644
index 000000000..19563b849
--- /dev/null
+++ b/flutter/assets/dots.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/file.svg b/flutter/assets/file.svg
new file mode 100644
index 000000000..21c7fb9de
--- /dev/null
+++ b/flutter/assets/file.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/folder.svg b/flutter/assets/folder.svg
new file mode 100644
index 000000000..3959f7874
--- /dev/null
+++ b/flutter/assets/folder.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/folder_new.svg b/flutter/assets/folder_new.svg
new file mode 100644
index 000000000..22b729204
--- /dev/null
+++ b/flutter/assets/folder_new.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/fullscreen.svg b/flutter/assets/fullscreen.svg
new file mode 100644
index 000000000..93f27bf7b
--- /dev/null
+++ b/flutter/assets/fullscreen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/fullscreen_exit.svg b/flutter/assets/fullscreen_exit.svg
new file mode 100644
index 000000000..f244631fe
--- /dev/null
+++ b/flutter/assets/fullscreen_exit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/home.svg b/flutter/assets/home.svg
new file mode 100644
index 000000000..45a018f5d
--- /dev/null
+++ b/flutter/assets/home.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/insecure.svg b/flutter/assets/insecure.svg
index 37bb196e3..5a344dd04 100644
--- a/flutter/assets/insecure.svg
+++ b/flutter/assets/insecure.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/insecure_relay.svg b/flutter/assets/insecure_relay.svg
index f08bee6a6..17b474e6e 100644
--- a/flutter/assets/insecure_relay.svg
+++ b/flutter/assets/insecure_relay.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg
index 69f0c96cb..163e045e1 100644
--- a/flutter/assets/kb_layout_iso.svg
+++ b/flutter/assets/kb_layout_iso.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg
index 09a055be3..cfbb046ca 100644
--- a/flutter/assets/kb_layout_not_iso.svg
+++ b/flutter/assets/kb_layout_not_iso.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg
new file mode 100644
index 000000000..d72033f6d
--- /dev/null
+++ b/flutter/assets/keyboard.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg
index 74248b5f0..2c3697be9 100644
--- a/flutter/assets/linux.svg
+++ b/flutter/assets/linux.svg
@@ -1,6 +1 @@
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/flutter/assets/logo.ico b/flutter/assets/logo.ico
deleted file mode 100644
index d5080c1f7..000000000
Binary files a/flutter/assets/logo.ico and /dev/null differ
diff --git a/flutter/assets/logo.png b/flutter/assets/logo.png
deleted file mode 100644
index ede0e00c4..000000000
Binary files a/flutter/assets/logo.png and /dev/null differ
diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg
index 0001d0762..4d43f8bcd 100644
--- a/flutter/assets/logo.svg
+++ b/flutter/assets/logo.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/mac.svg b/flutter/assets/mac.svg
index 8092b3af3..ccf9c7aab 100644
--- a/flutter/assets/mac.svg
+++ b/flutter/assets/mac.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg
new file mode 100644
index 000000000..a8715011b
--- /dev/null
+++ b/flutter/assets/pinned.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/rec.svg b/flutter/assets/rec.svg
new file mode 100644
index 000000000..09aa55e2a
--- /dev/null
+++ b/flutter/assets/rec.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/record_screen.svg b/flutter/assets/record_screen.svg
new file mode 100644
index 000000000..bbd948c73
--- /dev/null
+++ b/flutter/assets/record_screen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/refresh.svg b/flutter/assets/refresh.svg
new file mode 100644
index 000000000..f77fcfd4c
--- /dev/null
+++ b/flutter/assets/refresh.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/search.svg b/flutter/assets/search.svg
new file mode 100644
index 000000000..295136d7e
--- /dev/null
+++ b/flutter/assets/search.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/secure.svg b/flutter/assets/secure.svg
index 29e1d3c4f..fcd99f2f5 100644
--- a/flutter/assets/secure.svg
+++ b/flutter/assets/secure.svg
@@ -1,3 +1 @@
-
-
-
+
\ No newline at end of file
diff --git a/flutter/assets/secure_relay.svg b/flutter/assets/secure_relay.svg
index 8ecbdb47b..af54808a8 100644
--- a/flutter/assets/secure_relay.svg
+++ b/flutter/assets/secure_relay.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/assets/transfer.svg b/flutter/assets/transfer.svg
new file mode 100644
index 000000000..24149bf58
--- /dev/null
+++ b/flutter/assets/transfer.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/trash.svg b/flutter/assets/trash.svg
new file mode 100644
index 000000000..f9037e0e1
--- /dev/null
+++ b/flutter/assets/trash.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/flutter/assets/unpinned.svg b/flutter/assets/unpinned.svg
new file mode 100644
index 000000000..7e93a7a35
--- /dev/null
+++ b/flutter/assets/unpinned.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg
new file mode 100644
index 000000000..bf90ec958
--- /dev/null
+++ b/flutter/assets/voice_call.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg
new file mode 100644
index 000000000..f1771c3fd
--- /dev/null
+++ b/flutter/assets/voice_call_waiting.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/flutter/assets/win.svg b/flutter/assets/win.svg
index 326f7829d..a0f7e3def 100644
--- a/flutter/assets/win.svg
+++ b/flutter/assets/win.svg
@@ -1 +1 @@
-
+
\ No newline at end of file
diff --git a/flutter/build_android.sh b/flutter/build_android.sh
index 01ff23488..c6b639f87 100755
--- a/flutter/build_android.sh
+++ b/flutter/build_android.sh
@@ -1,8 +1,10 @@
#!/usr/bin/env bash
-$ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
-flutter build apk --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info
-flutter build apk ---split-per-abi --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info
-flutter build appbundle --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info
+
+MODE=${MODE:=release}
+$ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
+flutter build apk --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info
+flutter build apk --split-per-abi --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info
+flutter build appbundle --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info
# build in linux
# $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/*
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index c35862a8c..16cef3177 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 900bd13fa..298f4d9af 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index 5fc34ce9a..fd3b01b6d 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index ab315a4c6..18ebaab69 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 6d69c01e1..a8ee14a31 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index b6c8034cd..a83f88b05 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index cf6c7c775..331e72531 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index 5fc34ce9a..fd3b01b6d 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 6928a4e6d..aee7e4321 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index a13129e15..2d0da17b1 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index a13129e15..2d0da17b1 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index 319e70f91..7ee56922e 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 229bdf563..76abd423b 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index caffb26a3..e08138333 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 751104548..46de51af6 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index ed78a8e09..ceff7480e 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -3,13 +3,11 @@ import 'dart:convert';
import 'dart:ffi' hide Size;
import 'dart:io';
import 'dart:math';
-import 'dart:typed_data';
import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:desktop_multi_window/desktop_multi_window.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
-import 'package:win32/win32.dart' as win32;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -18,14 +16,18 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/main.dart';
import 'package:flutter_hbb/models/peer_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
+import 'package:flutter_hbb/utils/platform_channel.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
+import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
import 'package:uni_links/uni_links.dart';
import 'package:uni_links_desktop/uni_links_desktop.dart';
-import 'package:window_manager/window_manager.dart';
-import 'package:flutter_svg/flutter_svg.dart';
-import 'package:window_size/window_size.dart' as window_size;
import 'package:url_launcher/url_launcher.dart';
+import 'package:win32/win32.dart' as win32;
+import 'package:window_manager/window_manager.dart';
+import 'package:window_size/window_size.dart' as window_size;
+import '../consts.dart';
import 'common/widgets/overlay.dart';
import 'mobile/pages/file_manager_page.dart';
import 'mobile/pages/remote_page.dart';
@@ -33,8 +35,6 @@ import 'models/input_model.dart';
import 'models/model.dart';
import 'models/platform_model.dart';
-import '../consts.dart';
-
final globalKey = GlobalKey();
final navigationBarKey = GlobalKey();
@@ -46,10 +46,20 @@ var isWebDesktop = false;
var version = "";
int androidVersion = 0;
+/// Incriment count for textureId.
+int _textureId = 0;
+int get newTextureId => _textureId++;
+final textureRenderer = TextureRgbaRenderer();
+
/// only available for Windows target
int windowsBuildNumber = 0;
DesktopType? desktopType;
+/// Check if the app is running with single view mode.
+bool isSingleViewApp() {
+ return desktopType == DesktopType.cm;
+}
+
/// * debug or test only, DO NOT enable in release build
bool isTest = false;
@@ -99,27 +109,32 @@ class IconFont {
class ColorThemeExtension extends ThemeExtension {
const ColorThemeExtension({
required this.border,
+ required this.border2,
required this.highlight,
});
final Color? border;
+ final Color? border2;
final Color? highlight;
static const light = ColorThemeExtension(
border: Color(0xFFCCCCCC),
+ border2: Color(0xFFBBBBBB),
highlight: Color(0xFFE5E5E5),
);
static const dark = ColorThemeExtension(
border: Color(0xFF555555),
+ border2: Color(0xFFE5E5E5),
highlight: Color(0xFF3F3F3F),
);
@override
ThemeExtension copyWith(
- {Color? border, Color? highlight}) {
+ {Color? border, Color? border2, Color? highlight}) {
return ColorThemeExtension(
border: border ?? this.border,
+ border2: border2 ?? this.border2,
highlight: highlight ?? this.highlight,
);
}
@@ -132,6 +147,7 @@ class ColorThemeExtension extends ThemeExtension {
}
return ColorThemeExtension(
border: Color.lerp(border, other.border, t),
+ border2: Color.lerp(border2, other.border2, t),
highlight: Color.lerp(highlight, other.highlight, t),
);
}
@@ -148,7 +164,7 @@ class MyTheme {
static const Color canvasColor = Color(0xFF212121);
static const Color border = Color(0xFFCCCCCC);
static const Color idColor = Color(0xFF00B6F0);
- static const Color darkGray = Color(0xFFB9BABC);
+ static const Color darkGray = Color.fromARGB(255, 148, 148, 148);
static const Color cmIdColor = Color(0xFF21790B);
static const Color dark = Colors.black87;
static const Color button = Color(0xFF2C8CFF);
@@ -156,8 +172,29 @@ class MyTheme {
static ThemeData lightTheme = ThemeData(
brightness: Brightness.light,
- backgroundColor: Color(0xFFFFFFFF),
- scaffoldBackgroundColor: Color(0xFFEEEEEE),
+ hoverColor: Color.fromARGB(255, 224, 224, 224),
+ scaffoldBackgroundColor: Color(0xFFFFFFFF),
+ dialogBackgroundColor: Color(0xFFFFFFFF),
+ dialogTheme: DialogTheme(
+ elevation: 15,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ side: BorderSide(
+ width: 1,
+ color: Color(0xFFEEEEEE),
+ ),
+ ),
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ fillColor: Color(0xFFEEEEEE),
+ filled: true,
+ isDense: true,
+ contentPadding: EdgeInsets.all(15),
+ border: UnderlineInputBorder(
+ borderRadius: BorderRadius.circular(18),
+ borderSide: BorderSide.none,
+ ),
+ ),
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 19, color: Colors.black87),
titleSmall: TextStyle(fontSize: 14, color: Colors.black87),
@@ -165,8 +202,8 @@ class MyTheme {
bodyMedium:
TextStyle(fontSize: 14, color: Colors.black87, height: 1.25),
labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)),
+ cardColor: Color(0xFFEEEEEE),
hintColor: Color(0xFFAAAAAA),
- primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: const TabBarTheme(
labelColor: Colors.black87,
@@ -176,9 +213,54 @@ class MyTheme {
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
? TextButtonThemeData(
- style: ButtonStyle(splashFactory: NoSplash.splashFactory),
+ style: TextButton.styleFrom(
+ splashFactory: NoSplash.splashFactory,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ ),
+ ),
)
: null,
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: MyTheme.accent,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ ),
+ ),
+ outlinedButtonTheme: OutlinedButtonThemeData(
+ style: OutlinedButton.styleFrom(
+ backgroundColor: Color(
+ 0xFFEEEEEE,
+ ),
+ foregroundColor: Colors.black87,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ ),
+ ),
+ checkboxTheme: const CheckboxThemeData(
+ splashRadius: 0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(
+ Radius.circular(5),
+ ),
+ ),
+ ),
+ listTileTheme: ListTileThemeData(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(
+ Radius.circular(5),
+ ),
+ ),
+ ),
+ colorScheme: ColorScheme.fromSwatch(
+ primarySwatch: Colors.blue,
+ ).copyWith(
+ brightness: Brightness.light,
+ background: Color(0xFFEEEEEE),
+ ),
).copyWith(
extensions: >[
ColorThemeExtension.light,
@@ -187,8 +269,29 @@ class MyTheme {
);
static ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
- backgroundColor: Color(0xFF252525),
- scaffoldBackgroundColor: Color(0xFF141414),
+ hoverColor: Color.fromARGB(255, 45, 46, 53),
+ scaffoldBackgroundColor: Color(0xFF18191E),
+ dialogBackgroundColor: Color(0xFF18191E),
+ dialogTheme: DialogTheme(
+ elevation: 15,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ side: BorderSide(
+ width: 1,
+ color: Color(0xFF24252B),
+ ),
+ ),
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ fillColor: Color(0xFF24252B),
+ filled: true,
+ isDense: true,
+ contentPadding: EdgeInsets.all(15),
+ border: UnderlineInputBorder(
+ borderRadius: BorderRadius.circular(18),
+ borderSide: BorderSide.none,
+ ),
+ ),
textTheme: const TextTheme(
titleLarge: TextStyle(fontSize: 19),
titleSmall: TextStyle(fontSize: 14),
@@ -196,20 +299,72 @@ class MyTheme {
bodyMedium: TextStyle(fontSize: 14, height: 1.25),
labelLarge: TextStyle(
fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)),
- cardColor: Color(0xFF252525),
- primarySwatch: Colors.blue,
+ cardColor: Color(0xFF24252B),
visualDensity: VisualDensity.adaptivePlatformDensity,
tabBarTheme: const TabBarTheme(
labelColor: Colors.white70,
),
+ scrollbarTheme: ScrollbarThemeData(
+ thumbColor: MaterialStateProperty.all(Colors.grey[500]),
+ ),
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
splashFactory: isDesktop ? NoSplash.splashFactory : null,
textButtonTheme: isDesktop
? TextButtonThemeData(
- style: ButtonStyle(splashFactory: NoSplash.splashFactory),
+ style: TextButton.styleFrom(
+ splashFactory: NoSplash.splashFactory,
+ disabledForegroundColor: Colors.white70,
+ foregroundColor: Colors.white70,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ ),
+ ),
)
: null,
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ backgroundColor: MyTheme.accent,
+ disabledForegroundColor: Colors.white70,
+ disabledBackgroundColor: Colors.white10,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ ),
+ ),
+ outlinedButtonTheme: OutlinedButtonThemeData(
+ style: OutlinedButton.styleFrom(
+ backgroundColor: Color(0xFF24252B),
+ side: BorderSide(color: Colors.white12, width: 0.5),
+ disabledForegroundColor: Colors.white70,
+ foregroundColor: Colors.white70,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8.0),
+ ),
+ ),
+ ),
+ checkboxTheme: const CheckboxThemeData(
+ checkColor: MaterialStatePropertyAll(dark),
+ splashRadius: 0,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(
+ Radius.circular(5),
+ ),
+ ),
+ ),
+ listTileTheme: ListTileThemeData(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(
+ Radius.circular(5),
+ ),
+ ),
+ ),
+ colorScheme: ColorScheme.fromSwatch(
+ primarySwatch: Colors.blue,
+ ).copyWith(
+ brightness: Brightness.dark,
+ background: Color(0xFF24252B),
+ ),
).copyWith(
extensions: >[
ColorThemeExtension.dark,
@@ -221,16 +376,18 @@ class MyTheme {
return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme));
}
- static void changeDarkMode(ThemeMode mode) {
+ static void changeDarkMode(ThemeMode mode) async {
Get.changeThemeMode(mode);
if (desktopType == DesktopType.main) {
if (mode == ThemeMode.system) {
- bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
+ await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: '');
} else {
- bind.mainSetLocalOption(
+ await bind.mainSetLocalOption(
key: kCommConfKeyTheme, value: mode.toShortString());
}
- bind.mainChangeTheme(dark: mode.toShortString());
+ await bind.mainChangeTheme(dark: mode.toShortString());
+ // Synchronize the window theme of the system.
+ updateSystemWindowTheme();
}
}
@@ -327,6 +484,9 @@ closeConnection({String? id}) {
}
void window_on_top(int? id) {
+ if (!isDesktop) {
+ return;
+ }
if (id == null) {
// main window
windowManager.restore();
@@ -363,20 +523,25 @@ class Dialog {
}
}
+class OverlayKeyState {
+ final _overlayKey = GlobalKey();
+
+ /// use global overlay by default
+ OverlayState? get state =>
+ _overlayKey.currentState ?? globalKey.currentState?.overlay;
+
+ GlobalKey? get key => _overlayKey;
+}
+
class OverlayDialogManager {
- OverlayState? _overlayState;
final Map _dialogs = {};
+ var _overlayKeyState = OverlayKeyState();
int _tagCount = 0;
OverlayEntry? _mobileActionsOverlayEntry;
- /// By default OverlayDialogManager use global overlay
- OverlayDialogManager() {
- _overlayState = globalKey.currentState?.overlay;
- }
-
- void setOverlayState(OverlayState? overlayState) {
- _overlayState = overlayState;
+ void setOverlayState(OverlayKeyState overlayKeyState) {
+ _overlayKeyState = overlayKeyState;
}
void dismissAll() {
@@ -400,7 +565,7 @@ class OverlayDialogManager {
bool useAnimation = true,
bool forceGlobal = false}) {
final overlayState =
- forceGlobal ? globalKey.currentState?.overlay : _overlayState;
+ forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state;
if (overlayState == null) {
return Future.error(
@@ -424,7 +589,7 @@ class OverlayDialogManager {
BackButtonInterceptor.removeByName(dialogTag);
}
- dialog.entry = OverlayEntry(builder: (_) {
+ dialog.entry = OverlayEntry(builder: (context) {
bool innerClicked = false;
return Listener(
onPointerUp: (_) {
@@ -434,7 +599,9 @@ class OverlayDialogManager {
innerClicked = false;
},
child: Container(
- color: Colors.black12,
+ color: Theme.of(context).brightness == Brightness.light
+ ? Colors.black12
+ : Colors.black45,
child: StatefulBuilder(builder: (context, setState) {
return Listener(
onPointerUp: (_) => innerClicked = true,
@@ -483,12 +650,14 @@ class OverlayDialogManager {
Offstage(
offstage: !showCancel,
child: Center(
- child: TextButton(
- style: flatButtonStyle,
- onPressed: cancel,
- child: Text(translate('Cancel'),
- style:
- const TextStyle(color: MyTheme.accent)))))
+ child: isDesktop
+ ? dialogButton('Cancel', onPressed: cancel)
+ : TextButton(
+ style: flatButtonStyle,
+ onPressed: cancel,
+ child: Text(translate('Cancel'),
+ style: const TextStyle(
+ color: MyTheme.accent)))))
])),
onCancel: showCancel ? cancel : null,
);
@@ -504,7 +673,8 @@ class OverlayDialogManager {
void showMobileActionsOverlay({FFI? ffi}) {
if (_mobileActionsOverlayEntry != null) return;
- if (_overlayState == null) return;
+ final overlayState = _overlayKeyState.state;
+ if (overlayState == null) return;
// compute overlay position
final screenW = MediaQuery.of(globalKey.currentContext!).size.width;
@@ -530,7 +700,7 @@ class OverlayDialogManager {
onHidePressed: () => hideMobileActionsOverlay(),
);
});
- _overlayState!.insert(overlay);
+ overlayState.insert(overlay);
_mobileActionsOverlayEntry = overlay;
}
@@ -549,6 +719,10 @@ class OverlayDialogManager {
hideMobileActionsOverlay();
}
}
+
+ bool existing(String tag) {
+ return _dialogs.keys.contains(tag);
+ }
}
void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) {
@@ -604,13 +778,15 @@ class CustomAlertDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
- FocusNode focusNode = FocusNode();
- // request focus if there is no focused FocusNode in the dialog
+ // request focus
+ FocusScopeNode scopeNode = FocusScopeNode();
Future.delayed(Duration.zero, () {
- if (!focusNode.hasFocus) focusNode.requestFocus();
+ if (!scopeNode.hasFocus) scopeNode.requestFocus();
});
- return Focus(
- focusNode: focusNode,
+ const double padding = 30;
+ bool tabTapped = false;
+ return FocusScope(
+ node: scopeNode,
autofocus: true,
onKey: (node, key) {
if (key.logicalKey == LogicalKeyboardKey.escape) {
@@ -618,21 +794,37 @@ class CustomAlertDialog extends StatelessWidget {
onCancel?.call();
}
return KeyEventResult.handled; // avoid TextField exception on escape
- } else if (onSubmit != null &&
+ } else if (!tabTapped &&
+ onSubmit != null &&
key.logicalKey == LogicalKeyboardKey.enter) {
if (key is RawKeyDownEvent) onSubmit?.call();
return KeyEventResult.handled;
+ } else if (key.logicalKey == LogicalKeyboardKey.tab) {
+ if (key is RawKeyDownEvent) {
+ scopeNode.nextFocus();
+ tabTapped = true;
+ }
+ return KeyEventResult.handled;
}
return KeyEventResult.ignored;
},
child: AlertDialog(
scrollable: true,
title: title,
- contentPadding: EdgeInsets.symmetric(
- horizontal: contentPadding ?? 25, vertical: 10),
- content:
- ConstrainedBox(constraints: contentBoxConstraints, child: content),
+ titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0),
+ contentPadding: EdgeInsets.fromLTRB(
+ contentPadding ?? padding,
+ 25,
+ contentPadding ?? padding,
+ actions is List ? 10 : padding,
+ ),
+ content: ConstrainedBox(
+ constraints: contentBoxConstraints,
+ child: content,
+ ),
actions: actions,
+ actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding),
+ actionsAlignment: MainAxisAlignment.center,
),
);
}
@@ -640,7 +832,7 @@ class CustomAlertDialog extends StatelessWidget {
void msgBox(String id, String type, String title, String text, String link,
OverlayDialogManager dialogManager,
- {bool? hasCancel}) {
+ {bool? hasCancel, ReconnectHandle? reconnect}) {
dialogManager.dismissAll();
List buttons = [];
bool hasOk = false;
@@ -664,30 +856,36 @@ void msgBox(String id, String type, String title, String text, String link,
if (type != "connecting" && type != "success" && !type.contains("nook")) {
hasOk = true;
- buttons.insert(0, msgBoxButton(translate('OK'), submit));
+ buttons.insert(0, dialogButton('OK', onPressed: submit));
}
hasCancel ??= !type.contains("error") &&
!type.contains("nocancel") &&
type != "restarting";
if (hasCancel) {
- buttons.insert(0, msgBoxButton(translate('Cancel'), cancel));
+ buttons.insert(
+ 0, dialogButton('Cancel', onPressed: cancel, isOutline: true));
}
- // TODO: test this button
if (type.contains("hasclose")) {
buttons.insert(
0,
- msgBoxButton(translate('Close'), () {
+ dialogButton('Close', onPressed: () {
dialogManager.dismissAll();
}));
}
+ if (reconnect != null && title == "Connection Error") {
+ buttons.insert(
+ 0,
+ dialogButton('Reconnect', isOutline: true, onPressed: () {
+ reconnect(dialogManager, id, false);
+ }));
+ }
if (link.isNotEmpty) {
- buttons.insert(0, msgBoxButton(translate('JumpLink'), jumplink));
+ buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink));
}
dialogManager.show(
(setState, close) => CustomAlertDialog(
- title: _msgBoxTitle(title),
- content:
- SelectableText(translate(text), style: const TextStyle(fontSize: 15)),
+ title: null,
+ content: SelectionArea(child: msgboxContent(type, title, text)),
actions: buttons,
onSubmit: hasOk ? submit : null,
onCancel: hasCancel == true ? cancel : null,
@@ -696,30 +894,74 @@ void msgBox(String id, String type, String title, String text, String link,
);
}
-Widget msgBoxButton(String text, void Function() onPressed) {
- return ButtonTheme(
- padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
- materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
- //limits the touch area to the button area
- minWidth: 0,
- //wraps child's width
- height: 0,
- child: TextButton(
- style: flatButtonStyle,
- onPressed: onPressed,
- child:
- Text(translate(text), style: TextStyle(color: MyTheme.accent))));
+Color? _msgboxColor(String type) {
+ if (type == "input-password" || type == "custom-os-password") {
+ return Color(0xFFAD448E);
+ }
+ if (type.contains("success")) {
+ return Color(0xFF32bea6);
+ }
+ if (type.contains("error") || type == "re-input-password") {
+ return Color(0xFFE04F5F);
+ }
+ return Color(0xFF2C8CFF);
}
-Widget _msgBoxTitle(String title) =>
- Text(translate(title), style: TextStyle(fontSize: 21));
+Widget msgboxIcon(String type) {
+ IconData? iconData;
+ if (type.contains("error") || type == "re-input-password") {
+ iconData = Icons.cancel;
+ }
+ if (type.contains("success")) {
+ iconData = Icons.check_circle;
+ }
+ if (type == "wait-uac" || type == "wait-remote-accept-nook") {
+ iconData = Icons.hourglass_top;
+ }
+ if (type == 'on-uac' || type == 'on-foreground-elevated') {
+ iconData = Icons.admin_panel_settings;
+ }
+ if (type == "info") {
+ iconData = Icons.info;
+ }
+ if (iconData != null) {
+ return Icon(iconData, size: 50, color: _msgboxColor(type))
+ .marginOnly(right: 16);
+ }
+
+ return Offstage();
+}
+
+// title should be null
+Widget msgboxContent(String type, String title, String text) {
+ return Row(
+ children: [
+ msgboxIcon(type),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ translate(title),
+ style: TextStyle(fontSize: 21),
+ ).marginOnly(bottom: 10),
+ Text(translate(text), style: const TextStyle(fontSize: 15)),
+ ],
+ ),
+ ),
+ ],
+ ).marginOnly(bottom: 12);
+}
void msgBoxCommon(OverlayDialogManager dialogManager, String title,
Widget content, List buttons,
{bool hasCancel = true}) {
dialogManager.dismissAll();
dialogManager.show((setState, close) => CustomAlertDialog(
- title: _msgBoxTitle(title),
+ title: Text(
+ translate(title),
+ style: TextStyle(fontSize: 21),
+ ),
content: content,
actions: buttons,
onCancel: hasCancel ? close : null,
@@ -797,21 +1039,14 @@ class AccessibilityListener extends StatelessWidget {
}
}
-class PermissionManager {
+class AndroidPermissionManager {
static Completer? _completer;
static Timer? _timer;
static var _current = "";
- static final permissions = [
- "audio",
- "file",
- "ignore_battery_optimizations",
- "application_details_settings"
- ];
-
static bool isWaitingFile() {
if (_completer != null) {
- return !_completer!.isCompleted && _current == "file";
+ return !_completer!.isCompleted && _current == kManageExternalStorage;
}
return false;
}
@@ -820,31 +1055,33 @@ class PermissionManager {
if (isDesktop) {
return Future.value(true);
}
- if (!permissions.contains(type)) {
- return Future.error("Wrong permission!$type");
- }
return gFFI.invokeMethod("check_permission", type);
}
+ // startActivity goto Android Setting's page to request permission manually by user
+ static void startAction(String action) {
+ gFFI.invokeMethod(AndroidChannel.kStartAction, action);
+ }
+
+ /// We use XXPermissions to request permissions,
+ /// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java
static Future request(String type) {
if (isDesktop) {
return Future.value(true);
}
- if (!permissions.contains(type)) {
- return Future.error("Wrong permission!$type");
- }
gFFI.invokeMethod("request_permission", type);
- if (type == "ignore_battery_optimizations") {
- return Future.value(false);
+
+ // clear last task
+ if (_completer?.isCompleted == false) {
+ _completer?.complete(false);
}
+ _timer?.cancel();
+
_current = type;
_completer = Completer();
- gFFI.invokeMethod("request_permission", type);
- // timeout
- _timer?.cancel();
- _timer = Timer(Duration(seconds: 60), () {
+ _timer = Timer(Duration(seconds: 120), () {
if (_completer == null) return;
if (!_completer!.isCompleted) {
_completer!.complete(false);
@@ -977,11 +1214,13 @@ Future matchPeer(String searchText, Peer peer) async {
/// Get the image for the current [platform].
Widget getPlatformImage(String platform, {double size = 50}) {
- platform = platform.toLowerCase();
- if (platform == 'mac os') {
+ if (platform == kPeerPlatformMacOS) {
platform = 'mac';
- } else if (platform != 'linux' && platform != 'android') {
+ } else if (platform != kPeerPlatformLinux &&
+ platform != kPeerPlatformAndroid) {
platform = 'win';
+ } else {
+ platform = platform.toLowerCase();
}
return SvgPicture.asset('assets/$platform.svg', height: size, width: size);
}
@@ -1204,10 +1443,12 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async {
/// [Availability]
/// initUniLinks should only be used on macos/windows.
/// we use dbus for linux currently.
-Future initUniLinks() async {
- if (!Platform.isWindows && !Platform.isMacOS) {
- return;
+Future initUniLinks() async {
+ if (Platform.isLinux) {
+ return false;
}
+ // Register uni links for Windows. The required info of url scheme is already
+ // declared in `Info.plist` for macOS.
if (Platform.isWindows) {
registerProtocol('rustdesk');
}
@@ -1215,22 +1456,33 @@ Future initUniLinks() async {
try {
final initialLink = await getInitialLink();
if (initialLink == null) {
- return;
+ return false;
}
- parseRustdeskUri(initialLink);
+ return parseRustdeskUri(initialLink);
} catch (err) {
debugPrintStack(label: "$err");
+ return false;
}
}
-StreamSubscription? listenUniLinks() {
- if (!(Platform.isWindows || Platform.isMacOS)) {
+/// Listen for uni links.
+///
+/// * handleByFlutter: Should uni links be handled by Flutter.
+///
+/// Returns a [StreamSubscription] which can listen the uni links.
+StreamSubscription? listenUniLinks({handleByFlutter = true}) {
+ if (Platform.isLinux) {
return null;
}
final sub = uriLinkStream.listen((Uri? uri) {
+ debugPrint("A uri was received: $uri.");
if (uri != null) {
- callUniLinksUriHandler(uri);
+ if (handleByFlutter) {
+ callUniLinksUriHandler(uri);
+ } else {
+ bind.sendUrlScheme(url: uri.toString());
+ }
} else {
print("uni listen error: uri is empty.");
}
@@ -1240,25 +1492,38 @@ StreamSubscription? listenUniLinks() {
return sub;
}
-/// Returns true if we successfully handle the startup arguments.
+/// Handle command line arguments
+///
+/// * Returns true if we successfully handle the startup arguments.
bool checkArguments() {
+ if (kBootArgs.isNotEmpty) {
+ final ret = parseRustdeskUri(kBootArgs.first);
+ if (ret) {
+ return true;
+ }
+ }
+ // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05]
// check connect args
- final connectIndex = bootArgs.indexOf("--connect");
+ var connectIndex = kBootArgs.indexOf("--connect");
if (connectIndex == -1) {
return false;
}
- String? arg =
- bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1];
- if (arg != null) {
- if (arg.startsWith(kUniLinksPrefix)) {
- return parseRustdeskUri(arg);
+ String? id =
+ kBootArgs.length < connectIndex + 1 ? null : kBootArgs[connectIndex + 1];
+ final switchUuidIndex = kBootArgs.indexOf("--switch_uuid");
+ String? switchUuid = kBootArgs.length < switchUuidIndex + 1
+ ? null
+ : kBootArgs[switchUuidIndex + 1];
+ if (id != null) {
+ if (id.startsWith(kUniLinksPrefix)) {
+ return parseRustdeskUri(id);
} else {
// remove "--connect xxx" in the `bootArgs` array
- bootArgs.removeAt(connectIndex);
- bootArgs.removeAt(connectIndex);
+ kBootArgs.removeAt(connectIndex);
+ kBootArgs.removeAt(connectIndex);
// fallback to peer id
Future.delayed(Duration.zero, () {
- rustDeskWinManager.newRemoteDesktop(arg);
+ rustDeskWinManager.newRemoteDesktop(id, switch_uuid: switchUuid);
});
return true;
}
@@ -1274,7 +1539,7 @@ bool checkArguments() {
bool parseRustdeskUri(String uriPath) {
final uri = Uri.tryParse(uriPath);
if (uri == null) {
- print("uri is not valid: $uriPath");
+ debugPrint("uri is not valid: $uriPath");
return false;
}
return callUniLinksUriHandler(uri);
@@ -1288,8 +1553,10 @@ bool callUniLinksUriHandler(Uri uri) {
// new connection
if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
final peerId = uri.path.substring("/new/".length);
+ var param = uri.queryParameters;
+ String? switch_uuid = param["switch_uuid"];
Future.delayed(Duration.zero, () {
- rustDeskWinManager.newRemoteDesktop(peerId);
+ rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid);
});
return true;
}
@@ -1299,13 +1566,14 @@ bool callUniLinksUriHandler(Uri uri) {
connectMainDesktop(String id,
{required bool isFileTransfer,
required bool isTcpTunneling,
- required bool isRDP}) async {
+ required bool isRDP,
+ bool? forceRelay}) async {
if (isFileTransfer) {
- await rustDeskWinManager.newFileTransfer(id);
+ await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay);
} else if (isTcpTunneling || isRDP) {
- await rustDeskWinManager.newPortForward(id, isRDP);
+ await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay);
} else {
- await rustDeskWinManager.newRemoteDesktop(id);
+ await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay);
}
}
@@ -1319,29 +1587,32 @@ connect(BuildContext context, String id,
bool isRDP = false}) async {
if (id == '') return;
id = id.replaceAll(' ', '');
+ final oldId = id;
+ id = await bind.mainHandleRelayId(id: id);
+ final forceRelay = id != oldId;
assert(!(isFileTransfer && isTcpTunneling && isRDP),
"more than one connect type");
if (isDesktop) {
if (desktopType == DesktopType.main) {
- await connectMainDesktop(
- id,
- isFileTransfer: isFileTransfer,
- isTcpTunneling: isTcpTunneling,
- isRDP: isRDP,
- );
+ await connectMainDesktop(id,
+ isFileTransfer: isFileTransfer,
+ isTcpTunneling: isTcpTunneling,
+ isRDP: isRDP,
+ forceRelay: forceRelay);
} else {
await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
'id': id,
'isFileTransfer': isFileTransfer,
'isTcpTunneling': isTcpTunneling,
'isRDP': isRDP,
+ "forceRelay": forceRelay,
});
}
} else {
if (isFileTransfer) {
- if (!await PermissionManager.check("file")) {
- if (!await PermissionManager.request("file")) {
+ if (!await AndroidPermissionManager.check(kManageExternalStorage)) {
+ if (!await AndroidPermissionManager.request(kManageExternalStorage)) {
return;
}
}
@@ -1431,8 +1702,12 @@ Future onActiveWindowChanged() async {
} catch (err) {
debugPrintStack(label: "$err");
} finally {
+ debugPrint("Start closing RustDesk...");
await windowManager.setPreventClose(false);
await windowManager.close();
+ if (Platform.isMacOS) {
+ RdPlatformChannel.instance.terminate();
+ }
}
}
}
@@ -1560,3 +1835,151 @@ class ServerConfig {
apiServer = options['api-server'] ?? "",
key = options['key'] ?? "";
}
+
+Widget dialogButton(String text,
+ {required VoidCallback? onPressed,
+ bool isOutline = false,
+ Widget? icon,
+ TextStyle? style,
+ ButtonStyle? buttonStyle}) {
+ if (isDesktop) {
+ if (isOutline) {
+ return icon == null
+ ? OutlinedButton(
+ onPressed: onPressed,
+ child: Text(translate(text), style: style),
+ )
+ : OutlinedButton.icon(
+ icon: icon,
+ onPressed: onPressed,
+ label: Text(translate(text), style: style),
+ );
+ } else {
+ return icon == null
+ ? ElevatedButton(
+ style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
+ onPressed: onPressed,
+ child: Text(translate(text), style: style),
+ )
+ : ElevatedButton.icon(
+ icon: icon,
+ style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle),
+ onPressed: onPressed,
+ label: Text(translate(text), style: style),
+ );
+ }
+ } else {
+ return TextButton(
+ onPressed: onPressed,
+ child: Text(
+ translate(text),
+ style: style,
+ ),
+ );
+ }
+}
+
+int version_cmp(String v1, String v2) {
+ return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2);
+}
+
+String getWindowName({WindowType? overrideType}) {
+ switch (overrideType ?? kWindowType) {
+ case WindowType.Main:
+ return "RustDesk";
+ case WindowType.FileTransfer:
+ return "File Transfer - RustDesk";
+ case WindowType.PortForward:
+ return "Port Forward - RustDesk";
+ case WindowType.RemoteDesktop:
+ return "Remote Desktop - RustDesk";
+ default:
+ break;
+ }
+ return "RustDesk";
+}
+
+String getWindowNameWithId(String id, {WindowType? overrideType}) {
+ return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}";
+}
+
+Future updateSystemWindowTheme() async {
+ // Set system window theme for macOS.
+ final userPreference = MyTheme.getThemeModePreference();
+ if (userPreference != ThemeMode.system) {
+ if (Platform.isMacOS) {
+ await RdPlatformChannel.instance.changeSystemWindowTheme(
+ userPreference == ThemeMode.light
+ ? SystemWindowTheme.light
+ : SystemWindowTheme.dark);
+ }
+ }
+}
+
+/// macOS only
+///
+/// Note: not found a general solution for rust based AVFoundation bingding.
+/// [AVFoundation] crate has compile error.
+const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos");
+
+enum PermissionAuthorizeType {
+ undetermined,
+ authorized,
+ denied, // and restricted
+}
+
+Future osxCanRecordAudio() async {
+ int res = await kMacOSPermChannel.invokeMethod("canRecordAudio");
+ print(res);
+ if (res > 0) {
+ return PermissionAuthorizeType.authorized;
+ } else if (res == 0) {
+ return PermissionAuthorizeType.undetermined;
+ } else {
+ return PermissionAuthorizeType.denied;
+ }
+}
+
+Future osxRequestAudio() async {
+ return await kMacOSPermChannel.invokeMethod("requestRecordAudio");
+}
+
+class DraggableNeverScrollableScrollPhysics extends ScrollPhysics {
+ /// Creates scroll physics that does not let the user scroll.
+ const DraggableNeverScrollableScrollPhysics({super.parent});
+
+ @override
+ DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) {
+ return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor));
+ }
+
+ @override
+ bool shouldAcceptUserOffset(ScrollMetrics position) {
+ // TODO: find a better solution to check if the offset change is caused by the scrollbar.
+ // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity].
+ if (position is ScrollPositionWithSingleContext) {
+ // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
+ return position.activity is IdleScrollActivity;
+ }
+ return false;
+ }
+
+ @override
+ bool get allowImplicitScrolling => false;
+}
+
+Widget futureBuilder(
+ {required Future? future, required Widget Function(dynamic data) hasData}) {
+ return FutureBuilder(
+ future: future,
+ builder: (BuildContext context, AsyncSnapshot snapshot) {
+ if (snapshot.hasData) {
+ return hasData(snapshot.data!);
+ } else {
+ if (snapshot.hasError) {
+ debugPrint(snapshot.error.toString());
+ }
+ return Container();
+ }
+ });
+}
diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart
index 27238db67..4717143fd 100644
--- a/flutter/lib/common/hbbs/hbbs.dart
+++ b/flutter/lib/common/hbbs/hbbs.dart
@@ -1,5 +1,9 @@
+import 'dart:io';
+
import 'package:flutter_hbb/models/peer_model.dart';
+import '../../models/platform_model.dart';
+
class HttpType {
static const kAuthReqTypeAccount = "account";
static const kAuthReqTypeMobile = "mobile";
@@ -48,6 +52,16 @@ class PeerPayload {
}
}
+class DeviceInfo {
+ static Map toJson() {
+ final Map data = {};
+ data['os'] = Platform.operatingSystem;
+ data['type'] = "client";
+ data['name'] = bind.mainGetHostname();
+ return data;
+ }
+}
+
class LoginRequest {
String? username;
String? password;
@@ -56,7 +70,7 @@ class LoginRequest {
bool? autoLogin;
String? type;
String? verificationCode;
- String? deviceInfo;
+ Map deviceInfo = DeviceInfo.toJson();
LoginRequest(
{this.username,
@@ -65,19 +79,7 @@ class LoginRequest {
this.uuid,
this.autoLogin,
this.type,
- this.verificationCode,
- this.deviceInfo});
-
- LoginRequest.fromJson(Map json) {
- username = json['username'];
- password = json['password'];
- id = json['id'];
- uuid = json['uuid'];
- autoLogin = json['autoLogin'];
- type = json['type'];
- verificationCode = json['verificationCode'];
- deviceInfo = json['deviceInfo'];
- }
+ this.verificationCode});
Map toJson() {
final Map data = {};
@@ -88,7 +90,7 @@ class LoginRequest {
data['autoLogin'] = autoLogin ?? '';
data['type'] = type ?? '';
data['verificationCode'] = verificationCode ?? '';
- data['deviceInfo'] = deviceInfo ?? '';
+ data['deviceInfo'] = deviceInfo;
return data;
}
}
diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart
index 34d5af485..88a5aaaa3 100644
--- a/flutter/lib/common/widgets/address_book.dart
+++ b/flutter/lib/common/widgets/address_book.dart
@@ -43,14 +43,8 @@ class _AddressBookState extends State {
return Obx(() {
if (gFFI.userModel.userName.value.isEmpty) {
return Center(
- child: InkWell(
- onTap: loginDialog,
- child: Text(
- translate("Login"),
- style: const TextStyle(decoration: TextDecoration.underline),
- ),
- ),
- );
+ child: ElevatedButton(
+ onPressed: loginDialog, child: Text(translate("Login"))));
} else {
if (gFFI.abModel.abLoading.value) {
return const Center(
@@ -156,13 +150,13 @@ class _AddressBookState extends State {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(translate('Tags')),
- GestureDetector(
- onTapDown: (e) {
- final x = e.globalPosition.dx;
- final y = e.globalPosition.dy;
+ Listener(
+ onPointerDown: (e) {
+ final x = e.position.dx;
+ final y = e.position.dy;
menuPos = RelativeRect.fromLTRB(x, y, x, y);
},
- onTap: () => _showMenu(menuPos),
+ onPointerUp: (_) => _showMenu(menuPos),
child: ActionMore()),
],
);
@@ -335,8 +329,8 @@ class _AddressBookState extends State {
],
),
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(onPressed: submit, child: Text(translate("OK"))),
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@@ -389,7 +383,7 @@ class _AddressBookState extends State {
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
),
],
@@ -402,8 +396,8 @@ class _AddressBookState extends State {
],
),
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(onPressed: submit, child: Text(translate("OK"))),
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart
index 510ce1f73..c1991633a 100644
--- a/flutter/lib/common/widgets/chat_page.dart
+++ b/flutter/lib/common/widgets/chat_page.dart
@@ -51,7 +51,7 @@ class ChatPage extends StatelessWidget implements PageShape {
return Stack(
children: [
LayoutBuilder(builder: (context, constraints) {
- return DashChat(
+ final chat = DashChat(
onSend: (chatMsg) {
chatModel.send(chatMsg);
chatModel.inputNode.requestFocus();
@@ -75,7 +75,8 @@ class ChatPage extends StatelessWidget implements PageShape {
hintText:
"${translate('Write a message')}...",
filled: true,
- fillColor: Theme.of(context).backgroundColor,
+ fillColor:
+ Theme.of(context).colorScheme.background,
contentPadding: EdgeInsets.all(10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6),
@@ -88,17 +89,39 @@ class ChatPage extends StatelessWidget implements PageShape {
: defaultInputDecoration(
hintText:
"${translate('Write a message')}...",
- fillColor: Theme.of(context).backgroundColor),
+ fillColor:
+ Theme.of(context).colorScheme.background),
sendButtonBuilder: defaultSendButton(
padding: EdgeInsets.symmetric(
horizontal: 6, vertical: 0),
color: Theme.of(context).colorScheme.primary)),
messageOptions: MessageOptions(
showOtherUsersAvatar: false,
- showTime: true,
- currentUserTextColor: Colors.white,
textColor: Colors.white,
maxWidth: constraints.maxWidth * 0.7,
+ messageTextBuilder: (message, _, __) {
+ final isOwnMessage =
+ message.user.id == currentUser.id;
+ return Column(
+ crossAxisAlignment: isOwnMessage
+ ? CrossAxisAlignment.end
+ : CrossAxisAlignment.start,
+ children: [
+ Text(message.text,
+ style: TextStyle(color: Colors.white)),
+ Padding(
+ padding: const EdgeInsets.only(top: 5),
+ child: Text(
+ "${message.createdAt.hour}:${message.createdAt.minute}",
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 10,
+ ),
+ ),
+ ),
+ ],
+ );
+ },
messageDecorationBuilder: (_, __, ___) =>
defaultMessageDecoration(
color: MyTheme.accent80,
@@ -108,6 +131,7 @@ class ChatPage extends StatelessWidget implements PageShape {
borderBottomLeft: 8,
)),
);
+ return SelectionArea(child: chat);
}),
desktopType == DesktopType.cm ||
chatModel.currentID == ChatModel.clientModeID
diff --git a/flutter/lib/common/widgets/custom_password.dart b/flutter/lib/common/widgets/custom_password.dart
new file mode 100644
index 000000000..99ece2434
--- /dev/null
+++ b/flutter/lib/common/widgets/custom_password.dart
@@ -0,0 +1,121 @@
+// https://github.com/rodrigobastosv/fancy_password_field
+import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:get/get.dart';
+import 'package:password_strength/password_strength.dart';
+
+abstract class ValidationRule {
+ String get name;
+ bool validate(String value);
+}
+
+class UppercaseValidationRule extends ValidationRule {
+ @override
+ String get name => translate('uppercase');
+ @override
+ bool validate(String value) {
+ return value.contains(RegExp(r'[A-Z]'));
+ }
+}
+
+class LowercaseValidationRule extends ValidationRule {
+ @override
+ String get name => translate('lowercase');
+
+ @override
+ bool validate(String value) {
+ return value.contains(RegExp(r'[a-z]'));
+ }
+}
+
+class DigitValidationRule extends ValidationRule {
+ @override
+ String get name => translate('digit');
+
+ @override
+ bool validate(String value) {
+ return value.contains(RegExp(r'[0-9]'));
+ }
+}
+
+class SpecialCharacterValidationRule extends ValidationRule {
+ @override
+ String get name => translate('special character');
+
+ @override
+ bool validate(String value) {
+ return value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'));
+ }
+}
+
+class MinCharactersValidationRule extends ValidationRule {
+ final int _numberOfCharacters;
+ MinCharactersValidationRule(this._numberOfCharacters);
+
+ @override
+ String get name => translate('length>=$_numberOfCharacters');
+
+ @override
+ bool validate(String value) {
+ return value.length >= _numberOfCharacters;
+ }
+}
+
+class PasswordStrengthIndicator extends StatelessWidget {
+ final RxString password;
+ final double weakMedium = 0.33;
+ final double mediumStrong = 0.67;
+ const PasswordStrengthIndicator({Key? key, required this.password})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return Obx(() {
+ var strength = estimatePasswordStrength(password.value);
+ return Row(
+ children: [
+ Expanded(
+ child: _indicator(
+ password.isEmpty ? Colors.grey : _getColor(strength))),
+ Expanded(
+ child: _indicator(password.isEmpty || strength < weakMedium
+ ? Colors.grey
+ : _getColor(strength))),
+ Expanded(
+ child: _indicator(password.isEmpty || strength < mediumStrong
+ ? Colors.grey
+ : _getColor(strength))),
+ Text(password.isEmpty ? '' : translate(_getLabel(strength)))
+ .marginOnly(left: password.isEmpty ? 0 : 8),
+ ],
+ );
+ });
+ }
+
+ Widget _indicator(Color color) {
+ return Container(
+ height: 8,
+ color: color,
+ );
+ }
+
+ String _getLabel(double strength) {
+ if (strength < weakMedium) {
+ return 'Weak';
+ } else if (strength < mediumStrong) {
+ return 'Medium';
+ } else {
+ return 'Strong';
+ }
+ }
+
+ Color _getColor(double strength) {
+ if (strength < weakMedium) {
+ return Colors.yellow;
+ } else if (strength < mediumStrong) {
+ return Colors.blue;
+ } else {
+ return Colors.green;
+ }
+ }
+}
diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart
index a6de0384f..cdce6f12a 100644
--- a/flutter/lib/common/widgets/dialog.dart
+++ b/flutter/lib/common/widgets/dialog.dart
@@ -1,18 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
+import 'package:get/get.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
+abstract class ValidationRule {
+ String get name;
+ bool validate(String value);
+}
+
+class LengthRangeValidationRule extends ValidationRule {
+ final int _min;
+ final int _max;
+
+ LengthRangeValidationRule(this._min, this._max);
+
+ @override
+ String get name => translate('length %min% to %max%')
+ .replaceAll('%min%', _min.toString())
+ .replaceAll('%max%', _max.toString());
+
+ @override
+ bool validate(String value) {
+ return value.length >= _min && value.length <= _max;
+ }
+}
+
+class RegexValidationRule extends ValidationRule {
+ final String _name;
+ final RegExp _regex;
+
+ RegexValidationRule(this._name, this._regex);
+
+ @override
+ String get name => translate(_name);
+
+ @override
+ bool validate(String value) {
+ return value.isNotEmpty ? value.contains(_regex) : false;
+ }
+}
+
void changeIdDialog() {
var newId = "";
var msg = "";
var isInProgress = false;
TextEditingController controller = TextEditingController();
+ final RxString rxId = controller.text.trim().obs;
+
+ final rules = [
+ RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')),
+ LengthRangeValidationRule(6, 16),
+ RegexValidationRule('allowed characters', RegExp(r'^\w*$'))
+ ];
+
gFFI.dialogManager.show((setState, close) {
submit() async {
debugPrint("onSubmit");
newId = controller.text.trim();
+
+ final Iterable violations = rules.where((r) => !r.validate(newId));
+ if (violations.isNotEmpty) {
+ setState(() {
+ msg =
+ '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
+ });
+ return;
+ }
+
setState(() {
msg = "";
isInProgress = true;
@@ -31,7 +87,7 @@ void changeIdDialog() {
}
setState(() {
isInProgress = false;
- msg = translate(status);
+ msg = '${translate('Prompt')}: ${translate(status)}';
});
}
@@ -46,26 +102,55 @@ void changeIdDialog() {
),
TextField(
decoration: InputDecoration(
+ labelText: translate('Your new ID'),
border: const OutlineInputBorder(),
- errorText: msg.isEmpty ? null : translate(msg)),
+ errorText: msg.isEmpty ? null : translate(msg),
+ suffixText: '${rxId.value.length}/16',
+ suffixStyle: const TextStyle(fontSize: 12, color: Colors.grey)),
inputFormatters: [
LengthLimitingTextInputFormatter(16),
// FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true)
],
- maxLength: 16,
controller: controller,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
+ onChanged: (value) {
+ setState(() {
+ rxId.value = value.trim();
+ msg = '';
+ });
+ },
),
const SizedBox(
- height: 4.0,
+ height: 8.0,
+ ),
+ Obx(() => Wrap(
+ runSpacing: 8,
+ spacing: 4,
+ children: rules.map((e) {
+ var checked = e.validate(rxId.value);
+ return Chip(
+ label: Text(
+ e.name,
+ style: TextStyle(
+ color: checked
+ ? const Color(0xFF0A9471)
+ : Color.fromARGB(255, 198, 86, 157)),
+ ),
+ backgroundColor: checked
+ ? const Color(0xFFD0F7ED)
+ : Color.fromARGB(255, 247, 205, 232));
+ }).toList(),
+ )),
+ const SizedBox(
+ height: 8.0,
),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
),
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(onPressed: submit, child: Text(translate("OK"))),
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@@ -99,7 +184,7 @@ void changeWhiteList({Function()? callback}) async {
errorText: msg.isEmpty ? null : translate(msg),
),
controller: controller,
- focusNode: FocusNode()..requestFocus()),
+ autofocus: true),
),
],
),
@@ -111,48 +196,46 @@ void changeWhiteList({Function()? callback}) async {
],
),
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(
- onPressed: () async {
- await bind.mainSetOption(key: 'whitelist', value: '');
- callback?.call();
- close();
- },
- child: Text(translate("Clear"))),
- TextButton(
- onPressed: () async {
- setState(() {
- msg = "";
- isInProgress = true;
- });
- newWhiteListField = controller.text.trim();
- var newWhiteList = "";
- if (newWhiteListField.isEmpty) {
- // pass
- } else {
- final ips =
- newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
- // test ip
- final ipMatch = RegExp(
- r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
- final ipv6Match = RegExp(
- r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
- for (final ip in ips) {
- if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
- msg = "${translate("Invalid IP")} $ip";
- setState(() {
- isInProgress = false;
- });
- return;
- }
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("Clear", onPressed: () async {
+ await bind.mainSetOption(key: 'whitelist', value: '');
+ callback?.call();
+ close();
+ }, isOutline: true),
+ dialogButton(
+ "OK",
+ onPressed: () async {
+ setState(() {
+ msg = "";
+ isInProgress = true;
+ });
+ newWhiteListField = controller.text.trim();
+ var newWhiteList = "";
+ if (newWhiteListField.isEmpty) {
+ // pass
+ } else {
+ final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+"));
+ // test ip
+ final ipMatch = RegExp(
+ r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$");
+ final ipv6Match = RegExp(
+ r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$");
+ for (final ip in ips) {
+ if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) {
+ msg = "${translate("Invalid IP")} $ip";
+ setState(() {
+ isInProgress = false;
+ });
+ return;
}
- newWhiteList = ips.join(',');
}
- await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
- callback?.call();
- close();
- },
- child: Text(translate("OK"))),
+ newWhiteList = ips.join(',');
+ }
+ await bind.mainSetOption(key: 'whitelist', value: newWhiteList);
+ callback?.call();
+ close();
+ },
+ ),
],
onCancel: close,
);
@@ -188,21 +271,19 @@ Future changeDirectAccessPort(
r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')),
],
controller: controller,
- focusNode: FocusNode()..requestFocus()),
+ autofocus: true),
),
],
),
],
),
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(
- onPressed: () async {
- await bind.mainSetOption(
- key: 'direct-access-port', value: controller.text);
- close();
- },
- child: Text(translate("OK"))),
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("OK", onPressed: () async {
+ await bind.mainSetOption(
+ key: 'direct-access-port', value: controller.text);
+ close();
+ }),
],
onCancel: close,
);
diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart
index ce27ceb2c..43dc3a658 100644
--- a/flutter/lib/common/widgets/login.dart
+++ b/flutter/lib/common/widgets/login.dart
@@ -197,24 +197,25 @@ class _WidgetOPState extends State {
_failedMsg = '';
}
return Offstage(
- offstage:
- _failedMsg.isEmpty && widget.curOP.value != widget.config.op,
- child: Row(
- children: [
- Text(
- _stateMsg,
- style: TextStyle(fontSize: 12),
- ),
- SizedBox(width: 8),
- Text(
- _failedMsg,
- style: TextStyle(
- fontSize: 14,
- color: Colors.red,
- ),
+ offstage:
+ _failedMsg.isEmpty && widget.curOP.value != widget.config.op,
+ child: RichText(
+ text: TextSpan(
+ text: '$_stateMsg ',
+ style:
+ DefaultTextStyle.of(context).style.copyWith(fontSize: 12),
+ children: [
+ TextSpan(
+ text: _failedMsg,
+ style: DefaultTextStyle.of(context).style.copyWith(
+ fontSize: 14,
+ color: Colors.red,
+ ),
),
],
- ));
+ ),
+ ),
+ );
}),
Obx(
() => Offstage(
@@ -323,13 +324,13 @@ class LoginWidgetUserPass extends StatelessWidget {
children: [
const SizedBox(height: 8.0),
DialogTextField(
- title: '${translate("Username")}:',
+ title: translate("Username"),
controller: username,
focusNode: userFocusNode,
prefixIcon: Icon(Icons.account_circle_outlined),
errorText: usernameMsg),
DialogTextField(
- title: '${translate("Password")}:',
+ title: translate("Password"),
obscureText: true,
controller: pass,
prefixIcon: Icon(Icons.lock_outline),
@@ -538,7 +539,7 @@ Future loginDialog() async {
),
LoginWidgetOP(
ops: [
- ConfigOP(op: 'Github', iconWidth: 20),
+ ConfigOP(op: 'GitHub', iconWidth: 20),
ConfigOP(op: 'Google', iconWidth: 20),
ConfigOP(op: 'Okta', iconWidth: 38),
],
@@ -550,7 +551,7 @@ Future loginDialog() async {
),
],
),
- actions: [msgBoxButton(translate('Close'), onDialogCancel)],
+ actions: [dialogButton('Close', onPressed: onDialogCancel)],
onCancel: onDialogCancel,
);
});
@@ -666,9 +667,11 @@ Future verificationCodeDialog(UserPayload? user) async {
child: const LinearProgressIndicator()),
],
),
+ onCancel: close,
+ onSubmit: onVerify,
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(onPressed: onVerify, child: Text(translate("Verify"))),
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("Verify", onPressed: onVerify),
]);
});
diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart
index 81797962e..ba7b8a059 100644
--- a/flutter/lib/common/widgets/overlay.dart
+++ b/flutter/lib/common/widgets/overlay.dart
@@ -1,5 +1,7 @@
+import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
+import 'package:get/get.dart';
import 'package:provider/provider.dart';
import '../../consts.dart';
@@ -95,12 +97,14 @@ class DraggableChatWindow extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8),
- child: Row(children: [
- Icon(Icons.chat_bubble_outline,
- size: 20, color: Theme.of(context).colorScheme.primary),
- SizedBox(width: 6),
- Text(translate("Chat"))
- ])),
+ child: Obx(() => Opacity(
+ opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4,
+ child: Row(children: [
+ Icon(Icons.chat_bubble_outline,
+ size: 20, color: Theme.of(context).colorScheme.primary),
+ SizedBox(width: 6),
+ Text(translate("Chat"))
+ ])))),
Padding(
padding: EdgeInsets.all(2),
child: ActionIcon(
@@ -303,59 +307,119 @@ class _DraggableState extends State {
if (widget.checkKeyboard) {
checkKeyboard();
}
- if (widget.checkKeyboard) {
+ if (widget.checkScreenSize) {
checkScreenSize();
}
- return Positioned(
- top: _position.dy,
- left: _position.dx,
- width: widget.width,
- height: widget.height,
- child: widget.builder(context, onPanUpdate));
+ return Stack(children: [
+ Positioned(
+ top: _position.dy,
+ left: _position.dx,
+ width: widget.width,
+ height: widget.height,
+ child: widget.builder(context, onPanUpdate))
+ ]);
}
}
class QualityMonitor extends StatelessWidget {
- static const textStyle = TextStyle(color: MyTheme.grayBg);
final QualityMonitorModel qualityMonitorModel;
QualityMonitor(this.qualityMonitorModel);
+ Widget _row(String info, String? value, {Color? rightColor}) {
+ return Row(
+ children: [
+ Expanded(
+ flex: 8,
+ child: AutoSizeText(info,
+ style: TextStyle(color: MyTheme.darkGray),
+ textAlign: TextAlign.right,
+ maxLines: 1)),
+ Spacer(flex: 1),
+ Expanded(
+ flex: 8,
+ child: AutoSizeText(value ?? '',
+ style: TextStyle(color: rightColor ?? Colors.white),
+ maxLines: 1)),
+ ],
+ );
+ }
+
@override
Widget build(BuildContext context) => ChangeNotifierProvider.value(
value: qualityMonitorModel,
child: Consumer(
- builder: (context, qualityMonitorModel, child) => Positioned(
- top: 10,
- right: 10,
- child: qualityMonitorModel.show
- ? Container(
- padding: const EdgeInsets.all(8),
- color: MyTheme.canvasColor.withAlpha(120),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- "Speed: ${qualityMonitorModel.data.speed ?? ''}",
- style: textStyle,
- ),
- Text(
- "FPS: ${qualityMonitorModel.data.fps ?? ''}",
- style: textStyle,
- ),
- Text(
- "Delay: ${qualityMonitorModel.data.delay ?? ''} ms",
- style: textStyle,
- ),
- Text(
- "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb",
- style: textStyle,
- ),
- Text(
- "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}",
- style: textStyle,
- ),
- ],
- ),
- )
- : const SizedBox.shrink())));
+ builder: (context, qualityMonitorModel, child) => qualityMonitorModel
+ .show
+ ? Container(
+ constraints: BoxConstraints(maxWidth: 200),
+ padding: const EdgeInsets.all(8),
+ color: MyTheme.canvasColor.withAlpha(120),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _row("Speed", qualityMonitorModel.data.speed ?? '-'),
+ _row("FPS", qualityMonitorModel.data.fps ?? '-'),
+ _row(
+ "Delay", "${qualityMonitorModel.data.delay ?? '-'}ms",
+ rightColor: Colors.green),
+ _row("Target Bitrate",
+ "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"),
+ _row(
+ "Codec", qualityMonitorModel.data.codecFormat ?? '-'),
+ ],
+ ),
+ )
+ : const SizedBox.shrink()));
+}
+
+class BlockableOverlayState extends OverlayKeyState {
+ final _middleBlocked = false.obs;
+
+ VoidCallback? onMiddleBlockedClick; // to-do use listener
+
+ RxBool get middleBlocked => _middleBlocked;
+
+ void addMiddleBlockedListener(void Function(bool) cb) {
+ _middleBlocked.listen(cb);
+ }
+
+ void setMiddleBlocked(bool blocked) {
+ if (blocked != _middleBlocked.value) {
+ _middleBlocked.value = blocked;
+ }
+ }
+}
+
+class BlockableOverlay extends StatelessWidget {
+ final Widget underlying;
+ final List? upperLayer;
+
+ final BlockableOverlayState state;
+
+ BlockableOverlay(
+ {required this.underlying, required this.state, this.upperLayer});
+
+ @override
+ Widget build(BuildContext context) {
+ final initialEntries = [
+ OverlayEntry(builder: (_) => underlying),
+
+ /// middle layer
+ OverlayEntry(
+ builder: (context) => Obx(() => Listener(
+ onPointerDown: (_) {
+ state.onMiddleBlockedClick?.call();
+ },
+ child: Container(
+ color:
+ state.middleBlocked.value ? Colors.transparent : null)))),
+ ];
+
+ if (upperLayer != null) {
+ initialEntries.addAll(upperLayer!);
+ }
+
+ /// set key
+ return Overlay(key: state.key, initialEntries: initialEntries);
+ }
}
diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart
index a98739606..5a7f2bfa7 100644
--- a/flutter/lib/common/widgets/peer_card.dart
+++ b/flutter/lib/common/widgets/peer_card.dart
@@ -170,8 +170,8 @@ class _PeerCardState extends State<_PeerCard>
),
Expanded(
child: Container(
- decoration:
- BoxDecoration(color: Theme.of(context).backgroundColor),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.background),
child: Row(
children: [
Expanded(
@@ -266,7 +266,7 @@ class _PeerCardState extends State<_PeerCard>
),
),
Container(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@@ -316,21 +316,11 @@ class _PeerCardState extends State<_PeerCard>
bool get wantKeepAlive => true;
}
-enum CardType {
- recent,
- fav,
- lan,
- ab,
- grp,
-}
-
abstract class BasePeerCard extends StatelessWidget {
final Peer peer;
final EdgeInsets? menuPadding;
- final CardType cardType;
- BasePeerCard(
- {required this.peer, required this.cardType, this.menuPadding, Key? key})
+ BasePeerCard({required this.peer, this.menuPadding, Key? key})
: super(key: key);
@override
@@ -435,7 +425,7 @@ abstract class BasePeerCard extends StatelessWidget {
if (Navigator.canPop(context)) {
Navigator.pop(context);
}
- _rdpDialog(id, cardType);
+ _rdpDialog(id);
},
)),
))
@@ -480,6 +470,12 @@ abstract class BasePeerCard extends StatelessWidget {
);
}
+ @protected
+ Future _isForceAlwaysRelay(String id) async {
+ return (await bind.mainGetPeerOption(id: id, key: 'force-always-relay'))
+ .isNotEmpty;
+ }
+
@protected
Future> _forceAlwaysRelayAction(String id) async {
const option = 'force-always-relay';
@@ -487,16 +483,12 @@ abstract class BasePeerCard extends StatelessWidget {
switchType: SwitchType.scheckbox,
text: translate('Always connect via relay'),
getter: () async {
- if (cardType == CardType.ab) {
- return gFFI.abModel.find(id)?.forceAlwaysRelay ?? false;
- } else {
- return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty;
- }
+ return await _isForceAlwaysRelay(id);
},
setter: (bool v) async {
gFFI.abModel.setPeerForceAlwaysRelay(id, v);
await bind.mainSetPeerOption(
- id: id, key: option, value: bool2option('force-always-relay', v));
+ id: id, key: option, value: bool2option(option, v));
},
padding: menuPadding,
dismissOnClicked: true,
@@ -523,20 +515,24 @@ abstract class BasePeerCard extends StatelessWidget {
String id, Future Function() reloadFunc,
{bool isLan = false}) {
return MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Remove'),
- style: style,
+ childBuilder: (TextStyle? style) => Row(
+ children: [
+ Text(
+ translate('Delete'),
+ style: style?.copyWith(color: Colors.red),
+ ),
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Transform.scale(
+ scale: 0.8,
+ child: Icon(Icons.delete_forever, color: Colors.red),
+ ),
+ ).marginOnly(right: 4)),
+ ],
),
proc: () {
- () async {
- if (isLan) {
- // TODO
- } else {
- await bind.mainRemovePeer(id: id);
- }
- removePreference(id);
- await reloadFunc();
- }();
+ _delete(id, isLan, reloadFunc);
},
padding: menuPadding,
dismissOnClicked: true,
@@ -561,9 +557,21 @@ abstract class BasePeerCard extends StatelessWidget {
@protected
MenuEntryBase _addFavAction(String id) {
return MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Add to Favorites'),
- style: style,
+ childBuilder: (TextStyle? style) => Row(
+ children: [
+ Text(
+ translate('Add to Favorites'),
+ style: style,
+ ),
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Transform.scale(
+ scale: 0.8,
+ child: Icon(Icons.star_outline),
+ ),
+ ).marginOnly(right: 4)),
+ ],
),
proc: () {
() async {
@@ -583,9 +591,21 @@ abstract class BasePeerCard extends StatelessWidget {
MenuEntryBase _rmFavAction(
String id, Future Function() reloadFunc) {
return MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Remove from Favorites'),
- style: style,
+ childBuilder: (TextStyle? style) => Row(
+ children: [
+ Text(
+ translate('Remove from Favorites'),
+ style: style,
+ ),
+ Expanded(
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Transform.scale(
+ scale: 0.8,
+ child: Icon(Icons.star),
+ ),
+ ).marginOnly(right: 4)),
+ ],
),
proc: () {
() async {
@@ -621,14 +641,13 @@ abstract class BasePeerCard extends StatelessWidget {
);
}
+ @protected
+ Future _getAlias(String id) async =>
+ await bind.mainGetPeerOption(id: id, key: 'alias');
+
void _rename(String id) async {
RxBool isInProgress = false.obs;
- String name;
- if (cardType == CardType.ab) {
- name = gFFI.abModel.find(id)?.alias ?? "";
- } else {
- name = await bind.mainGetPeerOption(id: id, key: 'alias');
- }
+ String name = await _getAlias(id);
var controller = TextEditingController(text: name);
gFFI.dialogManager.show((setState, close) {
submit() async {
@@ -636,13 +655,19 @@ abstract class BasePeerCard extends StatelessWidget {
String name = controller.text.trim();
await bind.mainSetPeerAlias(id: id, alias: name);
gFFI.abModel.setPeerAlias(id, name);
- update();
+ _update();
close();
isInProgress.value = false;
}
return CustomAlertDialog(
- title: Text(translate('Rename')),
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(Icons.edit_rounded, color: MyTheme.accent),
+ Text(translate('Rename')).paddingOnly(left: 10),
+ ],
+ ),
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -650,9 +675,8 @@ abstract class BasePeerCard extends StatelessWidget {
child: Form(
child: TextFormField(
controller: controller,
- focusNode: FocusNode()..requestFocus(),
- decoration:
- const InputDecoration(border: OutlineInputBorder()),
+ autofocus: true,
+ decoration: InputDecoration(labelText: translate('Name')),
),
),
),
@@ -662,8 +686,17 @@ abstract class BasePeerCard extends StatelessWidget {
],
),
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(onPressed: submit, child: Text(translate("OK"))),
+ dialogButton(
+ "Cancel",
+ icon: Icon(Icons.close_rounded),
+ onPressed: close,
+ isOutline: true,
+ ),
+ dialogButton(
+ "OK",
+ icon: Icon(Icons.done_rounded),
+ onPressed: submit,
+ ),
],
onSubmit: submit,
onCancel: close,
@@ -671,34 +704,65 @@ abstract class BasePeerCard extends StatelessWidget {
});
}
- void update() {
- switch (cardType) {
- case CardType.recent:
- bind.mainLoadRecentPeers();
- break;
- case CardType.fav:
- bind.mainLoadFavPeers();
- break;
- case CardType.lan:
- bind.mainLoadLanPeers();
- break;
- case CardType.ab:
- gFFI.abModel.pullAb();
- break;
- case CardType.grp:
- gFFI.groupModel.pull();
- break;
- }
+ @protected
+ void _update();
+
+ void _delete(String id, bool isLan, Function reloadFunc) async {
+ gFFI.dialogManager.show(
+ (setState, close) {
+ submit() async {
+ if (isLan) {
+ bind.mainRemoveDiscovered(id: id);
+ } else {
+ final favs = (await bind.mainGetFav()).toList();
+ if (favs.remove(id)) {
+ await bind.mainStoreFav(favs: favs);
+ }
+ await bind.mainRemovePeer(id: id);
+ }
+ removePreference(id);
+ await reloadFunc();
+ close();
+ }
+
+ return CustomAlertDialog(
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Icon(
+ Icons.delete_rounded,
+ color: Colors.red,
+ ),
+ Text(translate('Delete')).paddingOnly(
+ left: 10,
+ ),
+ ],
+ ),
+ content: SizedBox.shrink(),
+ actions: [
+ dialogButton(
+ "Cancel",
+ icon: Icon(Icons.close_rounded),
+ onPressed: close,
+ isOutline: true,
+ ),
+ dialogButton(
+ "OK",
+ icon: Icon(Icons.done_rounded),
+ onPressed: submit,
+ ),
+ ],
+ onSubmit: submit,
+ onCancel: close,
+ );
+ },
+ );
}
}
class RecentPeerCard extends BasePeerCard {
RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(
- peer: peer,
- cardType: CardType.recent,
- menuPadding: menuPadding,
- key: key);
+ : super(peer: peer, menuPadding: menuPadding, key: key);
@override
Future>> _buildMenuItems(
@@ -707,6 +771,9 @@ class RecentPeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
+
+ final List favs = (await bind.mainGetFav()).toList();
+
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
@@ -720,27 +787,37 @@ class RecentPeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
- menuItems.add(_removeAction(peer.id, () async {
- await bind.mainLoadRecentPeers();
- }));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
- menuItems.add(_addFavAction(peer.id));
- if (!gFFI.abModel.idContainBy(peer.id)) {
- menuItems.add(_addToAb(peer));
+
+ if (!favs.contains(peer.id)) {
+ menuItems.add(_addFavAction(peer.id));
+ } else {
+ menuItems.add(_rmFavAction(peer.id, () async {}));
}
+
+ if (gFFI.userModel.userName.isNotEmpty) {
+ if (!gFFI.abModel.idContainBy(peer.id)) {
+ menuItems.add(_addToAb(peer));
+ }
+ }
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {
+ await bind.mainLoadRecentPeers();
+ }));
return menuItems;
}
+
+ @protected
+ @override
+ void _update() => bind.mainLoadRecentPeers();
}
class FavoritePeerCard extends BasePeerCard {
FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(
- peer: peer,
- cardType: CardType.fav,
- menuPadding: menuPadding,
- key: key);
+ : super(peer: peer, menuPadding: menuPadding, key: key);
@override
Future>> _buildMenuItems(
@@ -762,29 +839,34 @@ class FavoritePeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
- menuItems.add(_removeAction(peer.id, () async {
- await bind.mainLoadFavPeers();
- }));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
menuItems.add(_rmFavAction(peer.id, () async {
await bind.mainLoadFavPeers();
}));
- if (!gFFI.abModel.idContainBy(peer.id)) {
- menuItems.add(_addToAb(peer));
+
+ if (gFFI.userModel.userName.isNotEmpty) {
+ if (!gFFI.abModel.idContainBy(peer.id)) {
+ menuItems.add(_addToAb(peer));
+ }
}
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {
+ await bind.mainLoadFavPeers();
+ }));
return menuItems;
}
+
+ @protected
+ @override
+ void _update() => bind.mainLoadFavPeers();
}
class DiscoveredPeerCard extends BasePeerCard {
DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(
- peer: peer,
- cardType: CardType.lan,
- menuPadding: menuPadding,
- key: key);
+ : super(peer: peer, menuPadding: menuPadding, key: key);
@override
Future>> _buildMenuItems(
@@ -793,6 +875,9 @@ class DiscoveredPeerCard extends BasePeerCard {
_connectAction(context, peer),
_transferFileAction(context, peer.id),
];
+
+ final List favs = (await bind.mainGetFav()).toList();
+
if (isDesktop && peer.platform != 'Android') {
menuItems.add(_tcpTunnelingAction(context, peer.id));
}
@@ -804,22 +889,39 @@ class DiscoveredPeerCard extends BasePeerCard {
if (Platform.isWindows) {
menuItems.add(_createShortCutAction(peer.id));
}
- menuItems.add(MenuEntryDivider());
- menuItems.add(_removeAction(peer.id, () async {}));
- if (!gFFI.abModel.idContainBy(peer.id)) {
- menuItems.add(_addToAb(peer));
+
+ final inRecent = await bind.mainIsInRecentPeers(id: peer.id);
+ if (inRecent) {
+ if (!favs.contains(peer.id)) {
+ menuItems.add(_addFavAction(peer.id));
+ } else {
+ menuItems.add(_rmFavAction(peer.id, () async {}));
+ }
}
+
+ if (gFFI.userModel.userName.isNotEmpty) {
+ if (!gFFI.abModel.idContainBy(peer.id)) {
+ menuItems.add(_addToAb(peer));
+ }
+ }
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(
+ _removeAction(peer.id, () async {
+ await bind.mainLoadLanPeers();
+ }, isLan: true),
+ );
return menuItems;
}
+
+ @protected
+ @override
+ void _update() => bind.mainLoadLanPeers();
}
class AddressBookPeerCard extends BasePeerCard {
AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(
- peer: peer,
- cardType: CardType.ab,
- menuPadding: menuPadding,
- key: key);
+ : super(peer: peer, menuPadding: menuPadding, key: key);
@override
Future>> _buildMenuItems(
@@ -841,16 +943,32 @@ class AddressBookPeerCard extends BasePeerCard {
}
menuItems.add(MenuEntryDivider());
menuItems.add(_renameAction(peer.id));
- menuItems.add(_removeAction(peer.id, () async {}));
if (await bind.mainPeerHasPassword(id: peer.id)) {
menuItems.add(_unrememberPasswordAction(peer.id));
}
if (gFFI.abModel.tags.isNotEmpty) {
menuItems.add(_editTagAction(peer.id));
}
+
+ menuItems.add(MenuEntryDivider());
+ menuItems.add(_removeAction(peer.id, () async {}));
return menuItems;
}
+ @protected
+ @override
+ Future _isForceAlwaysRelay(String id) async =>
+ gFFI.abModel.find(id)?.forceAlwaysRelay ?? false;
+
+ @protected
+ @override
+ Future _getAlias(String id) async =>
+ gFFI.abModel.find(id)?.alias ?? '';
+
+ @protected
+ @override
+ void _update() => gFFI.abModel.pullAb();
+
@protected
@override
MenuEntryBase _removeAction(
@@ -931,8 +1049,8 @@ class AddressBookPeerCard extends BasePeerCard {
],
),
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(onPressed: submit, child: Text(translate("OK"))),
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@@ -943,11 +1061,7 @@ class AddressBookPeerCard extends BasePeerCard {
class MyGroupPeerCard extends BasePeerCard {
MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
- : super(
- peer: peer,
- cardType: CardType.grp,
- menuPadding: menuPadding,
- key: key);
+ : super(peer: peer, menuPadding: menuPadding, key: key);
@override
Future>> _buildMenuItems(
@@ -974,18 +1088,15 @@ class MyGroupPeerCard extends BasePeerCard {
}
return menuItems;
}
+
+ @protected
+ @override
+ void _update() => gFFI.groupModel.pull();
}
-void _rdpDialog(String id, CardType card) async {
- String port, username;
- if (card == CardType.ab) {
- port = gFFI.abModel.find(id)?.rdpPort ?? '';
- username = gFFI.abModel.find(id)?.rdpUsername ?? '';
- } else {
- port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
- username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
- }
-
+void _rdpDialog(String id) async {
+ final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port');
+ final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username');
final portController = TextEditingController(text: port);
final userController = TextEditingController(text: username);
final passwordController = TextEditingController(
@@ -1019,14 +1130,11 @@ void _rdpDialog(String id, CardType card) async {
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
+ constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Port')}:",
- textAlign: TextAlign.start,
- ).marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
inputFormatters: [
@@ -1036,25 +1144,19 @@ void _rdpDialog(String id, CardType card) async {
decoration: const InputDecoration(
border: OutlineInputBorder(), hintText: '3389'),
controller: portController,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
+ constraints: const BoxConstraints(minWidth: 140),
child: Text(
"${translate('Username')}:",
- textAlign: TextAlign.start,
- ).marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
decoration:
@@ -1063,19 +1165,15 @@ void _rdpDialog(String id, CardType card) async {
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text("${translate('Password')}:")
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ "${translate('Password')}:",
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: Obx(() => TextField(
obscureText: secure.value,
@@ -1090,13 +1188,13 @@ void _rdpDialog(String id, CardType card) async {
)),
),
],
- ),
+ ).marginOnly(bottom: 8),
],
),
),
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(onPressed: submit, child: Text(translate("OK"))),
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
@@ -1115,21 +1213,21 @@ Widget getOnline(double rightPadding, bool online) {
}
class ActionMore extends StatelessWidget {
- final RxBool _iconMoreHover = false.obs;
+ final RxBool _hover = false.obs;
@override
Widget build(BuildContext context) {
- return MouseRegion(
- onEnter: (_) => _iconMoreHover.value = true,
- onExit: (_) => _iconMoreHover.value = false,
+ return InkWell(
+ onTap: () {},
+ onHover: (value) => _hover.value = value,
child: Obx(() => CircleAvatar(
radius: 14,
- backgroundColor: _iconMoreHover.value
+ backgroundColor: _hover.value
? Theme.of(context).scaffoldBackgroundColor
- : Theme.of(context).backgroundColor,
+ : Theme.of(context).colorScheme.background,
child: Icon(Icons.more_vert,
size: 18,
- color: _iconMoreHover.value
+ color: _hover.value
? Theme.of(context).textTheme.titleLarge?.color
: Theme.of(context)
.textTheme
diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart
index 0c24fe7ea..da7e37e6b 100644
--- a/flutter/lib/common/widgets/peer_tab_page.dart
+++ b/flutter/lib/common/widgets/peer_tab_page.dart
@@ -1,6 +1,7 @@
import 'dart:ui' as ui;
import 'package:bot_toast/bot_toast.dart';
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common/widgets/address_book.dart';
import 'package:flutter_hbb/common/widgets/my_group.dart';
@@ -11,106 +12,15 @@ import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart'
as mod_menu;
+import 'package:flutter_hbb/models/peer_tab_model.dart';
import 'package:get/get.dart';
+import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
+import 'package:provider/provider.dart';
+import 'package:visibility_detector/visibility_detector.dart';
import '../../common.dart';
import '../../models/platform_model.dart';
-const int groupTabIndex = 4;
-const String defaultGroupTabname = 'Group';
-
-class StatePeerTab {
- final RxInt currentTab = 0.obs;
- final RxInt tabHiddenFlag = 0.obs;
- final RxList tabNames = [
- 'Recent Sessions',
- 'Favorites',
- 'Discovered',
- 'Address Book',
- defaultGroupTabname,
- ].obs;
-
- StatePeerTab._() {
- tabHiddenFlag.value = (int.tryParse(
- bind.getLocalFlutterConfig(k: 'hidden-peer-card'),
- radix: 2) ??
- 0);
- var tabs = _notHiddenTabs();
- currentTab.value =
- int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
- if (!tabs.contains(currentTab.value)) {
- currentTab.value = 0;
- }
- }
- static final StatePeerTab instance = StatePeerTab._();
-
- check() {
- var tabs = _notHiddenTabs();
- if (filterGroupCard()) {
- if (currentTab.value == groupTabIndex) {
- currentTab.value =
- tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0;
- bind.setLocalFlutterConfig(
- k: 'peer-tab-index', v: currentTab.value.toString());
- }
- } else {
- if (gFFI.userModel.isAdmin.isFalse &&
- gFFI.userModel.groupName.isNotEmpty) {
- tabNames[groupTabIndex] = gFFI.userModel.groupName.value;
- } else {
- tabNames[groupTabIndex] = defaultGroupTabname;
- }
- if (tabs.contains(groupTabIndex) &&
- int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ==
- groupTabIndex) {
- currentTab.value = groupTabIndex;
- }
- }
- }
-
- List currentTabs() {
- var v = List.empty(growable: true);
- for (int i = 0; i < tabNames.length; i++) {
- if (!_isTabHidden(i) && !_isTabFilter(i)) {
- v.add(i);
- }
- }
- return v;
- }
-
- bool filterGroupCard() {
- if (gFFI.groupModel.users.isEmpty ||
- (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) {
- return true;
- } else {
- return false;
- }
- }
-
- bool _isTabHidden(int tabindex) {
- return tabHiddenFlag & (1 << tabindex) != 0;
- }
-
- bool _isTabFilter(int tabIndex) {
- if (tabIndex == groupTabIndex) {
- return filterGroupCard();
- }
- return false;
- }
-
- List _notHiddenTabs() {
- var v = List.empty(growable: true);
- for (int i = 0; i < tabNames.length; i++) {
- if (!_isTabHidden(i)) {
- v.add(i);
- }
- }
- return v;
- }
-}
-
-final statePeerTab = StatePeerTab.instance;
-
class PeerTabPage extends StatefulWidget {
const PeerTabPage({Key? key}) : super(key: key);
@override
@@ -156,11 +66,10 @@ class _PeerTabPageState extends State
),
() => {}),
];
+ final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
@override
void initState() {
- adjustTab();
-
final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
if (uiType != '') {
peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index
@@ -172,16 +81,11 @@ class _PeerTabPageState extends State
Future handleTabSelection(int tabIndex) async {
if (tabIndex < entries.length) {
- statePeerTab.currentTab.value = tabIndex;
+ gFFI.peerTabModel.setCurrentTab(tabIndex);
entries[tabIndex].load();
}
}
- @override
- void dispose() {
- super.dispose();
- }
-
@override
Widget build(BuildContext context) {
return Column(
@@ -199,6 +103,7 @@ class _PeerTabPageState extends State
Expanded(
child: visibleContextMenuListener(
_createSwitchBar(context))),
+ buildScrollJumper(),
const PeerSearchBar(),
Offstage(
offstage: !isDesktop,
@@ -213,87 +118,121 @@ class _PeerTabPageState extends State
}
Widget _createSwitchBar(BuildContext context) {
- final textColor = Theme.of(context).textTheme.titleLarge?.color;
- return Obx(() {
- var tabs = statePeerTab.currentTabs();
- return ListView(
- scrollDirection: Axis.horizontal,
- physics: NeverScrollableScrollPhysics(),
- controller: ScrollController(),
- children: tabs.map((t) {
- return InkWell(
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- decoration: BoxDecoration(
- color: statePeerTab.currentTab.value == t
- ? Theme.of(context).backgroundColor
- : null,
- borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
- ),
- child: Align(
- alignment: Alignment.center,
- child: Text(
- translatedTabname(t),
- textAlign: TextAlign.center,
- style: TextStyle(
- height: 1,
- fontSize: 14,
- color: statePeerTab.currentTab.value == t
- ? textColor
- : textColor
- ?..withOpacity(0.5)),
- ),
- )),
- onTap: () async {
- await handleTabSelection(t);
- await bind.setLocalFlutterConfig(
- k: 'peer-tab-index', v: t.toString());
+ final model = Provider.of(context);
+ int indexCounter = -1;
+ return ReorderableListView(
+ buildDefaultDragHandles: false,
+ onReorder: (oldIndex, newIndex) {
+ model.onReorder(oldIndex, newIndex);
+ },
+ scrollDirection: Axis.horizontal,
+ physics: NeverScrollableScrollPhysics(),
+ scrollController: model.sc,
+ children: model.visibleOrderedTabs.map((t) {
+ indexCounter++;
+ return ReorderableDragStartListener(
+ key: ValueKey(t),
+ index: indexCounter,
+ child: VisibilityDetector(
+ key: ValueKey(t),
+ onVisibilityChanged: (info) {
+ final id = (info.key as ValueKey).value;
+ model.setTabFullyVisible(id, info.visibleFraction > 0.99);
},
- );
- }).toList());
- });
+ child: Listener(
+ // handle mouse wheel
+ onPointerSignal: (e) {
+ if (e is PointerScrollEvent) {
+ if (!model.sc.canScroll) return;
+ _scrollDebounce.call(() {
+ model.sc.animateTo(model.sc.offset + e.scrollDelta.dy,
+ duration: Duration(milliseconds: 200),
+ curve: Curves.ease);
+ });
+ }
+ },
+ child: InkWell(
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ decoration: BoxDecoration(
+ color: model.currentTab == t
+ ? Theme.of(context).colorScheme.background
+ : null,
+ borderRadius: BorderRadius.circular(isDesktop ? 2 : 6),
+ ),
+ child: Align(
+ alignment: Alignment.center,
+ child: Text(
+ model.translatedTabname(t),
+ textAlign: TextAlign.center,
+ style: TextStyle(
+ height: 1,
+ fontSize: 14,
+ color: model.currentTab == t
+ ? MyTheme.tabbar(context).selectedTextColor
+ : MyTheme.tabbar(context).unSelectedTextColor
+ ?..withOpacity(0.5)),
+ ),
+ )),
+ onTap: () async {
+ await handleTabSelection(t);
+ await bind.setLocalFlutterConfig(
+ k: 'peer-tab-index', v: t.toString());
+ },
+ ),
+ ),
+ ),
+ );
+ }).toList());
}
- translatedTabname(int index) {
- if (index < statePeerTab.tabNames.length) {
- final name = statePeerTab.tabNames[index];
- if (index == groupTabIndex) {
- if (name == defaultGroupTabname) {
- return translate(name);
- } else {
- return name;
- }
- } else {
- return translate(name);
- }
- }
- assert(false);
- return index.toString();
+ Widget buildScrollJumper() {
+ final model = Provider.of(context);
+ return Offstage(
+ offstage: !model.showScrollBtn,
+ child: Row(
+ children: [
+ GestureDetector(
+ child: Icon(Icons.arrow_left,
+ size: 22,
+ color: model.leftFullyVisible
+ ? Theme.of(context).disabledColor
+ : null),
+ onTap: model.sc.backward),
+ GestureDetector(
+ child: Icon(Icons.arrow_right,
+ size: 22,
+ color: model.rightFullyVisible
+ ? Theme.of(context).disabledColor
+ : null),
+ onTap: model.sc.forward)
+ ],
+ ));
}
Widget _createPeersView() {
- final verticalMargin = isDesktop ? 12.0 : 6.0;
- return Expanded(
- child: Obx(() {
- var tabs = statePeerTab.currentTabs();
- if (tabs.isEmpty) {
- return visibleContextMenuListener(Center(
- child: Text(translate('Right click to select tabs')),
- ));
+ final model = Provider.of(context);
+ Widget child;
+ if (model.visibleOrderedTabs.isEmpty) {
+ child = visibleContextMenuListener(Center(
+ child: Text(translate('Right click to select tabs')),
+ ));
+ } else {
+ if (model.visibleOrderedTabs.contains(model.currentTab)) {
+ child = entries[model.currentTab].widget;
} else {
- if (tabs.contains(statePeerTab.currentTab.value)) {
- return entries[statePeerTab.currentTab.value].widget;
- } else {
- statePeerTab.currentTab.value = tabs[0];
- return entries[statePeerTab.currentTab.value].widget;
- }
+ model.setCurrentTab(model.visibleOrderedTabs[0]);
+ child = entries[0].widget;
}
- }).marginSymmetric(vertical: verticalMargin));
+ }
+ return Expanded(
+ child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0));
}
Widget _createPeerViewTypeSwitch(BuildContext context) {
final textColor = Theme.of(context).textTheme.titleLarge?.color;
- final activeDeco = BoxDecoration(color: Theme.of(context).backgroundColor);
+ final activeDeco =
+ BoxDecoration(color: Theme.of(context).colorScheme.background);
return Row(
children: [PeerUiType.grid, PeerUiType.list]
.map((type) => Obx(
@@ -321,13 +260,6 @@ class _PeerTabPageState extends State
);
}
- adjustTab() {
- var tabs = statePeerTab.currentTabs();
- if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) {
- statePeerTab.currentTab.value = tabs[0];
- }
- }
-
Widget visibleContextMenuListener(Widget child) {
return Listener(
onPointerDown: (e) {
@@ -347,44 +279,36 @@ class _PeerTabPageState extends State
}
Widget visibleContextMenu(CancelFunc cancelFunc) {
- return Obx(() {
- final List menu = List.empty(growable: true);
- for (int i = 0; i < statePeerTab.tabNames.length; i++) {
- if (i == groupTabIndex && statePeerTab.filterGroupCard()) {
- continue;
- }
- int bitMask = 1 << i;
- menu.add(MenuEntrySwitch(
- switchType: SwitchType.scheckbox,
- text: translatedTabname(i),
- getter: () async {
- return statePeerTab.tabHiddenFlag & bitMask == 0;
- },
- setter: (show) async {
- if (show) {
- statePeerTab.tabHiddenFlag.value &= ~bitMask;
- } else {
- statePeerTab.tabHiddenFlag.value |= bitMask;
- }
- await bind.setLocalFlutterConfig(
- k: 'hidden-peer-card',
- v: statePeerTab.tabHiddenFlag.value.toRadixString(2));
- cancelFunc();
- adjustTab();
- }));
- }
- return mod_menu.PopupMenu(
- items: menu
- .map((entry) => entry.build(
- context,
- const MenuConfig(
- commonColor: MyTheme.accent,
- height: 20.0,
- dividerHeight: 12.0,
- )))
- .expand((i) => i)
- .toList());
- });
+ final model = Provider.of(context);
+ final List menu = List.empty(growable: true);
+ final List menuIndex = List.empty(growable: true);
+ var list = model.orderedNotFilteredTabs();
+ for (int i = 0; i < list.length; i++) {
+ int tabIndex = list[i];
+ int bitMask = 1 << tabIndex;
+ menuIndex.add(tabIndex);
+ menu.add(MenuEntrySwitch(
+ switchType: SwitchType.scheckbox,
+ text: model.translatedTabname(tabIndex),
+ getter: () async {
+ return model.tabHiddenFlag & bitMask == 0;
+ },
+ setter: (show) async {
+ model.onHideShow(tabIndex, show);
+ cancelFunc();
+ }));
+ }
+ return mod_menu.PopupMenu(
+ items: menu
+ .map((entry) => entry.build(
+ context,
+ const MenuConfig(
+ commonColor: MyTheme.accent,
+ height: 20.0,
+ dividerHeight: 12.0,
+ )))
+ .expand((i) => i)
+ .toList());
}
}
@@ -419,11 +343,16 @@ class _PeerSearchBarState extends State {
Widget _buildSearchBar() {
RxBool focused = false.obs;
FocusNode focusNode = FocusNode();
- focusNode.addListener(() => focused.value = focusNode.hasFocus);
+ focusNode.addListener(() {
+ focused.value = focusNode.hasFocus;
+ peerSearchTextController.selection = TextSelection(
+ baseOffset: 0,
+ extentOffset: peerSearchTextController.value.text.length);
+ });
return Container(
width: 120,
decoration: BoxDecoration(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6),
),
child: Obx(() => Row(
diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart
index 017850cf5..dd39cbdfd 100644
--- a/flutter/lib/common/widgets/remote_input.dart
+++ b/flutter/lib/common/widgets/remote_input.dart
@@ -62,11 +62,9 @@ class RawPointerMouseRegion extends StatelessWidget {
},
onPointerMove: inputModel.onPointMoveImage,
onPointerSignal: inputModel.onPointerSignalImage,
- /*
onPointerPanZoomStart: inputModel.onPointerPanZoomStart,
onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
- */
child: MouseRegion(
cursor: cursor ?? MouseCursor.defer,
onEnter: onEnter,
diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart
index 7aa200ae9..95e4d17e7 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -1,25 +1,33 @@
-import 'package:flutter/material.dart';
import 'dart:io';
+import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/models/state_model.dart';
const double kDesktopRemoteTabBarHeight = 28.0;
+const int kMainWindowId = 0;
-/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page"
+const String kPeerPlatformWindows = "Windows";
+const String kPeerPlatformLinux = "Linux";
+const String kPeerPlatformMacOS = "Mac OS";
+const String kPeerPlatformAndroid = "Android";
+
+/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page"
const String kAppTypeMain = "main";
+const String kAppTypeConnectionManager = "cm";
const String kAppTypeDesktopRemote = "remote";
const String kAppTypeDesktopFileTransfer = "file transfer";
const String kAppTypeDesktopPortForward = "port forward";
const String kWindowMainWindowOnTop = "main_window_on_top";
const String kWindowGetWindowInfo = "get_window_info";
+const String kWindowDisableGrabKeyboard = "disable_grab_keyboard";
const String kWindowActionRebuild = "rebuild";
const String kWindowEventHide = "hide";
const String kWindowEventShow = "show";
const String kWindowConnect = "connect";
const String kUniLinksPrefix = "rustdesk://";
-const String kActionNewConnection = "connection/new/";
const String kTabLabelHomePage = "Home";
const String kTabLabelSettingPage = "Settings";
@@ -44,6 +52,26 @@ const int kMobileMaxDisplayHeight = 1280;
const int kDesktopMaxDisplayWidth = 1920;
const int kDesktopMaxDisplayHeight = 1080;
+const double kDesktopFileTransferNameColWidth = 200;
+const double kDesktopFileTransferModifiedColWidth = 120;
+const double kDesktopFileTransferMinimumWidth = 100;
+const double kDesktopFileTransferMaximumWidth = 300;
+const double kDesktopFileTransferRowHeight = 30.0;
+const double kDesktopFileTransferHeaderHeight = 25.0;
+
+EdgeInsets get kDragToResizeAreaPadding =>
+ !kUseCompatibleUiMode && Platform.isLinux
+ ? stateGlobal.fullscreen || stateGlobal.maximize
+ ? EdgeInsets.zero
+ : EdgeInsets.all(5.0)
+ : EdgeInsets.zero;
+// https://en.wikipedia.org/wiki/Non-breaking_space
+const int $nbsp = 0x00A0;
+
+extension StringExtension on String {
+ String get nonBreaking => replaceAll(' ', String.fromCharCode($nbsp));
+}
+
const Size kConnectionManagerWindowSize = Size(300, 400);
// Tabbar transition duration, now we remove the duration
const Duration kTabTransitionDuration = Duration.zero;
@@ -58,6 +86,7 @@ const kDefaultScrollAmountMultiplier = 5.0;
const kDefaultScrollDuration = Duration(milliseconds: 50);
const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50);
const kFullScreenEdgeSize = 0.0;
+const kMaximizeEdgeSize = 0.0;
var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0;
const kWindowBorderWidth = 1.0;
const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0);
@@ -100,8 +129,33 @@ const kRemoteImageQualityLow = 'low';
/// [kRemoteImageQualityCustom] Custom image quality.
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;
+/// Android constants
+const kActionApplicationDetailsSettings =
+ "android.settings.APPLICATION_DETAILS_SETTINGS";
+const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS";
+
+const kRecordAudio = "android.permission.RECORD_AUDIO";
+const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE";
+const kRequestIgnoreBatteryOptimizations =
+ "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
+const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW";
+
+/// Android channel invoke type key
+class AndroidChannel {
+ static final kStartAction = "start_action";
+ static final kGetStartOnBootOpt = "get_start_on_boot_opt";
+ static final kSetStartOnBootOpt = "set_start_on_boot_opt";
+ static final kSyncAppDirConfigPath = "sync_app_dir";
+}
+
/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels
/// see [LogicalKeyboardKey.keyLabel]
const Map logicalKeyMap = {
diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart
index 85749a256..edbd5b7c6 100644
--- a/flutter/lib/desktop/pages/connection_page.dart
+++ b/flutter/lib/desktop/pages/connection_page.dart
@@ -8,6 +8,7 @@ import 'package:auto_size_text/auto_size_text.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/models/state_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:window_manager/window_manager.dart';
@@ -44,7 +45,7 @@ class _ConnectionPageState extends State
var svcStatusCode = 0.obs;
var svcIsUsingPublicServer = true.obs;
- bool isWindowMinisized = false;
+ bool isWindowMinimized = false;
@override
void initState() {
@@ -64,6 +65,9 @@ class _ConnectionPageState extends State
});
_idFocusNode.addListener(() {
_idInputFocused.value = _idFocusNode.hasFocus;
+ // select all to faciliate removing text, just following the behavior of address input of chrome
+ _idController.selection = TextSelection(
+ baseOffset: 0, extentOffset: _idController.value.text.length);
});
windowManager.addListener(this);
}
@@ -80,16 +84,28 @@ class _ConnectionPageState extends State
void onWindowEvent(String eventName) {
super.onWindowEvent(eventName);
if (eventName == 'minimize') {
- isWindowMinisized = true;
+ isWindowMinimized = true;
} else if (eventName == 'maximize' || eventName == 'restore') {
- if (isWindowMinisized && Platform.isWindows) {
- // windows can't update when minisized.
+ if (isWindowMinimized && Platform.isWindows) {
+ // windows can't update when minimized.
Get.forceAppUpdate();
}
- isWindowMinisized = false;
+ isWindowMinimized = false;
}
}
+ @override
+ void onWindowEnterFullScreen() {
+ // Remove edge border by setting the value to zero.
+ stateGlobal.resizeEdgeSize.value = 0;
+ }
+
+ @override
+ void onWindowLeaveFullScreen() {
+ // Restore edge border to default edge size.
+ stateGlobal.resizeEdgeSize.value = kWindowEdgeSize;
+ }
+
@override
void onWindowClose() {
super.onWindowClose();
@@ -105,7 +121,7 @@ class _ConnectionPageState extends State
scrollController: _scrollController,
child: CustomScrollView(
controller: _scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
slivers: [
SliverList(
delegate: SliverChildListDelegate([
@@ -134,7 +150,7 @@ class _ConnectionPageState extends State
/// Callback for the connect button.
/// Connects to the selected peer.
void onConnect({bool isFileTransfer = false}) {
- final id = _idController.id;
+ var id = _idController.id;
connect(context, id, isFileTransfer: isFileTransfer);
}
@@ -145,7 +161,7 @@ class _ConnectionPageState extends State
width: 320 + 20 * 2,
padding: const EdgeInsets.fromLTRB(20, 24, 20, 22),
decoration: BoxDecoration(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(13)),
),
child: Ink(
diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart
index fd9814cc2..dfa5762b0 100644
--- a/flutter/lib/desktop/pages/desktop_home_page.dart
+++ b/flutter/lib/desktop/pages/desktop_home_page.dart
@@ -6,6 +6,7 @@ import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart' hide MenuItem;
import 'package:flutter/services.dart';
import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/common/widgets/custom_password.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/pages/connection_page.dart';
import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart';
@@ -13,6 +14,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/models/server_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:flutter_hbb/utils/multi_window_manager.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
@@ -43,6 +45,7 @@ class _DesktopHomePageState extends State
var watchIsCanScreenRecording = false;
var watchIsProcessTrust = false;
var watchIsInputMonitoring = false;
+ var watchIsCanRecordAudio = false;
Timer? _updateTimer;
@override
@@ -68,17 +71,27 @@ class _DesktopHomePageState extends State
value: gFFI.serverModel,
child: Container(
width: 200,
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
child: DesktopScrollWrapper(
scrollController: _leftPaneScrollController,
child: SingleChildScrollView(
controller: _leftPaneScrollController,
+ physics: DraggableNeverScrollableScrollPhysics(),
child: Column(
children: [
buildTip(context),
buildIDBoard(context),
buildPasswordBoard(context),
- buildHelpCards(),
+ FutureBuilder(
+ future: buildHelpCards(),
+ builder: (_, data) {
+ if (data.hasData) {
+ return data.data!;
+ } else {
+ return const Offstage();
+ }
+ },
+ ),
],
),
),
@@ -172,7 +185,7 @@ class _DesktopHomePageState extends State
radius: 15,
backgroundColor: hover.value
? Theme.of(context).scaffoldBackgroundColor
- : Theme.of(context).backgroundColor,
+ : Theme.of(context).colorScheme.background,
child: Icon(
Icons.more_vert_outlined,
size: 20,
@@ -301,7 +314,7 @@ class _DesktopHomePageState extends State
);
}
- Widget buildHelpCards() {
+ Future buildHelpCards() async {
if (updateUrl.isNotEmpty) {
return buildInstallCard(
"Status",
@@ -348,6 +361,15 @@ class _DesktopHomePageState extends State
bind.mainIsInstalledDaemon(prompt: true);
});
}
+ //// Disable microphone configuration for macOS. We will request the permission when needed.
+ // else if ((await osxCanRecordAudio() !=
+ // PermissionAuthorizeType.authorized)) {
+ // return buildInstallCard("Permissions", "config_microphone", "Configure",
+ // () async {
+ // osxRequestAudio();
+ // watchIsCanRecordAudio = true;
+ // });
+ // }
} else if (Platform.isLinux) {
if (bind.mainCurrentIsWayland()) {
return buildInstallCard(
@@ -477,6 +499,24 @@ class _DesktopHomePageState extends State
if (watchIsInputMonitoring) {
if (bind.mainIsCanInputMonitoring(prompt: false)) {
watchIsInputMonitoring = false;
+ // Do not notify for now.
+ // Monitoring may not take effect until the process is restarted.
+ // rustDeskWinManager.call(
+ // WindowType.RemoteDesktop, kWindowDisableGrabKeyboard, '');
+ setState(() {});
+ }
+ }
+ if (watchIsCanRecordAudio) {
+ if (Platform.isMacOS) {
+ Future.microtask(() async {
+ if ((await osxCanRecordAudio() ==
+ PermissionAuthorizeType.authorized)) {
+ watchIsCanRecordAudio = false;
+ setState(() {});
+ }
+ });
+ } else {
+ watchIsCanRecordAudio = false;
setState(() {});
}
}
@@ -522,6 +562,7 @@ class _DesktopHomePageState extends State
isFileTransfer: call.arguments['isFileTransfer'],
isTcpTunneling: call.arguments['isTcpTunneling'],
isRDP: call.arguments['isRDP'],
+ forceRelay: call.arguments['forceRelay'],
);
}
});
@@ -543,6 +584,14 @@ void setPasswordDialog() async {
final p1 = TextEditingController(text: pw);
var errMsg0 = "";
var errMsg1 = "";
+ final RxString rxPass = pw.trim().obs;
+ final rules = [
+ DigitValidationRule(),
+ UppercaseValidationRule(),
+ LowercaseValidationRule(),
+ // SpecialCharacterValidationRule(),
+ MinCharactersValidationRule(8),
+ ];
gFFI.dialogManager.show((setState, close) {
submit() {
@@ -551,15 +600,20 @@ void setPasswordDialog() async {
errMsg1 = "";
});
final pass = p0.text.trim();
- if (pass.length < 6 && pass.isNotEmpty) {
- setState(() {
- errMsg0 = translate("Too short, at least 6 characters.");
- });
- return;
+ if (pass.isNotEmpty) {
+ final Iterable violations = rules.where((r) => !r.validate(pass));
+ if (violations.isNotEmpty) {
+ setState(() {
+ errMsg0 =
+ '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}';
+ });
+ return;
+ }
}
if (p1.text.trim() != pass) {
setState(() {
- errMsg1 = translate("The confirmation is not identical.");
+ errMsg1 =
+ '${translate('Prompt')}: ${translate("The confirmation is not identical.")}';
});
return;
}
@@ -579,23 +633,48 @@ void setPasswordDialog() async {
),
Row(
children: [
- ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text(
- "${translate('Password')}:",
- textAlign: TextAlign.start,
- ).marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
+ Expanded(
+ child: TextField(
+ obscureText: true,
+ decoration: InputDecoration(
+ labelText: translate('Password'),
+ border: const OutlineInputBorder(),
+ errorText: errMsg0.isNotEmpty ? errMsg0 : null),
+ controller: p0,
+ autofocus: true,
+ onChanged: (value) {
+ rxPass.value = value.trim();
+ setState(() {
+ errMsg0 = '';
+ });
+ },
+ ),
),
+ ],
+ ),
+ Row(
+ children: [
+ Expanded(child: PasswordStrengthIndicator(password: rxPass)),
+ ],
+ ).marginSymmetric(vertical: 8),
+ const SizedBox(
+ height: 8.0,
+ ),
+ Row(
+ children: [
Expanded(
child: TextField(
obscureText: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
- errorText: errMsg0.isNotEmpty ? errMsg0 : null),
- controller: p0,
- focusNode: FocusNode()..requestFocus(),
+ labelText: translate('Confirmation'),
+ errorText: errMsg1.isNotEmpty ? errMsg1 : null),
+ controller: p1,
+ onChanged: (value) {
+ setState(() {
+ errMsg1 = '';
+ });
+ },
),
),
],
@@ -603,32 +682,30 @@ void setPasswordDialog() async {
const SizedBox(
height: 8.0,
),
- Row(
- children: [
- ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text("${translate('Confirmation')}:")
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
- Expanded(
- child: TextField(
- obscureText: true,
- decoration: InputDecoration(
- border: const OutlineInputBorder(),
- errorText: errMsg1.isNotEmpty ? errMsg1 : null),
- controller: p1,
- ),
- ),
- ],
- ),
+ Obx(() => Wrap(
+ runSpacing: 8,
+ spacing: 4,
+ children: rules.map((e) {
+ var checked = e.validate(rxPass.value.trim());
+ return Chip(
+ label: Text(
+ e.name,
+ style: TextStyle(
+ color: checked
+ ? const Color(0xFF0A9471)
+ : Color.fromARGB(255, 198, 86, 157)),
+ ),
+ backgroundColor: checked
+ ? const Color(0xFFD0F7ED)
+ : Color.fromARGB(255, 247, 205, 232));
+ }).toList(),
+ ))
],
),
),
actions: [
- TextButton(onPressed: close, child: Text(translate("Cancel"))),
- TextButton(onPressed: submit, child: Text(translate("OK"))),
+ dialogButton("Cancel", onPressed: close, isOutline: true),
+ dialogButton("OK", onPressed: submit),
],
onSubmit: submit,
onCancel: close,
diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart
index ac92da14c..0aafd48bb 100644
--- a/flutter/lib/desktop/pages/desktop_setting_page.dart
+++ b/flutter/lib/desktop/pages/desktop_setting_page.dart
@@ -19,7 +19,7 @@ import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
import '../../common/widgets/dialog.dart';
import '../../common/widgets/login.dart';
-const double _kTabWidth = 235;
+const double _kTabWidth = 200;
const double _kTabHeight = 42;
const double _kCardFixedWidth = 540;
const double _kCardLeftMargin = 15;
@@ -33,6 +33,7 @@ const double _kContentFontSize = 15;
const Color _accentColor = MyTheme.accent;
const String _kSettingPageControllerTag = 'settingPageController';
const String _kSettingPageIndexTag = 'settingPageIndex';
+const int _kPageCount = 6;
class _TabInfo {
late final String label;
@@ -51,7 +52,7 @@ class DesktopSettingPage extends StatefulWidget {
State createState() => _DesktopSettingPageState();
static void switch2page(int page) {
- if (page >= 5) return;
+ if (page >= _kPageCount) return;
try {
if (Get.isRegistered(tag: _kSettingPageControllerTag)) {
DesktopTabPage.onAddSetting(initialPage: page);
@@ -75,6 +76,7 @@ class _DesktopSettingPageState extends State
_TabInfo('Security', Icons.enhanced_encryption_outlined,
Icons.enhanced_encryption),
_TabInfo('Network', Icons.link_outlined, Icons.link),
+ _TabInfo('Display', Icons.desktop_windows_outlined, Icons.desktop_windows),
_TabInfo('Account', Icons.person_outline, Icons.person),
_TabInfo('About', Icons.info_outline, Icons.info)
];
@@ -88,7 +90,8 @@ class _DesktopSettingPageState extends State
@override
void initState() {
super.initState();
- selectedIndex = (widget.initialPage < 5 ? widget.initialPage : 0).obs;
+ selectedIndex =
+ (widget.initialPage < _kPageCount ? widget.initialPage : 0).obs;
Get.put(selectedIndex, tag: _kSettingPageIndexTag);
controller = PageController(initialPage: widget.initialPage);
Get.put(controller, tag: _kSettingPageControllerTag);
@@ -105,7 +108,7 @@ class _DesktopSettingPageState extends State
Widget build(BuildContext context) {
super.build(context);
return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
body: Row(
children: [
SizedBox(
@@ -125,11 +128,12 @@ class _DesktopSettingPageState extends State
scrollController: controller,
child: PageView(
controller: controller,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
children: const [
_General(),
_Safety(),
_Network(),
+ _Display(),
_Account(),
_About(),
],
@@ -166,7 +170,7 @@ class _DesktopSettingPageState extends State
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: tabs
.asMap()
@@ -230,7 +234,7 @@ class _GeneralState extends State<_General> {
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: [
theme(),
@@ -315,7 +319,7 @@ class _GeneralState extends State<_General> {
bind.mainSetOption(key: 'audio-input', value: device);
}
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
List devices = (await bind.mainGetSoundInputs()).toList();
if (Platform.isWindows) {
devices.insert(0, 'System Sound');
@@ -342,7 +346,7 @@ class _GeneralState extends State<_General> {
}
Widget record(BuildContext context) {
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
String customDirectory =
await bind.mainGetOption(key: 'video-save-directory');
String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
@@ -395,7 +399,7 @@ class _GeneralState extends State<_General> {
}
Widget language() {
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
String langs = await bind.mainGetLangs();
String lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
return {'langs': langs, 'lang': lang};
@@ -452,7 +456,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return DesktopScrollWrapper(
scrollController: scrollController,
child: SingleChildScrollView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
child: Column(
children: [
@@ -483,7 +487,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget _permissions(context, bool stopService) {
bool enabled = !locked;
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
return await bind.mainGetOption(key: 'access-mode');
}(), hasData: (data) {
String accessMode = data! as String;
@@ -534,6 +538,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
translate('Screen Share'),
translate('Deny remote access'),
],
+ enabled: enabled,
initialKey: initialKey,
onChanged: (mode) async {
String modeValue;
@@ -646,7 +651,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
context, onChanged != null)),
),
],
- ).paddingSymmetric(horizontal: 10),
+ ).paddingOnly(right: 10),
onTap: () => onChanged?.call(value),
))
.toList();
@@ -663,6 +668,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return _Card(title: 'Password', children: [
_ComboBox(
+ enabled: !locked,
keys: modeKeys,
values: modeValues,
initialKey: modeInitialKey,
@@ -671,6 +677,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
if (usePassword) radios[0],
if (usePassword)
_SubLabeledWidget(
+ context,
'One-time password length',
Row(
children: [
@@ -697,6 +704,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp',
enabled: enabled),
),
+ shareRdp(context, enabled),
_OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery',
reverse: true, enabled: enabled),
...directIp(context),
@@ -704,6 +712,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
]);
}
+ shareRdp(BuildContext context, bool enabled) {
+ onChanged(bool b) async {
+ await bind.mainSetShareRdp(enable: b);
+ setState(() {});
+ }
+
+ bool value = bind.mainIsShareRdp();
+ return Offstage(
+ offstage: !(Platform.isWindows && bind.mainIsRdpServiceOpen()),
+ child: GestureDetector(
+ child: Row(
+ children: [
+ Checkbox(
+ value: value,
+ onChanged: enabled ? (_) => onChanged(!value) : null)
+ .marginOnly(right: 5),
+ Expanded(
+ child: Text(translate('Enable RDP session sharing'),
+ style:
+ TextStyle(color: _disabledTextColor(context, enabled))),
+ )
+ ],
+ ).marginOnly(left: _kCheckBoxLeftMargin),
+ onTap: enabled ? () => onChanged(!value) : null),
+ );
+ }
+
List directIp(BuildContext context) {
TextEditingController controller = TextEditingController();
update() => setState(() {});
@@ -711,7 +746,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
return [
_OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server',
update: update, enabled: !locked),
- _futureBuilder(
+ futureBuilder(
future: () async {
String enabled = await bind.mainGetOption(key: 'direct-server');
String port = await bind.mainGetOption(key: 'direct-access-port');
@@ -724,9 +759,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
controller.text = data['port'].toString();
return Offstage(
offstage: !enabled,
- child: Row(children: [
- _SubLabeledWidget(
- 'Port',
+ child: _SubLabeledWidget(
+ context,
+ 'Port',
+ Row(children: [
SizedBox(
width: 80,
child: TextField(
@@ -740,28 +776,29 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
textAlign: TextAlign.end,
decoration: const InputDecoration(
hintText: '21118',
- border: InputBorder.none,
- contentPadding: EdgeInsets.only(right: 5),
+ border: OutlineInputBorder(),
+ contentPadding:
+ EdgeInsets.only(bottom: 10, top: 10, right: 10),
isCollapsed: true,
),
- ),
+ ).marginOnly(right: 15),
),
- enabled: enabled && !locked,
- ).marginOnly(left: 5),
- Obx(() => ElevatedButton(
- onPressed: applyEnabled.value && enabled && !locked
- ? () async {
- applyEnabled.value = false;
- await bind.mainSetOption(
- key: 'direct-access-port',
- value: controller.text);
- }
- : null,
- child: Text(
- translate('Apply'),
- ),
- ).marginOnly(left: 20))
- ]),
+ Obx(() => ElevatedButton(
+ onPressed: applyEnabled.value && enabled && !locked
+ ? () async {
+ applyEnabled.value = false;
+ await bind.mainSetOption(
+ key: 'direct-access-port',
+ value: controller.text);
+ }
+ : null,
+ child: Text(
+ translate('Apply'),
+ ),
+ ))
+ ]),
+ enabled: enabled && !locked,
+ ),
);
},
),
@@ -770,7 +807,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
Widget whitelist() {
bool enabled = !locked;
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
return await bind.mainGetOption(key: 'whitelist');
}(), hasData: (data) {
RxBool hasWhitelist = (data as String).isNotEmpty.obs;
@@ -876,7 +913,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
scrollController: scrollController,
child: ListView(
controller: scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
children: [
_lock(locked, 'Unlock Network Settings', () {
locked = false;
@@ -896,7 +933,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
}
server(bool enabled) {
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
return await bind.mainGetOptions();
}(), hasData: (data) {
// Setting page is not modal, oldOptions should only be used when getting options, never when setting.
@@ -1039,7 +1076,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [_Button('Apply', submit, enabled: enabled)],
- ).marginOnly(top: 15),
+ ).marginOnly(top: 10),
],
)
]);
@@ -1047,6 +1084,247 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
}
}
+class _Display extends StatefulWidget {
+ const _Display({Key? key}) : super(key: key);
+
+ @override
+ State<_Display> createState() => _DisplayState();
+}
+
+class _DisplayState extends State<_Display> {
+ @override
+ Widget build(BuildContext context) {
+ final scrollController = ScrollController();
+ return DesktopScrollWrapper(
+ scrollController: scrollController,
+ child: ListView(
+ controller: scrollController,
+ physics: DraggableNeverScrollableScrollPhysics(),
+ children: [
+ viewStyle(context),
+ scrollStyle(context),
+ imageQuality(context),
+ codec(context),
+ other(context),
+ ]).marginOnly(bottom: _kListViewBottomMargin));
+ }
+
+ Widget viewStyle(BuildContext context) {
+ final key = 'view_style';
+ onChanged(String value) async {
+ await bind.mainSetUserDefaultOption(key: key, value: value);
+ setState(() {});
+ }
+
+ final groupValue = bind.mainGetUserDefaultOption(key: key);
+ return _Card(title: 'Default View Style', children: [
+ _Radio(context,
+ value: kRemoteViewStyleOriginal,
+ groupValue: groupValue,
+ label: 'Scale original',
+ onChanged: onChanged),
+ _Radio(context,
+ value: kRemoteViewStyleAdaptive,
+ groupValue: groupValue,
+ label: 'Scale adaptive',
+ onChanged: onChanged),
+ ]);
+ }
+
+ Widget scrollStyle(BuildContext context) {
+ final key = 'scroll_style';
+ onChanged(String value) async {
+ await bind.mainSetUserDefaultOption(key: key, value: value);
+ setState(() {});
+ }
+
+ final groupValue = bind.mainGetUserDefaultOption(key: key);
+ return _Card(title: 'Default Scroll Style', children: [
+ _Radio(context,
+ value: kRemoteScrollStyleAuto,
+ groupValue: groupValue,
+ label: 'ScrollAuto',
+ onChanged: onChanged),
+ _Radio(context,
+ value: kRemoteScrollStyleBar,
+ groupValue: groupValue,
+ label: 'Scrollbar',
+ onChanged: onChanged),
+ ]);
+ }
+
+ Widget imageQuality(BuildContext context) {
+ final key = 'image_quality';
+ onChanged(String value) async {
+ await bind.mainSetUserDefaultOption(key: key, value: value);
+ setState(() {});
+ }
+
+ final groupValue = bind.mainGetUserDefaultOption(key: key);
+ final qualityKey = 'custom_image_quality';
+ final qualityValue =
+ (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
+ 50.0)
+ .obs;
+ final fpsKey = 'custom-fps';
+ final fpsValue =
+ (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0)
+ .obs;
+ return _Card(title: 'Default Image Quality', children: [
+ _Radio(context,
+ value: kRemoteImageQualityBest,
+ groupValue: groupValue,
+ label: 'Good image quality',
+ onChanged: onChanged),
+ _Radio(context,
+ value: kRemoteImageQualityBalanced,
+ groupValue: groupValue,
+ label: 'Balanced',
+ onChanged: onChanged),
+ _Radio(context,
+ value: kRemoteImageQualityLow,
+ groupValue: groupValue,
+ label: 'Optimize reaction time',
+ onChanged: onChanged),
+ _Radio(context,
+ value: kRemoteImageQualityCustom,
+ groupValue: groupValue,
+ label: 'Custom',
+ onChanged: onChanged),
+ Offstage(
+ offstage: groupValue != kRemoteImageQualityCustom,
+ child: Column(
+ children: [
+ Obx(() => Row(
+ children: [
+ Slider(
+ value: qualityValue.value,
+ min: 10.0,
+ max: 100.0,
+ divisions: 18,
+ onChanged: (double value) async {
+ qualityValue.value = value;
+ await bind.mainSetUserDefaultOption(
+ key: qualityKey, value: value.toString());
+ },
+ ),
+ SizedBox(
+ width: 40,
+ child: Text(
+ '${qualityValue.value.round()}%',
+ style: const TextStyle(fontSize: 15),
+ )),
+ SizedBox(
+ width: 50,
+ child: Text(
+ translate('Bitrate'),
+ style: const TextStyle(fontSize: 15),
+ ))
+ ],
+ )),
+ Obx(() => Row(
+ children: [
+ Slider(
+ value: fpsValue.value,
+ min: 10.0,
+ max: 120.0,
+ divisions: 22,
+ onChanged: (double value) async {
+ fpsValue.value = value;
+ await bind.mainSetUserDefaultOption(
+ key: fpsKey, value: value.toString());
+ },
+ ),
+ SizedBox(
+ width: 40,
+ child: Text(
+ '${fpsValue.value.round()}',
+ style: const TextStyle(fontSize: 15),
+ )),
+ SizedBox(
+ width: 50,
+ child: Text(
+ translate('FPS'),
+ style: const TextStyle(fontSize: 15),
+ ))
+ ],
+ )),
+ ],
+ ),
+ )
+ ]);
+ }
+
+ Widget codec(BuildContext context) {
+ if (!bind.mainHasHwcodec()) {
+ return Offstage();
+ }
+ final key = 'codec-preference';
+ onChanged(String value) async {
+ await bind.mainSetUserDefaultOption(key: key, value: value);
+ setState(() {});
+ }
+
+ final groupValue = bind.mainGetUserDefaultOption(key: key);
+
+ return _Card(title: 'Default Codec', children: [
+ _Radio(context,
+ value: 'auto',
+ groupValue: groupValue,
+ label: 'Auto',
+ onChanged: onChanged),
+ _Radio(context,
+ value: 'vp9',
+ groupValue: groupValue,
+ label: 'VP9',
+ onChanged: onChanged),
+ _Radio(context,
+ value: 'h264',
+ groupValue: groupValue,
+ label: 'H264',
+ onChanged: onChanged),
+ _Radio(context,
+ value: 'h265',
+ groupValue: groupValue,
+ label: 'H265',
+ onChanged: onChanged),
+ ]);
+ }
+
+ Widget otherRow(String label, String key) {
+ final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
+ onChanged(bool b) async {
+ await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
+ setState(() {});
+ }
+
+ return GestureDetector(
+ child: Row(
+ children: [
+ Checkbox(value: value, onChanged: (_) => onChanged(!value))
+ .marginOnly(right: 5),
+ Expanded(
+ child: Text(translate(label)),
+ )
+ ],
+ ).marginOnly(left: _kCheckBoxLeftMargin),
+ onTap: () => onChanged(!value));
+ }
+
+ Widget other(BuildContext context) {
+ return _Card(title: 'Other Default Options', children: [
+ otherRow('Show remote cursor', 'show_remote_cursor'),
+ otherRow('Zoom cursor', 'zoom-cursor'),
+ otherRow('Show quality monitor', 'show_quality_monitor'),
+ otherRow('Mute', 'disable_audio'),
+ otherRow('Allow file copy and paste', 'enable_file_transfer'),
+ otherRow('Disable clipboard', 'disable_clipboard'),
+ otherRow('Lock after session end', 'lock_after_session_end'),
+ otherRow('Privacy mode', 'privacy_mode'),
+ ]);
+ }
+}
+
class _Account extends StatefulWidget {
const _Account({Key? key}) : super(key: key);
@@ -1061,7 +1339,7 @@ class _AccountState extends State<_Account> {
return DesktopScrollWrapper(
scrollController: scrollController,
child: ListView(
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
controller: scrollController,
children: [
_Card(title: 'Account', children: [accountAction()]),
@@ -1090,7 +1368,7 @@ class _About extends StatefulWidget {
class _AboutState extends State<_About> {
@override
Widget build(BuildContext context) {
- return _futureBuilder(future: () async {
+ return futureBuilder(future: () async {
final license = await bind.mainGetLicense();
final version = await bind.mainGetVersion();
final buildDate = await bind.mainGetBuildDate();
@@ -1105,7 +1383,7 @@ class _AboutState extends State<_About> {
scrollController: scrollController,
child: SingleChildScrollView(
controller: scrollController,
- physics: NeverScrollableScrollPhysics(),
+ physics: DraggableNeverScrollableScrollPhysics(),
child: _Card(title: '${translate('About')} RustDesk', children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -1113,10 +1391,12 @@ class _AboutState extends State<_About> {
const SizedBox(
height: 8.0,
),
- Text('${translate('Version')}: $version')
- .marginSymmetric(vertical: 4.0),
- Text('${translate('Build Date')}: $buildDate')
- .marginSymmetric(vertical: 4.0),
+ SelectionArea(
+ child: Text('${translate('Version')}: $version')
+ .marginSymmetric(vertical: 4.0)),
+ SelectionArea(
+ child: Text('${translate('Build Date')}: $buildDate')
+ .marginSymmetric(vertical: 4.0)),
InkWell(
onTap: () {
launchUrlString('https://rustdesk.com/privacy');
@@ -1137,7 +1417,8 @@ class _AboutState extends State<_About> {
decoration: const BoxDecoration(color: Color(0xFF2c8cff)),
padding:
const EdgeInsets.symmetric(vertical: 24, horizontal: 8),
- child: Row(
+ child: SelectionArea(
+ child: Row(
children: [
Expanded(
child: Column(
@@ -1157,7 +1438,7 @@ class _AboutState extends State<_About> {
),
),
],
- ),
+ )),
).marginSymmetric(vertical: 4.0)
],
).marginOnly(left: _kContentHMargin)
@@ -1221,7 +1502,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
bool enabled = true,
Icon? checkedIcon,
bool? fakeValue}) {
- return _futureBuilder(
+ return futureBuilder(
future: bind.mainGetOption(key: key),
hasData: (data) {
bool value = option2bool(key, data.toString());
@@ -1338,63 +1619,22 @@ Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) {
}
// ignore: non_constant_identifier_names
-Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) {
- RxBool hover = false.obs;
+Widget _SubLabeledWidget(BuildContext context, String label, Widget child,
+ {bool enabled = true}) {
return Row(
children: [
- MouseRegion(
- onEnter: (_) => hover.value = true,
- onExit: (_) => hover.value = false,
- child: Obx(
- () {
- return Container(
- height: 32,
- decoration: BoxDecoration(
- border: Border.all(
- color: hover.value && enabled
- ? const Color(0xFFD7D7D7)
- : const Color(0xFFCBCBCB),
- width: hover.value && enabled ? 2 : 1)),
- child: Row(
- children: [
- Container(
- height: 28,
- color: (hover.value && enabled)
- ? const Color(0xFFD7D7D7)
- : const Color(0xFFCBCBCB),
- alignment: Alignment.center,
- padding: const EdgeInsets.symmetric(
- horizontal: 5, vertical: 2),
- child: Text(
- '${translate(label)}: ',
- style: const TextStyle(fontWeight: FontWeight.w300),
- ),
- ).paddingAll(2),
- child,
- ],
- ));
- },
- )),
+ Text(
+ '${translate(label)}: ',
+ style: TextStyle(color: _disabledTextColor(context, enabled)),
+ ),
+ SizedBox(
+ width: 10,
+ ),
+ child,
],
).marginOnly(left: _kContentHSubMargin);
}
-Widget _futureBuilder(
- {required Future? future, required Widget Function(dynamic data) hasData}) {
- return FutureBuilder(
- future: future,
- builder: (BuildContext context, AsyncSnapshot snapshot) {
- if (snapshot.hasData) {
- return hasData(snapshot.data!);
- } else {
- if (snapshot.hasError) {
- debugPrint(snapshot.error.toString());
- }
- return Container();
- }
- });
-}
-
Widget _lock(
bool locked,
String label,
@@ -1436,40 +1676,37 @@ Widget _lock(
_LabeledTextField(
BuildContext context,
- String lable,
+ String label,
TextEditingController controller,
String errorText,
bool enabled,
bool secure) {
return Row(
children: [
- Spacer(flex: 1),
+ ConstrainedBox(
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate(label)}:',
+ textAlign: TextAlign.right,
+ style: TextStyle(
+ fontSize: 16, color: _disabledTextColor(context, enabled)),
+ ).marginOnly(right: 10)),
Expanded(
- flex: 4,
- child: Text(
- '${translate(lable)}:',
- textAlign: TextAlign.right,
- style: TextStyle(color: _disabledTextColor(context, enabled)),
- ),
- ),
- Spacer(flex: 1),
- Expanded(
- flex: 10,
child: TextField(
controller: controller,
enabled: enabled,
obscureText: secure,
decoration: InputDecoration(
isDense: true,
- contentPadding: EdgeInsets.symmetric(vertical: 15),
+ border: OutlineInputBorder(),
+ contentPadding: EdgeInsets.fromLTRB(14, 15, 14, 15),
errorText: errorText.isNotEmpty ? errorText : null),
style: TextStyle(
color: _disabledTextColor(context, enabled),
)),
),
- Spacer(flex: 1),
],
- );
+ ).marginOnly(bottom: 8);
}
// ignore: must_be_immutable
@@ -1487,7 +1724,6 @@ class _ComboBox extends StatelessWidget {
required this.values,
required this.initialKey,
required this.onChanged,
- // ignore: unused_element
this.enabled = true,
}) : super(key: key);
@@ -1500,7 +1736,12 @@ class _ComboBox extends StatelessWidget {
var ref = values[index].obs;
current = keys[index];
return Container(
- decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: enabled
+ ? MyTheme.color(context).border2 ?? MyTheme.border
+ : MyTheme.border,
+ )),
height: 30,
child: Obx(() => DropdownButton(
isExpanded: true,
@@ -1509,6 +1750,10 @@ class _ComboBox extends StatelessWidget {
underline: Container(
height: 25,
),
+ style: TextStyle(
+ color: enabled
+ ? Theme.of(context).textTheme.titleMedium?.color
+ : _disabledTextColor(context, enabled)),
icon: const Icon(
Icons.expand_more_sharp,
size: 20,
@@ -1556,6 +1801,7 @@ void changeSocks5Proxy() async {
var proxyController = TextEditingController(text: proxy);
var userController = TextEditingController(text: username);
var pwdController = TextEditingController(text: password);
+ RxBool obscure = true.obs;
var isInProgress = false;
gFFI.dialogManager.show((setState, close) {
@@ -1601,35 +1847,30 @@ void changeSocks5Proxy() async {
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text('${translate("Hostname")}:')
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate("Hostname")}:',
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
decoration: InputDecoration(
border: const OutlineInputBorder(),
errorText: proxyMsg.isNotEmpty ? proxyMsg : null),
controller: proxyController,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text('${translate("Username")}:')
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate("Username")}:',
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
child: TextField(
decoration: const InputDecoration(
@@ -1639,40 +1880,38 @@ void changeSocks5Proxy() async {
),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Row(
children: [
ConstrainedBox(
- constraints: const BoxConstraints(minWidth: 100),
- child: Text('${translate("Password")}:')
- .marginOnly(bottom: 16.0)),
- const SizedBox(
- width: 24.0,
- ),
+ constraints: const BoxConstraints(minWidth: 140),
+ child: Text(
+ '${translate("Password")}:',
+ textAlign: TextAlign.right,
+ ).marginOnly(right: 10)),
Expanded(
- child: TextField(
- decoration: const InputDecoration(
- border: OutlineInputBorder(),
- ),
- controller: pwdController,
- ),
+ child: Obx(() => TextField(
+ obscureText: obscure.value,
+ decoration: InputDecoration(
+ border: const OutlineInputBorder(),
+ suffixIcon: IconButton(
+ onPressed: () => obscure.value = !obscure.value,
+ icon: Icon(obscure.value
+ ? Icons.visibility_off
+ : Icons.visibility))),
+ controller: pwdController,
+ )),
),
],
- ),
- const SizedBox(
- height: 8.0,
- ),
+ ).marginOnly(bottom: 8),
Offstage(
offstage: !isInProgress, child: const LinearProgressIndicator())
],
),
),
actions: [
- TextButton(onPressed: close, child: Text(translate('Cancel'))),
- TextButton(onPressed: submit, child: Text(translate('OK'))),
+ dialogButton('Cancel', onPressed: close, isOutline: true),
+ dialogButton('OK', onPressed: submit),
],
onSubmit: submit,
onCancel: close,
diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart
index 57c7fe4b8..4a1a40242 100644
--- a/flutter/lib/desktop/pages/desktop_tab_page.dart
+++ b/flutter/lib/desktop/pages/desktop_tab_page.dart
@@ -23,7 +23,7 @@ class DesktopTabPage extends StatefulWidget {
DesktopTabController tabController = Get.find();
tabController.add(TabInfo(
key: kTabLabelSettingPage,
- label: kTabLabelSettingPage,
+ label: translate(kTabLabelSettingPage),
selectedIcon: Icons.build_sharp,
unselectedIcon: Icons.build_outlined,
page: DesktopSettingPage(
@@ -46,7 +46,7 @@ class _DesktopTabPageState extends State {
RemoteCountState.init();
tabController.add(TabInfo(
key: kTabLabelHomePage,
- label: kTabLabelHomePage,
+ label: translate(kTabLabelHomePage),
selectedIcon: Icons.home_sharp,
unselectedIcon: Icons.home_outlined,
closable: false,
@@ -64,24 +64,18 @@ class _DesktopTabPageState extends State {
@override
Widget build(BuildContext context) {
final tabWidget = Container(
- child: Overlay(initialEntries: [
- OverlayEntry(builder: (context) {
- gFFI.dialogManager.setOverlayState(Overlay.of(context));
- return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
- body: DesktopTab(
- controller: tabController,
- tail: ActionIcon(
- message: 'Settings',
- icon: IconFont.menu,
- onTap: DesktopTabPage.onAddSetting,
- isClose: false,
- ),
- ));
- })
- ]),
- );
- return Platform.isMacOS
+ child: Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.background,
+ body: DesktopTab(
+ controller: tabController,
+ tail: ActionIcon(
+ message: 'Settings',
+ icon: IconFont.menu,
+ onTap: DesktopTabPage.onAddSetting,
+ isClose: false,
+ ),
+ )));
+ return Platform.isMacOS || kUseCompatibleUiMode
? tabWidget
: Obx(
() => DragToResizeArea(
diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart
index 60b22a516..9c72caa5f 100644
--- a/flutter/lib/desktop/pages/file_manager_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_page.dart
@@ -2,20 +2,24 @@ import 'dart:async';
import 'dart:io';
import 'dart:math';
+import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart';
+import 'package:percent_indicator/percent_indicator.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart';
+import 'package:flutter_hbb/desktop/widgets/menu_button.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/file_model.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:wakelock/wakelock.dart';
+
import '../../consts.dart';
import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
-
import '../../common.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
@@ -46,8 +50,10 @@ enum MouseFocusScope {
}
class FileManagerPage extends StatefulWidget {
- const FileManagerPage({Key? key, required this.id}) : super(key: key);
+ const FileManagerPage({Key? key, required this.id, this.forceRelay})
+ : super(key: key);
final String id;
+ final bool? forceRelay;
@override
State createState() => _FileManagerPageState();
@@ -73,6 +79,10 @@ class _FileManagerPageState extends State
final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote");
final _listSearchBufferLocal = TimeoutStringBuffer();
final _listSearchBufferRemote = TimeoutStringBuffer();
+ final _nameColWidthLocal = kDesktopFileTransferNameColWidth.obs;
+ final _modifiedColWidthLocal = kDesktopFileTransferModifiedColWidth.obs;
+ final _nameColWidthRemote = kDesktopFileTransferNameColWidth.obs;
+ final _modifiedColWidthRemote = kDesktopFileTransferModifiedColWidth.obs;
/// [_lastClickTime], [_lastClickEntry] help to handle double click
int _lastClickTime =
@@ -80,6 +90,7 @@ class _FileManagerPageState extends State
Entry? _lastClickEntry;
final _dropMaskVisible = false.obs; // TODO impl drop mask
+ final _overlayKeyState = OverlayKeyState();
ScrollController getBreadCrumbScrollController(bool isLocal) {
return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote;
@@ -101,7 +112,7 @@ class _FileManagerPageState extends State
void initState() {
super.initState();
_ffi = FFI();
- _ffi.start(widget.id, isFileTransfer: true);
+ _ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay);
WidgetsBinding.instance.addPostFrameCallback((_) {
_ffi.dialogManager
.showLoading(translate('Connecting...'), onCancel: closeConnection);
@@ -115,6 +126,7 @@ class _FileManagerPageState extends State
// register location listener
_locationNodeLocal.addListener(onLocalLocationFocusChanged);
_locationNodeRemote.addListener(onRemoteLocationFocusChanged);
+ _ffi.dialogManager.setOverlayState(_overlayKeyState);
}
@override
@@ -137,14 +149,13 @@ class _FileManagerPageState extends State
@override
Widget build(BuildContext context) {
super.build(context);
- return Overlay(initialEntries: [
- OverlayEntry(builder: (context) {
- _ffi.dialogManager.setOverlayState(Overlay.of(context));
+ return Overlay(key: _overlayKeyState.key, initialEntries: [
+ OverlayEntry(builder: (_) {
return ChangeNotifierProvider.value(
value: _ffi.fileModel,
child: Consumer(builder: (context, model, child) {
return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: Row(
children: [
Flexible(flex: 3, child: body(isLocal: true)),
@@ -189,35 +200,42 @@ class _FileManagerPageState extends State
];
return Listener(
- onPointerDown: (e) {
- final x = e.position.dx;
- final y = e.position.dy;
- menuPos = RelativeRect.fromLTRB(x, y, x, y);
- },
- child: IconButton(
- icon: const Icon(Icons.more_vert),
- splashRadius: kDesktopIconButtonSplashRadius,
- onPressed: () => mod_menu.showMenu(
- context: context,
- position: menuPos,
- items: items
- .map((e) => e.build(
- context,
- MenuConfig(
- commonColor: CustomPopupMenuTheme.commonColor,
- height: CustomPopupMenuTheme.height,
- dividerHeight: CustomPopupMenuTheme.dividerHeight)))
- .expand((i) => i)
- .toList(),
- elevation: 8,
- ),
- ));
+ onPointerDown: (e) {
+ final x = e.position.dx;
+ final y = e.position.dy;
+ menuPos = RelativeRect.fromLTRB(x, y, x, y);
+ },
+ child: MenuButton(
+ onPressed: () => mod_menu.showMenu(
+ context: context,
+ position: menuPos,
+ items: items
+ .map(
+ (e) => e.build(
+ context,
+ MenuConfig(
+ commonColor: CustomPopupMenuTheme.commonColor,
+ height: CustomPopupMenuTheme.height,
+ dividerHeight: CustomPopupMenuTheme.dividerHeight),
+ ),
+ )
+ .expand((i) => i)
+ .toList(),
+ elevation: 8,
+ ),
+ child: SvgPicture.asset(
+ "assets/dots.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ );
}
Widget body({bool isLocal = false}) {
final scrollController = ScrollController();
return Container(
- decoration: BoxDecoration(border: Border.all(color: Colors.black26)),
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(8.0),
child: DropTarget(
@@ -228,44 +246,31 @@ class _FileManagerPageState extends State
onDragExited: (exit) {
_dropMaskVisible.value = false;
},
- child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
- headTools(isLocal),
- Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ headTools(isLocal),
+ Expanded(
child: Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: SingleChildScrollView(
- controller: scrollController,
- child: _buildDataTable(context, isLocal, scrollController),
- ),
- )
- ],
- )),
- ]),
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: _buildFileList(context, isLocal, scrollController),
+ )
+ ],
+ ),
+ ),
+ ],
+ ),
),
);
}
- Widget _buildDataTable(
+ Widget _buildFileList(
BuildContext context, bool isLocal, ScrollController scrollController) {
- const rowHeight = 25.0;
final fd = model.getCurrentDir(isLocal);
final entries = fd.entries;
- final sortIndex = (SortBy style) {
- switch (style) {
- case SortBy.name:
- return 0;
- case SortBy.type:
- return 0;
- case SortBy.modified:
- return 1;
- case SortBy.size:
- return 2;
- }
- }(model.getSortStyle(isLocal));
- final sortAscending =
- isLocal ? model.localSortAscending : model.remoteSortAscending;
+ final selectedEntries = getSelectedItems(isLocal);
return MouseRegion(
onEnter: (evt) {
@@ -286,7 +291,6 @@ class _FileManagerPageState extends State
onNext: (buffer) {
debugPrint("searching next for $buffer");
assert(buffer.length == 1);
- final selectedEntries = getSelectedItems(isLocal);
assert(selectedEntries.length <= 1);
var skipCount = 0;
if (selectedEntries.items.isNotEmpty) {
@@ -296,13 +300,13 @@ class _FileManagerPageState extends State
}
skipCount = index + 1;
}
- var searchResult = entries
- .skip(skipCount)
- .where((element) => element.name.startsWith(buffer));
+ var searchResult = entries.skip(skipCount).where(
+ (element) => element.name.toLowerCase().startsWith(buffer));
if (searchResult.isEmpty) {
// cannot find next, lets restart search from head
- searchResult =
- entries.where((element) => element.name.startsWith(buffer));
+ debugPrint("restart search from head");
+ searchResult = entries.where(
+ (element) => element.name.toLowerCase().startsWith(buffer));
}
if (searchResult.isEmpty) {
setState(() {
@@ -310,14 +314,14 @@ class _FileManagerPageState extends State
});
return;
}
- _jumpToEntry(
- isLocal, searchResult.first, scrollController, rowHeight, buffer);
+ _jumpToEntry(isLocal, searchResult.first, scrollController,
+ kDesktopFileTransferRowHeight);
},
onSearch: (buffer) {
debugPrint("searching for $buffer");
final selectedEntries = getSelectedItems(isLocal);
- final searchResult =
- entries.where((element) => element.name.startsWith(buffer));
+ final searchResult = entries.where(
+ (element) => element.name.toLowerCase().startsWith(buffer));
selectedEntries.clear();
if (searchResult.isEmpty) {
setState(() {
@@ -325,8 +329,8 @@ class _FileManagerPageState extends State
});
return;
}
- _jumpToEntry(
- isLocal, searchResult.first, scrollController, rowHeight, buffer);
+ _jumpToEntry(isLocal, searchResult.first, scrollController,
+ kDesktopFileTransferRowHeight);
},
child: ObxValue(
(searchText) {
@@ -335,118 +339,154 @@ class _FileManagerPageState extends State
return element.name.contains(searchText.value);
}).toList(growable: false)
: entries;
- return DataTable(
- key: ValueKey(isLocal ? 0 : 1),
- showCheckboxColumn: false,
- dataRowHeight: rowHeight,
- headingRowHeight: 30,
- horizontalMargin: 8,
- columnSpacing: 8,
- showBottomBorder: true,
- sortColumnIndex: sortIndex,
- sortAscending: sortAscending,
- columns: [
- DataColumn(
- label: Text(
- translate("Name"),
- ).marginSymmetric(horizontal: 4),
- onSort: (columnIndex, ascending) {
- model.changeSortStyle(SortBy.name,
- isLocal: isLocal, ascending: ascending);
- }),
- DataColumn(
- label: Text(
- translate("Modified"),
- ),
- onSort: (columnIndex, ascending) {
- model.changeSortStyle(SortBy.modified,
- isLocal: isLocal, ascending: ascending);
- }),
- DataColumn(
- label: Text(translate("Size")),
- onSort: (columnIndex, ascending) {
- model.changeSortStyle(SortBy.size,
- isLocal: isLocal, ascending: ascending);
- }),
- ],
- rows: filteredEntries.map((entry) {
- final sizeStr =
- entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
- final lastModifiedStr = entry.isDrive
- ? " "
- : "${entry.lastModified().toString().replaceAll(".000", "")} ";
- return DataRow(
- key: ValueKey(entry.name),
- onSelectChanged: (s) {
- _onSelectedChanged(getSelectedItems(isLocal),
- filteredEntries, entry, isLocal);
- },
- selected: getSelectedItems(isLocal).contains(entry),
- cells: [
- DataCell(
- Container(
- width: 200,
- child: Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: entry.name,
- child: Row(children: [
- entry.isDrive
- ? Image(
- image: iconHardDrive,
- fit: BoxFit.scaleDown,
- color: Theme.of(context)
- .iconTheme
- .color
- ?.withOpacity(0.7))
- .paddingAll(4)
- : Icon(
- entry.isFile
- ? Icons.feed_outlined
- : Icons.folder,
- size: 20,
- color: Theme.of(context)
- .iconTheme
- .color
- ?.withOpacity(0.7),
- ).marginSymmetric(horizontal: 2),
- Expanded(
- child: Text(entry.name,
- overflow: TextOverflow.ellipsis))
- ]),
- )),
- onTap: () {
- final items = getSelectedItems(isLocal);
-
- // handle double click
- if (_checkDoubleClick(entry)) {
- openDirectory(entry.path, isLocal: isLocal);
- items.clear();
- return;
- }
- _onSelectedChanged(
- items, filteredEntries, entry, isLocal);
- },
+ final rows = filteredEntries.map((entry) {
+ final sizeStr =
+ entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
+ final lastModifiedStr = entry.isDrive
+ ? " "
+ : "${entry.lastModified().toString().replaceAll(".000", "")} ";
+ final isSelected = selectedEntries.contains(entry);
+ return Padding(
+ padding: EdgeInsets.symmetric(vertical: 1),
+ child: Container(
+ decoration: BoxDecoration(
+ color: isSelected
+ ? Theme.of(context).hoverColor
+ : Theme.of(context).cardColor,
+ borderRadius: BorderRadius.all(
+ Radius.circular(5.0),
),
- DataCell(FittedBox(
- child: Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: lastModifiedStr,
- child: Text(
- lastModifiedStr,
- style: TextStyle(
- fontSize: 12, color: MyTheme.darkGray),
- )))),
- DataCell(Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: sizeStr,
- child: Text(
- sizeStr,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(
- fontSize: 10, color: MyTheme.darkGray),
- ))),
- ]);
- }).toList(growable: false),
+ ),
+ key: ValueKey(entry.name),
+ height: kDesktopFileTransferRowHeight,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ Expanded(
+ child: InkWell(
+ child: Row(
+ children: [
+ GestureDetector(
+ child: Obx(
+ () => Container(
+ width: isLocal
+ ? _nameColWidthLocal.value
+ : _nameColWidthRemote.value,
+ child: Tooltip(
+ waitDuration:
+ Duration(milliseconds: 500),
+ message: entry.name,
+ child: Row(children: [
+ entry.isDrive
+ ? Image(
+ image: iconHardDrive,
+ fit: BoxFit.scaleDown,
+ color: Theme.of(context)
+ .iconTheme
+ .color
+ ?.withOpacity(0.7))
+ .paddingAll(4)
+ : SvgPicture.asset(
+ entry.isFile
+ ? "assets/file.svg"
+ : "assets/folder.svg",
+ color: Theme.of(context)
+ .tabBarTheme
+ .labelColor,
+ ),
+ Expanded(
+ child: Text(
+ entry.name.nonBreaking,
+ overflow:
+ TextOverflow.ellipsis))
+ ]),
+ )),
+ ),
+ onTap: () {
+ final items = getSelectedItems(isLocal);
+ // handle double click
+ if (_checkDoubleClick(entry)) {
+ openDirectory(entry.path,
+ isLocal: isLocal);
+ items.clear();
+ return;
+ }
+ _onSelectedChanged(
+ items, filteredEntries, entry, isLocal);
+ },
+ ),
+ SizedBox(
+ width: 2.0,
+ ),
+ GestureDetector(
+ child: Obx(
+ () => SizedBox(
+ width: isLocal
+ ? _modifiedColWidthLocal.value
+ : _modifiedColWidthRemote.value,
+ child: Tooltip(
+ waitDuration:
+ Duration(milliseconds: 500),
+ message: lastModifiedStr,
+ child: Text(
+ lastModifiedStr,
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ )),
+ ),
+ ),
+ ),
+ // Divider from header.
+ SizedBox(
+ width: 2.0,
+ ),
+ Expanded(
+ // width: 100,
+ child: GestureDetector(
+ child: Tooltip(
+ waitDuration: Duration(milliseconds: 500),
+ message: sizeStr,
+ child: Text(
+ sizeStr,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(
+ fontSize: 10,
+ color: MyTheme.darkGray),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ )),
+ );
+ }).toList(growable: false);
+
+ return Column(
+ children: [
+ // Header
+ Row(
+ children: [
+ Expanded(child: _buildFileBrowserHeader(context, isLocal)),
+ ],
+ ),
+ // Body
+ Expanded(
+ child: ListView.builder(
+ controller: scrollController,
+ itemExtent: kDesktopFileTransferRowHeight,
+ itemBuilder: (context, index) {
+ return rows[index];
+ },
+ itemCount: rows.length,
+ ),
+ ),
+ ],
);
},
isLocal ? _searchTextLocal : _searchTextRemote,
@@ -456,15 +496,14 @@ class _FileManagerPageState extends State
}
void _jumpToEntry(bool isLocal, Entry entry,
- ScrollController scrollController, double rowHeight, String buffer) {
+ ScrollController scrollController, double rowHeight) {
final entries = model.getCurrentDir(isLocal).entries;
final index = entries.indexOf(entry);
if (index == -1) {
debugPrint("entry is not valid: ${entry.path}");
}
final selectedEntries = getSelectedItems(isLocal);
- final searchResult =
- entries.where((element) => element.name.startsWith(buffer));
+ final searchResult = entries.where((element) => element == entry);
selectedEntries.clear();
if (searchResult.isEmpty) {
return;
@@ -526,103 +565,187 @@ class _FileManagerPageState extends State
return false;
}
+ Widget generateCard(Widget child) {
+ return Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).cardColor,
+ borderRadius: BorderRadius.all(
+ Radius.circular(15.0),
+ ),
+ ),
+ child: child,
+ );
+ }
+
/// transfer status list
/// watch transfer status
Widget statusList() {
return PreferredSize(
- preferredSize: const Size(200, double.infinity),
- child: Container(
- margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
- padding: const EdgeInsets.all(8.0),
- decoration: BoxDecoration(border: Border.all(color: Colors.grey)),
- child: Obx(
- () => ListView.builder(
- controller: ScrollController(),
- itemBuilder: (BuildContext context, int index) {
- final item = model.jobTable[index];
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Transform.rotate(
- angle: item.isRemote ? pi : 0,
- child: const Icon(Icons.send)),
- const SizedBox(
- width: 16.0,
- ),
- Expanded(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Tooltip(
- waitDuration: Duration(milliseconds: 500),
- message: item.jobName,
- child: Text(
- item.jobName,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- )),
- Wrap(
- children: [
- Text(
- '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '),
- Text(
- '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '),
- Offstage(
- offstage:
- item.state != JobState.inProgress,
- child: Text(
- '${"${readableFileSize(item.speed)}/s"} ')),
- Offstage(
- offstage: item.totalSize <= 0,
- child: Text(
- '${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'),
- ),
- ],
- ),
- ],
- ),
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.end,
+ preferredSize: const Size(200, double.infinity),
+ child: Container(
+ margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
+ padding: const EdgeInsets.all(8.0),
+ child: model.jobTable.isEmpty
+ ? generateCard(
+ Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset(
+ "assets/transfer.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ height: 40,
+ ).paddingOnly(bottom: 10),
+ Text(
+ translate("No transfers in progress"),
+ textAlign: TextAlign.center,
+ textScaleFactor: 1.20,
+ style: TextStyle(
+ color: Theme.of(context).tabBarTheme.labelColor),
+ ),
+ ],
+ ),
+ ),
+ )
+ : Obx(
+ () => ListView.builder(
+ controller: ScrollController(),
+ itemBuilder: (BuildContext context, int index) {
+ final item = model.jobTable[index];
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 5),
+ child: generateCard(
+ Column(
+ mainAxisSize: MainAxisSize.min,
children: [
- Offstage(
- offstage: item.state != JobState.paused,
- child: IconButton(
- onPressed: () {
- model.resumeJob(item.id);
- },
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: const Icon(Icons.restart_alt_rounded)),
- ),
- IconButton(
- icon: const Icon(Icons.close),
- splashRadius: 1,
- onPressed: () {
- model.jobTable.removeAt(index);
- model.cancelJob(item.id);
- },
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Transform.rotate(
+ angle: item.isRemote ? pi : 0,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ color: Theme.of(context)
+ .tabBarTheme
+ .labelColor,
+ ),
+ ).paddingOnly(left: 15),
+ const SizedBox(
+ width: 16.0,
+ ),
+ Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment:
+ CrossAxisAlignment.start,
+ children: [
+ Tooltip(
+ waitDuration:
+ Duration(milliseconds: 500),
+ message: item.jobName,
+ child: Text(
+ item.jobName,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ).paddingSymmetric(vertical: 10),
+ ),
+ Text(
+ '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ ),
+ Offstage(
+ offstage:
+ item.state != JobState.inProgress,
+ child: Text(
+ '${translate("Speed")} ${readableFileSize(item.speed)}/s',
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ ),
+ ),
+ Offstage(
+ offstage:
+ item.state == JobState.inProgress,
+ child: Text(
+ translate(
+ item.display(),
+ ),
+ style: TextStyle(
+ fontSize: 12,
+ color: MyTheme.darkGray,
+ ),
+ ),
+ ),
+ Offstage(
+ offstage:
+ item.state != JobState.inProgress,
+ child: LinearPercentIndicator(
+ padding: EdgeInsets.only(right: 15),
+ animateFromLastPercent: true,
+ center: Text(
+ '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
+ ),
+ barRadius: Radius.circular(15),
+ percent: item.finishedSize /
+ item.totalSize,
+ progressColor: MyTheme.accent,
+ backgroundColor:
+ Theme.of(context).hoverColor,
+ lineHeight:
+ kDesktopFileTransferRowHeight,
+ ).paddingSymmetric(vertical: 15),
+ ),
+ ],
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Offstage(
+ offstage: item.state != JobState.paused,
+ child: MenuButton(
+ onPressed: () {
+ model.resumeJob(item.id);
+ },
+ child: SvgPicture.asset(
+ "assets/refresh.svg",
+ color: Colors.white,
+ ),
+ color: MyTheme.accent,
+ hoverColor: MyTheme.accent80,
+ ),
+ ),
+ MenuButton(
+ padding: EdgeInsets.only(right: 15),
+ child: SvgPicture.asset(
+ "assets/close.svg",
+ color: Colors.white,
+ ),
+ onPressed: () {
+ model.jobTable.removeAt(index);
+ model.cancelJob(item.id);
+ },
+ color: MyTheme.accent,
+ hoverColor: MyTheme.accent80,
+ ),
+ ],
+ ),
+ ],
),
],
- )
- ],
- ),
- SizedBox(
- height: 8.0,
- ),
- Divider(
- height: 2.0,
- )
- ],
- );
- },
- itemCount: model.jobTable.length,
- ),
- ),
- ));
+ ).paddingSymmetric(vertical: 10),
+ ),
+ );
+ },
+ itemCount: model.jobTable.length,
+ ),
+ ),
+ ),
+ );
}
Widget headTools(bool isLocal) {
@@ -631,95 +754,131 @@ class _FileManagerPageState extends State
final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote;
final selectedItems = getSelectedItems(isLocal);
return Container(
- child: Column(
- children: [
- // symbols
- PreferredSize(
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Container(
- width: 50,
- height: 50,
- decoration: BoxDecoration(color: Colors.blue),
- padding: EdgeInsets.all(8.0),
- child: FutureBuilder(
- future: bind.sessionGetPlatform(
- id: _ffi.id, isRemote: !isLocal),
- builder: (context, snapshot) {
- if (snapshot.hasData && snapshot.data!.isNotEmpty) {
- return getPlatformImage('${snapshot.data}');
- } else {
- return CircularProgressIndicator(
- color: Colors.white,
- );
- }
- })),
- Text(isLocal
- ? translate("Local Computer")
- : translate("Remote Computer"))
- .marginOnly(left: 8.0)
- ],
- ),
- preferredSize: Size(double.infinity, 70)),
- // buttons
- Row(
- children: [
- Row(
- children: [
- IconButton(
- icon: const Icon(Icons.arrow_back),
- splashRadius: kDesktopIconButtonSplashRadius,
- onPressed: () {
- selectedItems.clear();
- model.goBack(isLocal: isLocal);
- },
- ),
- IconButton(
- icon: const Icon(Icons.arrow_upward),
- splashRadius: kDesktopIconButtonSplashRadius,
- onPressed: () {
- selectedItems.clear();
- model.goToParentDirectory(isLocal: isLocal);
- },
- ),
- ],
- ),
- Expanded(
- child: GestureDetector(
- onTap: () {
- locationStatus.value =
- locationStatus.value == LocationStatus.bread
- ? LocationStatus.pathLocation
- : LocationStatus.bread;
- Future.delayed(Duration.zero, () {
- if (locationStatus.value == LocationStatus.pathLocation) {
- locationFocus.requestFocus();
- }
- });
- },
- child: Obx(() => Container(
- decoration: BoxDecoration(
- border: Border.all(
- color: locationStatus.value == LocationStatus.bread
- ? Colors.black12
- : Theme.of(context)
- .colorScheme
- .primary
- .withOpacity(0.5))),
+ child: Column(
+ children: [
+ // symbols
+ PreferredSize(
child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
children: [
- Expanded(
- child: locationStatus.value == LocationStatus.bread
- ? buildBread(isLocal)
- : buildPathLocation(isLocal)),
+ Container(
+ width: 50,
+ height: 50,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.all(Radius.circular(8)),
+ color: MyTheme.accent,
+ ),
+ padding: EdgeInsets.all(8.0),
+ child: FutureBuilder(
+ future: bind.sessionGetPlatform(
+ id: _ffi.id, isRemote: !isLocal),
+ builder: (context, snapshot) {
+ if (snapshot.hasData &&
+ snapshot.data!.isNotEmpty) {
+ return getPlatformImage('${snapshot.data}');
+ } else {
+ return CircularProgressIndicator(
+ color: Theme.of(context)
+ .tabBarTheme
+ .labelColor,
+ );
+ }
+ })),
+ Text(isLocal
+ ? translate("Local Computer")
+ : translate("Remote Computer"))
+ .marginOnly(left: 8.0)
],
- ))),
- )),
- Obx(() {
- switch (locationStatus.value) {
- case LocationStatus.bread:
- return IconButton(
+ ),
+ preferredSize: Size(double.infinity, 70))
+ .paddingOnly(bottom: 15),
+ // buttons
+ Row(
+ children: [
+ Row(
+ children: [
+ MenuButton(
+ padding: EdgeInsets.only(
+ right: 3,
+ ),
+ child: RotatedBox(
+ quarterTurns: 2,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ onPressed: () {
+ selectedItems.clear();
+ model.goBack(isLocal: isLocal);
+ },
+ ),
+ MenuButton(
+ child: RotatedBox(
+ quarterTurns: 3,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ onPressed: () {
+ selectedItems.clear();
+ model.goToParentDirectory(isLocal: isLocal);
+ },
+ ),
+ ],
+ ),
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 3.0),
+ child: Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).cardColor,
+ borderRadius: BorderRadius.all(
+ Radius.circular(8.0),
+ ),
+ ),
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 2.5),
+ child: GestureDetector(
+ onTap: () {
+ locationStatus.value =
+ locationStatus.value == LocationStatus.bread
+ ? LocationStatus.pathLocation
+ : LocationStatus.bread;
+ Future.delayed(Duration.zero, () {
+ if (locationStatus.value ==
+ LocationStatus.pathLocation) {
+ locationFocus.requestFocus();
+ }
+ });
+ },
+ child: Obx(
+ () => Container(
+ child: Row(
+ children: [
+ Expanded(
+ child: locationStatus.value ==
+ LocationStatus.bread
+ ? buildBread(isLocal)
+ : buildPathLocation(isLocal)),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ Obx(() {
+ switch (locationStatus.value) {
+ case LocationStatus.bread:
+ return MenuButton(
onPressed: () {
locationStatus.value = LocationStatus.fileSearchBar;
final focusNode =
@@ -727,49 +886,77 @@ class _FileManagerPageState extends State
Future.delayed(
Duration.zero, () => focusNode.requestFocus());
},
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: Icon(Icons.search));
- case LocationStatus.pathLocation:
- return IconButton(
- color: Theme.of(context).disabledColor,
+ child: SvgPicture.asset(
+ "assets/search.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ );
+ case LocationStatus.pathLocation:
+ return MenuButton(
onPressed: null,
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: Icon(Icons.close));
- case LocationStatus.fileSearchBar:
- return IconButton(
+ child: SvgPicture.asset(
+ "assets/close.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
color: Theme.of(context).disabledColor,
+ hoverColor: Theme.of(context).hoverColor,
+ );
+ case LocationStatus.fileSearchBar:
+ return MenuButton(
onPressed: () {
onSearchText("", isLocal);
locationStatus.value = LocationStatus.bread;
},
- splashRadius: 1,
- icon: Icon(Icons.close));
- }
- }),
- IconButton(
+ child: SvgPicture.asset(
+ "assets/close.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ );
+ }
+ }),
+ MenuButton(
+ padding: EdgeInsets.only(
+ left: 3,
+ ),
onPressed: () {
model.refresh(isLocal: isLocal);
},
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: const Icon(Icons.refresh)),
- ],
- ),
- Row(
- textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl,
- children: [
- Expanded(
- child: Row(
- mainAxisAlignment:
- isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
- children: [
- IconButton(
- onPressed: () {
- model.goHome(isLocal: isLocal);
- },
- icon: const Icon(Icons.home_outlined),
- splashRadius: kDesktopIconButtonSplashRadius,
- ),
- IconButton(
+ child: SvgPicture.asset(
+ "assets/refresh.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ ],
+ ),
+ Row(
+ textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl,
+ children: [
+ Expanded(
+ child: Row(
+ mainAxisAlignment:
+ isLocal ? MainAxisAlignment.start : MainAxisAlignment.end,
+ children: [
+ MenuButton(
+ padding: EdgeInsets.only(
+ right: 3,
+ ),
+ onPressed: () {
+ model.goHome(isLocal: isLocal);
+ },
+ child: SvgPicture.asset(
+ "assets/home.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ MenuButton(
onPressed: () {
final name = TextEditingController();
_ffi.dialogManager.show((setState, close) {
@@ -787,38 +974,58 @@ class _FileManagerPageState extends State
cancel() => close(false);
return CustomAlertDialog(
- title: Text(translate("Create Folder")),
+ title: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ SvgPicture.asset("assets/folder_new.svg",
+ color: MyTheme.accent),
+ Text(
+ translate("Create Folder"),
+ ).paddingOnly(
+ left: 10,
+ ),
+ ],
+ ),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
decoration: InputDecoration(
labelText: translate(
- "Please enter the folder name"),
+ "Please enter the folder name",
+ ),
),
controller: name,
- focusNode: FocusNode()..requestFocus(),
+ autofocus: true,
),
],
),
actions: [
- TextButton(
- style: flatButtonStyle,
- onPressed: cancel,
- child: Text(translate("Cancel"))),
- ElevatedButton(
- style: flatButtonStyle,
- onPressed: submit,
- child: Text(translate("OK")))
+ dialogButton(
+ "Cancel",
+ icon: Icon(Icons.close_rounded),
+ onPressed: cancel,
+ isOutline: true,
+ ),
+ dialogButton(
+ "Ok",
+ icon: Icon(Icons.done_rounded),
+ onPressed: submit,
+ ),
],
onSubmit: submit,
onCancel: cancel,
);
});
},
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: const Icon(Icons.create_new_folder_outlined)),
- IconButton(
+ child: SvgPicture.asset(
+ "assets/folder_new.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ MenuButton(
onPressed: validItems(selectedItems)
? () async {
await (model.removeAction(selectedItems,
@@ -826,32 +1033,83 @@ class _FileManagerPageState extends State
selectedItems.clear();
}
: null,
- splashRadius: kDesktopIconButtonSplashRadius,
- icon: const Icon(Icons.delete_forever_outlined)),
- menu(isLocal: isLocal),
- ],
+ child: SvgPicture.asset(
+ "assets/trash.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ color: Theme.of(context).cardColor,
+ hoverColor: Theme.of(context).hoverColor,
+ ),
+ menu(isLocal: isLocal),
+ ],
+ ),
),
- ),
- TextButton.icon(
+ ElevatedButton.icon(
+ style: ButtonStyle(
+ padding: MaterialStateProperty.all(isLocal
+ ? EdgeInsets.only(left: 10)
+ : EdgeInsets.only(right: 10)),
+ backgroundColor: MaterialStateProperty.all(
+ selectedItems.length == 0
+ ? MyTheme.accent80
+ : MyTheme.accent,
+ ),
+ ),
onPressed: validItems(selectedItems)
? () {
model.sendFiles(selectedItems, isRemote: !isLocal);
selectedItems.clear();
}
: null,
- icon: Transform.rotate(
- angle: isLocal ? 0 : pi,
- child: const Icon(
- Icons.send,
- ),
- ),
- label: Text(
- isLocal ? translate('Send') : translate('Receive'),
- )),
- ],
- ).marginOnly(top: 8.0)
- ],
- ));
+ icon: isLocal
+ ? Text(
+ translate('Send'),
+ textAlign: TextAlign.right,
+ style: TextStyle(
+ color: selectedItems.length == 0
+ ? Theme.of(context).brightness == Brightness.light
+ ? MyTheme.grayBg
+ : MyTheme.darkGray
+ : Colors.white,
+ ),
+ )
+ : RotatedBox(
+ quarterTurns: 2,
+ child: SvgPicture.asset(
+ "assets/arrow.svg",
+ color: selectedItems.length == 0
+ ? Theme.of(context).brightness == Brightness.light
+ ? MyTheme.grayBg
+ : MyTheme.darkGray
+ : Colors.white,
+ alignment: Alignment.bottomRight,
+ ),
+ ),
+ label: isLocal
+ ? SvgPicture.asset(
+ "assets/arrow.svg",
+ color: selectedItems.length == 0
+ ? Theme.of(context).brightness == Brightness.light
+ ? MyTheme.grayBg
+ : MyTheme.darkGray
+ : Colors.white,
+ )
+ : Text(
+ translate('Receive'),
+ style: TextStyle(
+ color: selectedItems.length == 0
+ ? Theme.of(context).brightness == Brightness.light
+ ? MyTheme.grayBg
+ : MyTheme.darkGray
+ : Colors.white,
+ ),
+ ),
+ ),
+ ],
+ ).marginOnly(top: 8.0)
+ ],
+ ),
+ );
}
bool validItems(SelectedItems items) {
@@ -906,25 +1164,27 @@ class _FileManagerPageState extends State
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
- child: Listener(
- // handle mouse wheel
- onPointerSignal: (e) {
- if (e is PointerScrollEvent) {
- final sc = getBreadCrumbScrollController(isLocal);
- final scale = Platform.isWindows ? 2 : 4;
- sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
- }
- },
- child: BreadCrumb(
- items: items,
- divider: Icon(Icons.chevron_right),
- overflow: ScrollableOverflow(
- controller:
- getBreadCrumbScrollController(isLocal)),
- ))),
+ child: Listener(
+ // handle mouse wheel
+ onPointerSignal: (e) {
+ if (e is PointerScrollEvent) {
+ final sc = getBreadCrumbScrollController(isLocal);
+ final scale = Platform.isWindows ? 2 : 4;
+ sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
+ }
+ },
+ child: BreadCrumb(
+ items: items,
+ divider: const Icon(Icons.keyboard_arrow_right_rounded),
+ overflow: ScrollableOverflow(
+ controller: getBreadCrumbScrollController(isLocal),
+ ),
+ ),
+ ),
+ ),
ActionIcon(
message: "",
- icon: Icons.arrow_drop_down,
+ icon: Icons.keyboard_arrow_down_rounded,
onTap: () async {
final renderBox = locationBarKey.currentContext
?.findRenderObject() as RenderBox;
@@ -1037,13 +1297,23 @@ class _FileManagerPageState extends State
.marginSymmetric(horizontal: 4)));
} else {
final list = PathUtil.split(path, isWindows);
- breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem(
- content: TextButton(
+ breadCrumbList.addAll(
+ list.asMap().entries.map(
+ (e) => BreadCrumbItem(
+ content: TextButton(
child: Text(e.value),
style: ButtonStyle(
- minimumSize: MaterialStateProperty.all(Size(0, 0))),
- onPressed: () => onPressed(list.sublist(0, e.key + 1)))
- .marginSymmetric(horizontal: 4))));
+ minimumSize: MaterialStateProperty.all(
+ Size(0, 0),
+ ),
+ ),
+ onPressed: () => onPressed(
+ list.sublist(0, e.key + 1),
+ ),
+ ).marginSymmetric(horizontal: 4),
+ ),
+ ),
+ );
}
return breadCrumbList;
}
@@ -1070,29 +1340,35 @@ class _FileManagerPageState extends State
: searchTextObs.value;
final textController = TextEditingController(text: text)
..selection = TextSelection.collapsed(offset: text.length);
- return Row(children: [
- Icon(
- locationStatus.value == LocationStatus.pathLocation
- ? Icons.folder
- : Icons.search,
- color: Theme.of(context).hintColor,
- ).paddingSymmetric(horizontal: 2),
- Expanded(
+ return Row(
+ children: [
+ SvgPicture.asset(
+ locationStatus.value == LocationStatus.pathLocation
+ ? "assets/folder.svg"
+ : "assets/search.svg",
+ color: Theme.of(context).tabBarTheme.labelColor,
+ ),
+ Expanded(
child: TextField(
- focusNode: focusNode,
- decoration: InputDecoration(
- border: InputBorder.none,
- isDense: true,
- prefix: Padding(padding: EdgeInsets.only(left: 4.0))),
- controller: textController,
- onSubmitted: (path) {
- openDirectory(path, isLocal: isLocal);
- },
- onChanged: locationStatus.value == LocationStatus.fileSearchBar
- ? (searchText) => onSearchText(searchText, isLocal)
- : null,
- ))
- ]);
+ focusNode: focusNode,
+ decoration: InputDecoration(
+ border: InputBorder.none,
+ isDense: true,
+ prefix: Padding(
+ padding: EdgeInsets.only(left: 4.0),
+ ),
+ ),
+ controller: textController,
+ onSubmitted: (path) {
+ openDirectory(path, isLocal: isLocal);
+ },
+ onChanged: locationStatus.value == LocationStatus.fileSearchBar
+ ? (searchText) => onSearchText(searchText, isLocal)
+ : null,
+ ),
+ )
+ ],
+ );
}
onSearchText(String searchText, bool isLocal) {
@@ -1137,4 +1413,97 @@ class _FileManagerPageState extends State
}
});
}
+
+ Widget headerItemFunc(
+ double? width, SortBy sortBy, String name, bool isLocal) {
+ final headerTextStyle =
+ Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle();
+ return ObxValue>(
+ (ascending) => InkWell(
+ onTap: () {
+ if (ascending.value == null) {
+ ascending.value = true;
+ } else {
+ ascending.value = !ascending.value!;
+ }
+ model.changeSortStyle(sortBy,
+ isLocal: isLocal, ascending: ascending.value!);
+ },
+ child: SizedBox(
+ width: width,
+ height: kDesktopFileTransferHeaderHeight,
+ child: Row(
+ children: [
+ Flexible(
+ flex: 2,
+ child: Text(
+ name,
+ style: headerTextStyle,
+ overflow: TextOverflow.ellipsis,
+ ).marginSymmetric(horizontal: 4),
+ ),
+ Flexible(
+ flex: 1,
+ child: ascending.value != null
+ ? Icon(
+ ascending.value!
+ ? Icons.keyboard_arrow_up_rounded
+ : Icons.keyboard_arrow_down_rounded,
+ )
+ : const Offstage())
+ ],
+ ),
+ ),
+ ), () {
+ if (model.getSortStyle(isLocal) == sortBy) {
+ return model.getSortAscending(isLocal).obs;
+ } else {
+ return Rx(null);
+ }
+ }());
+ }
+
+ Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) {
+ final nameColWidth = isLocal ? _nameColWidthLocal : _nameColWidthRemote;
+ final modifiedColWidth =
+ isLocal ? _modifiedColWidthLocal : _modifiedColWidthRemote;
+ final padding = EdgeInsets.all(1.0);
+ return SizedBox(
+ height: kDesktopFileTransferHeaderHeight,
+ child: Row(
+ children: [
+ Obx(
+ () => headerItemFunc(
+ nameColWidth.value, SortBy.name, translate("Name"), isLocal),
+ ),
+ DraggableDivider(
+ axis: Axis.vertical,
+ onPointerMove: (dx) {
+ nameColWidth.value += dx;
+ nameColWidth.value = min(kDesktopFileTransferMaximumWidth,
+ max(kDesktopFileTransferMinimumWidth, nameColWidth.value));
+ },
+ padding: padding,
+ ),
+ Obx(
+ () => headerItemFunc(modifiedColWidth.value, SortBy.modified,
+ translate("Modified"), isLocal),
+ ),
+ DraggableDivider(
+ axis: Axis.vertical,
+ onPointerMove: (dx) {
+ modifiedColWidth.value += dx;
+ modifiedColWidth.value = min(
+ kDesktopFileTransferMaximumWidth,
+ max(kDesktopFileTransferMinimumWidth,
+ modifiedColWidth.value));
+ },
+ padding: padding),
+ Expanded(
+ child:
+ headerItemFunc(null, SortBy.size, translate("Size"), isLocal))
+ ],
+ ),
+ );
+ }
}
diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart
index 7e07eaa9a..39958e88e 100644
--- a/flutter/lib/desktop/pages/file_manager_tab_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart
@@ -31,13 +31,21 @@ class _FileManagerTabPageState extends State {
_FileManagerTabPageState(Map params) {
Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer));
+ tabController.onSelected = (_, id) {
+ WindowController.fromWindowId(windowId())
+ .setTitle(getWindowNameWithId(id));
+ };
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => () => tabController.closeBy(params['id']),
- page: FileManagerPage(key: ValueKey(params['id']), id: params['id'])));
+ page: FileManagerPage(
+ key: ValueKey(params['id']),
+ id: params['id'],
+ forceRelay: params['forceRelay'],
+ )));
}
@override
@@ -60,7 +68,11 @@ class _FileManagerTabPageState extends State {
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
onTabCloseButton: () => tabController.closeBy(id),
- page: FileManagerPage(key: ValueKey(id), id: id)));
+ page: FileManagerPage(
+ key: ValueKey(id),
+ id: id,
+ forceRelay: args['forceRelay'],
+ )));
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
@@ -78,7 +90,7 @@ class _FileManagerTabPageState extends State {
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).cardColor,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
@@ -86,7 +98,7 @@ class _FileManagerTabPageState extends State {
labelGetter: DesktopTab.labelGetterAlias,
)),
);
- return Platform.isMacOS
+ return Platform.isMacOS || kUseCompatibleUiMode
? tabWidget
: SubWindowDragToResizeArea(
child: tabWidget,
diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart
index e7bb28813..00ca2bb23 100644
--- a/flutter/lib/desktop/pages/install_page.dart
+++ b/flutter/lib/desktop/pages/install_page.dart
@@ -1,7 +1,9 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:get/get.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:window_manager/window_manager.dart';
@@ -13,7 +15,51 @@ class InstallPage extends StatefulWidget {
State createState() => _InstallPageState();
}
-class _InstallPageState extends State with WindowListener {
+class _InstallPageState extends State {
+ final tabController = DesktopTabController(tabType: DesktopTabType.main);
+
+ @override
+ void initState() {
+ super.initState();
+ Get.put(tabController);
+ const lable = "install";
+ tabController.add(TabInfo(
+ key: lable,
+ label: lable,
+ closable: false,
+ page: _InstallPageBody(
+ key: const ValueKey(lable),
+ )));
+ }
+
+ @override
+ void dispose() {
+ super.dispose();
+ Get.delete();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return DragToResizeArea(
+ resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ child: Container(
+ child: Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.background,
+ body: DesktopTab(controller: tabController)),
+ ),
+ );
+ }
+}
+
+class _InstallPageBody extends StatefulWidget {
+ const _InstallPageBody({Key? key}) : super(key: key);
+
+ @override
+ State<_InstallPageBody> createState() => _InstallPageBodyState();
+}
+
+class _InstallPageBodyState extends State<_InstallPageBody>
+ with WindowListener {
late final TextEditingController controller;
final RxBool startmenu = true.obs;
final RxBool desktopicon = true.obs;
@@ -46,15 +92,19 @@ class _InstallPageState extends State with WindowListener {
final double em = 13;
final btnFontSize = 0.9 * em;
final double button_radius = 6;
+ final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark;
final buttonStyle = OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(button_radius)),
));
final inputBorder = OutlineInputBorder(
borderRadius: BorderRadius.zero,
- borderSide: BorderSide(color: Colors.black12));
+ borderSide:
+ BorderSide(color: isDarkTheme ? Colors.white70 : Colors.black12));
+ final textColor = isDarkTheme ? null : Colors.black87;
+ final dividerColor = isDarkTheme ? Colors.white70 : Colors.black87;
return Scaffold(
- backgroundColor: Colors.white,
+ backgroundColor: null,
body: SingleChildScrollView(
child: Column(
children: [
@@ -91,8 +141,7 @@ class _InstallPageState extends State with WindowListener {
style: buttonStyle,
child: Text(translate('Change Path'),
style: TextStyle(
- color: Colors.black87,
- fontSize: btnFontSize)))
+ color: textColor, fontSize: btnFontSize)))
.marginOnly(left: em))
],
).marginSymmetric(vertical: 2 * em),
@@ -127,8 +176,7 @@ class _InstallPageState extends State with WindowListener {
)).marginOnly(top: 2 * em),
Row(children: [Text(translate('agreement_tip'))])
.marginOnly(top: em),
- Divider(color: Colors.black87)
- .marginSymmetric(vertical: 0.5 * em),
+ Divider(color: dividerColor).marginSymmetric(vertical: 0.5 * em),
Row(
children: [
Expanded(
@@ -143,8 +191,7 @@ class _InstallPageState extends State with WindowListener {
style: buttonStyle,
child: Text(translate('Cancel'),
style: TextStyle(
- color: Colors.black87,
- fontSize: btnFontSize)))
+ color: textColor, fontSize: btnFontSize)))
.marginOnly(right: 2 * em)),
Obx(() => ElevatedButton(
onPressed: btnEnabled.value ? install : null,
@@ -167,8 +214,7 @@ class _InstallPageState extends State with WindowListener {
style: buttonStyle,
child: Text(translate('Run without install'),
style: TextStyle(
- color: Colors.black87,
- fontSize: btnFontSize)))
+ color: textColor, fontSize: btnFontSize)))
.marginOnly(left: 2 * em)),
),
],
diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart
index b2458d096..ae070b47b 100644
--- a/flutter/lib/desktop/pages/port_forward_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_page.dart
@@ -26,10 +26,12 @@ class _PortForward {
}
class PortForwardPage extends StatefulWidget {
- const PortForwardPage({Key? key, required this.id, required this.isRDP})
+ const PortForwardPage(
+ {Key? key, required this.id, required this.isRDP, this.forceRelay})
: super(key: key);
final String id;
final bool isRDP;
+ final bool? forceRelay;
@override
State createState() => _PortForwardPageState();
@@ -47,7 +49,7 @@ class _PortForwardPageState extends State
void initState() {
super.initState();
_ffi = FFI();
- _ffi.start(widget.id, isPortForward: true);
+ _ffi.start(widget.id, isPortForward: true, forceRelay: widget.forceRelay);
Get.put(_ffi, tag: 'pf_${widget.id}');
if (!Platform.isLinux) {
Wakelock.enable();
@@ -89,7 +91,7 @@ class _PortForwardPageState extends State
Flexible(
child: Container(
decoration: BoxDecoration(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
border: Border.all(width: 1, color: MyTheme.border)),
child:
widget.isRDP ? buildRdp(context) : buildTunnel(context),
@@ -127,12 +129,12 @@ class _PortForwardPageState extends State
}
buildTunnel(BuildContext context) {
- text(String lable) => Expanded(
- child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
+ text(String label) => Expanded(
+ child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
return Theme(
data: Theme.of(context)
- .copyWith(backgroundColor: Theme.of(context).backgroundColor),
+ .copyWith(backgroundColor: Theme.of(context).colorScheme.background),
child: Obx(() => ListView.builder(
controller: ScrollController(),
itemCount: pfs.length + 2,
@@ -167,7 +169,8 @@ class _PortForwardPageState extends State
return Container(
height: _kRowHeight,
- decoration: BoxDecoration(color: Theme.of(context).backgroundColor),
+ decoration:
+ BoxDecoration(color: Theme.of(context).colorScheme.background),
child: Row(children: [
buildTunnelInputCell(context,
controller: localPortController,
@@ -179,36 +182,33 @@ class _PortForwardPageState extends State
buildTunnelInputCell(context,
controller: remotePortController,
inputFormatters: portInputFormatter),
- SizedBox(
- width: _kColumn4Width,
- child: ElevatedButton(
- style: ElevatedButton.styleFrom(
- elevation: 0, side: const BorderSide(color: MyTheme.border)),
- onPressed: () async {
- int? localPort = int.tryParse(localPortController.text);
- int? remotePort = int.tryParse(remotePortController.text);
- if (localPort != null &&
- remotePort != null &&
- (remoteHostController.text.isEmpty ||
- remoteHostController.text.trim().isNotEmpty)) {
- await bind.sessionAddPortForward(
- id: 'pf_${widget.id}',
- localPort: localPort,
- remoteHost: remoteHostController.text.trim().isEmpty
- ? 'localhost'
- : remoteHostController.text.trim(),
- remotePort: remotePort);
- localPortController.clear();
- remoteHostController.clear();
- remotePortController.clear();
- refreshTunnelConfig();
- }
- },
- child: Text(
- translate('Add'),
- ),
- ).marginAll(10),
- ),
+ ElevatedButton(
+ style: ElevatedButton.styleFrom(
+ elevation: 0, side: const BorderSide(color: MyTheme.border)),
+ onPressed: () async {
+ int? localPort = int.tryParse(localPortController.text);
+ int? remotePort = int.tryParse(remotePortController.text);
+ if (localPort != null &&
+ remotePort != null &&
+ (remoteHostController.text.isEmpty ||
+ remoteHostController.text.trim().isNotEmpty)) {
+ await bind.sessionAddPortForward(
+ id: 'pf_${widget.id}',
+ localPort: localPort,
+ remoteHost: remoteHostController.text.trim().isEmpty
+ ? 'localhost'
+ : remoteHostController.text.trim(),
+ remotePort: remotePort);
+ localPortController.clear();
+ remoteHostController.clear();
+ remotePortController.clear();
+ refreshTunnelConfig();
+ }
+ },
+ child: Text(
+ translate('Add'),
+ ),
+ ).marginAll(10),
]),
);
}
@@ -230,7 +230,7 @@ class _PortForwardPageState extends State
borderSide: BorderSide(color: MyTheme.color(context).border!)),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: MyTheme.color(context).border!)),
- fillColor: Theme.of(context).backgroundColor,
+ fillColor: Theme.of(context).colorScheme.background,
contentPadding: const EdgeInsets.all(10),
hintText: hint,
hintStyle:
@@ -241,8 +241,8 @@ class _PortForwardPageState extends State
}
Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) {
- text(String lable) => Expanded(
- child: Text(lable, style: const TextStyle(fontSize: 20))
+ text(String label) => Expanded(
+ child: Text(label, style: const TextStyle(fontSize: 20))
.marginOnly(left: _kTextLeftMargin));
return Container(
@@ -252,7 +252,7 @@ class _PortForwardPageState extends State
? MyTheme.currentThemeMode() == ThemeMode.dark
? const Color(0xFF202020)
: const Color(0xFFF4F5F6)
- : Theme.of(context).backgroundColor),
+ : Theme.of(context).colorScheme.background),
child: Row(children: [
text(pf.localPort.toString()),
const SizedBox(width: _kColumn1Width),
@@ -285,16 +285,16 @@ class _PortForwardPageState extends State
}
buildRdp(BuildContext context) {
- text1(String lable) => Expanded(
- child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin));
- text2(String lable) => Expanded(
+ text1(String label) => Expanded(
+ child: Text(translate(label)).marginOnly(left: _kTextLeftMargin));
+ text2(String label) => Expanded(
child: Text(
- lable,
+ label,
style: const TextStyle(fontSize: 20),
).marginOnly(left: _kTextLeftMargin));
return Theme(
data: Theme.of(context)
- .copyWith(backgroundColor: Theme.of(context).backgroundColor),
+ .copyWith(backgroundColor: Theme.of(context).colorScheme.background),
child: ListView.builder(
controller: ScrollController(),
itemCount: 2,
@@ -313,8 +313,8 @@ class _PortForwardPageState extends State
} else {
return Container(
height: _kRowHeight,
- decoration:
- BoxDecoration(color: Theme.of(context).backgroundColor),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.background),
child: Row(children: [
Expanded(
child: Align(
diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart
index d4c0a86f8..32f02c9b7 100644
--- a/flutter/lib/desktop/pages/port_forward_tab_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart
@@ -31,6 +31,10 @@ class _PortForwardTabPageState extends State {
isRDP = params['isRDP'];
tabController =
Get.put(DesktopTabController(tabType: DesktopTabType.portForward));
+ tabController.onSelected = (_, id) {
+ WindowController.fromWindowId(windowId())
+ .setTitle(getWindowNameWithId(id));
+ };
tabController.add(TabInfo(
key: params['id'],
label: params['id'],
@@ -40,6 +44,7 @@ class _PortForwardTabPageState extends State {
key: ValueKey(params['id']),
id: params['id'],
isRDP: isRDP,
+ forceRelay: params['forceRelay'],
)));
}
@@ -68,7 +73,12 @@ class _PortForwardTabPageState extends State {
label: id,
selectedIcon: selectedIcon,
unselectedIcon: unselectedIcon,
- page: PortForwardPage(id: id, isRDP: isRDP)));
+ page: PortForwardPage(
+ key: ValueKey(args['id']),
+ id: id,
+ isRDP: isRDP,
+ forceRelay: args['forceRelay'],
+ )));
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
@@ -86,7 +96,7 @@ class _PortForwardTabPageState extends State {
decoration: BoxDecoration(
border: Border.all(color: MyTheme.color(context).border!)),
child: Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: () async {
@@ -97,13 +107,15 @@ class _PortForwardTabPageState extends State {
labelGetter: DesktopTab.labelGetterAlias,
)),
);
- return Platform.isMacOS
+ return Platform.isMacOS || kUseCompatibleUiMode
? tabWidget
- : SubWindowDragToResizeArea(
- child: tabWidget,
- resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
- windowId: stateGlobal.windowId,
- );
+ : Obx(
+ () => SubWindowDragToResizeArea(
+ child: tabWidget,
+ resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ windowId: stateGlobal.windowId,
+ ),
+ );
}
void onRemoveId(String id) {
diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index 55a5bbaef..ab0daece7 100644
--- a/flutter/lib/desktop/pages/remote_page.dart
+++ b/flutter/lib/desktop/pages/remote_page.dart
@@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui' as ui;
import 'package:desktop_multi_window/desktop_multi_window.dart';
-import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_custom_cursor/cursor_manager.dart'
@@ -22,6 +21,7 @@ import '../../mobile/widgets/dialog.dart';
import '../../models/model.dart';
import '../../models/platform_model.dart';
import '../../common/shared_state.dart';
+import '../../utils/image.dart';
import '../widgets/remote_menubar.dart';
import '../widgets/kb_layout_type_chooser.dart';
@@ -33,10 +33,14 @@ class RemotePage extends StatefulWidget {
Key? key,
required this.id,
required this.menubarState,
+ this.switchUuid,
+ this.forceRelay,
}) : super(key: key);
final String id;
final MenubarState menubarState;
+ final String? switchUuid;
+ final bool? forceRelay;
final SimpleWrapper?> _lastState = SimpleWrapper(null);
FFI get ffi => (_lastState.value! as _RemotePageState)._ffi;
@@ -59,6 +63,11 @@ class _RemotePageState extends State
late RxBool _zoomCursor;
late RxBool _remoteCursorMoved;
late RxBool _keyboardEnabled;
+ late RxInt _textureId;
+ late int _textureKey;
+ final useTextureRender = bind.mainUseTextureRender();
+
+ final _blockableOverlayState = BlockableOverlayState();
final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode");
@@ -79,6 +88,8 @@ class _RemotePageState extends State
_showRemoteCursor = ShowRemoteCursorState.find(id);
_keyboardEnabled = KeyboardEnabledState.find(id);
_remoteCursorMoved = RemoteCursorMovedState.find(id);
+ _textureKey = newTextureId;
+ _textureId = RxInt(-1);
}
void _removeStates(String id) {
@@ -100,7 +111,11 @@ class _RemotePageState extends State
showKBLayoutTypeChooserIfNeeded(
_ffi.ffiModel.pi.platform, _ffi.dialogManager);
});
- _ffi.start(widget.id);
+ _ffi.start(
+ widget.id,
+ switchUuid: widget.switchUuid,
+ forceRelay: widget.forceRelay,
+ );
WidgetsBinding.instance.addPostFrameCallback((_) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
_ffi.dialogManager
@@ -109,6 +124,18 @@ class _RemotePageState extends State
if (!Platform.isLinux) {
Wakelock.enable();
}
+ // Register texture.
+ _textureId.value = -1;
+ if (useTextureRender) {
+ textureRenderer.createTexture(_textureKey).then((id) async {
+ debugPrint("id: $id, texture_key: $_textureKey");
+ if (id != -1) {
+ final ptr = await textureRenderer.getTexturePtr(_textureKey);
+ platformFFI.registerTexture(widget.id, ptr);
+ _textureId.value = id;
+ }
+ });
+ }
_ffi.ffiModel.updateEventListener(widget.id);
_ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id);
// Session option should be set after models.dart/FFI.start
@@ -128,6 +155,13 @@ class _RemotePageState extends State
// });
// _isCustomCursorInited = true;
// }
+
+ _ffi.dialogManager.setOverlayState(_blockableOverlayState);
+ _ffi.chatModel.setOverlayState(_blockableOverlayState);
+ // make remote page penetrable automatically, effective for chat over remote
+ _blockableOverlayState.onMiddleBlockedClick = () {
+ _blockableOverlayState.setMiddleBlocked(false);
+ };
}
@override
@@ -166,6 +200,10 @@ class _RemotePageState extends State
@override
void dispose() {
debugPrint("REMOTE PAGE dispose ${widget.id}");
+ if (useTextureRender) {
+ platformFFI.registerTexture(widget.id, 0);
+ textureRenderer.closeTexture(_textureKey);
+ }
// ensure we leave this session, this is a double check
bind.sessionEnterOrLeave(id: widget.id, enter: false);
DesktopMultiWindow.removeListener(this);
@@ -187,39 +225,50 @@ class _RemotePageState extends State
Widget buildBody(BuildContext context) {
return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
- body: Overlay(
- initialEntries: [
- OverlayEntry(builder: (context) {
- _ffi.chatModel.setOverlayState(Overlay.of(context));
- _ffi.dialogManager.setOverlayState(Overlay.of(context));
- return Container(
- color: Colors.black,
- child: RawKeyFocusScope(
- focusNode: _rawKeyFocusNode,
- onFocusChange: (bool imageFocused) {
- debugPrint(
- "onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
- // See [onWindowBlur].
- if (Platform.isWindows) {
- if (_isWindowBlur) {
- imageFocused = false;
- Future.delayed(Duration.zero, () {
- _rawKeyFocusNode.unfocus();
- });
- }
- if (imageFocused) {
- _ffi.inputModel.enterOrLeave(true);
- } else {
- _ffi.inputModel.enterOrLeave(false);
- }
- }
- },
- inputModel: _ffi.inputModel,
- child: getBodyForDesktop(context)));
- })
- ],
- ));
+ backgroundColor: Theme.of(context).colorScheme.background,
+
+ /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
+ /// see override build() in [BlockableOverlay]
+ body: BlockableOverlay(
+ state: _blockableOverlayState,
+ underlying: Container(
+ color: Colors.black,
+ child: RawKeyFocusScope(
+ focusNode: _rawKeyFocusNode,
+ onFocusChange: (bool imageFocused) {
+ debugPrint(
+ "onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
+ // See [onWindowBlur].
+ if (Platform.isWindows) {
+ if (_isWindowBlur) {
+ imageFocused = false;
+ Future.delayed(Duration.zero, () {
+ _rawKeyFocusNode.unfocus();
+ });
+ }
+ if (imageFocused) {
+ _ffi.inputModel.enterOrLeave(true);
+ } else {
+ _ffi.inputModel.enterOrLeave(false);
+ }
+ }
+ },
+ inputModel: _ffi.inputModel,
+ child: getBodyForDesktop(context))),
+ upperLayer: [
+ OverlayEntry(
+ builder: (context) => RemoteMenubar(
+ id: widget.id,
+ ffi: _ffi,
+ state: widget.menubarState,
+ onEnterOrLeaveImageSetter: (func) =>
+ _onEnterOrLeaveImage4Menubar = func,
+ onEnterOrLeaveImageCleaner: () =>
+ _onEnterOrLeaveImage4Menubar = null,
+ ))
+ ],
+ ),
+ );
}
@override
@@ -274,6 +323,34 @@ class _RemotePageState extends State
}
}
+ Widget _buildRawPointerMouseRegion(
+ Widget child,
+ PointerEnterEventListener? onEnter,
+ PointerExitEventListener? onExit,
+ ) {
+ return RawPointerMouseRegion(
+ onEnter: enterView,
+ onExit: leaveView,
+ onPointerDown: (event) {
+ // A double check for blur status.
+ // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
+ // Sometimes the system does not send the necessary focus event to flutter. We should manually
+ // handle this inconsistent status by setting `_isWindowBlur` to false. So we can
+ // ensure the grab-key thread is running when our users are clicking the remote canvas.
+ if (_isWindowBlur) {
+ debugPrint(
+ "Unexpected status: onPointerDown is triggered while the remote window is in blur status");
+ _isWindowBlur = false;
+ }
+ if (!_rawKeyFocusNode.hasFocus) {
+ _rawKeyFocusNode.requestFocus();
+ }
+ },
+ inputModel: _ffi.inputModel,
+ child: child,
+ );
+ }
+
Widget getBodyForDesktop(BuildContext context) {
var paints = [
MouseRegion(onEnter: (evt) {
@@ -290,27 +367,10 @@ class _RemotePageState extends State
cursorOverImage: _cursorOverImage,
keyboardEnabled: _keyboardEnabled,
remoteCursorMoved: _remoteCursorMoved,
- listenerBuilder: (child) => RawPointerMouseRegion(
- onEnter: enterView,
- onExit: leaveView,
- onPointerDown: (event) {
- // A double check for blur status.
- // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false.
- // Sometimes the system does not send the necessary focus event to flutter. We should manually
- // handle this inconsistent status by setting `_isWindowBlur` to false. So we can
- // ensure the grab-key thread is running when our users are clicking the remote canvas.
- if (_isWindowBlur) {
- debugPrint(
- "Unexpected status: onPointerDown is triggered while the remote window is in blur status");
- _isWindowBlur = false;
- }
- if (!_rawKeyFocusNode.hasFocus) {
- _rawKeyFocusNode.requestFocus();
- }
- },
- inputModel: _ffi.inputModel,
- child: child,
- ),
+ textureId: _textureId,
+ useTextureRender: useTextureRender,
+ listenerBuilder: (child) =>
+ _buildRawPointerMouseRegion(child, enterView, leaveView),
);
}))
];
@@ -323,14 +383,14 @@ class _RemotePageState extends State
zoomCursor: _zoomCursor,
))));
}
- paints.add(QualityMonitor(_ffi.qualityMonitorModel));
- paints.add(RemoteMenubar(
- id: widget.id,
- ffi: _ffi,
- state: widget.menubarState,
- onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func,
- onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null,
- ));
+ paints.add(
+ Positioned(
+ top: 10,
+ right: 10,
+ child: _buildRawPointerMouseRegion(
+ QualityMonitor(_ffi.qualityMonitorModel), null, null),
+ ),
+ );
return Stack(
children: paints,
);
@@ -342,10 +402,12 @@ class _RemotePageState extends State
class ImagePaint extends StatefulWidget {
final String id;
- final Rx zoomCursor;
- final Rx cursorOverImage;
- final Rx keyboardEnabled;
- final Rx remoteCursorMoved;
+ final RxBool zoomCursor;
+ final RxBool cursorOverImage;
+ final RxBool keyboardEnabled;
+ final RxBool remoteCursorMoved;
+ final RxInt textureId;
+ final bool useTextureRender;
final Widget Function(Widget)? listenerBuilder;
ImagePaint(
@@ -355,6 +417,8 @@ class ImagePaint extends StatefulWidget {
required this.cursorOverImage,
required this.keyboardEnabled,
required this.remoteCursorMoved,
+ required this.textureId,
+ required this.useTextureRender,
this.listenerBuilder})
: super(key: key);
@@ -368,10 +432,10 @@ class _ImagePaintState extends State {
final ScrollController _vertical = ScrollController();
String get id => widget.id;
- Rx get zoomCursor => widget.zoomCursor;
- Rx get cursorOverImage => widget.cursorOverImage;
- Rx get keyboardEnabled => widget.keyboardEnabled;
- Rx get remoteCursorMoved => widget.remoteCursorMoved;
+ RxBool get zoomCursor => widget.zoomCursor;
+ RxBool get cursorOverImage => widget.cursorOverImage;
+ RxBool get keyboardEnabled => widget.keyboardEnabled;
+ RxBool get remoteCursorMoved => widget.remoteCursorMoved;
Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder;
@override
@@ -380,36 +444,68 @@ class _ImagePaintState extends State {
var c = Provider.of(context);
final s = c.scale;
- mouseRegion({child}) => Obx(() => MouseRegion(
- cursor: cursorOverImage.isTrue
- ? c.cursorEmbedded
- ? SystemMouseCursors.none
- : keyboardEnabled.isTrue
- ? (() {
- if (remoteCursorMoved.isTrue) {
- _lastRemoteCursorMoved = true;
- return SystemMouseCursors.none;
- } else {
- if (_lastRemoteCursorMoved) {
- _lastRemoteCursorMoved = false;
- _firstEnterImage.value = true;
- }
- return _buildCustomCursor(context, s);
- }
- }())
- : _buildDisabledCursor(context, s)
- : MouseCursor.defer,
- onHover: (evt) {},
- child: child));
+ mouseRegion({child}) => Obx(() {
+ double getCursorScale() {
+ var c = Provider.of(context);
+ var cursorScale = 1.0;
+ if (Platform.isWindows) {
+ // debug win10
+ final isViewAdaptive =
+ c.viewStyle.style == kRemoteViewStyleAdaptive;
+ if (zoomCursor.value && isViewAdaptive) {
+ cursorScale = s * c.devicePixelRatio;
+ }
+ } else {
+ final isViewOriginal =
+ c.viewStyle.style == kRemoteViewStyleOriginal;
+ if (zoomCursor.value || isViewOriginal) {
+ cursorScale = s;
+ }
+ }
+ return cursorScale;
+ }
+
+ return MouseRegion(
+ cursor: cursorOverImage.isTrue
+ ? c.cursorEmbedded
+ ? SystemMouseCursors.none
+ : keyboardEnabled.isTrue
+ ? (() {
+ if (remoteCursorMoved.isTrue) {
+ _lastRemoteCursorMoved = true;
+ return SystemMouseCursors.none;
+ } else {
+ if (_lastRemoteCursorMoved) {
+ _lastRemoteCursorMoved = false;
+ _firstEnterImage.value = true;
+ }
+ return _buildCustomCursor(
+ context, getCursorScale());
+ }
+ }())
+ : _buildDisabledCursor(context, getCursorScale())
+ : MouseCursor.defer,
+ onHover: (evt) {},
+ child: child);
+ });
if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) {
final imageWidth = c.getDisplayWidth() * s;
final imageHeight = c.getDisplayHeight() * s;
final imageSize = Size(imageWidth, imageHeight);
- final imageWidget = CustomPaint(
- size: imageSize,
- painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
- );
+ late final Widget imageWidget;
+ if (widget.useTextureRender) {
+ imageWidget = SizedBox(
+ width: imageWidth,
+ height: imageHeight,
+ child: Obx(() => Texture(textureId: widget.textureId.value)),
+ );
+ } else {
+ imageWidget = CustomPaint(
+ size: imageSize,
+ painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s),
+ );
+ }
return NotificationListener(
onNotification: (notification) {
@@ -433,11 +529,31 @@ class _ImagePaintState extends State {
context, _buildListener(imageWidget), c.size, imageSize)),
));
} else {
- final imageWidget = CustomPaint(
- size: Size(c.size.width, c.size.height),
- painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
- );
- return mouseRegion(child: _buildListener(imageWidget));
+ late final Widget imageWidget;
+ if (c.size.width > 0 && c.size.height > 0) {
+ if (widget.useTextureRender) {
+ imageWidget = Stack(
+ children: [
+ Positioned(
+ left: c.x,
+ top: c.y,
+ width: c.getDisplayWidth() * s,
+ height: c.getDisplayHeight() * s,
+ child: Texture(textureId: widget.textureId.value),
+ )
+ ],
+ );
+ } else {
+ imageWidget = CustomPaint(
+ size: Size(c.size.width, c.size.height),
+ painter:
+ ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s),
+ );
+ }
+ return mouseRegion(child: _buildListener(imageWidget));
+ } else {
+ return Container();
+ }
}
}
@@ -446,7 +562,7 @@ class _ImagePaintState extends State {
if (cache == null) {
return MouseCursor.defer;
} else {
- final key = cache.updateGetKey(scale, zoomCursor.value);
+ final key = cache.updateGetKey(scale);
if (!cursor.cachedKeys.contains(key)) {
debugPrint("Register custom cursor with key $key");
// [Safety]
@@ -612,7 +728,8 @@ class CursorPaint extends StatelessWidget {
double x = (m.x - hotx) * c.scale + cx;
double y = (m.y - hoty) * c.scale + cy;
double scale = 1.0;
- if (zoomCursor.isTrue) {
+ final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal;
+ if (zoomCursor.value || isViewOriginal) {
x = m.x - hotx + cx / c.scale;
y = m.y - hoty + cy / c.scale;
scale = c.scale;
@@ -628,40 +745,3 @@ class CursorPaint extends StatelessWidget {
);
}
}
-
-class ImagePainter extends CustomPainter {
- ImagePainter({
- required this.image,
- required this.x,
- required this.y,
- required this.scale,
- });
-
- ui.Image? image;
- double x;
- double y;
- double scale;
-
- @override
- void paint(Canvas canvas, Size size) {
- if (image == null) return;
- if (x.isNaN || y.isNaN) return;
- canvas.scale(scale, scale);
- // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161
- // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html
- var paint = Paint();
- if ((scale - 1.0).abs() > 0.001) {
- paint.filterQuality = FilterQuality.medium;
- if (scale > 10.00000) {
- paint.filterQuality = FilterQuality.high;
- }
- }
- canvas.drawImage(
- image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint);
- }
-
- @override
- bool shouldRepaint(CustomPainter oldDelegate) {
- return oldDelegate != this;
- }
-}
diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart
index 604787290..d810650fd 100644
--- a/flutter/lib/desktop/pages/remote_tab_page.dart
+++ b/flutter/lib/desktop/pages/remote_tab_page.dart
@@ -22,7 +22,10 @@ import 'package:bot_toast/bot_toast.dart';
import '../../models/platform_model.dart';
class _MenuTheme {
- static const Color commonColor = MyTheme.accent;
+ static const Color blueColor = MyTheme.button;
+ static const Color hoverBlueColor = MyTheme.accent;
+ static const Color redColor = Colors.redAccent;
+ static const Color hoverRedColor = Colors.red;
// kMinInteractiveDimension
static const double height = 20.0;
static const double dividerHeight = 12.0;
@@ -38,9 +41,9 @@ class ConnectionTabPage extends StatefulWidget {
}
class _ConnectionTabPageState extends State {
- final tabController = Get.put(DesktopTabController(
- tabType: DesktopTabType.remoteScreen,
- onSelected: (_, id) => bind.setCurSessionId(id: id)));
+ final tabController =
+ Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen));
+ final contentKey = UniqueKey();
static const IconData selectedIcon = Icons.desktop_windows_sharp;
static const IconData unselectedIcon = Icons.desktop_windows_outlined;
@@ -54,6 +57,11 @@ class _ConnectionTabPageState extends State {
final peerId = params['id'];
if (peerId != null) {
ConnectionTypeState.init(peerId);
+ tabController.onSelected = (_, id) {
+ bind.setCurSessionId(id: id);
+ WindowController.fromWindowId(windowId())
+ .setTitle(getWindowNameWithId(id));
+ };
tabController.add(TabInfo(
key: peerId,
label: peerId,
@@ -64,6 +72,8 @@ class _ConnectionTabPageState extends State {
key: ValueKey(peerId),
id: peerId,
menubarState: _menubarState,
+ switchUuid: params['switch_uuid'],
+ forceRelay: params['forceRelay'],
),
));
_update_remote_count();
@@ -84,6 +94,7 @@ class _ConnectionTabPageState extends State {
if (call.method == "new_remote_desktop") {
final args = jsonDecode(call.arguments);
final id = args['id'];
+ final switchUuid = args['switch_uuid'];
window_on_top(windowId());
ConnectionTypeState.init(id);
tabController.add(TabInfo(
@@ -96,8 +107,12 @@ class _ConnectionTabPageState extends State {
key: ValueKey(id),
id: id,
menubarState: _menubarState,
+ switchUuid: switchUuid,
+ forceRelay: args['forceRelay'],
),
));
+ } else if (call.method == kWindowDisableGrabKeyboard) {
+ stateGlobal.grabKeyboard = false;
} else if (call.method == "onDestroy") {
tabController.clear();
} else if (call.method == kWindowActionRebuild) {
@@ -105,6 +120,9 @@ class _ConnectionTabPageState extends State {
}
_update_remote_count();
});
+ Future.delayed(Duration.zero, () {
+ restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId());
+ });
}
@override
@@ -123,7 +141,7 @@ class _ConnectionTabPageState extends State {
width: stateGlobal.windowBorderWidth.value),
),
child: Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
+ backgroundColor: Theme.of(context).colorScheme.background,
body: DesktopTab(
controller: tabController,
onWindowCloseButton: handleWindowCloseButton,
@@ -187,13 +205,16 @@ class _ConnectionTabPageState extends State {
),
),
);
- return Platform.isMacOS
+ return Platform.isMacOS || kUseCompatibleUiMode
? tabWidget
- : SubWindowDragToResizeArea(
- child: tabWidget,
- resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
- windowId: stateGlobal.windowId,
- );
+ : Obx(() => SubWindowDragToResizeArea(
+ key: contentKey,
+ child: tabWidget,
+ // Specially configured for a better resize area and remote control.
+ childPadding: kDragToResizeAreaPadding,
+ resizeEdgeSize: stateGlobal.resizeEdgeSize.value,
+ windowId: stateGlobal.windowId,
+ ));
}
// Note: Some dup code to ../widgets/remote_menubar
@@ -231,96 +252,35 @@ class _ConnectionTabPageState extends State {
padding: padding,
),
MenuEntryDivider(),
- MenuEntryRadios(
- text: translate('Ratio'),
- optionsGetter: () => [
- MenuEntryRadioOption(
- text: translate('Scale original'),
- value: kRemoteViewStyleOriginal,
- dismissOnClicked: true,
- ),
- MenuEntryRadioOption(
- text: translate('Scale adaptive'),
- value: kRemoteViewStyleAdaptive,
- dismissOnClicked: true,
- ),
- ],
- curOptionGetter: () async =>
- // null means peer id is not found, which there's no need to care about
- await bind.sessionGetViewStyle(id: key) ?? '',
- optionSetter: (String oldValue, String newValue) async {
- await bind.sessionSetViewStyle(id: key, value: newValue);
- ffi.canvasModel.updateViewStyle();
- cancelFunc();
- },
- padding: padding,
+ RemoteMenuEntry.viewStyle(
+ key,
+ ffi,
+ padding,
+ dismissFunc: cancelFunc,
),
]);
if (!ffi.canvasModel.cursorEmbedded) {
menu.add(MenuEntryDivider());
- menu.add(() {
- final state = ShowRemoteCursorState.find(key);
- return MenuEntrySwitch2(
- switchType: SwitchType.scheckbox,
- text: translate('Show remote cursor'),
- getter: () {
- return state;
- },
- setter: (bool v) async {
- state.value = v;
- await bind.sessionToggleOption(
- id: key, value: 'show-remote-cursor');
- cancelFunc();
- },
- padding: padding,
- );
- }());
+ menu.add(RemoteMenuEntry.showRemoteCursor(
+ key,
+ padding,
+ dismissFunc: cancelFunc,
+ ));
}
if (perms['keyboard'] != false) {
if (perms['clipboard'] != false) {
- menu.add(MenuEntrySwitch(
- switchType: SwitchType.scheckbox,
- text: translate('Disable clipboard'),
- getter: () async {
- return bind.sessionGetToggleOptionSync(
- id: key, arg: 'disable-clipboard');
- },
- setter: (bool v) async {
- await bind.sessionToggleOption(id: key, value: 'disable-clipboard');
- cancelFunc();
- },
- padding: padding,
- ));
+ menu.add(RemoteMenuEntry.disableClipboard(key, padding,
+ dismissFunc: cancelFunc));
}
- menu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- translate('Insert Lock'),
- style: style,
- ),
- proc: () {
- bind.sessionLockScreen(id: key);
- cancelFunc();
- },
- padding: padding,
- dismissOnClicked: true,
- ));
+ menu.add(
+ RemoteMenuEntry.insertLock(key, padding, dismissFunc: cancelFunc));
- if (pi.platform == 'Linux' || pi.sasEnabled) {
- menu.add(MenuEntryButton(
- childBuilder: (TextStyle? style) => Text(
- '${translate("Insert")} Ctrl + Alt + Del',
- style: style,
- ),
- proc: () {
- bind.sessionCtrlAltDel(id: key);
- cancelFunc();
- },
- padding: padding,
- dismissOnClicked: true,
- ));
+ if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) {
+ menu.add(RemoteMenuEntry.insertCtrlAltDel(key, padding,
+ dismissFunc: cancelFunc));
}
}
@@ -329,7 +289,7 @@ class _ConnectionTabPageState extends State {
.map((entry) => entry.build(
context,
const MenuConfig(
- commonColor: _MenuTheme.commonColor,
+ commonColor: _MenuTheme.blueColor,
height: _MenuTheme.height,
dividerHeight: _MenuTheme.dividerHeight,
)))
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index fa367f488..45591b79b 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -1,11 +1,13 @@
// original cm window in Sciter version.
import 'dart:async';
+import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_hbb/consts.dart';
import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
import 'package:flutter_hbb/models/chat_model.dart';
+import 'package:flutter_hbb/utils/platform_channel.dart';
import 'package:get/get.dart';
import 'package:provider/provider.dart';
import 'package:window_manager/window_manager.dart';
@@ -30,7 +32,12 @@ class _DesktopServerPageState extends State
void initState() {
gFFI.ffiModel.updateEventListener("");
windowManager.addListener(this);
- tabController.onRemoved = (_, id) => onRemoveId(id);
+ tabController.onRemoved = (_, id) {
+ onRemoveId(id);
+ };
+ tabController.onSelected = (_, id) {
+ windowManager.setTitle(getWindowNameWithId(id));
+ };
super.initState();
}
@@ -42,8 +49,14 @@ class _DesktopServerPageState extends State
@override
void onWindowClose() {
- gFFI.serverModel.closeAll();
- gFFI.close();
+ Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) {
+ if (Platform.isMacOS) {
+ RdPlatformChannel.instance.terminate();
+ } else {
+ windowManager.setPreventClose(false);
+ windowManager.close();
+ }
+ });
super.onWindowClose();
}
@@ -63,26 +76,19 @@ class _DesktopServerPageState extends State
],
child: Consumer(
builder: (context, serverModel, child) => Container(
- decoration: BoxDecoration(
- border:
- Border.all(color: MyTheme.color(context).border!)),
- child: Overlay(initialEntries: [
- OverlayEntry(builder: (context) {
- gFFI.dialogManager.setOverlayState(Overlay.of(context));
- return Scaffold(
- backgroundColor: Theme.of(context).backgroundColor,
- body: Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.start,
- children: [
- Expanded(child: ConnectionManager()),
- ],
- ),
- ),
- );
- })
- ]),
- )));
+ decoration: BoxDecoration(
+ border: Border.all(color: MyTheme.color(context).border!)),
+ child: Scaffold(
+ backgroundColor: Theme.of(context).colorScheme.background,
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ Expanded(child: ConnectionManager()),
+ ],
+ ),
+ ),
+ ))));
}
@override
@@ -180,7 +186,7 @@ class ConnectionManagerState extends State {
windowManager.startDragging();
},
child: Container(
- color: Theme.of(context).backgroundColor,
+ color: Theme.of(context).colorScheme.background,
),
),
),
@@ -516,6 +522,48 @@ class _CmControlPanel extends StatelessWidget {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
+ Offstage(
+ offstage: !client.inVoiceCall,
+ child: buildButton(context,
+ color: Colors.red,
+ onClick: () => closeVoiceCall(),
+ icon: Icon(Icons.phone_disabled_rounded, color: Colors.white),
+ text: "Stop voice call",
+ textColor: Colors.white),
+ ),
+ Offstage(
+ offstage: !client.incomingVoiceCall,
+ child: Row(
+ children: [
+ Expanded(
+ child: buildButton(context,
+ color: MyTheme.accent,
+ onClick: () => handleVoiceCall(true),
+ icon: Icon(Icons.phone_enabled, color: Colors.white),
+ text: "Accept",
+ textColor: Colors.white),
+ ),
+ Expanded(
+ child: buildButton(context,
+ color: Colors.red,
+ onClick: () => handleVoiceCall(false),
+ icon:
+ Icon(Icons.phone_disabled_rounded, color: Colors.white),
+ text: "Dismiss",
+ textColor: Colors.white),
+ )
+ ],
+ ),
+ ),
+ Offstage(
+ offstage: !client.fromSwitch,
+ child: buildButton(context,
+ color: Colors.purple,
+ onClick: () => handleSwitchBack(context),
+ icon: Icon(Icons.reply, color: Colors.white),
+ text: "Switch Sides",
+ textColor: Colors.white),
+ ),
Offstage(
offstage: !showElevation,
child: buildButton(context, color: Colors.green[700], onClick: () {
@@ -612,7 +660,7 @@ class _CmControlPanel extends StatelessWidget {
.marginSymmetric(horizontal: showElevation ? 0 : bigMargin);
}
- buildButton(
+ Widget buildButton(
BuildContext context, {
required Color? color,
required Function() onClick,
@@ -674,6 +722,18 @@ class _CmControlPanel extends StatelessWidget {
windowManager.close();
}
}
+
+ void handleSwitchBack(BuildContext context) {
+ bind.cmSwitchBack(connId: client.id);
+ }
+
+ void handleVoiceCall(bool accept) {
+ bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept);
+ }
+
+ void closeVoiceCall() {
+ bind.cmCloseVoiceCall(id: client.id);
+ }
}
void checkClickTime(int id, Function() callback) async {
diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart
index e8361a652..64af41401 100644
--- a/flutter/lib/desktop/screen/desktop_remote_screen.dart
+++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart
@@ -1,8 +1,11 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:flutter_hbb/common.dart';
import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
+import 'package:flutter_hbb/models/state_model.dart';
import 'package:provider/provider.dart';
/// multi-tab desktop remote screen
@@ -10,7 +13,9 @@ class DesktopRemoteScreen extends StatelessWidget {
final Map params;
DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) {
- bind.mainStartGrabKeyboard();
+ if (!bind.mainStartGrabKeyboard()) {
+ stateGlobal.grabKeyboard = true;
+ }
}
@override
@@ -23,6 +28,9 @@ class DesktopRemoteScreen extends StatelessWidget {
ChangeNotifierProvider.value(value: gFFI.canvasModel),
],
child: Scaffold(
+ // Set transparent background for padding the resize area out of the flutter view.
+ // This allows the wallpaper goes through our resize area. (Linux only now).
+ backgroundColor: Platform.isLinux ? Colors.transparent : null,
body: ConnectionTabPage(
params: params,
),
diff --git a/flutter/lib/desktop/widgets/dragable_divider.dart b/flutter/lib/desktop/widgets/dragable_divider.dart
new file mode 100644
index 000000000..3821b7e0d
--- /dev/null
+++ b/flutter/lib/desktop/widgets/dragable_divider.dart
@@ -0,0 +1,53 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:flutter/src/widgets/placeholder.dart';
+
+class DraggableDivider extends StatefulWidget {
+ final Axis axis;
+ final double thickness;
+ final Color color;
+ final Function(double)? onPointerMove;
+ final VoidCallback? onHover;
+ final EdgeInsets padding;
+ const DraggableDivider({
+ super.key,
+ this.axis = Axis.horizontal,
+ this.thickness = 1.0,
+ this.color = const Color.fromARGB(200, 177, 175, 175),
+ this.onPointerMove,
+ this.padding = const EdgeInsets.symmetric(horizontal: 1.0),
+ this.onHover,
+ });
+
+ @override
+ State createState() => _DraggableDividerState();
+}
+
+class _DraggableDividerState extends State {
+ @override
+ Widget build(BuildContext context) {
+ return Listener(
+ onPointerMove: (event) {
+ final dl =
+ widget.axis == Axis.horizontal ? event.localDelta.dy : event.localDelta.dx;
+ widget.onPointerMove?.call(dl);
+ },
+ onPointerHover: (event) => widget.onHover?.call(),
+ child: MouseRegion(
+ cursor: SystemMouseCursors.resizeLeftRight,
+ child: Padding(
+ padding: widget.padding,
+ child: Container(
+ decoration: BoxDecoration(color: widget.color),
+ width: widget.axis == Axis.horizontal
+ ? double.infinity
+ : widget.thickness,
+ height: widget.axis == Axis.horizontal
+ ? widget.thickness
+ : double.infinity,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
index cfbdb0c4e..90e72cd40 100644
--- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
+++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart
@@ -1,12 +1,13 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
+import 'package:flutter_hbb/consts.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_hbb/models/platform_model.dart';
import '../../common.dart';
-typedef KBChoosedCallback = Future Function(String);
+typedef KBChosenCallback = Future Function(String);
const double _kImageMarginVertical = 6.0;
const double _kImageMarginHorizontal = 10.0;
@@ -25,12 +26,12 @@ const _kKBLayoutImageMap = {
class _KBImage extends StatelessWidget {
final String kbLayoutType;
final double imageWidth;
- final RxString choosedType;
+ final RxString chosenType;
const _KBImage({
Key? key,
required this.kbLayoutType,
required this.imageWidth,
- required this.choosedType,
+ required this.chosenType,
}) : super(key: key);
@override
@@ -40,7 +41,7 @@ class _KBImage extends StatelessWidget {
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_kBorderRadius),
border: Border.all(
- color: choosedType.value == kbLayoutType
+ color: chosenType.value == kbLayoutType
? _kImageBorderColor
: Colors.transparent,
width: _kImageBoarderWidth,
@@ -66,13 +67,13 @@ class _KBImage extends StatelessWidget {
class _KBChooser extends StatelessWidget {
final String kbLayoutType;
final double imageWidth;
- final RxString choosedType;
- final KBChoosedCallback cb;
+ final RxString chosenType;
+ final KBChosenCallback cb;
const _KBChooser({
Key? key,
required this.kbLayoutType,
required this.imageWidth,
- required this.choosedType,
+ required this.chosenType,
required this.cb,
}) : super(key: key);
@@ -81,7 +82,7 @@ class _KBChooser extends StatelessWidget {
onChanged(String? v) async {
if (v != null) {
if (await cb(v)) {
- choosedType.value = v;
+ chosenType.value = v;
}
}
}
@@ -95,7 +96,7 @@ class _KBChooser extends StatelessWidget {
child: _KBImage(
kbLayoutType: kbLayoutType,
imageWidth: imageWidth,
- choosedType: choosedType,
+ chosenType: chosenType,
),
style: TextButton.styleFrom(padding: EdgeInsets.zero),
),
@@ -105,7 +106,7 @@ class _KBChooser extends StatelessWidget {
Obx(() => Radio(
splashRadius: 0,
value: kbLayoutType,
- groupValue: choosedType.value,
+ groupValue: chosenType.value,
onChanged: onChanged,
)),
Text(kbLayoutType),
@@ -121,14 +122,14 @@ class _KBChooser extends StatelessWidget {
}
class KBLayoutTypeChooser extends StatelessWidget {
- final RxString choosedType;
+ final RxString chosenType;
final double width;
final double height;
final double dividerWidth;
- final KBChoosedCallback cb;
+ final KBChosenCallback cb;
KBLayoutTypeChooser({
Key? key,
- required this.choosedType,
+ required this.chosenType,
required this.width,
required this.height,
required this.dividerWidth,
@@ -147,7 +148,7 @@ class KBLayoutTypeChooser extends StatelessWidget {
_KBChooser(
kbLayoutType: _kKBLayoutTypeISO,
imageWidth: imageWidth,
- choosedType: choosedType,
+ chosenType: chosenType,
cb: cb,
),
VerticalDivider(
@@ -156,7 +157,7 @@ class KBLayoutTypeChooser extends StatelessWidget {
_KBChooser(
kbLayoutType: _kKBLayoutTypeNotISO,
imageWidth: imageWidth,
- choosedType: choosedType,
+ chosenType: chosenType,
cb: cb,
),
],
@@ -170,14 +171,14 @@ RxString KBLayoutType = ''.obs;
String getLocalPlatformForKBLayoutType(String peerPlatform) {
String localPlatform = '';
- if (peerPlatform != 'Mac OS') {
+ if (peerPlatform != kPeerPlatformMacOS) {
return localPlatform;
}
if (Platform.isWindows) {
- localPlatform = 'Windows';
+ localPlatform = kPeerPlatformWindows;
} else if (Platform.isLinux) {
- localPlatform = 'Linux';
+ localPlatform = kPeerPlatformLinux;
}
// to-do: web desktop support ?
return localPlatform;
@@ -208,7 +209,7 @@ showKBLayoutTypeChooser(
title:
Text('${translate('Select local keyboard type')} ($localPlatform)'),
content: KBLayoutTypeChooser(
- choosedType: KBLayoutType,
+ chosenType: KBLayoutType,
width: 360,
height: 200,
dividerWidth: 4.0,
@@ -217,7 +218,7 @@ showKBLayoutTypeChooser(
KBLayoutType.value = bind.getLocalKbLayoutType();
return v == KBLayoutType.value;
}),
- actions: [msgBoxButton(translate('Close'), close)],
+ actions: [dialogButton('Close', onPressed: close)],
onCancel: close,
);
});
diff --git a/flutter/lib/desktop/widgets/list_search_action_listener.dart b/flutter/lib/desktop/widgets/list_search_action_listener.dart
index 9598c3400..36128bf26 100644
--- a/flutter/lib/desktop/widgets/list_search_action_listener.dart
+++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart
@@ -55,6 +55,7 @@ class TimeoutStringBuffer {
}
ListSearchAction input(String ch) {
+ ch = ch.toLowerCase();
final curr = DateTime.now();
try {
if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) {
diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
index a371e8f52..3e85cb296 100644
--- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
+++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart
@@ -5,6 +5,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/desktop/widgets/menu_button.dart';
// Examples can assume:
// enum Commands { heroAndScholar, hurricaneCame }
@@ -790,6 +792,7 @@ class _PopupMenuRoute extends PopupRoute {
_PopupMenuRoute({
required this.position,
required this.items,
+ this.menuWrapper,
this.initialValue,
this.elevation,
required this.barrierLabel,
@@ -802,6 +805,7 @@ class _PopupMenuRoute extends PopupRoute {
final RelativeRect position;
final List> items;
+ final MenuWrapper? menuWrapper;
final List itemSizes;
final T? initialValue;
final double? elevation;
@@ -844,11 +848,14 @@ class _PopupMenuRoute extends PopupRoute {
}
}
- final Widget menu = _PopupMenu(
+ Widget menu = _PopupMenu(
route: this,
semanticLabel: semanticLabel,
constraints: constraints,
);
+ if (this.menuWrapper != null) {
+ menu = this.menuWrapper!(menu);
+ }
final MediaQueryData mediaQuery = MediaQuery.of(context);
return MediaQuery.removePadding(
context: context,
@@ -1035,6 +1042,7 @@ Future showMenu