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 aa1f5595b..bba114315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,11 +7,18 @@ name: CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master tags: - '*' + paths-ignore: + - ".github/**" + - "docs/**" + - "README.md" jobs: # ensure_cargo_fmt: @@ -24,7 +31,7 @@ jobs: # default: true # profile: minimal # components: rustfmt - # - uses: actions/checkout@v2 + # - uses: actions/checkout@v3 # - run: cargo fmt -- --check # min_version: @@ -32,7 +39,7 @@ jobs: # runs-on: ubuntu-20.04 # steps: # - name: Checkout source code - # uses: actions/checkout@v2 + # uses: actions/checkout@v3 # - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }}) # uses: actions-rs/toolchain@v1 @@ -72,13 +79,13 @@ jobs: # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - name: Checkout source code - uses: actions/checkout@v2 + 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 libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ;; + 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 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 diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml new file mode 100644 index 000000000..74e4efa99 --- /dev/null +++ b/.github/workflows/flutter-ci.yml @@ -0,0 +1,975 @@ +name: Full Flutter CI + +on: + workflow_dispatch: + pull_request: + paths-ignore: + - "docs/**" + - "README.md" + push: + branches: + - master + tags: + - '*' + paths-ignore: + - ".github/**" + - "docs/**" + - "README.md" + +env: + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.7.0" + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" + VERSION: "1.2.0" + +jobs: + build-for-windows: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: x86_64-pc-windows-gnu , os: windows-2019 } + - { target: x86_64-pc-windows-msvc, os: windows-2019 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "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 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk + run: | + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + build-vcpkg-deps-linux: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + # - { arch: armv7, os: ubuntu-20.04 } + - { arch: x86_64, os: ubuntu-20.04 } + - { arch: aarch64, os: ubuntu-20.04 } + steps: + - name: Create vcpkg artifacts folder + run: mkdir -p /opt/artifacts + + - name: Cache Vcpkg + id: cache-vcpkg + uses: actions/cache@v3 + with: + path: /opt/artifacts + key: vcpkg-${{ matrix.job.arch }} + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "/opt/artifacts" + dockerRunArgs: | + --volume "/opt/artifacts:/artifacts" + shell: /bin/bash + install: | + apt update -y + case "${{ matrix.job.arch }}" in + x86_64) + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev + ;; + aarch64|armv7) + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool + esac + cmake --version + gcc -v + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + case "${{ matrix.job.arch }}" in + x86_64) + export VCPKG_FORCE_SYSTEM_BINARIES=1 + pushd /artifacts + git clone https://github.com/microsoft/vcpkg.git || true + pushd vcpkg + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.sh + ./vcpkg install libvpx libyuv opus + ;; + aarch64|armv7) + pushd /artifacts + # libyuv + git clone https://chromium.googlesource.com/libyuv/libyuv || true + pushd libyuv + git pull + mkdir -p build + pushd build + mkdir -p /artifacts/vcpkg/installed + cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed + make -j4 && make install + popd + popd + # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC + wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz + tar -zxvf opus.tar.gz; ls -l + pushd opus-1.1.2 + ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed + make -j4; make install + ;; + esac + - name: Upload artifacts + uses: actions/upload-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: | + /opt/artifacts/vcpkg/installed + + generate-bridge-linux: + name: generate bridge + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.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-18.04, + extra-build-features: "", + } + # - { + # arch: x86_64, + # target: armv7-linux-androideabi, + # os: ubuntu-18.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 + 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-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.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 new file mode 100644 index 000000000..b08193971 --- /dev/null +++ b/.github/workflows/flutter-nightly.yml @@ -0,0 +1,1522 @@ +name: Flutter Nightly Build + +on: + schedule: + # schedule build every night + - cron: "0 0 * * *" + workflow_dispatch: + +env: + 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: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2019 } + # - { target: x86_64-pc-windows-gnu , os: windows-2019 } + - { target: x86_64-pc-windows-msvc, os: windows-2019 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + 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 + + - name: Sign rustdesk files + uses: GermanBluefox/code-sign-action@v7 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.CERTNAME }}' + folder: './flutter/build/windows/runner/Release/' + recursive: true + + - name: Build self-extracted executable + shell: bash + run: | + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe + + # - name: Rename rustdesk + # shell: bash + # run: | + # for name in rustdesk*??-install.exe; do + # mv "$name" ./SignOutput/"${name%%-install.exe}-${{ matrix.job.target }}.exe" + # done + + - name: Sign rustdesk self-extracted file + uses: GermanBluefox/code-sign-action@v7 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' + folder: './SignOutput' + recursive: false + + - name: Publish Release + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./SignOutput/rustdesk-*.exe + + build-for-macOS: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-latest, + extra-build-args: "", + } + steps: + - name: Checkout source code + 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 }} + p12-password: ${{ secrets.MACOS_P12_PASSWORD }} + keychain: rustdesk + + - name: Check sign and import sign key + if: env.MACOS_P12_BASE64 != null + run: | + security default-keychain -s rustdesk.keychain + security find-identity -v + + - name: Import notarize key + if: env.MACOS_P12_BASE64 != null + uses: timheuer/base64-to-file@v1.2 + with: + # https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_rcodesign.html#notarizing-and-stapling + fileName: rustdesk.json + fileDir: ${{ github.workspace }} + encodedString: ${{ secrets.MACOS_NOTARIZE_JSON }} + + - name: Install rcodesign tool + if: env.MACOS_P12_BASE64 != null + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx libyuv opus + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: Build rustdesk + run: | + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + - 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 --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv + create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv + # notarize the rustdesk-${{ env.VERSION }}.dmg + rcodesign notary-submit --api-key-path ${{ github.workspace }}/rustdesk.json --staple rustdesk-${{ env.VERSION }}.dmg + + - name: Rename rustdesk + run: | + for name in rustdesk*??.dmg; do + mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg" + done + + - name: Publish DMG package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk*-${{ matrix.job.target }}.dmg + + build-vcpkg-deps-linux: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + 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: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.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: false + matrix: + job: + - { + arch: x86_64, + target: aarch64-linux-android, + os: ubuntu-18.04, + extra-build-features: "", + } + # - { + # arch: x86_64, + # target: armv7-linux-androideabi, + # os: ubuntu-18.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 + 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 . + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + if: env.ANDROID_SIGNING_KEY != null + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # override default build-tools version (29.0.3) -- optional + 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 signed apk package + if: env.ANDROID_SIGNING_KEY != null + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + 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 }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + 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: false + 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: false + 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 + # edit to corresponding arch + 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 + # rpm package + echo -e "start packaging fedora package" + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.spec + sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter.spec + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.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}-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 + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + done + + - name: Publish debian package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + 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 + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-features == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/linux\/x64/linux\/arm/g" ./res/PKGBUILD + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/PKGBUILD + ;; + esac + + # Temporary disable for there is no many archlinux arm hosts + # - name: Build archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: vufa/arch-makepkg-action@master + # with: + # packages: > + # llvm + # clang + # libva + # libvdpau + # rust + # gstreamer + # unzip + # git + # cmake + # gcc + # curl + # wget + # yasm + # nasm + # zip + # make + # pkg-config + # clang + # gtk3 + # xdotool + # libxcb + # libxfixes + # alsa-lib + # pipewire + # python + # ttf-arphic-uming + # libappindicator-gtk3 + # scripts: | + # cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + # - name: Publish archlinux package + # if: ${{ matrix.job.extra-build-features == '' }} + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # res/rustdesk*.zst + + - name: Publish fedora28/centos8 package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + /opt/artifacts/rpm/*.rpm + + 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: false + 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-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.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 + # rpm package + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter.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}-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 + run: | + for name in rustdesk*??.deb; do + # use cp to duplicate deb files to fit other packages. + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + done + + - name: Publish debian package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Upload Artifact + uses: actions/upload-artifact@master + if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Patch archlinux PKGBUILD + if: ${{ matrix.job.extra-build-features == '' }} + run: | + sed -i "s/arch=('x86_64')/arch=('${{ matrix.job.arch }}')/g" res/PKGBUILD + + - name: Build archlinux package + if: ${{ matrix.job.extra-build-features == '' }} + uses: vufa/arch-makepkg-action@master + with: + packages: > + llvm + clang + libva + libvdpau + rust + gstreamer + unzip + git + cmake + gcc + curl + wget + yasm + nasm + zip + make + pkg-config + clang + gtk3 + xdotool + libxcb + libxfixes + alsa-lib + pipewire + python + ttf-arphic-uming + libappindicator-gtk3 + scripts: | + cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + - name: Publish archlinux package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + res/rustdesk*.zst + + - 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-x86_64.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: Publish fedora28/centos8 package + if: ${{ matrix.job.extra-build-features == '' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + /opt/artifacts/rpm/*.rpm + + # Temporary disable flatpak arm build + # + # build-flatpak-arm: + # name: Build Flatpak + # needs: [build-rustdesk-linux-arm] + # runs-on: ${{ matrix.job.os }} + # strategy: + # fail-fast: false + # matrix: + # job: + # # - { target: aarch64-unknown-linux-gnu , os: ubuntu-18.04, arch: arm64 } + # - { target: aarch64-unknown-linux-gnu, os: ubuntu-20.04, arch: arm64 } + # steps: + # - name: Checkout source code + # uses: actions/checkout@v3 + + # - name: Download Binary + # uses: actions/download-artifact@master + # with: + # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + # path: . + + # - name: Rename Binary + # run: | + # mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + # - uses: Kingtous/run-on-arch-action@amd64-support + # name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + # id: rpm + # with: + # arch: ${{ matrix.job.arch }} + # distro: ubuntu18.04 + # githubToken: ${{ github.token }} + # setup: | + # ls -l "${PWD}" + # dockerRunArgs: | + # --volume "${PWD}:/workspace" + # shell: /bin/bash + # install: | + # apt update -y + # apt install -y rpm + # run: | + # pushd /workspace + # # install + # apt update -y + # apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # # flatpak deps + # flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + # flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + # flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # # package + # pushd flatpak + # git clone https://github.com/flathub/shared-modules.git --depth=1 + # flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + # flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + # - name: Publish flatpak package + # uses: softprops/action-gh-release@v1 + # with: + # prerelease: true + # tag_name: ${{ env.TAG_NAME }} + # files: | + # flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak + + build-flatpak-amd64: + name: Build Flatpak + needs: [build-rustdesk-linux-amd64] + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + job: + - { target: x86_64-unknown-linux-gnu, os: ubuntu-18.04, arch: x86_64 } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Download Binary + uses: actions/download-artifact@master + with: + name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + path: . + + - name: Rename Binary + run: | + mv rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb rustdesk-${{ env.VERSION }}.deb + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + id: rpm + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + shell: /bin/bash + install: | + apt update -y + apt install -y rpm git wget curl + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # install + apt update -y + apt install -y flatpak flatpak-builder cmake g++ gcc git curl wget nasm yasm libgtk-3-dev git + # flatpak deps + flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo + flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/21.08 + flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/21.08 + # package + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak org.rustdesk.rustdesk + + - name: Publish flatpak package + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 82243264c..cdc12cdd8 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -6,7 +6,8 @@ jobs: publish: runs-on: windows-latest # action can only be run on windows steps: - - uses: vedantmgoyal2009/winget-releaser@latest + - uses: vedantmgoyal2009/winget-releaser@v1 with: identifier: RustDesk.RustDesk + version: ${{ github.event.release.tag_name }} token: ${{ secrets.WINGET_TOKEN }} diff --git a/.gitignore b/.gitignore index 53bd9cf94..a71c71a4e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/build /target .vscode .idea @@ -18,9 +19,29 @@ cert.pfx sciter.dll **pdb src/bridge_generated.rs +src/bridge_generated.io.rs *deb rustdesk +*.cache # appimage appimage/AppDir appimage/*.AppImage -appimage/appimage-build \ No newline at end of file +appimage/appimage-build +# flutter +flutter/linux/build/** +flutter/linux/cmake-build-debug/** +# flatpak +flatpak/.flatpak-builder/** +flatpak/ccache/** +flatpak/.flatpak-builder/build/** +flatpak/.flatpak-builder/shared-modules/** +flatpak/.flatpak-builder/shared-modules/*.tar.xz +flatpak/.flatpak-builder/debian-binary +flatpak/build/** +# bridge file +lib/generated_bridge.dart +# vscode devcontainer +.gitconfig +.vscode-server/ +.ssh +.devcontainer/.* diff --git a/128x128.png b/128x128.png deleted file mode 100644 index 045d8f894..000000000 Binary files a/128x128.png and /dev/null differ diff --git a/128x128@2x.png b/128x128@2x.png deleted file mode 100644 index 39e2b23cf..000000000 Binary files a/128x128@2x.png and /dev/null differ diff --git a/32x32.png b/32x32.png deleted file mode 100644 index bba85feb6..000000000 Binary files a/32x32.png and /dev/null differ diff --git a/Cargo.lock b/Cargo.lock index 2d9ac2cac..a2cdf91a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,20 +36,37 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] [[package]] name = "allo-isolate" -version = "0.1.13-beta.5" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1a52c9b965fdaf940102bcb1a0aef4bc2f56489056f5872cef705651c7972e" +checksum = "8ed55848be9f41d44c79df6045b680a74a78bc579e0813f7f196cd7928e22fb1" dependencies = [ + "anyhow", "atomic", + "chrono", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", ] [[package]] @@ -61,7 +78,7 @@ dependencies = [ "alsa-sys", "bitflags", "libc", - "nix 0.23.1", + "nix 0.23.2", ] [[package]] @@ -94,16 +111,25 @@ dependencies = [ [[package]] name = "android_logger" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b74b7ddf197de32e415d197aa21c1c0cb36e01e4794fd801302280ac7847ee02" +checksum = "b5e9dd62f37dea550caf48c77591dc50bd1a378ce08855be1a0c42a97b7550fb" dependencies = [ "android_log-sys", - "env_logger 0.9.0", + "env_logger 0.9.3", "log", "once_cell", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -115,9 +141,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.58" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "arboard" @@ -127,7 +153,7 @@ checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb" dependencies = [ "clipboard-win", "core-graphics 0.22.3", - "image", + "image 0.23.14", "log", "objc", "objc-foundation", @@ -139,10 +165,21 @@ dependencies = [ ] [[package]] -name = "async-channel" -version = "1.6.1" +name = "async-broadcast" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot 0.12.1", +] + +[[package]] +name = "async-channel" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ "concurrent-queue", "event-listener", @@ -150,39 +187,76 @@ dependencies = [ ] [[package]] -name = "async-io" -version = "1.7.0" +name = "async-executor" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg 1.1.0", "concurrent-queue", "futures-lite", "libc", "log", - "once_cell", "parking", "polling", "slab", - "socket2 0.4.4", + "socket2 0.4.7", "waker-fn", - "winapi 0.3.9", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", ] [[package]] name = "async-process" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2c06e30a24e8c78a3987d07f0930edf76ef35e027e7bdb063fccafdad1f60c" +checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" dependencies = [ "async-io", + "async-lock", + "autocfg 1.1.0", "blocking", "cfg-if 1.0.0", "event-listener", "futures-lite", "libc", - "once_cell", "signal-hook", - "winapi 0.3.9", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-recursion" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -193,37 +267,37 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +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.2", + "system-deps 6.0.3", ] [[package]] @@ -277,16 +351,16 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide 0.5.3", + "miniz_oxide 0.5.4", "object", "rustc-demangle", ] [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bindgen" @@ -298,19 +372,45 @@ dependencies = [ "cexpr", "clang-sys", "clap 2.34.0", - "env_logger 0.9.0", + "env_logger 0.9.3", "lazy_static", "lazycell", "log", "peeking_take_while", - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "regex", "rustc-hash", "shlex", - "which 4.2.5", + "which 4.3.0", ] +[[package]] +name = "bindgen" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a022e58a142a46fea340d68012b9201c094e93ec3d033a944a24f8fd4a4f09a" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2 1.0.47", + "quote 1.0.21", + "regex", + "rustc-hash", + "shlex", + "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" @@ -337,38 +437,65 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] [[package]] name = "blocking" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" dependencies = [ "async-channel", + "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", - "once_cell", ] [[package]] -name = "bumpalo" -version = "3.10.0" +name = "brotli" +version = "3.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "bytemuck" -version = "1.10.0" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53dfa917ec274df8ed3c572698f381a24eef2efba9492d797301b72b6db408a" +checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" [[package]] name = "byteorder" @@ -378,41 +505,36 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" dependencies = [ - "serde 1.0.139", + "serde 1.0.149", ] -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" - [[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.2", + "system-deps 6.0.3", ] [[package]] @@ -427,11 +549,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.0.9" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412" +checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" dependencies = [ - "serde 1.0.139", + "serde 1.0.149", ] [[package]] @@ -440,7 +562,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.139", + "serde 1.0.149", ] [[package]] @@ -451,35 +573,35 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver 1.0.12", - "serde 1.0.139", - "serde_json 1.0.82", + "semver 1.0.14", + "serde 1.0.149", + "serde_json 1.0.89", ] [[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.12", + "clap 3.2.23", "heck 0.4.0", "indexmap", "log", - "proc-macro2", - "quote", - "serde 1.0.139", - "serde_json 1.0.82", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "serde 1.0.149", + "serde_json 1.0.89", + "syn 1.0.105", "tempfile", "toml", ] [[package]] name = "cc" -version = "1.0.73" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" dependencies = [ "jobserver", ] @@ -501,9 +623,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.10.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aacacf4d96c24b2ad6eb8ee6df040e4f27b0d0b39a5710c30091baa830485db" +checksum = "b0357a6402b295ca3a86bc148e84df46c02e41f41fef186bda662557ef6328aa" dependencies = [ "smallvec", ] @@ -522,21 +644,37 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ - "libc", + "iana-time-zone", + "js-sys", "num-integer", "num-traits 0.2.15", + "time 0.1.45", + "wasm-bindgen", "winapi 0.3.9", ] [[package]] -name = "clang-sys" -version = "1.3.3" +name = "cidr-utils" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" +checksum = "355d5b5df67e58b523953d0c1a8d3d2c05f5af51f1332b0199b9c92263614ed0" +dependencies = [ + "debug-helper", + "num-bigint", + "num-traits 0.2.15", + "once_cell", + "regex", +] + +[[package]] +name = "clang-sys" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" dependencies = [ "glob", "libc", @@ -560,9 +698,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.12" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8b79fe3946ceb4a0b1c080b4018992b8d27e9ff363644c1c9b6387c854614d" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", @@ -572,20 +710,20 @@ dependencies = [ "once_cell", "strsim 0.10.0", "termcolor", - "textwrap 0.15.0", + "textwrap 0.16.0", ] [[package]] name = "clap_derive" -version = "3.2.7" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" +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]] @@ -604,7 +742,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.139", + "serde 1.0.149", "serde_derive", "thiserror", ] @@ -631,33 +769,18 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.48" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a" +checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c" dependencies = [ "cc", ] [[package]] name = "cocoa" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667fdc068627a2816b9ff831201dd9864249d6ee8d190b9532357f1fc0f61ea7" -dependencies = [ - "bitflags", - "block", - "core-foundation 0.9.3", - "core-graphics 0.21.0", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" dependencies = [ "bitflags", "block", @@ -684,6 +807,16 @@ dependencies = [ "objc", ] +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -692,9 +825,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "combine" -version = "4.6.4" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ "bytes", "memchr", @@ -702,11 +835,11 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "1.2.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83827793632c72fa4f73c2edb31e7a997527dd8ffe7077344621fc62c5478157" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" dependencies = [ - "cache-padded", + "crossbeam-utils", ] [[package]] @@ -715,11 +848,21 @@ version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.139", + "serde 1.0.149", "thiserror", "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" @@ -770,18 +913,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-graphics" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a67c4378cf203eace8fb6567847eb641fd6ff933c1145a115c6ee820ebb978" -dependencies = [ - "bitflags", - "core-foundation 0.9.3", - "foreign-types", - "libc", -] - [[package]] name = "core-graphics" version = "0.22.3" @@ -832,11 +963,11 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dff444d80630d7073077d38d40b4501fd518bd2b922c2a55edcc8b0f7be57e6" +checksum = "1a9444b94b8024feecc29e01a9706c69c1e26bfee480221c90764200cfd778fb" dependencies = [ - "bindgen", + "bindgen 0.61.0", ] [[package]] @@ -848,14 +979,14 @@ dependencies = [ "alsa", "core-foundation-sys 0.8.3", "coreaudio-rs", - "jni", + "jni 0.19.0", "js-sys", "lazy_static", "libc", "mach", "ndk 0.6.0", "ndk-glue 0.6.2", - "nix 0.23.1", + "nix 0.23.2", "oboe", "parking_lot 0.11.2", "stdweb", @@ -866,9 +997,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -884,9 +1015,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -894,9 +1025,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", @@ -905,23 +1036,22 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.9" +version = "0.9.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", "crossbeam-utils", - "memoffset", - "once_cell", + "memoffset 0.7.1", "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -929,14 +1059,19 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if 1.0.0", - "once_cell", ] +[[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" @@ -947,24 +1082,14 @@ dependencies = [ "typenum", ] -[[package]] -name = "cstr_core" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644828c273c063ab0d39486ba42a5d1f3a499d35529c759e763a9c6cb8a0fb08" -dependencies = [ - "cty", - "memchr", -] - [[package]] name = "ctrlc" -version = "3.2.2" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b37feaa84e6861e00a1f5e5aa8da3ee56d605c9992d33e082786754828e20865" +checksum = "1631ca6e3c59112501a9d87fd86f21591ff77acd31331e8a73f8d80a65bbdd71" dependencies = [ - "nix 0.24.2", - "winapi 0.3.9", + "nix 0.26.1", + "windows-sys 0.42.0", ] [[package]] @@ -973,14 +1098,99 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "cxx" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2 1.0.47", + "quote 1.0.21", + "scratch", + "syn 1.0.105", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" +dependencies = [ + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", +] + +[[package]] +name = "dark-light" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62007a65515b3cd88c733dd3464431f05d2ad066999a824259d8edc3cf6f645" +dependencies = [ + "dconf_rs", + "detect-desktop-environment", + "dirs 4.0.0", + "objc", + "rust-ini", + "web-sys", + "winreg 0.10.1", + "zbus", + "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]] @@ -991,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]] @@ -1003,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]] @@ -1139,15 +1360,36 @@ dependencies = [ ] [[package]] -name = "default-net" -version = "0.11.0" +name = "dbus-crossroads" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e70d471b0ba4e722c85651b3bb04b6880dfdb1224a43ade80c1295314db646" +checksum = "554114296d012b33fdaf362a733db6dc5f73c4c9348b8b620ddd42e61b406e30" +dependencies = [ + "dbus", +] + +[[package]] +name = "dconf_rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7046468a81e6a002061c01e6a7c83139daf91b11c30e66795b13217c2d885c8b" + +[[package]] +name = "debug-helper" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" + +[[package]] +name = "default-net" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e349ed1e06fb344a7dd8b5a676375cf671b31e8900075dd2be816efc063a63" dependencies = [ "libc", "memalloc", "system-configuration", - "windows", + "windows 0.30.0", ] [[package]] @@ -1161,10 +1403,50 @@ dependencies = [ ] [[package]] -name = "digest" -version = "0.10.3" +name = "delegate" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +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 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]] +name = "detect-desktop-environment" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8ad60dd5b13a4ee6bd8fa2d5d88965c597c67bce32b5fc49c94f55cb50810" + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -1180,6 +1462,25 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1190,6 +1491,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1216,6 +1528,35 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + [[package]] name = "docopt" version = "1.1.1" @@ -1224,7 +1565,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.139", + "serde 1.0.149", "strsim 0.10.0", ] @@ -1240,6 +1581,18 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "dylib_virtual_display" +version = "0.1.0" +dependencies = [ + "cc", + "hbb_common", + "lazy_static", + "serde 1.0.149", + "serde_derive", + "thiserror", +] + [[package]] name = "ed25519" version = "1.5.2" @@ -1251,9 +1604,22 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "embed-resource" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e62abb876c07e4754fae5c14cafa77937841f01740637e17d78dc04352f32a5e" +dependencies = [ + "cc", + "rustc_version 0.4.0", + "toml", + "vswhom", + "winreg 0.10.1", +] [[package]] name = "encoding_rs" @@ -1270,16 +1636,37 @@ version = "0.0.14" dependencies = [ "core-graphics 0.22.3", "hbb_common", - "libc", "log", "objc", "pkg-config", - "serde 1.0.139", + "rdev", + "serde 1.0.149", "serde_derive", + "tfc", "unicode-segmentation", "winapi 0.3.9", ] +[[package]] +name = "enum-map" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a56d54c8dd9b3ad34752ed197a4eb2a6601bc010808eb097a04a58ae4c43e1" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27" +dependencies = [ + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", +] + [[package]] name = "enum_dispatch" version = "0.3.8" @@ -1287,9 +1674,30 @@ 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]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde 1.0.149", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -1304,9 +1712,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" dependencies = [ "atty", "humantime", @@ -1315,6 +1723,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "epoll" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20df693c700404f7e19d4d6fae6b15215d2913c27955d2b9d6f2c0f537511cd0" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "err-derive" version = "0.3.1" @@ -1322,13 +1740,34 @@ 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", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "error-code" version = "2.3.1" @@ -1346,14 +1785,42 @@ source = "git+https://github.com/fufesou/evdev#cec616e37790293d2cd2aa54a96601ed6 dependencies = [ "bitvec", "libc", - "nix 0.23.1", + "nix 0.23.2", ] [[package]] name = "event-listener" -version = "2.5.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +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" @@ -1366,9 +1833,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] @@ -1379,37 +1846,37 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" dependencies = [ - "memoffset", - "rustc_version", + "memoffset 0.6.5", + "rustc_version 0.3.3", ] [[package]] name = "filetime" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "miniz_oxide 0.5.3", + "miniz_oxide 0.6.2", ] [[package]] name = "flexi_logger" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee9a6796ff68a1014f6665dac55341820f26e63ec706e58bfaee468cf0ac174f" +checksum = "0c76a80dd14a27fc3d8bc696502132cb52b3f227256fd8601166c3a35e45f409" dependencies = [ "ansi_term", "atty", @@ -1422,54 +1889,81 @@ dependencies = [ "regex", "rustversion", "thiserror", - "time", + "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.30.0" +version = "1.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e7e4af55d6a36aad9573737a12fba774999e4d6dd5e668e29c25bb473f85f3" +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.30.0" +version = "1.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3209735fd687b06b8d770ec008874119b91f7f46b4a73d17226d5c337435bb74" +checksum = "efd7396bc479eae8aa24243e4c0e3d3dbda1909134f8de6bde4f080d262c9a0d" dependencies = [ "anyhow", "cargo_metadata", "cbindgen", + "clap 3.2.23", "convert_case", + "delegate", "enum_dispatch", - "env_logger 0.9.0", + "env_logger 0.9.3", + "extend", + "itertools 0.10.5", "lazy_static", "log", "pathdiff", - "quote", + "quote 1.0.21", "regex", - "serde 1.0.139", + "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.38.0" +version = "1.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13652b9b71bc3bf4ea3bbb5cadc9bc2350fe0fba5145f6a949309fc452576d6d" +checksum = "8d5cd827645690ef378be57a890d0581e17c28d07b712872af7d744f454fd27d" [[package]] name = "fnv" @@ -1494,14 +1988,26 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "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" @@ -1532,9 +2038,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" dependencies = [ "futures-channel", "futures-core", @@ -1547,9 +2053,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", "futures-sink", @@ -1557,15 +2063,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" dependencies = [ "futures-core", "futures-task", @@ -1574,9 +2080,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] name = "futures-lite" @@ -1595,32 +2101,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-channel", "futures-core", @@ -1645,68 +2151,95 @@ 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", - "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.2", + "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", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", "pkg-config", - "system-deps 6.0.2", + "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.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -1724,13 +2257,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", - "wasi", + "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]] @@ -1741,31 +2286,34 @@ 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", + "futures-util", "gio-sys", - "glib 0.15.12", + "glib 0.16.5", "libc", "once_cell", + "pin-project-lite", + "smallvec", "thiserror", ] [[package]] name = "gio-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" 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.2", + "system-deps 6.0.3", "winapi 0.3.9", ] @@ -1790,18 +2338,20 @@ dependencies = [ [[package]] name = "glib" -version = "0.15.12" +version = "0.16.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +checksum = "0cd04d150a2c63e6779f43aec7e04f5374252479b7bed5f45146d9c0e821f161" 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", + "futures-util", + "gio-sys", + "glib-macros 0.16.3", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "once_cell", "smallvec", @@ -1816,27 +2366,27 @@ 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", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] name = "glib-macros" -version = "0.15.11" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" +checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate 1.1.3", + "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]] @@ -1851,12 +2401,12 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" dependencies = [ "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -1878,13 +2428,13 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" dependencies = [ - "glib-sys 0.15.10", + "glib-sys 0.16.3", "libc", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[package]] @@ -1904,7 +2454,7 @@ dependencies = [ "gstreamer-sys", "libc", "muldiv", - "num-rational", + "num-rational 0.3.2", "once_cell", "paste", "pretty-hex", @@ -2022,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", @@ -2034,7 +2584,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gio", - "glib 0.15.12", + "glib 0.16.5", "gtk-sys", "gtk3-macros", "libc", @@ -2045,41 +2595,41 @@ 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", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", - "system-deps 6.0.2", + "system-deps 6.0.3", ] [[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.1.3", + "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]] name = "h2" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ "bytes", "fnv", @@ -2094,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" @@ -2108,29 +2667,33 @@ name = "hbb_common" version = "0.1.0" dependencies = [ "anyhow", + "backtrace", "bytes", + "chrono", "confy", "directories-next", "dirs-next", - "env_logger 0.9.0", + "env_logger 0.9.3", "filetime", "futures", "futures-util", "lazy_static", + "libc", "log", "mac_address", "machine-uid", + "osascript", "protobuf", "protobuf-codegen", "quinn", "rand 0.8.5", "regex", - "serde 1.0.139", + "serde 1.0.149", "serde_derive", - "serde_json 1.0.82", - "serde_with", + "serde_json 1.0.89", "socket2 0.3.19", "sodiumoxide", + "sysinfo", "tokio", "tokio-socks", "tokio-util", @@ -2164,10 +2727,16 @@ dependencies = [ ] [[package]] -name = "hound" -version = "3.4.0" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hound" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" [[package]] name = "http" @@ -2177,7 +2746,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.2", + "itoa 1.0.4", ] [[package]] @@ -2193,9 +2762,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -2212,21 +2781,21 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#91d1cd327c88490f917457072aeef0676ddb2be7" +source = "git+https://github.com/21pages/hwcodec#64f885b3787694b16dfcff08256750b0376b2eba" dependencies = [ - "bindgen", + "bindgen 0.59.2", "cc", "log", - "serde 1.0.139", + "serde 1.0.149", "serde_derive", - "serde_json 1.0.82", + "serde_json 1.0.89", ] [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ "bytes", "futures-channel", @@ -2237,9 +2806,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.2", + "itoa 1.0.4", "pin-project-lite", - "socket2 0.4.4", + "socket2 0.4.7", "tokio", "tower-service", "tracing", @@ -2248,9 +2817,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", @@ -2259,6 +2828,30 @@ dependencies = [ "tokio-rustls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys 0.8.3", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi 0.3.9", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -2267,11 +2860,10 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -2286,41 +2878,88 @@ 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]] +name = "impersonate_system" +version = "0.1.0" +source = "git+https://github.com/21pages/impersonate-system#84b401893d5b6628c8b33b295328d13fbbe2674b" +dependencies = [ + "cc", ] [[package]] name = "include_dir" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "482a2e29200b7eed25d7fdbd14423326760b7f6658d21a4cf12d55a50713c69f" +checksum = "18762faeff7122e89e0857b02f7ce6fcc0d101d5e9ad2ad7846cc01d61b7f19e" dependencies = [ "include_dir_macros", ] [[package]] name = "include_dir_macros" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e074c19deab2501407c91ba1860fa3d6820bfde307db6d8cb851b55a10be89b" +checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", ] [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg 1.1.0", "hashbrown", ] +[[package]] +name = "inotify" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf888f9575c290197b2c948dc9e9ff10bd1a39ad1ea8585f734585fa6b9d3f9" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "instant" version = "0.1.12" @@ -2344,9 +2983,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" [[package]] name = "itertools" @@ -2357,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" @@ -2365,9 +3013,9 @@ checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "jni" @@ -2383,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" @@ -2391,9 +3053,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] @@ -2405,10 +3067,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] -name = "js-sys" -version = "0.3.58" +name = "jpeg-decoder" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -2423,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" @@ -2436,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", @@ -2450,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", @@ -2461,9 +3149,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.126" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libdbus-sys" @@ -2476,9 +3164,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if 1.0.0", "winapi 0.3.9", @@ -2553,6 +3241,34 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +dependencies = [ + "cc", +] + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2561,9 +3277,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -2580,11 +3296,11 @@ dependencies = [ [[package]] name = "mac_address" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d1bc1084549d60725ccc53a2bfa07f67fe4689fda07b05a36531f2988104a" +checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" dependencies = [ - "nix 0.23.1", + "nix 0.23.2", "winapi 0.3.9", ] @@ -2609,9 +3325,9 @@ dependencies = [ [[package]] name = "magnum-opus" version = "0.4.0" -source = "git+https://github.com/open-trade/magnum-opus#3c3d0b86ae95c84930bebffe4bcb03b3bd83342b" +source = "git+https://github.com/rustdesk/magnum-opus#79be072c939168e907fe851690759dcfd6a326af" dependencies = [ - "bindgen", + "bindgen 0.59.2", "target_build_utils", ] @@ -2625,10 +3341,10 @@ dependencies = [ ] [[package]] -name = "matches" -version = "0.1.9" +name = "md5" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memalloc" @@ -2660,6 +3376,15 @@ dependencies = [ "autocfg 1.1.0", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "mime" version = "0.3.16" @@ -2693,9 +3418,18 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] @@ -2721,14 +3455,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", - "wasi", - "windows-sys 0.36.1", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.42.0", ] [[package]] @@ -2776,10 +3510,29 @@ dependencies = [ [[package]] name = "mouce" version = "0.2.1" -source = "git+https://github.com/fufesou/mouce.git#26da8d4b0009b7f96996799c2a5c0990a8dbf08b" +source = "git+https://github.com/fufesou/mouce.git#ed83800d532b95d70e39915314f6052aa433e9b9" dependencies = [ "glob", - "libc", +] + +[[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]] @@ -2788,6 +3541,15 @@ 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" @@ -2814,6 +3576,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys 0.4.1+23.1.7779620", + "num_enum", + "raw-window-handle 0.5.0", + "thiserror", +] + [[package]] name = "ndk-context" version = "0.1.1" @@ -2856,11 +3632,11 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ - "darling", - "proc-macro-crate 1.1.3", - "proc-macro2", - "quote", - "syn", + "darling 0.13.4", + "proc-macro-crate 1.2.1", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -2879,10 +3655,19 @@ dependencies = [ ] [[package]] -name = "net2" -version = "0.2.37" +name = "ndk-sys" +version = "0.4.1+23.1.7779620" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "net2" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d0df99cfcd2530b2e694f6e17e7f37b8e26bb23983ac530c0c97408837c631" dependencies = [ "cfg-if 0.1.10", "libc", @@ -2899,31 +3684,58 @@ dependencies = [ "cc", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] name = "nix" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ "bitflags", "cc", "cfg-if 1.0.0", "libc", - "memoffset", + "memoffset 0.6.5", ] [[package]] name = "nix" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags", "cfg-if 1.0.0", "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg 1.1.0", + "bitflags", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", + "pin-utils", +] + +[[package]] +name = "nix" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "libc", + "static_assertions", ] [[package]] @@ -2945,6 +3757,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits 0.2.15", +] + [[package]] name = "num-complex" version = "0.4.2" @@ -2960,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]] @@ -2997,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" @@ -3017,9 +3851,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ "hermit-abi", "libc", @@ -3040,10 +3874,10 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate 1.1.3", - "proc-macro2", - "quote", - "syn", + "proc-macro-crate 1.2.1", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -3062,6 +3896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -3075,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" @@ -3099,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", @@ -3118,9 +3962,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "openssl-probe" @@ -3129,25 +3973,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] -name = "os_str_bytes" -version = "6.2.0" +name = "ordered-multimap" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown", +] [[package]] -name = "padlock" -version = "0.2.0" +name = "ordered-stream" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f" +checksum = "01ca8c99d73c6e92ac1358f9f692c22c0bfd9c4701fa086f5d365c0d4ea818ea" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "osascript" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", @@ -3155,14 +4025,14 @@ 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.2", + "system-deps 6.0.3", ] [[package]] @@ -3204,7 +4074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core 0.9.5", ] [[package]] @@ -3223,22 +4093,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] name = "paste" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" [[package]] name = "pathdiff" @@ -3254,16 +4124,17 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.1.3" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" dependencies = [ + "thiserror", "ucd-trie", ] @@ -3307,22 +4178,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -3339,9 +4210,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "png" @@ -3356,23 +4227,36 @@ dependencies = [ ] [[package]] -name = "polling" -version = "2.2.0" +name = "png" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" dependencies = [ + "bitflags", + "crc32fast", + "flate2", + "miniz_oxide 0.6.2", +] + +[[package]] +name = "polling" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" +dependencies = [ + "autocfg 1.1.0", "cfg-if 1.0.0", "libc", "log", "wepoll-ffi", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty-hex" @@ -3382,9 +4266,9 @@ checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "primal-check" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01419cee72c1a1ca944554e23d83e483e1bccf378753344e881de28b5487511d" +checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0" dependencies = [ "num-integer", ] @@ -3400,10 +4284,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ + "once_cell", "thiserror", "toml", ] @@ -3415,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", ] @@ -3427,25 +4312,34 @@ 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 = "1.0.40" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] [[package]] name = "protobuf" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee4a7d8b91800c8f167a6268d1a1026607368e1adc84e98fe044aeb905302f7" +checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" dependencies = [ "bytes", "once_cell", @@ -3455,9 +4349,9 @@ dependencies = [ [[package]] name = "protobuf-codegen" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b893e5e7d3395545d5244f8c0d33674025bd566b26c03bfda49b82c6dec45e" +checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" dependencies = [ "anyhow", "once_cell", @@ -3470,9 +4364,9 @@ dependencies = [ [[package]] name = "protobuf-parse" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1447dd751c434cc1b415579837ebd0411ed7d67d465f38010da5d7cd33af4d" +checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" dependencies = [ "anyhow", "indexmap", @@ -3481,14 +4375,14 @@ dependencies = [ "protobuf-support", "tempfile", "thiserror", - "which 4.2.5", + "which 4.3.0", ] [[package]] name = "protobuf-support" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca157fe12fc7ee2e315f2f735e27df41b3d97cdd70ea112824dac1ffb08ee1c" +checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" dependencies = [ "thiserror", ] @@ -3508,9 +4402,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7542006acd6e057ff632307d219954c44048f818898da03113d6c0086bfddd9" +checksum = "5b435e71d9bfa0d8889927231970c51fb89c58fa63bffcab117c9c7a41e5ef8f" dependencies = [ "bytes", "futures-channel", @@ -3527,9 +4421,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a13a5c0a674c1ce7150c9df7bc4a1e46c2fbbe7c710f56c0dc78b1a810e779e" +checksum = "3fce546b9688f767a57530652488420d419a8b1f44a478b451c3d1ab6d992a55" dependencies = [ "bytes", "fxhash", @@ -3547,25 +4441,34 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3149f7237331015f1a6adf065c397d1be71e032fcf110ba41da52e7926b882f" +checksum = "b07946277141531aea269befd949ed16b2c85a780ba1043244eda0969e538e54" dependencies = [ "futures-util", "libc", "quinn-proto", - "socket2 0.4.4", + "socket2 0.4.7", "tokio", "tracing", ] [[package]] name = "quote" -version = "1.0.20" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" dependencies = [ - "proc-macro2", + "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 1.0.47", ] [[package]] @@ -3601,7 +4504,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -3621,7 +4524,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -3641,9 +4544,9 @@ checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] @@ -3720,22 +4623,29 @@ dependencies = [ ] [[package]] -name = "rayon" -version = "1.5.3" +name = "raw-window-handle" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" +dependencies = [ + "cty", +] + +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" dependencies = [ - "autocfg 1.1.0", - "crossbeam-deque", "either", "rayon-core", ] [[package]] name = "rayon-core" -version = "1.9.3" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -3745,17 +4655,26 @@ dependencies = [ [[package]] name = "rdev" -version = "0.5.0-1" -source = "git+https://github.com/open-trade/rdev#a9b6ea462956f289b4a48e81f2ea7dda33cd8047" +version = "0.5.0-2" +source = "git+https://github.com/fufesou/rdev#5b9fb5e42117f44e0ce0fe7cf2bddf270c75f1dc" dependencies = [ - "cocoa 0.22.0", - "core-foundation 0.7.0", - "core-foundation-sys 0.7.0", - "core-graphics 0.19.2", + "cocoa", + "core-foundation 0.9.3", + "core-foundation-sys 0.8.3", + "core-graphics 0.22.3", + "dispatch", + "enum-map", + "epoll", + "inotify", "lazy_static", "libc", + "log", + "mio 0.8.5", + "strum 0.24.1", + "strum_macros 0.24.3", + "widestring 1.0.2", "winapi 0.3.9", - "x11", + "x11 2.20.1", ] [[package]] @@ -3769,18 +4688,18 @@ dependencies = [ [[package]] name = "realfft" -version = "3.0.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a83b876fe55da7e1bf5deeacb93d6411edf81eba0e1a497e79c067734729053a" +checksum = "93d6b8e8f0c6d2234aa58048d7290c60bf92cd36fd2888cd8331c66ad4f2e1d2" dependencies = [ "rustfft", ] [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -3798,9 +4717,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" dependencies = [ "aho-corasick", "memchr", @@ -3809,9 +4728,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -3834,9 +4753,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ "base64", "bytes", @@ -3850,15 +4769,15 @@ dependencies = [ "hyper-rustls", "ipnet", "js-sys", - "lazy_static", "log", "mime", + "once_cell", "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile 1.0.0", - "serde 1.0.139", - "serde_json 1.0.82", + "rustls-pemfile 1.0.1", + "serde 1.0.149", + "serde_json 1.0.89", "serde_urlencoded", "tokio", "tokio-rustls", @@ -3880,7 +4799,7 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", + "spin 0.5.2", "untrusted", "web-sys", "winapi 0.3.9", @@ -3899,9 +4818,20 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.0.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b763cb66df1c928432cc35053f8bd4cec3335d8559fc16010017d16b3c1680" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi 0.3.9", +] + +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" dependencies = [ "libc", "winapi 0.3.9", @@ -3929,6 +4859,16 @@ dependencies = [ "which 3.1.1", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if 1.0.0", + "ordered-multimap", +] + [[package]] name = "rust-pulsectl" version = "0.2.12" @@ -3959,10 +4899,19 @@ dependencies = [ ] [[package]] -name = "rustdesk" -version = "1.1.10" +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "android_logger 0.11.0", + "semver 1.0.14", +] + +[[package]] +name = "rustdesk" +version = "1.2.0" +dependencies = [ + "android_logger 0.11.1", "arboard", "async-process", "async-trait", @@ -3970,27 +4919,36 @@ dependencies = [ "bytes", "cc", "cfg-if 1.0.0", - "clap 3.2.12", + "chrono", + "cidr-utils", + "clap 3.2.23", "clipboard", - "cocoa 0.24.0", + "cocoa", "core-foundation 0.9.3", "core-graphics 0.22.3", "cpal", "ctrlc", + "dark-light", "dasp", + "dbus", + "dbus-crossroads", "default-net", "dispatch", + "dlopen", "enigo", + "errno", "evdev", "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", + "fruitbasket", "hbb_common", "hound", + "image 0.24.5", + "impersonate_system", "include_dir", - "jni", + "jni 0.19.0", "lazy_static", - "libc", "libpulse-binding", "libpulse-simple-binding", "mac_address", @@ -3999,27 +4957,31 @@ dependencies = [ "mouce", "num_cpus", "objc", + "objc_id", "parity-tokio-ipc", "rdev", "repng", "reqwest", - "rpassword 7.0.0", + "rpassword 7.2.0", "rubato", "runas", "rust-pulsectl", "samplerate", "sciter-rs", "scrap", - "serde 1.0.139", + "serde 1.0.149", "serde_derive", - "serde_json 1.0.82", + "serde_json 1.0.89", "sha2", + "shared_memory", + "shutdown_hooks", "simple_rc", "sys-locale", - "sysinfo", "system_shutdown", - "tray-item", + "tao", + "tray-icon", "trayicon", + "url", "uuid", "virtual_display", "whoami", @@ -4029,13 +4991,24 @@ dependencies = [ "winreg 0.10.1", "winres", "wol-rs", + "xrandr-parser", +] + +[[package]] +name = "rustdesk-portable-packer" +version = "0.1.0" +dependencies = [ + "brotli", + "dirs 4.0.0", + "embed-resource", + "md5", ] [[package]] name = "rustfft" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d089e5c57521629a59f5f39bca7434849ff89bd6873b521afe389c1c602543" +checksum = "e17d4f6cbdb180c9f4b2a26bbf01c4e647f1e1dea22fe8eb9db54198b32f9434" dependencies = [ "num-complex", "num-integer", @@ -4043,13 +5016,14 @@ dependencies = [ "primal-check", "strength_reduce", "transpose", + "version_check", ] [[package]] name = "rustls" -version = "0.20.6" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" dependencies = [ "log", "ring", @@ -4064,7 +5038,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.0", + "rustls-pemfile 1.0.1", "schannel", "security-framework", ] @@ -4080,24 +5054,24 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ "base64", ] [[package]] name = "rustversion" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "same-file" @@ -4140,9 +5114,15 @@ dependencies = [ [[package]] name = "scoped-tls" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" +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" @@ -4155,7 +5135,7 @@ name = "scrap" version = "0.5.0" dependencies = [ "android_logger 0.10.1", - "bindgen", + "bindgen 0.59.2", "block", "cfg-if 1.0.0", "dbus", @@ -4165,21 +5145,27 @@ dependencies = [ "gstreamer-video", "hbb_common", "hwcodec", - "jni", + "jni 0.19.0", "lazy_static", - "libc", "log", + "ndk 0.7.0", "num_cpus", "quest", "repng", - "serde 1.0.139", - "serde_json 1.0.82", + "serde 1.0.149", + "serde_json 1.0.89", "target_build_utils", "tracing", "webm", "winapi 0.3.9", ] +[[package]] +name = "scratch" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" + [[package]] name = "sct" version = "0.7.0" @@ -4192,9 +5178,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags", "core-foundation 0.9.3", @@ -4224,11 +5210,11 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" dependencies = [ - "serde 1.0.139", + "serde 1.0.149", ] [[package]] @@ -4248,22 +5234,22 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.139" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" +checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.139" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -4280,13 +5266,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.4", "ryu", - "serde 1.0.139", + "serde 1.0.149", +] + +[[package]] +name = "serde_repr" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +dependencies = [ + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -4296,31 +5293,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.2", + "itoa 1.0.4", "ryu", - "serde 1.0.139", -] - -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde 1.0.139", - "serde_with_macros", -] - -[[package]] -name = "serde_with_macros" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", + "serde 1.0.149", ] [[package]] @@ -4331,27 +5306,57 @@ checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", - "serde 1.0.139", + "serde 1.0.149", "yaml-rust", ] [[package]] -name = "sha2" -version = "0.10.2" +name = "sha1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if 1.0.0", "cpufeatures", "digest", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_memory" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8593196da75d9dc4f69349682bd4c2099f8cde114257d1ef7ef1b33d1aba54" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "nix 0.23.2", + "rand 0.8.5", + "win-sys", +] + [[package]] name = "shlex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "shutdown_hooks" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6057adedbec913419c92996f395ba69931acbd50b7d56955394cd3f7bedbfa45" + [[package]] name = "signal-hook" version = "0.3.14" @@ -4373,9 +5378,15 @@ dependencies = [ [[package]] name = "signature" -version = "1.5.0" +version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" +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" @@ -4383,7 +5394,7 @@ version = "0.1.0" dependencies = [ "confy", "hbb_common", - "serde 1.0.139", + "serde 1.0.149", "serde_derive", "walkdir", ] @@ -4396,15 +5407,18 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg 1.1.0", +] [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "smithay-client-toolkit" @@ -4438,9 +5452,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi 0.3.9", @@ -4455,7 +5469,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.139", + "serde 1.0.149", ] [[package]] @@ -4464,6 +5478,21 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stdweb" version = "0.1.3" @@ -4478,9 +5507,9 @@ checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "strength_reduce" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ff2f71c82567c565ba4b3009a9350a96a7269eaa4001ebedae926230bc2254" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" [[package]] name = "strsim" @@ -4488,42 +5517,30 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + [[package]] name = "strum_macros" version = "0.18.0" @@ -4531,19 +5548,43 @@ 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]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.0", + "proc-macro2 1.0.47", + "quote 1.0.21", + "rustversion", + "syn 1.0.105", ] [[package]] name = "syn" -version = "1.0.98" +version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] + +[[package]] +name = "syn" +version = "1.0.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +dependencies = [ + "proc-macro2 1.0.47", + "quote 1.0.21", "unicode-ident", ] @@ -4553,20 +5594,18 @@ 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]] name = "sys-locale" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658ee915b6c7b73ec4c1ffcd838506b5c5a4087eadc1ec8f862f1066cf2c8132" +checksum = "3358acbb4acd4146138b9bda219e904a6bb5aaaa237f8eed06f4d6bc1580ecee" dependencies = [ - "cc", - "cstr_core", "js-sys", "libc", "wasm-bindgen", @@ -4576,9 +5615,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6e19da72a8d75be4d40e4dd4686afca31507f26c3ffdf6bd3073278d9de0a0" +checksum = "54cb4ebf3d49308b99e6e9dc95e989e2fdbdc210e4f67c39db0bb89ba927001c" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", @@ -4618,8 +5657,8 @@ checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" dependencies = [ "heck 0.3.3", "pkg-config", - "strum", - "strum_macros", + "strum 0.18.0", + "strum_macros 0.18.0", "thiserror", "toml", "version-compare 0.0.10", @@ -4627,15 +5666,15 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.0.2" +version = "6.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709" +checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" dependencies = [ "cfg-expr", "heck 0.4.0", "pkg-config", "toml", - "version-compare 0.1.0", + "version-compare 0.1.1", ] [[package]] @@ -4647,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" @@ -4707,28 +5801,39 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "tfc" +version = "0.6.1" +source = "git+https://github.com/fufesou/The-Fat-Controller#a5f13e6ef80327eb8d860aeb26b0af93eb5aee2b" +dependencies = [ + "core-graphics 0.22.3", + "unicode-segmentation", + "winapi 0.3.9", + "x11 2.19.0", +] [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -4746,18 +5851,40 @@ 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 = "time" -version = "0.3.11" +name = "tiff" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" dependencies = [ - "itoa 1.0.2", + "flate2", + "jpeg-decoder 0.3.0", + "weezl", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa 1.0.4", "libc", "num_threads", "time-macros", @@ -4786,34 +5913,33 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.20.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" dependencies = [ "autocfg 1.1.0", "bytes", "libc", "memchr", - "mio 0.8.4", + "mio 0.8.5", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.4.4", + "socket2 0.4.7", "tokio-macros", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] @@ -4845,9 +5971,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", @@ -4867,9 +5993,15 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ - "serde 1.0.139", + "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" @@ -4878,9 +6010,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.35" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -4890,50 +6022,51 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", ] [[package]] name = "tracing-core" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", ] [[package]] name = "transpose" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95f9c900aa98b6ea43aee227fd680550cdec726526aab8ac801549eadb25e39f" +checksum = "e6522d49d03727ffb138ae4cbc1283d3774f0d10aa7f9bf52e6784c45daf9b23" dependencies = [ "num-integer", "strength_reduce", ] [[package]] -name = "tray-item" -version = "0.7.0" +name = "tray-icon" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76863575f7842ed64fda361f417a787efa82811b4617267709066969cd4ccf3b" +checksum = "d62801a4da61bb100b8d3174a5a46fed7b6ea03cc2ae93ee7340793b09a94ce3" dependencies = [ - "cocoa 0.24.0", + "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]] @@ -4953,15 +6086,25 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "ucd-trie" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi 0.3.9", +] [[package]] name = "unicode-bidi" @@ -4971,36 +6114,42 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "untrusted" @@ -5010,21 +6159,21 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", + "serde 1.0.149", ] [[package]] name = "uuid" -version = "1.1.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ "getrandom", ] @@ -5043,9 +6192,9 @@ checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" [[package]] name = "version-compare" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" [[package]] name = "version_check" @@ -5057,12 +6206,29 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "virtual_display" version = "0.1.0" dependencies = [ - "cc", "hbb_common", "lazy_static", - "serde 1.0.139", - "serde_derive", - "thiserror", + "libloading", +] + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", ] [[package]] @@ -5092,6 +6258,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5100,9 +6272,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.81" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -5110,24 +6282,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.81" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", - "lazy_static", "log", - "proc-macro2", - "quote", - "syn", + "once_cell", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.31" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -5137,43 +6309,43 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.81" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ - "quote", + "quote 1.0.21", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.81" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +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", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.81" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "wayland-client" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91223460e73257f697d9e23d401279123d36039a3f7a449e983f123292d4458f" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" dependencies = [ "bitflags", "downcast-rs", "libc", - "nix 0.22.3", + "nix 0.24.3", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -5182,11 +6354,11 @@ dependencies = [ [[package]] name = "wayland-commons" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f6e5e340d7c13490eca867898c4cec5af56c27a5ffe5c80c6fc4708e22d33e" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" dependencies = [ - "nix 0.22.3", + "nix 0.24.3", "once_cell", "smallvec", "wayland-sys", @@ -5194,20 +6366,20 @@ dependencies = [ [[package]] name = "wayland-cursor" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52758f13d5e7861fc83d942d3d99bf270c83269575e52ac29e5b73cb956a6bd" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" dependencies = [ - "nix 0.22.3", + "nix 0.24.3", "wayland-client", "xcursor", ] [[package]] name = "wayland-protocols" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60147ae23303402e41fe034f74fb2c35ad0780ee88a1c40ac09a3be1e7465741" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" dependencies = [ "bitflags", "wayland-client", @@ -5217,20 +6389,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a1ed3143f7a143187156a2ab52742e89dac33245ba505c17224df48939f9e0" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.47", + "quote 1.0.21", "xml-rs", ] [[package]] name = "wayland-sys" -version = "0.29.4" +version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9341df79a8975679188e37dab3889bfa57c44ac2cb6da166f519a81cbe452d4" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" dependencies = [ "dlib", "lazy_static", @@ -5239,9 +6411,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.58" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -5277,9 +6449,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.4" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] @@ -5311,21 +6483,22 @@ dependencies = [ [[package]] name = "which" -version = "4.2.5" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" dependencies = [ "either", - "lazy_static", "libc", + "once_cell", ] [[package]] name = "whoami" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524b58fa5a20a2fb3014dd6358b70e6579692a56ef6fce928834e488f42f65e8" +checksum = "d6631b6a2fd59b1841b622e8f1a7ad241ef0a46f2d580464ce8140ac94cbd571" dependencies = [ + "bumpalo", "wasm-bindgen", "web-sys", ] @@ -5336,6 +6509,21 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "win-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7b128a98c1cfa201b09eb49ba285887deb3cbe7466a98850eb1adabb452be5" +dependencies = [ + "windows 0.34.0", +] + [[package]] name = "winapi" version = "0.2.8" @@ -5401,6 +6589,52 @@ dependencies = [ "windows_x86_64_msvc 0.30.0", ] +[[package]] +name = "windows" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45296b64204227616fdbf2614cefa4c236b98ee64dfaaaa435207ed99fe7829f" +dependencies = [ + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "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" @@ -5409,7 +6643,7 @@ checksum = "0c643e10139d127d30d6d753398c8a6f0a43532e8370f6c9d29ebbff29b984ab" dependencies = [ "bitflags", "err-derive", - "widestring", + "widestring 0.4.3", "winapi 0.3.9", ] @@ -5439,6 +6673,51 @@ dependencies = [ "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +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-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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + [[package]] name = "windows_aarch64_msvc" version = "0.28.0" @@ -5451,12 +6730,24 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" +[[package]] +name = "windows_aarch64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + [[package]] name = "windows_i686_gnu" version = "0.28.0" @@ -5469,12 +6760,24 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + [[package]] name = "windows_i686_msvc" version = "0.28.0" @@ -5487,12 +6790,24 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + [[package]] name = "windows_x86_64_gnu" version = "0.28.0" @@ -5505,12 +6820,30 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + [[package]] name = "windows_x86_64_msvc" version = "0.28.0" @@ -5523,12 +6856,24 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + [[package]] name = "winit" version = "0.26.1" @@ -5536,7 +6881,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b43cc931d58b99461188607efd7acb2a093e65fc621f54cad78517a6063e73a" dependencies = [ "bitflags", - "cocoa 0.24.0", + "cocoa", "core-foundation 0.9.3", "core-graphics 0.22.3", "core-video-sys", @@ -5545,14 +6890,14 @@ dependencies = [ "lazy_static", "libc", "log", - "mio 0.8.4", + "mio 0.8.5", "ndk 0.5.0", "ndk-glue 0.5.2", "ndk-sys 0.2.2", "objc", "parking_lot 0.11.2", "percent-encoding", - "raw-window-handle", + "raw-window-handle 0.4.3", "smithay-client-toolkit", "wasm-bindgen", "wayland-client", @@ -5595,7 +6940,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7f97e69b28b256ccfb02472c25057132e234aa8368fea3bb0268def564ce1f2" dependencies = [ - "clap 3.2.12", + "clap 3.2.23", ] [[package]] @@ -5610,18 +6955,27 @@ dependencies = [ [[package]] name = "wyz" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b31594f29d27036c383b53b59ed3476874d518f0efb151b27a4c275141390e" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" dependencies = [ "tap", ] [[package]] name = "x11" -version = "2.19.1" +version = "2.19.0" +source = "git+https://github.com/bjornsnoen/x11-rs#c2e9bfaa7b196938f8700245564d8ac5d447786a" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11" +version = "2.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" +checksum = "c2638d5b9c17ac40575fb54bb461a4b1d2a8d1b4ffcc4ff237d254ec59ddeb82" dependencies = [ "libc", "pkg-config", @@ -5629,12 +6983,12 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.19.1" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" dependencies = [ - "lazy_static", "libc", + "once_cell", "pkg-config", ] @@ -5665,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" @@ -5674,6 +7038,69 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zbus" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938ea6da98c75c2c37a86007bd17fd8e208cbec24e086108c87ece98e9edec0d" +dependencies = [ + "async-broadcast", + "async-channel", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "dirs 4.0.0", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.25.1", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde 1.0.149", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi 0.3.9", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" +dependencies = [ + "proc-macro-crate 1.2.1", + "proc-macro2 1.0.47", + "quote 1.0.21", + "regex", + "syn 1.0.105", +] + +[[package]] +name = "zbus_names" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c737644108627748a660d038974160e0cbb62605536091bdfa28fd7f64d43c8" +dependencies = [ + "serde 1.0.149", + "static_assertions", + "zvariant", +] + [[package]] name = "zstd" version = "0.9.2+zstd.1.5.1" @@ -5702,3 +7129,38 @@ dependencies = [ "cc", "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8c89c183461e11867ded456db252eae90874bc6769b7adbea464caa777e51" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde 1.0.149", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" +dependencies = [ + "proc-macro-crate 1.2.1", + "proc-macro2 1.0.47", + "quote 1.0.21", + "syn 1.0.105", +] diff --git a/Cargo.toml b/Cargo.toml index 7f185db6b..f93f776a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.1.10" +version = "1.2.0" authors = ["rustdesk "] edition = "2021" build= "build.rs" @@ -20,12 +20,16 @@ inline = [] hbbs = [] cli = [] with_rc = ["simple_rc"] +flutter_texture_render = [] appimage = [] +flatpak = [] use_samplerate = ["samplerate"] use_rubato = ["rubato"] use_dasp = ["dasp"] -hwcodec = ["scrap/hwcodec"] +flutter = ["flutter_rust_bridge"] default = ["use_dasp"] +hwcodec = ["scrap/hwcodec"] +mediacodec = ["scrap/mediacodec"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -40,11 +44,10 @@ 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" -magnum-opus = { git = "https://github.com/open-trade/magnum-opus" } +magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" } dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } rubato = { version = "0.12", optional = true } samplerate = { version = "0.2", optional = true } @@ -53,14 +56,19 @@ 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 = "0.11.0" +default-net = "0.12.0" wol-rs = "0.9.1" +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" -[target.'cfg(not(target_os = "linux"))'.dependencies] -reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } +reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } +chrono = "0.4.23" +cidr-utils = "0.5.9" [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies] cpal = "0.13.5" @@ -72,20 +80,21 @@ sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" sys-locale = "0.2" enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } -rdev = { git = "https://github.com/open-trade/rdev" } ctrlc = "3.2" arboard = "2.0" #minreq = { version = "2.4", features = ["punycode", "https-native"] } 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"] } winreg = "0.10" windows-service = "0.4" virtual_display = { path = "libs/virtual_display" } +impersonate_system = { git = "https://github.com/21pages/impersonate-system" } +shared_memory = "0.12.4" +shutdown_hooks = "0.1.0" [target.'cfg(target_os = "macos")'.dependencies] objc = "0.2" @@ -94,7 +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 = "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" } @@ -103,16 +119,19 @@ rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } async-process = "1.3" mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } +dbus = "0.9" +dbus-crossroads = "0.5" +xrandr-parser = "0.3.0" [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 = "=1.30.0" +flutter_rust_bridge = "1.61.1" [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/simple_rc"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." @@ -127,18 +146,17 @@ 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 = "=1.30.0" +flutter_rust_bridge_codegen = "1.61.1" [dev-dependencies] -hound = "3.4" +hound = "3.5" [package.metadata.bundle] name = "RustDesk" identifier = "com.carriez.rustdesk" -icon = ["32x32.png", "128x128.png", "128x128@2x.png"] -deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "pulseaudio", "python3-pip", "curl"] +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 = ["mac-tray.png"] #https://github.com/johnthagen/min-sized-rust [profile.release] @@ -147,3 +165,4 @@ codegen-units = 1 panic = 'abort' strip = true #opt-level = 'z' # only have smaller size after strip +rpath = true diff --git a/DEBIAN/prerm b/DEBIAN/prerm deleted file mode 100755 index 3bb453198..000000000 --- a/DEBIAN/prerm +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash - -set -e - -case $1 in - remove|upgrade) - INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') - if [ "systemd" == "${INITSYS}" ]; then - systemctl stop rustdesk || true - systemctl disable rustdesk || true - - serverUser=$(ps -ef | grep -E 'rustdesk +--server' | awk '{print $1}' | head -1) - if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ] - then - systemctl --machine=${serverUser}@.host --user stop rustdesk || true - fi - - rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service || true - fi - ;; -esac - -exit 0 diff --git a/Dockerfile b/Dockerfile index c08563614..5d15ff723 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM debian WORKDIR / -RUN apt update -y && 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 +RUN apt update -y && 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 RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 134505003bb46e20fbace51ccfb69243fbbc5f82 RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 0d67a28b6..000000000 --- a/PKGBUILD +++ /dev/null @@ -1,31 +0,0 @@ -pkgname=rustdesk -pkgver=1.1.9 -pkgrel=0 -epoch= -pkgdesc="" -arch=('x86_64') -url="" -license=('GPL-3.0') -groups=() -depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'pulseaudio' 'ttf-arphic-uming' 'python-pip' 'curl') -makedepends=() -checkdepends=() -optdepends=() -provides=() -conflicts=() -replaces=() -backup=() -options=() -install=pacman_install -changelog= -noextract=() -md5sums=() #generate with 'makepkg -g' - -package() { - install -Dm 755 ${HBB}/target/release/${pkgname} -t "${pkgdir}/usr/bin" - install -Dm 644 ${HBB}/libsciter-gtk.so -t "${pkgdir}/usr/lib/rustdesk" - install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/pynput_service.py -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/256-no-margin.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" -} diff --git a/README-DE.md b/README-DE.md deleted file mode 100644 index 4e9929997..000000000 --- a/README-DE.md +++ /dev/null @@ -1,162 +0,0 @@ -

- RustDesk - Your remote desktop
- Server • - Kompilieren • - Docker • - Dateistruktur • - Screenshots
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [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 -

- -Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) - -[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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 heißt jegliche Mitarbeit willkommen. Schau dir [`CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start. - -[**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) - -## Kostenlose öffentliche Server - -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. - -| Standort | Serverart | Spezifikationen | Kommentare | -| --------- | ------------- | ------------------ | ---------- | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | | -| Singapore | Vultr | 1 VCPU / 1GB RAM | | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | - -## Abhängigkeiten - -Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek 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) - -## Die groben Schritte zum Kompilieren - -- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor - -- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu - - - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` - - Linux/MacOS: `vcpkg install libvpx libyuv opus` - -- Nutze `cargo run` - -## 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 -``` - -### Fedora 28 (CentOS 8) - -```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel -``` - -### Arch (Manjaro) - -```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### vcpkg installieren - -```sh -git clone https://github.com/microsoft/vcpkg -cd vcpkg -git checkout 2021.12.01 -cd .. -vcpkg/bootstrap-vcpkg.sh -export VCPKG_ROOT=$HOME/vcpkg -vcpkg/vcpkg install libvpx libyuv opus -``` - -### libvpx reparieren (Für Fedora) - -```sh -cd vcpkg/buildtrees/libvpx/src -cd * -./configure -sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile -sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile -make -cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ -cd -``` - -### Kompilieren - -```sh -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env -git clone https://github.com/rustdesk/rustdesk -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 -``` - -### Ändere Wayland zu X11 (Xorg) - -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. - -## Auf Docker Kompilieren - -Beginne damit das Repository zu klonen und den Docker Container zu bauen: - -```sh -git clone https://github.com/rustdesk/rustdesk -cd rustdesk -docker build -t "rustdesk-builder" . -``` - -Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl: - -```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: - -```sh -target/debug/rustdesk -``` - -Oder, wenn du eine Releaseversion benutzt: - -```sh -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. - -## 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/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 -- **[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/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/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code - -## Screenshots - -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) - -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) - -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) - -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/README-FA.md b/README-FA.md deleted file mode 100644 index 818e62fa8..000000000 --- a/README-FA.md +++ /dev/null @@ -1,178 +0,0 @@ -

- RustDesk - Your remote desktop
- اسنپ شات • - ساختار • - داکر • - ساخت • - سرور
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
- ‫برای ترجمه این RustDesk UI ،README و Doc به زبان مادری شما به کمکتون نیاز داریم -

- -با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) - - -[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) - -یک نرم افزار دیگر کنترل دسکتاپ از راه دور، که با Rust نوشته شده است. راه اندازی سریع وبدون نیاز به تنظیمات. شما کنترل کاملی بر داده های خود دارید، بدون هیچ گونه نگرانی امنیتی. -می‌توانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا -[ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk). - -‫راست دسک (RustDesk) از مشارکت همه استقبال می کند. برای راهنمایی جهت مشارکت به [`CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. - -[راست دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) - -[دانلود باینری](https://github.com/rustdesk/rustdesk/releases) - -## سرورهای عمومی رایگان - -سرورهایی زیر را به صورت رایگان میتوانید استفاده می کنید. این لیست ممکن است در طول زمان تغییر کند. اگر به این سرورها نزدیک نیستید، ممکن است سرویس شما کند شود. -| موقعیت | سرویس دهنده | مشخصات | -| --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | - -## وابستگی ها - -نسخه‌های دسکتاپ از [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) - -نسخه های موبایل از Flutter استفاده می کنند. بعداً نسخه دسکتاپ را از Sciter به Flutter منتقل خواهیم کرد. - -## مراحل بنیادین برای ساخت - -‫- محیط توسعه نرم افزار 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` - -## [ساخت](https://rustdesk.com/docs/en/dev/build/) - -## نحوه ساخت بر روی لینوکس - -### ساخت بر روی (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 -``` - -### ساخت بر روی (Fedora 28 (CentOS 8 - -```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel -``` - -### ساخت بر روی (Arch (Manjaro - -```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### بسته pynput را نصب کنید - -```sh -pip3 install pynput -``` - -### نرم افزار vcpkg را نصب کنید - -```sh -git clone https://github.com/microsoft/vcpkg -cd vcpkg -git checkout 2021.12.01 -cd .. -vcpkg/bootstrap-vcpkg.sh -export VCPKG_ROOT=$HOME/vcpkg -vcpkg/vcpkg install libvpx libyuv opus -``` - -### رفع ایراد libvpx (برای فدورا) - -```sh -cd vcpkg/buildtrees/libvpx/src -cd * -./configure -sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile -sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile -make -cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ -cd -``` - -### ساخت - -```sh -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env -git clone https://github.com/rustdesk/rustdesk -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 -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/) را کلیک کنید. - -## نحوه ساخت با داکر - -این مخزن گیت را کلون کنید و کانتینر را به روش زیر بسازید - -```sh -git clone https://github.com/rustdesk/rustdesk -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--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور: - -```sh -target/debug/rustdesk -``` - -یا برای نسخه بهینه سازی شده دستور زیر را اجرا کنید: - -```sh -target/release/rustdesk -``` - -لطفاً اطمینان حاصل کنید که این دستورات را از پوشه مخزن RustDesk اجرا می کنید، در غیر این صورت ممکن است برنامه نتواند منابع مورد نیاز را پیدا کند. همچنین توجه داشته باشید که سایر دستورات فرعی Cargo مانند `install` یا `run` در حال حاضر از طریق این روش پشتیبانی نمی شوند زیرا برنامه به جای سیستم عامل میزبان, در داخل کانتینر نصب و اجرا میشود. - -## ساختار پوشه ها - -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions -- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control -- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections -- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection -- **[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 - -## اسکرین شات ها - -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) - -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) - -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) - -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/README.md b/README.md index 346600f61..8af79915b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
ServersBuildDockerStructureSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [Українська] | [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk]
We need your help to translate this README, RustDesk UI and Doc to your native language

@@ -17,35 +17,48 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk welcomes contribution from everyone. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for help getting started. +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) +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + [Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) ## 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 | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | +| Ukraine (Kyiv) | dc.volia (2VM) | 2 vCPU / 4GB RAM | + +## Dev Container + +[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk) + +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/) for GUI, please download sciter dynamic library yourself. +Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. + +Please download sciter dynamic library yourself. [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) -Mobile versions use Flutter. We will migrate desktop version from Sciter to Flutter. - ## Raw steps to build - Prepare your Rust development env and C++ build env @@ -64,9 +77,16 @@ Mobile versions use Flutter. We will migrate desktop version from Sciter to Flut ### 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 @@ -76,13 +96,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### Install pynput package - -```sh -pip3 install pynput +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Install vcpkg @@ -127,6 +141,30 @@ VCPKG_ROOT=$HOME/vcpkg cargo run RustDesk does not support Wayland. Check [this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) to configuring Xorg as the default GNOME session. +## Wayland support + +Wayland does not seem to provide any API for sending keypresses to other windows. Therefore, the rustdesk uses an API from a lower level, namely the `/dev/uinput` device (Linux kernel level). + +When wayland is the controlled side, you have to start in the following way: +```bash +# Start uinput service +$ sudo rustdesk --service +$ rustdesk +``` +**Notice**: Wayland screen recording uses different interfaces. RustDesk currently only supports org.freedesktop.portal.ScreenCast. +```bash +$ dbus-send --session --print-reply \ + --dest=org.freedesktop.portal.Desktop \ + /org/freedesktop/portal/desktop \ + org.freedesktop.DBus.Properties.Get \ + string:org.freedesktop.portal.ScreenCast string:version +# Not support +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Support +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` + ## How to build with Docker Begin by cloning the repository and building the docker container: @@ -168,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/SECURITY.md b/SECURITY.md deleted file mode 100644 index f1114f913..000000000 --- a/SECURITY.md +++ /dev/null @@ -1,13 +0,0 @@ -# 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 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-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml new file mode 100644 index 000000000..59dd5164f --- /dev/null +++ b/appimage/AppImageBuilder-x86_64.yml @@ -0,0 +1,88 @@ +# 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: + - amd64 + allow_unauthenticated: true + sources: + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted + universe multiverse + - sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu + bionic main + include: + - libc6:amd64 + - 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: x86_64 + update-information: guess diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder.yml deleted file mode 100644 index 0ca62e97c..000000000 --- a/appimage/AppImageBuilder.yml +++ /dev/null @@ -1,98 +0,0 @@ -# appimage-builder recipe see https://appimage-builder.readthedocs.io for details -# Please build this AppImage on Ubuntu 18.04 -version: 1 -script: - # Remove any previous build - - rm -rf AppDir | true - # Install application dependencies - - pip3 install --upgrade pip && pip3 install --ignore-installed --prefix=/usr --root=AppDir -r ./requirements.txt - # Download sciter.so - - mkdir -p AppDir/usr/lib/rustdesk/ - - pushd AppDir/usr/lib/rustdesk && wget https://github.com/c-smile/sciter-sdk/raw/29a598b6d20220b93848b5e8abab704619296857/bin.lnx/x64/libsciter-gtk.so && popd - # pynput_service.py - - cp ../pynput_service.py ./AppDir/usr/lib/rustdesk - # Build rustdesk - - pushd .. && python3 inline-sciter.py && cargo build --features inline,appimage --release && popd - - mkdir -p AppDir/usr/bin - - cp ../target/release/rustdesk AppDir/usr/bin/rustdesk - # Make usr and icons dirs - - mkdir -p AppDir/usr/share/icons/hicolor/128x128 && cp ../128x128.png AppDir/usr/share/icons/hicolor/128x128/rustdesk.png - - mkdir -p AppDir/usr/share/icons/hicolor/32x32 && cp ../32x32.png AppDir/usr/share/icons/hicolor/32x32/rustdesk.png - - cp rustdesk.desktop AppDir/ - -AppDir: - path: ./AppDir - app_info: - id: rustdesk - name: RustDesk - icon: rustdesk - version: 1.1.10 - exec: usr/bin/rustdesk - exec_args: $@ - apt: - arch: - - amd64 - allow_unauthenticated: true - sources: - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted - universe multiverse - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security main restricted - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security universe - - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-security multiverse - include: - - libgcc1:amd64 - - libgcrypt20:amd64 - - libgtk-3-0:amd64 - - liblz4-1:amd64 - - liblzma5:amd64 - - libpcre3:amd64 - - libpulse0:amd64 - - libsystemd0:amd64 - - libxau6:amd64 - - libxcb-randr0:amd64 - - libxdmcp6:amd64 - - libxdo3:amd64 - - libxext6:amd64 - - libxfixes3:amd64 - - libxinerama1:amd64 - - libxrender1:amd64 - - libxtst6:amd64 - - python3:amd64 - - python3-pkg-resources:amd64 - files: - include: [] - exclude: - - usr/share/man - - usr/share/doc/*/README.* - - usr/share/doc/*/changelog.* - - usr/share/doc/*/NEWS.* - - usr/share/doc/*/TODO.* - runtime: - env: - PYTHONHOME: '${APPDIR}/usr' - PYTHONPATH: '${APPDIR}/usr/lib/python3.6/site-packages' - 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: x86_64 - update-information: guess \ No newline at end of file diff --git a/appimage/README.md b/appimage/README.md deleted file mode 100644 index 1dcfa0b35..000000000 --- a/appimage/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# How to build and run RustDesk in AppImage - -Begin by installing `appimage-builder` and predependencies mentioned in official website. - -Assume that `appimage-builder` is setup correctly, run commands below, `bash` or `zsh` is recommended: - -```bash -cd /path/to/rustdesk_root -./build_appimage.py -``` - -After a success package, you can see the message in console like: - -```shell -INFO:root:AppImage created successfully -``` - -The AppImage package is shown in `./appimage/RustDesk-VERSION-TARGET_PLATFORM.AppImage`. - -Note: AppImage version of rustdesk is an early version which requires more test. If you find problems, please open an issue. \ No newline at end of file diff --git a/appimage/requirements.txt b/appimage/requirements.txt deleted file mode 100644 index d632797e5..000000000 --- a/appimage/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -pynput \ No newline at end of file diff --git a/appimage/rustdesk.desktop b/appimage/rustdesk.desktop deleted file mode 100644 index a0227f256..000000000 --- a/appimage/rustdesk.desktop +++ /dev/null @@ -1,19 +0,0 @@ -[Desktop Entry] -Version=1.1.10 -Name=RustDesk -GenericName=Remote Desktop -Comment=Remote Desktop -Exec=rustdesk -Icon=rustdesk -Terminal=false -Type=Application -StartupNotify=true -Categories=Other; -Keywords=internet; -Actions=new-window; - -X-Desktop-File-Install-Version=0.23 - -[Desktop Action new-window] -Name=Open a New Window - diff --git a/build.py b/build.py index 341f4f4e6..727b53fe0 100755 --- a/build.py +++ b/build.py @@ -1,21 +1,34 @@ #!/usr/bin/env python3 import os +import pathlib import platform import zipfile import urllib.request import shutil import hashlib import argparse +import sys windows = platform.platform().startswith('Windows') -osx = platform.platform().startswith('Darwin') or platform.platform().startswith("macOS") +osx = platform.platform().startswith( + 'Darwin') or platform.platform().startswith("macOS") hbb_name = 'rustdesk' + ('.exe' if windows else '') exe_path = 'target/release/' + hbb_name +flutter_win_target_dir = 'flutter/build/windows/runner/Release/' +skip_cargo = False +def custom_os_system(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") as fh: + with open("Cargo.toml", encoding="utf-8") as fh: for line in fh: if line.startswith("version"): return line.replace("version", "").replace("=", "").replace('"', '').strip() @@ -26,22 +39,24 @@ def parse_rc_features(feature): available_features = { 'IddDriver': { 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip', - 'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1' - '/RustDeskIddDriver_x64.zip.checksum_md5', + 'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/checksum_md5', + 'exclude': ['README.md'], }, 'PrivacyMode': { 'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1' - '/TempTopMostWindow_x64.zip', - 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1' - '/TempTopMostWindow_x64.zip.checksum_md5', + '/TempTopMostWindow_x64_pic_en.zip', + 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5', + 'include': ['WindowInjection.dll'], } } apply_features = {} if not feature: - return apply_features - elif isinstance(feature, str) and feature.upper() == 'ALL': + feature = [] + if isinstance(feature, str) and feature.upper() == 'ALL': return available_features elif isinstance(feature, list): + # force add PrivacyMode + feature.append('PrivacyMode') for feat in feature: if isinstance(feat, str) and feat.upper() == 'ALL': return available_features @@ -66,63 +81,301 @@ def make_parser(): default='', help='Integrate features, windows only.' 'Available: IddDriver, PrivacyMode. Special value is "ALL" and empty "". Default is empty.') + parser.add_argument('--flutter', action='store_true', + help='Build flutter package', default=False) parser.add_argument( '--hwcodec', action='store_true', - help='Enable feature hwcodec, windows only.' + help='Enable feature hwcodec' + ( + '' if windows or osx else ', need libva-dev, libvdpau-dev.') + ) + parser.add_argument( + '--portable', + action='store_true', + help='Build windows portable' + ) + parser.add_argument( + '--flatpak', + action='store_true', + help='Build rustdesk libs with the flatpak feature enabled' + ) + parser.add_argument( + '--appimage', + action='store_true', + help='Build rustdesk libs with the appimage feature enabled' + ) + parser.add_argument( + '--skip-cargo', + action='store_true', + help='Skip cargo build process, only flutter version + Linux supported currently' ) return parser +# Generate build script for docker +# +# it assumes all build dependencies are installed in environments +# Note: do not use it in bare metal, or may break build environments +def generate_build_script_for_docker(): + with open("/tmp/build.sh", "w") as f: + f.write(''' + #!/bin/bash + # environment + export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation: " | cut -d' ' -f4-)/include" + # flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.0.5-stable.tar.xz + tar -xvf flutter_linux_3.0.5-stable.tar.xz + export PATH=`pwd`/flutter/bin:$PATH + popd + # flutter_rust_bridge + dart pub global activate ffigen --version 5.0.1 + 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 + 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 + # install vcpkg + pushd /opt + export VCPKG_ROOT=`pwd`/vcpkg + git clone https://github.com/microsoft/vcpkg + vcpkg/bootstrap-vcpkg.sh + vcpkg/vcpkg install libvpx libyuv opus + popd + # build rustdesk + ./build.py --flutter --hwcodec + ''') + os.system("chmod +x /tmp/build.sh") + os.system("bash /tmp/build.sh") + + def download_extract_features(features, res_dir): + import re + + proxy = '' + def req(url): + if not proxy: + return url + else: + r = urllib.request.Request(url) + r.set_proxy(proxy, 'http') + r.set_proxy(proxy, 'https') + return r + for (feat, feat_info) in features.items(): + includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else [] + includes = [ re.compile(p) for p in includes ] + excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else [] + excludes = [ re.compile(p) for p in excludes ] + print(f'{feat} download begin') - checksum_md5_response = urllib.request.urlopen(feat_info['checksum_url']) - checksum_md5 = checksum_md5_response.read().decode('utf-8').split()[0] download_filename = feat_info['zip_url'].split('/')[-1] - filename, _headers = urllib.request.urlretrieve(feat_info['zip_url'], download_filename) - md5 = hashlib.md5(open(filename, 'rb').read()).hexdigest() - if checksum_md5 != md5: - raise Exception(f'{feat} download failed') - print(f'{feat} download end. extract bein') - zip_file = zipfile.ZipFile(filename) - zip_list = zip_file.namelist() - for f in zip_list: - zip_file.extract(f, res_dir) - zip_file.close() - os.remove(download_filename) - print(f'{feat} extract end') + checksum_md5_response = urllib.request.urlopen( + req(feat_info['checksum_url'])) + for line in checksum_md5_response.read().decode('utf-8').splitlines(): + if line.split()[1] == download_filename: + checksum_md5 = line.split()[0] + filename, _headers = urllib.request.urlretrieve(feat_info['zip_url'], + download_filename) + md5 = hashlib.md5(open(filename, 'rb').read()).hexdigest() + if checksum_md5 != md5: + raise Exception(f'{feat} download failed') + print(f'{feat} download end. extract bein') + zip_file = zipfile.ZipFile(filename) + zip_list = zip_file.namelist() + for f in zip_list: + file_exclude = False + for p in excludes: + if p.match(f) is not None: + file_exclude = True + break + if file_exclude: + continue + + file_include = False if includes else True + for p in includes: + if p.match(f) is not None: + file_include = True + break + if file_include: + print(f'extract file {f}') + zip_file.extract(f, res_dir) + zip_file.close() + os.remove(download_filename) + print(f'{feat} extract end') def get_rc_features(args): + flutter = args.flutter features = parse_rc_features(args.feature) - if features: - print(f'Build with features {list(features.keys())}') - res_dir = 'resources' - if os.path.isdir(res_dir) and not os.path.islink(res_dir): - shutil.rmtree(res_dir) - elif os.path.exists(res_dir): - raise Exception(f'Find file {res_dir}, not a directory') - os.makedirs(res_dir, exist_ok=True) - download_extract_features(features, res_dir) - return ['with_rc'] if features else [] - + if not features: + return [] + + print(f'Build with features {list(features.keys())}') + res_dir = 'resources' + if os.path.isdir(res_dir) and not os.path.islink(res_dir): + shutil.rmtree(res_dir) + elif os.path.exists(res_dir): + raise Exception(f'Find file {res_dir}, not a directory') + os.makedirs(res_dir, exist_ok=True) + download_extract_features(features, res_dir) + if flutter: + os.makedirs(flutter_win_target_dir, exist_ok=True) + for f in pathlib.Path(res_dir).iterdir(): + print(f'{f}') + if f.is_file(): + shutil.copy2(f, flutter_win_target_dir) + else: + shutil.copytree(f, f'{flutter_win_target_dir}{f.stem}') + return [] + else: + return ['with_rc'] + def get_features(args): - features = ['inline'] + features = ['inline'] if not args.flutter else [] if windows: features.extend(get_rc_features(args)) if args.hwcodec: features.append('hwcodec') + if args.flutter: + features.append('flutter') + features.append('flutter_texture_render') + if args.flatpak: + features.append('flatpak') + if args.appimage: + features.append('appimage') print("features:", features) return features + +def generate_control_file(version): + control_file_path = "../res/DEBIAN/control" + os.system('/bin/rm -rf %s' % control_file_path) + + content = """Package: rustdesk +Version: %s +Architecture: amd64 +Maintainer: open-trade +Homepage: https://rustdesk.com +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0 +Description: A remote control software. + +""" % version + file = open(control_file_path, "w") + file.write(content) + file.close() + + +def ffi_bindgen_function_refactor(): + # workaround ffigen + os.system( + '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') + generate_control_file(version) + os.system('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + os.system('dpkg-deb -b tmpdeb rustdesk.deb;') + + os.system('/bin/rm -rf tmpdeb/') + os.system('/bin/rm -rf ../res/DEBIAN/control') + os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.chdir("..") + + +def build_flutter_dmg(version, features): + if not skip_cargo: + # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project + os.system(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') + # copy dylib + os.system( + "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") + 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') + ffi_bindgen_function_refactor() + os.chdir('flutter') + os.system('flutter build linux --release') + os.system('strip build/linux/x64/release/bundle/lib/librustdesk.so') + os.chdir('../res') + os.system('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') + 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') + 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( + 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'): + os.replace('./target/release/rustdesk-portable-packer.exe', + './rustdesk_portable.exe') + else: + os.rename('./target/release/rustdesk-portable-packer.exe', + './rustdesk_portable.exe') + print( + f'output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe') + os.rename('./rustdesk_portable.exe', f'./rustdesk-{version}-install.exe') + print( + f'output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe') + + def main(): + global skip_cargo parser = make_parser() args = parser.parse_args() - os.system("cp Cargo.toml Cargo.toml.bk") - os.system("cp src/main.rs src/main.rs.bk") + 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: @@ -130,104 +383,151 @@ def main(): '//#![windows_subsystem', '#![windows_subsystem')) if os.path.exists(exe_path): os.unlink(exe_path) - os.system('python3 inline-sciter.py') if os.path.isfile('/usr/bin/pacman'): os.system('git checkout src/ui/common.tis') version = get_version() - features = ",".join(get_features(args)) + features = ','.join(get_features(args)) + flutter = args.flutter + if not flutter: + os.system('python3 res/inline-sciter.py') + print(args.skip_cargo) + if args.skip_cargo: + skip_cargo = True + portable = args.portable if windows: + # build virtual display dynamic library + os.chdir('libs/virtual_display/dylib') + os.system('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') pa = os.environ.get('P') if pa: - os.system(f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com ' - 'target\\release\\rustdesk.exe') + os.system( + 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(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe') + os.system( + f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') elif os.path.isfile('/usr/bin/pacman'): - os.system('cargo build --release --features ' + features) - os.system('git checkout src/ui/common.tis') - os.system('strip target/release/rustdesk') - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) # pacman -S -needed base-devel - os.system('HBB=`pwd` makepkg -f') - os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) + os.system("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' % ( + 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("sed -i 's/Version: .*/Version: %s/g' rpm.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba rpm.spec') - os.system('mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( - version, version)) + os.system( + "sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) + os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec') + os.system( + '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("sed -i 's/Version: .*/Version: %s/g' rpm-suse.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba rpm-suse.spec') - os.system('mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % (version, version)) + os.system( + "sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) + os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') + os.system( + 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % ( + version, version)) # yum localinstall rustdesk.rpm else: - os.system('cargo bundle --release --features ' + features) - if osx: - os.system( - 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') - os.system( - 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') - # https://github.com/sindresorhus/create-dmg - os.system('/bin/rm -rf *.dmg') - plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" - txt = open(plist).read() - with open(plist, "wt") as fh: - fh.write(txt.replace("", """ - LSUIElement - 1 -""")) - pa = os.environ.get('P') - if pa: - os.system(''' -# buggy: rcodesign sign ... path/*, have to sign one by one -#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 -#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/libsciter.dylib -#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 -# goto "Keychain Access" -> "My Certificates" for below id which starts with "Developer ID Application:" -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') - os.rename('RustDesk %s.dmg' % version, 'rustdesk-%s.dmg' % version) - if pa: - os.system(''' -#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg -codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg -# https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html -rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg -# verify: spctl -a -t exec -v /Applications/RustDesk.app -'''.format(pa, version)) + if flutter: + if osx: + build_flutter_dmg(version, features) + pass else: - print('Not signed') + # os.system( + # 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') + build_flutter_deb(version, features) else: - os.system('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( - 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') - os.system('cp pynput_service.py tmpdeb/usr/share/rustdesk/files/') - os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') - os.system('strip tmpdeb/usr/bin/rustdesk') - os.system('mkdir -p tmpdeb/usr/lib/rustdesk') - os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') - md5_file('usr/share/rustdesk/files/pynput_service.py') - md5_file('usr/lib/rustdesk/libsciter-gtk.so') - os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') - os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) + os.system('cargo bundle --release --features ' + features) + if osx: + os.system( + 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') + os.system( + 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') + # https://github.com/sindresorhus/create-dmg + os.system('/bin/rm -rf *.dmg') + plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" + txt = open(plist).read() + with open(plist, "wt") as fh: + fh.write(txt.replace("", """ + LSUIElement + 1 + """)) + pa = os.environ.get('P') + if pa: + os.system(''' + # 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 + #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/libsciter.dylib + #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 + # goto "Keychain Access" -> "My Certificates" for below id which starts with "Developer ID Application:" + 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') + os.rename('RustDesk %s.dmg' % + version, 'rustdesk-%s.dmg' % version) + if pa: + os.system(''' + # 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/ + # goto xcode and login with apple id, manager certificates (Developer ID Application and/or Developer ID Installer) online there (only download and double click (install) cer file can not export p12 because no private key) + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg + codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg + # https://appstoreconnect.apple.com/access/api + # https://gregoryszorc.com/docs/apple-codesign/0.16.0/apple_codesign_rcodesign.html#notarizing-and-stapling + # p8 file is generated when you generate api key, download and put it under ~/.private_keys/ + rcodesign notarize --api-issuer {2} --api-key {3} --staple ./rustdesk-{1}.dmg + # verify: spctl -a -t exec -v /Applications/RustDesk.app + '''.format(pa, version, os.environ.get('api-issuer'), os.environ.get('api-key'))) + else: + print('Not signed') + else: + # buid deb package + os.system( + '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( + 'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') + os.system( + 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + os.system( + '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/') + 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/') + 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") diff --git a/build.rs b/build.rs index 4f7821012..d15f27424 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,16 @@ #[cfg(windows)] fn build_windows() { - cc::Build::new().file("src/windows.cc").compile("windows"); + let file = "src/platform/windows.cc"; + cc::Build::new().file(file).compile("windows"); println!("cargo:rustc-link-lib=WtsApi32"); - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=windows.cc"); + println!("cargo:rerun-if-changed={}", file); +} + +#[cfg(target_os = "macos")] +fn build_mac() { + let file = "src/platform/macos.mm"; + cc::Build::new().file(file).compile("macos"); + println!("cargo:rerun-if-changed={}", file); } #[cfg(all(windows, feature = "inline"))] @@ -11,12 +18,12 @@ fn build_manifest() { use std::io::Write; if std::env::var("PROFILE").unwrap() == "release" { let mut res = winres::WindowsResource::new(); - res.set_icon("icon.ico") + res.set_icon("res/icon.ico") .set_language(winapi::um::winnt::MAKELANGID( winapi::um::winnt::LANG_ENGLISH, winapi::um::winnt::SUBLANG_ENGLISH_US, )) - .set_manifest_file("manifest.xml"); + .set_manifest_file("res/manifest.xml"); match res.compile() { Err(e) => { write!(std::io::stderr(), "{}", e).unwrap(); @@ -76,31 +83,49 @@ fn install_oboe() { //cc::Build::new().file("oboe.cc").include(include).compile("oboe_wrapper"); } +#[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/mobile_ffi.rs"); - // settings for fbr_codegen - let opts = lib_flutter_rust_bridge_codegen::Opts { + println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); + // Options for frb_codegen + let raw_opts = RawOpts { // Path of input Rust code - rust_input: "src/mobile_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(), - // for other options lets use default + 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()]), + /// 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() { hbb_common::gen_version(); install_oboe(); // there is problem with cfg(target_os) in build.rs, so use our workaround - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); - if target_os == "android" || target_os == "ios" { - gen_flutter_rust_bridge(); - return; - } + // let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + // if target_os == "android" || target_os == "ios" { + #[cfg(feature = "flutter")] + gen_flutter_rust_bridge(); + // return; + // } #[cfg(all(windows, feature = "with_rc"))] build_rc_source(); #[cfg(all(windows, feature = "inline"))] @@ -108,5 +133,8 @@ fn main() { #[cfg(windows)] build_windows(); #[cfg(target_os = "macos")] + build_mac(); + #[cfg(target_os = "macos")] println!("cargo:rustc-link-lib=framework=ApplicationServices"); + println!("cargo:rerun-if-changed=build.rs"); } diff --git a/build_appimage.py b/build_appimage.py deleted file mode 100755 index 1c7ae2443..000000000 --- a/build_appimage.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python3 -import os - -def get_version(): - with open("Cargo.toml") as fh: - for line in fh: - if line.startswith("version"): - return line.replace("version", "").replace("=", "").replace('"', '').strip() - return '' - -if __name__ == '__main__': - # check version - version = get_version() - os.chdir("appimage") - os.system("sed -i 's/^Version=.*/Version=%s/g' rustdesk.desktop" % version) - os.system("sed -i 's/^ version: .*/ version: %s/g' AppImageBuilder.yml" % version) - # build appimage - ret = os.system("appimage-builder --recipe AppImageBuilder.yml --skip-test") - if ret == 0: - print("RustDesk AppImage build success :)") - print("Check AppImage in '/path/to/rustdesk/appimage/RustDesk-VERSION-TARGET_PLATFORM.AppImage'") - else: - print("RustDesk AppImage build failed :(") diff --git a/docs/CODE_OF_CONDUCT-RU.md b/docs/CODE_OF_CONDUCT-RU.md new file mode 100644 index 000000000..53f4ab8c8 --- /dev/null +++ b/docs/CODE_OF_CONDUCT-RU.md @@ -0,0 +1,134 @@ + +# Кодекс поведения участников и вкладчиков + +## Наше обещание + +Мы, как члены, вкладчики и лидеры, обязуемся сделать участие в нашем +сообществе свободным от притеснений для всех, независимо от возраста, +размера тела, видимой или невидимой инвалидности, этнической принадлежности, половых характеристик, гендерной +идентичности и самовыражения, уровня опыта, образования, социально-экономического статуса, +национальности, внешнего вида, расы, религии или сексуальной идентичности +и ориентации. + +Мы обязуемся действовать и взаимодействовать таким образом, чтобы способствовать созданию открытого, гостеприимного, +разнообразного, инклюзивного и здорового сообщества. + +## Наши Стандарты + +Примеры поведения, способствующего созданию благоприятной среды для нашего +сообщества, включают: + +* Демонстрация сочувствия и доброты по отношению к другим людям +* Уважительное отношение к различным мнениям, точкам зрения и опыту +* Предоставление и вежливое принятие конструктивной обратной связи +* Принятие ответственности и извинения перед теми, кто пострадал от наших ошибок, +а также извлечение уроков из накопленного опыта +* Сосредоточение внимания на том, что лучше не только для нас как отдельных людей, но и для +всего сообщества в целом. + +Примеры неприемлемого поведения включают: + +* Использование сексуализированных выражений или образов, а также сексуальное внимание или +заигрывания любого рода +* Троллинг, оскорбительные или уничижительные комментарии, а также личные или политические нападки +* Публичные или частные домогательства +* Публикация личной информации других лиц, такой как физический адрес или адрес электронной +почты, без их явного разрешения +* Другое поведение, которое можно обоснованно считать неуместным в +профессиональной среде + +## Правоприменительные обязанности + +Лидеры сообщества несут ответственность за разъяснение и обеспечение соблюдения наших стандартов +приемлемого поведения и предпримут надлежащие и справедливые корректирующие действия в +ответ на любое поведение, которое они сочтут неуместным, угрожающим, оскорбительным +или вредным. + +Лидеры сообщества имеют право и ответственность удалять, редактировать или отклонять +комментарии, коммиты, код, вики-правки, проблемы и другие материалы, которые +не соответствуют настоящему Кодексу поведения, и +при необходимости сообщат причины принятия решений о модерации. + +## Сфера действия + +Этот Кодекс поведения применяется во всех общественных местах, а также применяется, когда +физическое лицо официально представляет сообщество в общественных местах. +Примеры представления нашего сообщества включают использование официального адреса электронной почты, +размещение сообщений через официальную учетную запись в социальных сетях или выступление в качестве назначенного +представителя на онлайн- или оффлайн-мероприятии. + +## Правоприменение + +О случаях оскорбительного, домогательского или иного неприемлемого поведения можно +сообщать лидерам сообщества, ответственным за правоприменение в +[info@rustdesk.com ](mailto:info@rustdesk.com). +Все жалобы будут рассмотрены и расследованы быстро и справедливо. + +Все лидеры сообщества обязаны уважать частную жизнь и безопасность +репортера о любом инциденте. + +## Руководящие принципы воздействия + +Лидеры сообщества будут следовать этим руководящим принципам воздействия на сообщество при определении +последствий любого действия, которое они сочтут нарушением настоящего Кодекса поведения: + +### 1. Правки + +**Воздействие на сообщество**: Использование неподобающих выражений или другого поведения, которое считается +непрофессиональным или нежелательным в сообществе. + +**Последствие**: частное письменное предупреждение от лидеров сообщества, дающее +ясность в отношении характера нарушения и объяснение того, почему +поведение было неуместным. Могут быть запрошены публичные извинения. + + +### 2. Предупреждение + +**Воздействие на сообщество**: нарушение в результате одного инцидента или серии +действий. + +**Последствие**: Предупреждение с последствиями для дальнейшего поведения. Никакого +взаимодействия с вовлеченными лицами, включая нежелательное взаимодействие с +теми, кто обеспечивает соблюдение Кодекса поведения, в течение определенного периода времени. Это +включает в себя избегание взаимодействия в общественных пространствах, а также внешних каналов +, таких как социальные сети. Нарушение этих условий может привести к временному или +постоянному запрету. + +### 3. Временная блокировка + +**Воздействие на сообщество**: Серьезное нарушение стандартов сообщества, включая +длительное неподобающее поведение. + +**Последствие**: Временный запрет на любое взаимодействие или публичное +общение с сообществом в течение определенного периода времени. +В течение этого периода не допускается никакое публичное или частное взаимодействие с вовлеченными лицами, включая незапрашиваемое взаимодействие +с теми, кто обеспечивает соблюдение Кодекса поведения. +Нарушение этих условий может привести к постоянному запрету. + +### 4. Блокировка навсегда + +**Воздействие на сообщество**: Демонстрация модели нарушения +стандартов сообщества, включая постоянное неподобающее поведение, преследование отдельного +лица или агрессию по отношению к классам людей или пренебрежительное отношение к ним. + +**Последствие**: Постоянный запрет на любое публичное взаимодействие внутри +сообщества. + +## Определение + +Настоящий Кодекс поведения адаптирован из [Соглашения о вкладчиках][homepage], +версии 2.0, доступной по ссылке +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Руководящие принципы воздействия на сообщество были вдохновлены +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +Ответы на распространенные вопросы об этом кодексе поведения см. в разделе Часто задаваемые вопросы по адресу +[https://www.contributor-covenant.org/faq][FAQ]. Переводы доступны +по адресу [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to docs/CODE_OF_CONDUCT.md diff --git a/docs/CONTRIBUTING-RU.md b/docs/CONTRIBUTING-RU.md new file mode 100644 index 000000000..acc233d00 --- /dev/null +++ b/docs/CONTRIBUTING-RU.md @@ -0,0 +1,49 @@ +# Вклад в RustDesk + +RustDesk приветствует вклад каждого. +Ниже приведены рекомендации, если вы собираетесь помочь нам: + +## Вклад в развитие + +Вклады в развитие RustDesk или его зависимости должны быть +сделаны в виде `pull request` на GitHub. Каждый такой +`pull request` будет рассмотрен основным участником +(кем-то, у кого есть разрешение на влив исправлений) +и либо помещен в основное дерево, либо Вам будет дан отзыв +о необходимых правках. Все материалы должны соответствовать +этому формату, даже те, которые поступают от основных авторов. + +Если вы хотите поработать над какой-либо проблемой, то пожалуйста, +сначала напишите об этом, создав тикет на GitHub, и описав, +над чем вы хотите поработать. Это делается для того, чтобы +предотвратить дублирование усилий участников по одному и тому же вопросу. + +## Контрольный список для Ваших `pull request` + +- Ответвляйтесь от главной ветки и, при необходимости, делайте `rebase` в текущую `master` + ветку перед отправкой `pull request`. При наличии конфликтов слияния вам будет + предложено их устранить, возможно при помощи того же `rebase`. + +- Коммиты должны быть, по возможности, небольшим, при этом гарантируя, что каждаый + коммит является независимо правильным (т.е., каждый коммит должен компилироваться и проходить тесты). + +- Коммиты должны сопровождаться `Developer Certificate of Origin` + (http://developercertificate.org) подписью, которая укажет на то, что вы (и + ваш работодатель, если это применимо) согласны соблюдать условия + [лицензии проекта](../LICENCE). В `git` это флаг `-s` при использовании `git commit` + +- Если ваш патч не проходит рецензирование или вам нужно, + чтобы его проверил конкретный человек, Вы можете ответить рецензенту через `@`, + в обсуждениях вашего `pull request` или Вы можете запросить рецензию через[email](mailto:info@rustdesk.com). + +- Добавьте тесты, относящиеся к исправленной ошибке или новой функции. + +Для получения конкретных инструкций `git` см. [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow). + +## Кодекс поведения участников и вкладчиков + +Нормы поведения внутри сообщества подробно описаны [здесь](CODE_OF_CONDUCT-RU.md). + +## Общение + +RustDesk контрибьюторы могут посетить [Discord](https://discord.gg/nDceKgxnkV). diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 89% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md index 79e3ba4a2..31fd632e6 100644 --- a/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -27,7 +27,7 @@ efforts from contributors on the same issue. - Commits should be accompanied by a Developer Certificate of Origin (http://developercertificate.org) sign-off, which indicates that you (and your employer if applicable) agree to be bound by the terms of the - [project license](LICENSE). In git, this is the `-s` option to `git commit` + [project license](../LICENCE). In git, this is the `-s` option to `git commit` - If your patch is not getting reviewed or you need a specific person to review it, you can @-reply a reviewer asking for a review in the pull request or a @@ -35,11 +35,11 @@ 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 -https://github.com/rustdesk/rustdesk/blob/master/CODE_OF_CONDUCT.md +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md ## Communication 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/README-AR.md b/docs/README-AR.md similarity index 86% rename from README-AR.md rename to docs/README-AR.md index 2deb4914b..ad7303806 100644 --- a/README-AR.md +++ b/docs/README-AR.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
ServersBuildDockerStructureSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [Tiếng Việt]
لغتك الأم, Doc و RustDesk UI, README نحن بحاجة إلى مساعدتك لترجمة هذا

@@ -21,7 +21,7 @@ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -لمساعدتك على ذلك [`CONTRIBUTING.md`](CONTRIBUTING.md) يرحب بمساهمة الجميع. اطلع على RustDesk. +لمساعدتك على ذلك [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) يرحب بمساهمة الجميع. اطلع على RustDesk. [**؟ RustDesk كيفية يعمل**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -32,9 +32,11 @@ فيما يلي الخوادم التي تستخدمها مجانًا، وقد تتغير طوال الوقت. إذا لم تكن قريبًا من أحد هؤلاء، فقد تكون شبكتك بطيئة. | الموقع | المورد | المواصفات | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## التبعيات @@ -77,14 +79,9 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` -### pynput package تثبيت - -```sh -pip3 install pynput -``` ### vcpkg تثبيت diff --git a/README-CS.md b/docs/README-CS.md similarity index 86% rename from README-CS.md rename to docs/README-CS.md index f6fa2fbf0..d56464eff 100644 --- a/README-CS.md +++ b/docs/README-CS.md @@ -1,11 +1,11 @@

- RustDesk – vaše vzdálená plocha
+ RustDesk – vaše vzdálená plocha
ServerySestavení ze zdrojových kódůDockerStrukturaUkázky
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Potřebujeme Vaši pomoc s překláním textů tohoto ČTIMNE, uživatelského rozhraní aplikace RustDesk a dokumentace k ní do vašeho jazyka

@@ -16,7 +16,7 @@ Dopisujte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https Zase další software pro přístup k ploše na dálku, naprogramovaný v jazyce Rust. Funguje hned tak, jak je – není třeba žádného nastavování. Svá data máte ve svých rukách, bez obav o zabezpečení. Je možné používat námi poskytovaný propojovací/předávací (relay) server, [vytvořit si svůj vlastní](https://rustdesk.com/server), nebo [si dokonce svůj vlastní naprogramovat](https://github.com/rustdesk/rustdesk-server-demo), budete-li chtít. -Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se dozvíte z [`CONTRIBUTING.md`](CONTRIBUTING.md). +Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se dozvíte z [`docs/CONTRIBUTING.md`](CONTRIBUTING.md). [**Jak RustDesk funguje?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -27,9 +27,11 @@ Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se d Níže jsou uvedeny servery zdarma k vašemu použití (údaje se mohou v čase měnit). Pokud se nenacházíte v oblastech světa poblíž nich, spojení může být pomalé. | umístění | dodavatel | parametry | | --------- | ------------- | ------------------ | -| Soul | AWS lightsail | 1 VCPU / 0,5GB RAM | -| Singapur | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## Softwarové součásti, na kterých závisí @@ -71,13 +73,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### Instalace balíčku pynput - -```sh -pip3 install pynput +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Instalace vcpkg diff --git a/docs/README-DA.md b/docs/README-DA.md new file mode 100644 index 000000000..dde5c7a0d --- /dev/null +++ b/docs/README-DA.md @@ -0,0 +1,188 @@ +

+ RustDesk - Your remote desktop
+ Servere • + Byg • + Docker • + Filstruktur • + Skærmbilleder
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ Vi har brug for din hjælp til at oversætte denne README, RustDesk UI og Dokument til dit modersmål +

+ +Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Endnu en fjernskrivebordssoftware, skrevet i Rust. Fungerer ud af æsken, ingen konfiguration påkrævet. Du har fuld kontrol over dine data uden bekymringer om sikkerhed. Du kan bruge vores rendezvous/relay-server, [opsætte din egen](https://rustdesk.com/server), eller [skrive din egen rendezvous/relay-server](https://github.com/rustdesk/rustdesk- server-demo). + +RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for at få hjælp til at komme i gang. + +[**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) + +## Gratis offentlige servere + +Nedenfor er de servere, du bruger gratis, det kan ændre sig med tiden. Hvis du ikke er tæt på en af disse, kan dit netværk være langsomt. + +| Beliggenhed | Udbyder | Specifikation | +| ---------- | ------------- | ------------------ | +| 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 | + +## Afhængigheder + +Desktopversioner bruger [sciter](https://sciter.com/) eller Flutter til GUI, denne vejledning er kun for Sciter. + +Hent venligst sciter dynamic library selv. + +[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) + +## Rå trin til at bygge + +- Forbered din Rust-udviklings-env og C++ build-env + +- Installer [vcpkg](https://github.com/microsoft/vcpkg), og indstil env-variabelen "VCPKG_ROOT" korrekt + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/MacOS: vcpkg install libvpx libyuv opus + +- kør `cargo run` + +## [Byg](https://rustdesk.com/docs/en/dev/build/) + +## Sådan bygger du på 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 +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### vcpkg installation + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### libvpx rettelse (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Byg + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +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 +``` + +### Skift Wayland til X11 (Xorg) + +RustDesk understøtter ikke Wayland. Tjek [dette](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) for at konfigurere Xorg som standard GNOME-session. + +## Wayland-support + +Wayland ser ikke ud til at levere nogen API til at sende tastetryk til andre vinduer. Derfor bruger rustdesk et API fra et lavere niveau, nemlig `/dev/uinput`-enheden (Linux-kerneniveau). + +Når wayland er den kontrollerede side, skal du starte på følgende måde: +```bash +# Start uinput service +$ sudo rustdesk --service +$ rustdesk +``` +**Bemærk**: Wayland-skærmoptagelse bruger forskellige grænseflader. RustDesk understøtter i øjeblikket kun org.freedesktop.portal.ScreenCast. +```bash +$ dbus-send --session --print-reply \ + --dest=org.freedesktop.portal.Desktop \ + /org/freedesktop/portal/desktop \ + org.freedesktop.DBus.Properties.Get \ + string:org.freedesktop.portal.ScreenCast string:version +# Not support +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Support +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` +## Sådan bygger du med Docker + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Kør derefter følgende kommando, hver gang du skal bygge applikationen: +```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 +``` + +Bemærk, at den første bygning kan tage længere tid, før afhængigheder cachelagres, efterfølgende bygninger vil være hurtigere. Derudover, hvis du har brug for at angive forskellige argumenter til bygge-kommandoen, kan du gøre det i slutningen af kommandoen i ``-positionen. For eksempel, hvis du ville bygge en optimeret udgivelsesversion, ville du køre kommandoen ovenfor efterfulgt af `--release`. Den resulterende eksekverbare vil være tilgængelig i målmappen på dit system og kan køres med: + +```sh +target/debug/rustdesk +``` + +Eller, hvis du kører en udgivelses eksekverbar: + +```sh +target/release/rustdesk +``` + +Sørg for, at du kører disse kommandoer fra roden af RustDesk-lageret, ellers kan applikationen muligvis ikke finde de nødvendige ressourcer. Bemærk også, at andre cargo underkommandoer såsom 'install' eller 'run' i øjeblikket ikke understøttes via denne metode, da de ville installere eller køre programmet inde i containeren i stedet for værten. + +## Filstruktur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs funktioner til filoverførsel og nogle andre hjælpefunktioner +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Skærmbillede +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specifik tastatur/mus kontrol +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: lyd/udklipsholder/input/videotjenester og netværksforbindelser +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: starte en peer-forbindelse +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommuniker med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernforbindelse (TCP-hulning) eller relæforbindelse +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Javascript til Flutter webklient + +## Skærmbilleder + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/docs/README-DE.md b/docs/README-DE.md new file mode 100644 index 000000000..8ee4a51fa --- /dev/null +++ b/docs/README-DE.md @@ -0,0 +1,211 @@ +

+ RustDesk - Your remote desktop
+ 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] | [Dansk]
+ Wir brauchen deine Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in deine Muttersprache zu übersetzen. +

+ +Rede mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +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). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Unterstützung beim Start brauchst. + +[**Wie arbeitet RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) + +[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases) + +[**Nächtliche Erstellung**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](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 | + +## Abhängigkeiten + +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) + +## Grobe Schritte zum Kompilieren + +- Bereite deine Rust-Entwicklungsumgebung und C++-Build-Umgebung vor + +- 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` + +- Nutze `cargo run` + +## [Erstellen](https://rustdesk.com/docs/de/dev/build/) + +## Kompilieren auf Linux + +### Ubuntu 18 (Debian 10) + +```sh +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 +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### vcpkg installieren + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### libvpx reparieren (für Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Kompilieren + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +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 +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +### 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-Sitzung zu nutzen. + +## Wayland-Unterstützung + +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 +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +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 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 +``` + +Oder, wenn du eine Releaseversion benutzt: + +```sh +target/release/rustdesk +``` + +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/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 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 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, 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 + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/README-EO.md b/docs/README-EO.md similarity index 83% rename from README-EO.md rename to docs/README-EO.md index b9af26102..7471636eb 100644 --- a/README-EO.md +++ b/docs/README-EO.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
ServilojKompiliDockerStrukturoEkrankopio
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [한국어] | [العربي] | [Tiếng Việt]
Ni bezonas helpon traduki tiun README kaj la interfacon al via denaska lingvo

@@ -15,7 +15,7 @@ Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twit Denove alia fora labortabla programo, skribita en Rust. Ĝi funkcias elskatole, ne bezonas konfiguraĵon. Vi havas la tutan kontrolon sur viaj datumoj, sen zorgo pri sekureco. Vi povas uzi nian servilon rendezvous/relajsan, [agordi vian propran](https://rustdesk.com/server), aŭ [skribi vian propran servilon rendezvous/relajsan](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`CONTRIBUTING.md`](CONTRIBUTING.md) por helpo komenci. +RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) por helpo komenci. [**BINARA ELŜUTO**](https://github.com/rustdesk/rustdesk/releases) @@ -24,9 +24,11 @@ RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`CONTRIBUTING.md`](CONTRIBUTING.m Malsupre estas la serviloj, kiuj vi uzas senpage, ĝi povas ŝanĝi laŭlonge de la tempo. Se vi ne estas proksima de unu de tiuj, via reto povas esti malrapida. | Situo | Vendanto | Detaloj | | --------- | ------------- | ------------------ | -| Seulo | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapuro | Vultr | 1 VCPU / 1GB RAM | -| Dalaso | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## Dependantaĵoj @@ -64,7 +66,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Instali vcpkg diff --git a/README-ES.md b/docs/README-ES.md similarity index 53% rename from README-ES.md rename to docs/README-ES.md index 1aab59213..16f65adcc 100644 --- a/README-ES.md +++ b/docs/README-ES.md @@ -1,164 +1,198 @@ -

- RustDesk - Your remote desktop
- Servidores • - Compilar • - Docker • - Estructura • - Captura de pantalla
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
- Necesitamos tu ayuda para traducir este README a tu idioma -

- -Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) - -[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) - -Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de sus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [set up your own](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). - -RustDesk agradece la contribución de todo el mundo. Ve [`CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda inicial. - -[**DESCARGA DE BINARIOS**](https://github.com/rustdesk/rustdesk/releases) - -## Servidores gratis de uso público - -A continuación se muestran los servidores que está utilizando de forma gratuita, puede cambiar en algún momento. Si no estás cerca de uno de ellos, tu red puede ser lenta. - -| Ubicación | Vendedor | Especificación | -| --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | - -## Dependencies - -La versión Desktop usa [sciter](https://sciter.com/) para GUI, por favor bajate la librería sciter tu mismo.. - -[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) - -## Pasos para compilar desde el inicio - -- Prepara el entono de desarrollo de Rust y el entorno de compilación de C++ y Rust. - -- Instala [vcpkg](https://github.com/microsoft/vcpkg), y configura la variable de entono `VCPKG_ROOT` correctamente. - - - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static - - Linux/Osx: vcpkg install libvpx libyuv opus - -- run `cargo run` - -## Como compilar en 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 -``` - -### Fedora 28 (CentOS 8) - -```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel -``` - -### Arch (Manjaro) - -```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### Install vcpkg - -```sh -git clone https://github.com/microsoft/vcpkg -cd vcpkg -git checkout 2021.12.01 -cd .. -vcpkg/bootstrap-vcpkg.sh -export VCPKG_ROOT=$HOME/vcpkg -vcpkg/vcpkg install libvpx libyuv opus -``` - -### Soluciona libvpx (Para Fedora) - -```sh -cd vcpkg/buildtrees/libvpx/src -cd * -./configure -sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile -sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile -make -cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ -cd -``` - -### Compila - -```sh -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -source $HOME/.cargo/env -git clone https://github.com/rustdesk/rustdesk -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 -``` - -### Cambia Wayland a X11 (Xorg) - -RustDesk no soporta Wayland. Comprueba [aquí](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME. - -## Como compilar con Docker - -Empieza clonando el repositorio y compilando el contenedor de docker: - -```sh -git clone https://github.com/rustdesk/rustdesk -cd rustdesk -docker build -t "rustdesk-builder" . -``` - -Entonces, cada vez que necesites compilar una modificación, ejecuta el siguiente comando: - -```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 -``` - -Ten en cuenta que la primera compilación puede tardar más tiempo antes de que las dependencias se almacenen en la caché, las siguientes compilaciones serán más rápidas. Además, si necesitas especificar diferentes argumentos a la orden de compilación, puede hacerlo al final de la linea de comandos en el apartado ``. Por ejemplo, si desea compilar una versión optimizada para publicación, deberá ejecutar el comando anterior seguido de `--release`. El ejecutable resultante estará disponible en la carpeta de destino en su sistema, y puede ser ejecutado con: - -```sh -target/debug/rustdesk -``` - -O si estas ejecutando una versión para su publicación: - -```sh -target/release/rustdesk -``` - -Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del repositorio de RustDesk, de lo contrario la aplicación puede ser incapaz de encontrar los recursos necesarios. También hay que tener en cuenta que otros subcomandos de carga como `install` o `run` no estan actualmente soportados via este metodo y podrían requerir ser instalados dentro del contenedor y no en el host. - -## Estructura de archivos - -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, configuración, tcp/udp wrapper, protobuf, fs funciones para transferencia de ficheros, y alguna función de utilidad. -- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de pantalla -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control específico por cada plataforma para el teclado/ratón -- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: sonido/portapapeles/entrada/servicios de video, y conexiones de red -- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar una conexión "peer to peer" -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed") -- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma -- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para cliente web Flutter - -## Captura de pantalla - -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) - -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) - -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) - -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) +

+ RustDesk - Your remote desktop
+ Servidores • + Compilar • + Docker • + Estructura • + Capturas de pantalla
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ Necesitamos tu ayuda para traducir este README a tu idioma +

+ +Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de tus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [instalar el tuyo](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). + +RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda para empezar. + +[**¿Cómo funciona rustdesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) + +[**DESCARGA DE BINARIOS**](https://github.com/rustdesk/rustdesk/releases) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Servidores gratis de uso público + +A continuación se muestran los servidores gratuitos, pueden cambiar a medida que pasa el tiempo. Si no estás cerca de uno de ellos, tu conexión puede ser lenta. + +| Ubicación | Compañía | Especificación | +| --------- | ------------- | ------------------ | +| 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 | + +## Dependencias + +La versión Desktop usa [Sciter](https://sciter.com/) o Flutter para el GUI, este tutorial es solo para Sciter. + +Por favor descarga la librería dinámica de Sciter tu mismo. + +[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) + +## Pasos para compilar desde el inicio + +- Prepara el entorno de desarrollo de Rust y el entorno de compilación de C++ y Rust. + +- Instala [vcpkg](https://github.com/microsoft/vcpkg), y configura la variable de entono `VCPKG_ROOT` correctamente. + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + - Linux/Osx: vcpkg install libvpx libyuv opus + +- Corre `cargo run` + +## Como compilar en 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 +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Instala vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Arregla libvpx (Para Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Compila + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +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 +``` + +### Cambia Wayland a X11 (Xorg) + +RustDesk no soporta Wayland. Lee [esto](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) para configurar Xorg en la sesión por defecto de GNOME. + +## Soporte para Wayland + +Wayland no parece proporcionar ninguna API para enviar pulsaciones de teclas a otras ventanas. Por lo tanto, rustdesk usa una API de nivel bajo, a saber, el dispositivo `/dev/uinput` (a nivel del kernel de Linux). + +Cuando wayland esta del lado controlado, hay que iniciar de la siguiente manera: +```bash +# Empezar el servicio uinput +$ sudo rustdesk --service +$ rustdesk +``` +**Aviso**: La grabación de pantalla de Wayland utiliza diferentes interfaces. RustDesk actualmente sólo soporta 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 +# No soportado +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Soportado +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` + +## Como compilar con Docker + +Empieza clonando el repositorio y compilando el contenedor de docker: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Entonces, cada vez que necesites compilar una modificación, ejecuta el siguiente comando: + +```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 +``` + +Ten en cuenta que la primera compilación puede tardar más tiempo antes de que las dependencias se almacenen en la caché, las siguientes compilaciones serán más rápidas. Además, si necesitas especificar diferentes argumentos al comando de compilación, puedes hacerlo al final del comando en la posición ``. Por ejemplo, si deseas compilar una versión optimizada para publicación, deberas ejecutar el comando anterior seguido de `--release`. El ejecutable resultante estará disponible en la carpeta de destino en tu sistema, y puede ser ejecutado con: + +```sh +target/debug/rustdesk +``` + +O si estas ejecutando una versión para su publicación: + +```sh +target/release/rustdesk +``` + +Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del repositorio de RustDesk, de lo contrario la aplicación puede ser incapaz de encontrar los recursos necesarios. También ten en cuenta que otros subcomandos de cargo como `install` o `run` no estan actualmente soportados usando este metodo, ya que instalarían o ejecutarían el programa dentro del contenedor en lugar del host. + +## Estructura de archivos + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec de video, configuración, tcp/udp wrapper, protobuf, funciones para transferencia de archivos, y otras funciones de utilidad. +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: captura de pantalla +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: control del teclado/mouse especificos de cada plataforma +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: sonido/portapapeles/input/servicios de video, y conexiones de red +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: iniciar una conexión "peer to peer" +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicación con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), esperar la conexión remota directa ("TCP hole punching") o conexión indirecta ("relayed") +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico de cada plataforma +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter + +## Capturas de pantalla + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/docs/README-FA.md b/docs/README-FA.md new file mode 100644 index 000000000..496e81849 --- /dev/null +++ b/docs/README-FA.md @@ -0,0 +1,174 @@ +

+ RustDesk - Your remote desktop
+ تصاویر محیط نرم‌افزار • + ساختار • + داکر • + ساخت • + سرور +

+

[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) + + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +راست‌دسک (RustDesk) نرم‌افزاری برای کارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. + +می‌توانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا +[ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk). + +ما از مشارکت همه استقبال می کنیم. برای راهنمایی جهت مشارکت به[`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. + +[راست‌دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) + +[دریافت نرم‌افزار](https://github.com/rustdesk/rustdesk/releases) + +## سرورهای عمومی رایگان + +شما مي‌توانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر می‌کند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد. +| موقعیت | سرویس دهنده | مشخصات | +| --------- | ------------- | ------------------ | +| کره‌ی جنوبی، سئول | 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 را خودتان دانلود کنید از این منابع دریافت کنید. + +- [ویندوز](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 منتقل خواهیم کرد. + +## نیازمندی‌های ساخت + +- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید + +- نرم افزار [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/) + +## نحوه ساخت بر روی لینوکس + +### ساخت بر روی (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 +``` + +### ساخت بر روی (Fedora 28 (CentOS 8 + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### ساخت بر روی (Arch (Manjaro + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### نرم افزار vcpkg را نصب کنید + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### رفع ایراد libvpx (برای فدورا) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### ساخت + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +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 +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/) را کلیک کنید. + +## نحوه ساخت با داکر + +این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید + +```sh +git clone https://github.com/rustdesk/rustdesk +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--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور: + +```sh +target/debug/rustdesk +``` + +یا برای نسخه بهینه سازی شده دستور زیر را اجرا کنید: + +```sh +target/release/rustdesk +``` + +لطفاً اطمینان حاصل کنید که این دستورات را از پوشه مخزن RustDesk اجرا می کنید، در غیر این صورت ممکن است برنامه نتواند منابع مورد نیاز را پیدا کند. همچنین توجه داشته باشید که سایر دستورات فرعی Cargo مانند `install` یا `run` در حال حاضر از طریق این روش پشتیبانی نمی شوند زیرا برنامه به جای سیستم عامل میزبان, در داخل کانتینر نصب و اجرا میشود. + +## ساختار پوشه ها + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs functions for file transfer, and some other utility functions +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: screen capture +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform specific keyboard/mouse control +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: audio/clipboard/input/video services, and network connections +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start a peer connection +- **[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 + +## تصاویر محیط نرم‌افزار + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/README-FI.md b/docs/README-FI.md similarity index 84% rename from README-FI.md rename to docs/README-FI.md index 5f38d2e42..f7a087087 100644 --- a/README-FI.md +++ b/docs/README-FI.md @@ -1,11 +1,11 @@

- RustDesk - Etätyöpöytäsi
+ RustDesk - Etätyöpöytäsi
PalvelimetRakennaDockerRakenneTilannevedos
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi

@@ -15,7 +15,7 @@ Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](htt Vielä yksi etätyöpöytäohjelmisto, ohjelmoitu Rust-kielellä. Toimii suoraan pakkauksesta, ei tarvitse asetusta. Hallitset täysin tietojasi, ei tarvitse murehtia turvallisuutta. Voit käyttää meidän rendezvous/relay-palvelinta, [aseta omasi](https://rustdesk.com/server), tai [kirjoittaa oma rendezvous/relay-palvelin](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`CONTRIBUTING.md`](CONTRIBUTING.md) avun saamiseksi. +RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) avun saamiseksi. [**BINAARILATAUS**](https://github.com/rustdesk/rustdesk/releases) @@ -24,9 +24,11 @@ RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`CONT Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan mittaan. Jos et ole lähellä yhtä näistä, verkkosi voi olla hidas. | Sijainti | Myyjä | Määrittely | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## Riippuvuudet @@ -64,7 +66,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Asenna vcpkg diff --git a/README-FR.md b/docs/README-FR.md similarity index 83% rename from README-FR.md rename to docs/README-FR.md index 9a303e6e6..fdb253bd0 100644 --- a/README-FR.md +++ b/docs/README-FR.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
Serveurs - Build - Docker - Structure - Images
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle.

@@ -15,7 +15,7 @@ Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https:/ Encore un autre logiciel de bureau à distance, écrit en Rust. Fonctionne directement, aucune configuration n'est nécessaire. Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, [configurer le vôtre](https://rustdesk.com/server), ou [écrire votre propre serveur de rendez-vous/relais](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk accueille les contributions de tout le monde. Voir [`CONTRIBUTING.md`](CONTRIBUTING.md) pour plus d'informations. +RustDesk accueille les contributions de tout le monde. Voir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) pour plus d'informations. [**TÉLÉCHARGEMENT BINAIRE**](https://github.com/rustdesk/rustdesk/releases) @@ -23,9 +23,13 @@ RustDesk accueille les contributions de tout le monde. Voir [`CONTRIBUTING.md`]( Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut changer au fil du temps. Si vous n'êtes pas proche de l'un d'entre eux, votre réseau peut être lent. -- Séoul, AWS lightsail, 1 VCPU/0.5G RAM -- Singapour, Vultr, 1 VCPU/1G RAM -- Dallas, Vultr, 1 VCPU/1G RAM +| Location | Vendor | Specification | +| --------- | ------------- | ------------------ | +| 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 | ## Dépendances @@ -63,7 +67,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Installer vcpkg diff --git a/README-HU.md b/docs/README-HU.md similarity index 86% rename from README-HU.md rename to docs/README-HU.md index 7055ed446..6c22a3b7c 100644 --- a/README-HU.md +++ b/docs/README-HU.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
SzerverekÉpítésDockerStruktúraKépernyőképek
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Kell a segítséged, hogy lefordítsuk ezt a README-t, a RustDesk UI-t és a Dokumentációt az anyanyelvedre

@@ -17,7 +17,7 @@ A RustDesk egy távoli elérésű asztali szoftver, Rust-ban írva. Működik mi ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lásd a [`CONTRIBUTING.md`](CONTRIBUTING.md) fájlt a kezdéshez. +A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lásd a [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) fájlt a kezdéshez. [**Hogyan működik a RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -32,9 +32,11 @@ A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lás Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingyen használhatsz. Ezek a szerverek változhatnak a jövőben, illetve a hálózatuk lehet hogy lassú lehet. | Hely | Host | Specifikáció | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## Dependencies @@ -76,13 +78,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### Telepítsd a pynput csomagot - -```sh -pip3 install pynput +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Telepítsd a vcpkg-t diff --git a/README-ID.md b/docs/README-ID.md similarity index 83% rename from README-ID.md rename to docs/README-ID.md index 363d4263b..9616cd31d 100644 --- a/README-ID.md +++ b/docs/README-ID.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
ServersBuildDockerStructureSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Kami membutuhkan bantuan Anda untuk menerjemahkan README ini dan RustDesk UI ke bahasa asli anda

@@ -15,7 +15,7 @@ Birbincang bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](ht Perangkat lunak desktop jarak jauh lainnya, ditulis dengan Rust. Bekerja begitu saja, tidak memerlukan konfigurasi. Anda memiliki kendali penuh atas data Anda, tanpa khawatir tentang keamanan. Anda dapat menggunakan server rendezvous/relay kami, [konfigurasi server sendiri](https://rustdesk.com/server), or [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk menyambut baik kontribusi dari semua orang. Lihat [`CONTRIBUTING.md`](CONTRIBUTING.md) untuk membantu sebelum memulai. +RustDesk menyambut baik kontribusi dari semua orang. Lihat [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) untuk membantu sebelum memulai. [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) @@ -24,9 +24,11 @@ RustDesk menyambut baik kontribusi dari semua orang. Lihat [`CONTRIBUTING.md`](C Di bawah ini adalah server yang bisa Anda gunakan secara gratis, dapat berubah seiring waktu. Jika Anda tidak dekat dengan salah satu dari ini, jaringan Anda mungkin lambat. | Lokasi | Vendor | Spesifikasi | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## Dependencies @@ -64,7 +66,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Install vcpkg diff --git a/README-IT.md b/docs/README-IT.md similarity index 84% rename from README-IT.md rename to docs/README-IT.md index a3f36af55..f074510c9 100644 --- a/README-IT.md +++ b/docs/README-IT.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
ServersCompilazioneDockerStrutturaScreenshots
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Abbiamo bisogno del tuo aiuto per tradurre questo README e la RustDesk UI nella tua lingua nativa

@@ -15,7 +15,7 @@ Chatta con noi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twi Ancora un altro software per il controllo remoto del desktop, scritto in Rust. Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza. Puoi utilizzare il nostro server rendezvous/relay, [configurare il tuo](https://rustdesk.com/server) o [scrivere il tuo rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come inizare a contribuire, vedere [`CONTRIBUTING.md`](CONTRIBUTING.md). +RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come inizare a contribuire, vedere [`docs/CONTRIBUTING.md`](CONTRIBUTING.md). [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) @@ -24,9 +24,11 @@ RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come ini Qui sotto trovate i server che possono essere usati gratuitamente, la lista potrebbe cambiare nel tempo. Se non si è vicini a uno di questi server, la vostra connessione potrebbe essere lenta. | Posizione | Vendor | Specifiche | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## Dipendenze @@ -64,7 +66,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Installare vcpkg diff --git a/README-JP.md b/docs/README-JP.md similarity index 84% rename from README-JP.md rename to docs/README-JP.md index fb55d0ced..36c74dfed 100644 --- a/README-JP.md +++ b/docs/README-JP.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
ServersBuildDockerStructureSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
このREADMEをあなたの母国語に翻訳するために、あなたの助けが必要です。

@@ -14,11 +14,11 @@ Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitt [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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)。 ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDeskは誰からの貢献も歓迎します。 貢献するには [`CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。 +RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) を参照してください。 [**RustDeskはどの様に動くのか?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -29,9 +29,11 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`CONTRIBU 下記のサーバーは、無料で使用できますが、後々変更されることがあります。これらのサーバーから遠い場合、接続が遅い可能性があります。 | Location | Vendor | Specification | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## 依存関係 @@ -56,7 +58,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`CONTRIBU -## [Build](https://rustdesk.com/docs/en/dev/build/) +## [ビルド](https://rustdesk.com/docs/en/dev/build/) ## Linuxでのビルド手順 @@ -75,13 +77,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### Install pynput package - -```sh -pip3 install pynput +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Install vcpkg @@ -109,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 @@ -158,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)**: スクリーンキャプチャ @@ -169,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 +## スナップショット ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/README-KR.md b/docs/README-KR.md similarity index 85% rename from README-KR.md rename to docs/README-KR.md index 00564e298..8cefbbcee 100644 --- a/README-KR.md +++ b/docs/README-KR.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
ServersBuildDockerStructureSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [العربي] | [Tiếng Việt]
README를 모국어로 번역하기 위한 당신의 도움의 필요합니다.

@@ -18,7 +18,7 @@ Rust로 작성되었고, 설정없이 바로 사용할 수 있는 원격 데스 ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`CONTRIBUTING.md`](CONTRIBUTING.md)를 참조해주세요. +RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/CONTRIBUTING.md`](CONTRIBUTING.md)를 참조해주세요. [**RustDesk는 어떻게 작동하는가?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -29,9 +29,11 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`CONTRI 표에 있는 서버는 무료로 사용할 수 있지만 추후 변경될 수도 있습니다. 이 서버에서 멀다면, 네트워크가 느려질 가능성도 있습니다. | Location | Vendor | Specification | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## 의존관계 @@ -73,13 +75,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### Install pynput package - -```sh -pip3 install pynput +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Install vcpkg diff --git a/README-ML.md b/docs/README-ML.md similarity index 89% rename from README-ML.md rename to docs/README-ML.md index d2931a2c7..288a78db8 100644 --- a/README-ML.md +++ b/docs/README-ML.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
ServersBuildDockerStructureSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
ഈ README നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്

@@ -15,7 +15,7 @@ റസ്റ്റിൽ എഴുതിയ മറ്റൊരു റിമോട്ട് ഡെസ്ക്ടോപ്പ് സോഫ്റ്റ്‌വെയർ. ബോക്‌സിന് പുറത്ത് പ്രവർത്തിക്കുന്നു, കോൺഫിഗറേഷൻ ആവശ്യമില്ല. സുരക്ഷയെക്കുറിച്ച് ആശങ്കകളൊന്നുമില്ലാതെ, നിങ്ങളുടെ ഡാറ്റയുടെ പൂർണ്ണ നിയന്ത്രണം നിങ്ങൾക്കുണ്ട്. നിങ്ങൾക്ക് ഞങ്ങളുടെ rendezvous/relay സെർവർ ഉപയോഗിക്കാം, [സ്വന്തമായി സജ്ജീകരിക്കുക](https://rustdesk.com/server), അല്ലെങ്കിൽ [നിങ്ങളുടെ സ്വന്തം rendezvous/relay സെർവർ എഴുതുക](https://github.com/rustdesk/rustdesk-server-demo). -എല്ലാവരുടെയും സംഭാവനയെ RustDesk സ്വാഗതം ചെയ്യുന്നു. ആരംഭിക്കുന്നതിനുള്ള സഹായത്തിന് [`CONTRIBUTING.md`](CONTRIBUTING.md) കാണുക. +എല്ലാവരുടെയും സംഭാവനയെ RustDesk സ്വാഗതം ചെയ്യുന്നു. ആരംഭിക്കുന്നതിനുള്ള സഹായത്തിന് [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) കാണുക. [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) @@ -24,9 +24,11 @@ നിങ്ങൾ സൗജന്യമായി ഉപയോഗിക്കുന്ന സെർവറുകൾ ചുവടെയുണ്ട്, അത് സമയത്തിനനുസരിച്ച് മാറിയേക്കാം. നിങ്ങൾ ഇവയിലൊന്നിനോട് അടുത്തല്ലെങ്കിൽ, നിങ്ങളുടെ നെറ്റ്‌വർക്ക് സ്ലോ ആയേക്കാം. | സ്ഥാനം | കച്ചവടക്കാരൻ | വിവരണം | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## ഡിപെൻഡൻസികൾ @@ -64,7 +66,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### ആർച് (മഞ്ചാരോ) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### vcpkg ഇൻസ്റ്റാൾ ചെയ്യുക diff --git a/README-NL.md b/docs/README-NL.md similarity index 83% rename from README-NL.md rename to docs/README-NL.md index 5db299e7c..1aca2b893 100644 --- a/README-NL.md +++ b/docs/README-NL.md @@ -1,11 +1,11 @@

- RustDesk - Jouw verbinding op afstand
+ RustDesk - Jouw verbinding op afstand
ServersBouwenDockerStructuurSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
We hebben je hulp nodig om deze README te vertalen naar jouw moedertaal

@@ -15,7 +15,7 @@ Praat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twit Nog weer een applicatie voor toegang op afstand, geschreven in Rust. Werkt meteen, geen configuratie nodig. Je hebt volledig beheer over je data, zonder na te hoeven denken over veiligheid. Je kunt onze rendez-vous/relay-server gebruiken, [je eigen server opzetten](https://rustdesk.com/blog/id-relay-set), of [je eigen rendez-vous/relay-server schrijven](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk verwelkomt bijdragen van iedereen. Zie [`CONTRIBUTING.md`](CONTRIBUTING.md) om te lezen hoe je van start kunt gaan. +RustDesk verwelkomt bijdragen van iedereen. Zie [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) om te lezen hoe je van start kunt gaan. [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) @@ -24,9 +24,11 @@ RustDesk verwelkomt bijdragen van iedereen. Zie [`CONTRIBUTING.md`](CONTRIBUTING Onderstaande servers zijn de servers die je gratis kunt gebruiken, ze kunnen op termijn veranderen. Als je niet fysiek dichtbij een van deze servers bent, kan je verbinding traag werken. | Locatie | Aanbieder | Specificaties | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## Afhankelijkheden @@ -64,7 +66,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Installatie van vcpkg diff --git a/README-PL.md b/docs/README-PL.md similarity index 83% rename from README-PL.md rename to docs/README-PL.md index 119af95cf..85c5f4a61 100644 --- a/README-PL.md +++ b/docs/README-PL.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
SerweryKompilacjaDockerStrukturaSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język

@@ -15,7 +15,7 @@ Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](http Kolejny program do zdalnego pulpitu, napisany w Rust. Działa od samego początku, nie wymaga konfiguracji. Masz pełną kontrolę nad swoimi danymi, bez obaw o bezpieczeństwo. Możesz skorzystać z naszego darmowego serwera publicznego , [skonfigurować własny](https://rustdesk.com/server), lub [napisać własny serwer rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk zaprasza do współpracy każdego. Zobacz [`CONTRIBUTING.md`](CONTRIBUTING.md) pomoc w uruchomieniu programu. +RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) pomoc w uruchomieniu programu. [**POBIERZ KOMPILACJE**](https://github.com/rustdesk/rustdesk/releases) @@ -24,9 +24,11 @@ RustDesk zaprasza do współpracy każdego. Zobacz [`CONTRIBUTING.md`](CONTRIBUT Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska. | Lokalizacja | Dostawca | Specyfikacja | | --------- | ------------- | ------------------ | -| Seul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapur | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## Zależności @@ -64,7 +66,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Zainstaluj vcpkg diff --git a/README-PTBR.md b/docs/README-PTBR.md similarity index 84% rename from README-PTBR.md rename to docs/README-PTBR.md index 955456256..f9d5e0fc3 100644 --- a/README-PTBR.md +++ b/docs/README-PTBR.md @@ -1,11 +1,11 @@

- RustDesk - Seu desktop remoto
+ RustDesk - Seu desktop remoto
ServidoresCompilarDockerEstruturaScreenshots
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Precisamos de sua ajuda para traduzir este README e a UI do RustDesk para sua língua nativa

@@ -15,7 +15,7 @@ Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://t Mais um software de desktop remoto, escrito em Rust. Funciona por padrão, sem necessidade de configuração. Você tem completo controle de seus dados, sem se preocupar com segurança. Você pode usar nossos servidores de rendezvous/relay, [configurar seu próprio](https://rustdesk.com/server), ou [escrever seu próprio servidor de rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk acolhe contribuições de todos. Leia [`CONTRIBUTING.md`](CONTRIBUTING.md) para ver como começar. +RustDesk acolhe contribuições de todos. Leia [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ver como começar. [**DOWNLOAD DE BINÁRIOS**](https://github.com/rustdesk/rustdesk/releases) @@ -25,9 +25,11 @@ Abaixo estão os servidores que você está utilizando de graça, ele pode mudar | Localização | Fornecedor | Especificações | | ----------- | ------------- | ------------------ | -| Seul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapura | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | +| 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 | ## Dependências @@ -65,7 +67,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Instale vcpkg diff --git a/README-RU.md b/docs/README-RU.md similarity index 79% rename from README-RU.md rename to docs/README-RU.md index a9d81152c..242341a6b 100644 --- a/README-RU.md +++ b/docs/README-RU.md @@ -1,11 +1,11 @@

- RustDesk - Ваш удаленый рабочий стол
+ RustDesk - Ваш удаленый рабочий стол
ServersBuildDockerStructureSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
Нам нужна ваша помощь для перевода этого README и RustDesk UI на ваш родной язык

@@ -13,11 +13,11 @@ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk приветствует вклад каждого. Смотрите [`CONTRIBUTING.md`](CONTRIBUTING.md) для помощи в начале работы. +RustDesk приветствует вклад каждого. Ознакомьтесь с [`docs/CONTRIBUTING-RU.md`](CONTRIBUTING-RU.md) в начале работы для понимания. [**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -27,12 +27,16 @@ RustDesk приветствует вклад каждого. Смотрите [` ## Бесплатные общедоступные серверы -Ниже приведены серверы, для бесплатного использования, они могут меняться со временем. Если вы не находитесь рядом с одним из них, ваша сеть может работать медленно. -| Местонахождение | Поставщик | Технические характеристики | +Ниже приведены бесплатные публичные сервера, используемые по умолчанию. Имейте ввиду, они могут меняться со временем. Также стоит отметить, что скорость работы сети зависит от вашего местоположения и расстояния до серверов. Подключение происходит к ближайшему доступному. +| Расположение | Поставщик | Технические характеристики | | --------- | ------------- | ------------------ | -| Сеул | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Сингапур | Vultr | 1 VCPU / 1GB RAM | -| Даллас | Vultr | 1 VCPU / 1GB RAM | | +| Сеул | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Сингапур | Vultr | 1 vCPU / 1GB RAM | +| Даллас | Vultr | 1 vCPU / 1GB 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 | ## Зависимости @@ -72,7 +76,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Установка vcpkg @@ -158,7 +162,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 NAT) или ретранслируемого соединения - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфичный для платформы код -## Снимки +## Скриншоты ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-UA.md b/docs/README-UA.md new file mode 100644 index 000000000..3615b9064 --- /dev/null +++ b/docs/README-UA.md @@ -0,0 +1,197 @@ +

+ RustDesk - Ваш віддалений робочий стіл
+ Servers • + Build • + Docker • + Structure • + Snapshot
+ [English] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ Нам потрібна ваша допомога для перекладу цього README і RustDesk UI на вашу рідну мову +

+ +Спілкування з нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Ще одне програмне забезпечення для віддаленого робочого столу, написане на Rust. Працює з коробки, не потребує налаштування. Ви повністю контролюєте свої дані, не турбуючись про безпеку. Ви можете використовувати наш сервер ретрансляції, [налаштувати свій власний](https://rustdesk.com/server), або [написати свій власний сервер ретрансляції](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) для допомоги на початку роботи. + +[**Як працює RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) + +[**ЗАВАНТАЖИТИ ДОДАТОК**](https://github.com/rustdesk/rustdesk/releases) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Безкоштовні загальнодоступні сервери + +Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно. +| Місцезнаходження | Постачальник | Технічні характеристики | +| --------- | ------------- | ------------------ | +| Сеул | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Сінгапур | Vultr | 1 vCPU / 1GB RAM | +| Даллас | Vultr | 1 vCPU / 1GB RAM +Німеччина | Hetzner | 2 vCPU / 4GB RAM | 2 VCPU / 4GB RAM | Німеччина | 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 | + +## Залежності + +Настільні версії використовують [sciter](https://sciter.com/) для графічного інтерфейсу, завантажте динамічну бібліотеку sciter самостійно. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +Мобільні версії використовують Flutter. У майбутньому ми перенесемо настільну версію зі Sciter на Flutter. + +## Первинні кроки для складання + +- Підготуйте середовище розробки 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 + +- Запустіть `cargo run` + +## Як зібрати на 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 +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Встановлення vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd ... +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Fedora 28 (CentOS 8) + +````sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Встановлення vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2021.12.01 +cd ... +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus +``` + +### Виправлення libvpx (для Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Збірка + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +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 +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +### Змініть Wayland на X11 (Xorg) + +RustDesk не підтримує Wayland. Дивіться [цей документ](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) для налаштування Xorg як сеансу GNOME за замовчуванням. + +## Як зібрати за допомогою Docker + +Почніть з клонування сховища та створення docker-контейнера: + +```sh +git clone https://github.com/rustdesk/rustdesk +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`. Отриманий виконуваний файл буде доступний у цільовій папці вашої системи і може бути запущений за допомогою: + +```sh +target/debug/rustdesk +``` + +Або, якщо ви використовуєте виконуваний файл релізу: + +```sh +target/release/rustdesk +``` + +Будь ласка, переконайтеся, що ви запускаєте ці команди з кореня сховища RustDesk, інакше додаток не зможе знайти необхідні ресурси. Також зверніть увагу, що інші cargo підкоманди, такі як `install` або `run`, наразі не підтримуються цим методом, оскільки вони будуть встановлювати або запускати програму всередині контейнера, а не на хості. + +## Структура файлів + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: відеокодек, конфіг, обгортка tcp/udp, protobuf, функції fs для передавання файлів і деякі інші службові функції +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: захоплення екрана +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: специфічне для платформи керування клавіатурою/мишею +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графічний інтерфейс користувача +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: сервіси аудіо/буфера обміну/вводу/відео та мережевих підключень +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: однорангове з'єднання +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: зв'яжіться з [rustdesk-server](https://github.com/rustdesk/rustdesk-server), дочекайтеся віддаленого прямого (обхід TCP NAT) або ретрансльованого з'єднання +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: специфічний для платформи код + +## Знімки + +![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) + +![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) + +![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) + +![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) diff --git a/README-VN.md b/docs/README-VN.md similarity index 86% rename from README-VN.md rename to docs/README-VN.md index b39005a31..295f54c6b 100644 --- a/README-VN.md +++ b/docs/README-VN.md @@ -1,11 +1,11 @@

- RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn
+ RustDesk - Phần mềm điểu khiển máy tính từ xa dành cho bạn
Máy chủBuildDockerCấu trúc tệp tinSnapshot
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
Chúng tôi cần sự gíup đỡ của bạn để dịch trang README này, RustDesk UItài liệu sang ngôn ngữ bản địa của bạn

@@ -17,7 +17,7 @@ Một phần mềm điểu khiển máy tính từ xa, đuợc lập trình bằ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`CONTRIBUTING.md`](CONTRIBUTING.md). +Mọi người đều đuợc chào đón để đóng góp vào RustDesk. Để bắt đầu, hãy đọc [`docs/CONTRIBUTING.md`](CONTRIBUTING.md). [**RustDesk hoạt động như thế nào?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -33,9 +33,11 @@ Dưới đây là những máy chủ mà bạn có thể sử dụng mà không | Địa điểm | Nhà cung cấp | Cấu hình | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 VCPU / 0.5GB RAM | -| Singapore | Vultr | 1 VCPU / 1GB RAM | -| Dallas | Vultr | 1 VCPU / 1GB RAM | | +| 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 | ## Dependencies @@ -77,13 +79,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio -``` - -### Cách tải về gói hàng pynput - -```sh -pip3 install pynput +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### Cách cài vcpkg diff --git a/README-ZH.md b/docs/README-ZH.md similarity index 50% rename from README-ZH.md rename to docs/README-ZH.md index 0c3e7d5c1..7ec87ec50 100644 --- a/README-ZH.md +++ b/docs/README-ZH.md @@ -1,11 +1,11 @@

- RustDesk - Your remote desktop
+ RustDesk - Your remote desktop
服务器编译Docker结构截图
- [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [English] | [Українська] | [česky] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]

Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) @@ -16,17 +16,21 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https: 或者[自己设置](https://rustdesk.com/server), 亦或者[开发您的版本](https://github.com/rustdesk/rustdesk-server-demo)。 -欢迎大家贡献代码, 请看 [`CONTRIBUTING.md`](CONTRIBUTING.md). +欢迎大家贡献代码, 请看 [`docs/CONTRIBUTING.md`](CONTRIBUTING.md). [**可执行程序下载**](https://github.com/rustdesk/rustdesk/releases) -## 免费公共服务器 +## 免费的公共服务器 -以下是您免费使用的服务器,它可能会随着时间的推移而变化。如果您不靠近其中之一,您的网络可能会很慢。 +以下是您可以使用的、免费的、会随时更新的公共服务器列表,在国内也许网速会很慢或者无法访问。 -- 首尔, AWS lightsail, 1 VCPU/0.5G RAM -- 新加坡, Vultr, 1 VCPU/1G RAM -- 达拉斯, Vultr, 1 VCPU/1G RAM +| Location | Vendor | Specification | +| --------- | ------------- | ------------------ | +| 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 | ## 依赖 @@ -68,7 +72,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb- ### Arch (Manjaro) ```sh -sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pulseaudio +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire ``` ### 安装 vcpkg @@ -111,100 +115,102 @@ cargo run ### 把 Wayland 修改成 X11 (Xorg) -RustDesk 暂时不支持 Wayland,不过正在积极开发中. -请查看[this](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/)配置 X11. +RustDesk 暂时不支持 Wayland,不过正在积极开发中。 +> [点我](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) +查看 如何将Xorg设置成默认的GNOME session ## 使用 Docker 编译 -首先克隆存储库并构建 docker 容器: +### 构建Docker容器 ```sh -git clone https://github.com/rustdesk/rustdesk -cd rustdesk -docker build -t "rustdesk-builder" . +git clone https://github.com/rustdesk/rustdesk # 克隆Github存储库 +cd rustdesk # 进入文件夹 +docker build -t "rustdesk-builder" . # 构建容器 ``` +请注意: +* 针对国内网络访问问题,可以做以下几点优化: + 1. Dockerfile 中修改系统的源到国内镜像 + ``` + 在Dockerfile的RUN apt update之前插入两行: + + RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list + RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list + ``` -针对国内网络访问问题,可以做以下几点优化: + 2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码: -1. Dockerfile 中修改系统的源到国内镜像 + ``` + RUN echo '[source.crates-io]' > ~/.cargo/config \ + && echo 'registry = "https://github.com/rust-lang/crates.io-index"' >> ~/.cargo/config \ + && echo '# 替换成你偏好的镜像源' >> ~/.cargo/config \ + && echo "replace-with = 'sjtu'" >> ~/.cargo/config \ + && echo '# 上海交通大学' >> ~/.cargo/config \ + && echo '[source.sjtu]' >> ~/.cargo/config \ + && echo 'registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"' >> ~/.cargo/config \ + && echo '' >> ~/.cargo/config + ``` - ``` - 在Dockerfile的RUN apt update之前插入两行: + 3. Dockerfile 中加入代理的 env - RUN sed -i "s/deb.debian.org/mirrors.163.com/g" /etc/apt/sources.list - RUN sed -i "s/security.debian.org/mirrors.163.com/g" /etc/apt/sources.list - ``` + ``` + 在User root后插入两行 -2. 修改容器系统中的 cargo 源,在`RUN ./rustup.sh -y`后插入下面代码: + ENV http_proxy=http://host:port + ENV https_proxy=http://host:port + ``` - ``` - RUN echo '[source.crates-io]' > ~/.cargo/config \ - && echo 'registry = "https://github.com/rust-lang/crates.io-index"' >> ~/.cargo/config \ - && echo '# 替换成你偏好的镜像源' >> ~/.cargo/config \ - && echo "replace-with = 'sjtu'" >> ~/.cargo/config \ - && echo '# 上海交通大学' >> ~/.cargo/config \ - && echo '[source.sjtu]' >> ~/.cargo/config \ - && echo 'registry = "https://mirrors.sjtug.sjtu.edu.cn/git/crates.io-index"' >> ~/.cargo/config \ - && echo '' >> ~/.cargo/config - ``` + 4. docker build 命令后面加上 proxy 参数 -3. Dockerfile 中加入代理的 env + ``` + docker build -t "rustdesk-builder" . --build-arg http_proxy=http://host:port --build-arg https_proxy=http://host:port + ``` - ``` - 在User root后插入两行 - - ENV http_proxy=http://host:port - ENV https_proxy=http://host:port - ``` - -4. docker build 命令后面加上 proxy 参数 - - ``` - docker build -t "rustdesk-builder" . --build-arg http_proxy=http://host:port --build-arg https_proxy=http://host:port - ``` - -然后,每次需要构建应用程序时,运行以下命令: +### 构建RustDesk程序 +容器构建完成后,运行下列指令以完成对RustDesk应用程序的构建: ```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`即可。 +* 如果出现以下的提示,则是无权限问题,可以尝试把`-e PUID="$(id -u)" -e PGID="$(id -g)"`参数去掉。 + ``` + usermod: user user is currently used by process 1 + groupmod: Permission denied. + groupmod: cannot lock /etc/group; try again later. + ``` + > **原因:** 容器的entrypoint脚本会检测UID和GID,在度判和给定的环境变量的不一致时,会强行修改user的UID和GID并重新运行。但在重启后读不到环境中的UID和GID,然后再次进入判错重启环节 -``` -usermod: user user is currently used by process 1 -groupmod: Permission denied. -groupmod: cannot lock /etc/group; try again later. -``` -可以尝试把`-e PUID="$(id -u)" -e PGID="$(id -g)"`参数去掉。(出现这一问题的原因是容器中的 entrypoint 脚本中判定 uid 和 gid 与给定的环境变量不一致时会修改 user 的 uid 和 gid 重新运行,但是重新运行时取不到环境变量中的 uid 和 gid 了,会再次进入 uid 与 gid 与给定值不一致的逻辑分支) - -请注意,第一次构建可能需要比较长的时间,因为需要缓存依赖项(国内网络经常出现拉取失败,可多尝试几次),后续构建会更快。此外,如果您需要为构建命令指定不同的参数, -您可以在命令末尾的 `` 位置执行此操作。例如,如果你想构建一个优化的发布版本,你可以在命令后跟 `--release`。 -将在 target 下产生可执行程序,请通过以下方式运行调试版本: +### 运行RustDesk程序 +生成的可执行程序在target目录下,可直接通过指令运行调试(Debug)版本的RustDesk: ```sh target/debug/rustdesk ``` -或者运行发布版本: +或者您想运行发行(Release)版本: ```sh target/release/rustdesk ``` -请确保您从 RustDesk 存储库的根目录运行这些命令,否则应用程序可能无法找到所需的资源。另请注意,此方法当前不支持其他`Cargo`子命令, -例如 `install` 或 `run`,因为运行在容器里,而不是宿主机上。 +请注意: +* 请保证您运行的目录是在RustDesk库的根目录内,否则软件会读不到文件。 +* `install`、`run`等Cargo的子指令在容器内不可用,宿主机才行。 ## 文件结构 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 视频编解码, 配置, tcp/udp 封装, protobuf, 文件传输相关文件系统操作函数, 以及一些其他实用函数 -- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 截屏 +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 屏幕截取 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 平台相关的鼠标键盘输入 - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务,audio/clipboard/input/video 服务, 以及连接的实现 +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 被控端服务音频、剪切板、输入、视频服务、网络连接的实现 - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 控制端 -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持 UDP 通讯, 等待远程连接(通过打洞直连或者中继) +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: 与[rustdesk-server](https://github.com/rustdesk/rustdesk-server)保持UDP通讯, 等待远程连接(通过打洞直连或者中继) - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 平台服务相关代码 - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 移动版本的Flutter代码 - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter Web版本中的Javascript代码 diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 000000000..c595885f2 --- /dev/null +++ b/docs/SECURITY.md @@ -0,0 +1,9 @@ +# Security Policy + +## Reporting a 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/entrypoint b/entrypoint index 514de9b9d..8c7be0786 100755 --- a/entrypoint +++ b/entrypoint @@ -1,34 +1,36 @@ #!/bin/sh -cd $HOME/rustdesk -. $HOME/.cargo/env +cd "$HOME"/rustdesk || exit 1 +# shellcheck source=/dev/null +. "$HOME"/.cargo/env -argv=$@ +argv=$* while test $# -gt 0; do case "$1" in - --release) - mkdir -p target/release - test -f target/release/libsciter-gtk.so || cp $HOME/libsciter-gtk.so target/release/ - release=1 + --release) + mkdir -p target/release + test -f target/release/libsciter-gtk.so || cp "$HOME"/libsciter-gtk.so target/release/ + release=1 + shift + ;; + --target) + shift + if test $# -gt 0; then + rustup target add "$1" shift - ;; - --target) - shift - if test $# -gt 0; then - rustup target add $1 - shift - fi - ;; - *) - shift - ;; + fi + ;; + *) + shift + ;; esac done if [ -z $release ]; then mkdir -p target/debug - test -f target/debug/libsciter-gtk.so || cp $HOME/libsciter-gtk.so target/debug/ + test -f target/debug/libsciter-gtk.so || cp "$HOME"/libsciter-gtk.so target/debug/ fi - +set -f +#shellcheck disable=2086 VCPKG_ROOT=/vcpkg cargo build $argv diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 966ad3df8..f78b3a20b 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -2,9 +2,9 @@ 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 addtion to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. +In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, or self-hosting, or write your own rendezvous/relay server. Self-hosting server is free and open source: https://github.com/rustdesk/rustdesk-server diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index 543fe8346..3668c7106 100644 Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 32e7b3554..e84ed4d21 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index 0f9368545..5a83dc1f0 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index b59279552..629631ac7 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index a4048ae69..39a15ba77 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png index 5d726ab36..5574ee7dc 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png index 2c3fad113..8e0a83a6a 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png index 5873757f9..0618ae0b6 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png index faea2eb16..560902b03 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png differ diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 000000000..effb820d6 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,11 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. +Code source : https://github.com/rustdesk/rustdesk +Doc : https://rustdesk.com/docs/en/manual/mobile/ + +Pour qu'un appareil distant puisse contrôler votre appareil Android via la souris ou le toucher, vous devez autoriser RustDesk à utiliser le service "Accessibilité", RustDesk utilise l'API AccessibilityService pour implémenter la télécommande Addroid. + +En plus du contrôle à distance, vous pouvez également transférer facilement des fichiers entre des appareils Android et des PC avec RustDesk. + +Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, ou l'auto-hébergement, ou écrire votre propre serveur de rendez-vous/relais. Le serveur auto-hébergé est gratuit et open source : https://github.com/rustdesk/rustdesk-server + +Veuillez télécharger et installer la version de bureau à partir de : https://rustdesk.com, vous pourrez alors accéder et contrôler votre bureau à partir de votre mobile, ou contrôler votre mobile à partir du bureau. diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 000000000..e1f4b4b0f --- /dev/null +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json new file mode 100644 index 000000000..d7f6e316e --- /dev/null +++ b/flatpak/rustdesk.json @@ -0,0 +1,38 @@ +{ + "app-id": "org.rustdesk.rustdesk", + "runtime": "org.freedesktop.Platform", + "runtime-version": "21.08", + "sdk": "org.freedesktop.Sdk", + "command": "rustdesk", + "modules": [ + "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", + "xdotool.json", + { + "name": "rustdesk", + "buildsystem": "simple", + "build-commands": [ + "bsdtar -zxvf rustdesk-1.2.0.deb", + "tar -xvf ./data.tar.xz", + "cp -r ./usr /app/", + "mkdir -p /app/bin && ln -s /app/usr/lib/rustdesk/rustdesk /app/bin/rustdesk" + ], + "sources": [ + { + "type": "file", + "path": "../rustdesk-1.2.0.deb" + } + ] + } + ], + "finish-args": [ + "--share=ipc", + "--socket=x11", + "--socket=fallback-x11", + "--socket=wayland", + "--share=network", + "--filesystem=home", + "--device=dri", + "--socket=pulseaudio", + "--talk-name=org.freedesktop.Flatpak" + ] +} \ No newline at end of file diff --git a/flatpak/xdotool.json b/flatpak/xdotool.json new file mode 100644 index 000000000..d7f41bf0e --- /dev/null +++ b/flatpak/xdotool.json @@ -0,0 +1,15 @@ +{ + "name": "xdotool", + "buildsystem": "simple", + "build-commands": [ + "make -j4 && PREFIX=./build make install", + "cp -r ./build/* /app/" + ], + "sources": [ + { + "type": "archive", + "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", + "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" + } + ] +} diff --git a/flutter/.gitignore b/flutter/.gitignore index aa592ad7a..9c7e52c12 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -36,24 +36,22 @@ lib/generated_plugin_registrant.dart # Symbolication related app.*.symbols - # Obfuscation related app.*.map.json jniLibs - .vscode # flutter rust bridge lib/generated_bridge.dart +lib/generated_bridge.freezed.dart # Flutter Generated Files -linux/flutter/generated_plugin_registrant.cc -linux/flutter/generated_plugin_registrant.h -linux/flutter/generated_plugins.cmake -macos/Flutter/GeneratedPluginRegistrant.swift -windows/flutter/generated_plugin_registrant.cc -windows/flutter/generated_plugin_registrant.h -windows/flutter/generated_plugins.cmake +**/GeneratedPluginRegistrant.swift +**/flutter/generated_plugin_registrant.cc +**/flutter/generated_plugin_registrant.h +**/flutter/generated_plugins.cmake +**/Runner/bridge_generated.h flutter_export_environment.sh Flutter-Generated.xcconfig key.jks +macos/rustdesk.xcodeproj/project.xcworkspace/ diff --git a/flutter/.metadata b/flutter/.metadata index 107fcb7b5..8b4892cfb 100644 --- a/flutter/.metadata +++ b/flutter/.metadata @@ -1,10 +1,36 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 8874f21e79d7ec66d0457c7ab338348e31b17f1d + revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: linux + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: macos + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: windows + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/flutter/README.md b/flutter/README.md new file mode 100644 index 000000000..ca73a12b2 --- /dev/null +++ b/flutter/README.md @@ -0,0 +1,16 @@ +# flutter_hbb + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/flutter/analysis_options.yaml b/flutter/analysis_options.yaml new file mode 100644 index 000000000..a679f5774 --- /dev/null +++ b/flutter/analysis_options.yaml @@ -0,0 +1,6 @@ +include: package:lints/recommended.yaml + +linter: + rules: + non_constant_identifier_names: false + sort_child_properties_last: false diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index a2a1a02a3..326689e5e 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -32,7 +32,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 32 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } @@ -74,8 +74,8 @@ flutter { } dependencies { - implementation "androidx.media:media:1.4.3" - implementation 'com.github.getActivity:XXPermissions:13.2' + implementation "androidx.media:media:1.6.0" + implementation 'com.github.getActivity:XXPermissions:16.2' implementation("org.jetbrains.kotlin:kotlin-stdlib") { version { strictly("$kotlin_version") } } } diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 04b2ccc9a..9b25f4973 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -16,6 +16,8 @@ { - try { - val jsonObject = JSONObject(arg1) - val id = jsonObject["id"] as Int - val username = jsonObject["name"] as String - val peerId = jsonObject["peer_id"] as String - val type = if (jsonObject["is_file_transfer"] as Boolean) { - translate("File Connection") - } else { - translate("Screen Connection") - } - loginRequestNotification(id, type, username, peerId) - } catch (e: JSONException) { - e.printStackTrace() - } - } - "on_client_authorized" -> { - Log.d(logTag, "from rust:on_client_authorized") + "add_connection" -> { try { val jsonObject = JSONObject(arg1) val id = jsonObject["id"] as Int val username = jsonObject["name"] as String val peerId = jsonObject["peer_id"] as String + val authorized = jsonObject["authorized"] as Boolean val isFileTransfer = jsonObject["is_file_transfer"] as Boolean val type = if (isFileTransfer) { translate("File Connection") } else { translate("Screen Connection") } - if (!isFileTransfer && !isStart) { - startCapture() + if (authorized) { + if (!isFileTransfer && !isStart) { + startCapture() + } + onClientAuthorizedNotification(id, type, username, peerId) + } else { + loginRequestNotification(id, type, username, peerId) } - onClientAuthorizedNotification(id, type, username, peerId) } catch (e: JSONException) { e.printStackTrace() } - } "stop_capture" -> { Log.d(logTag, "from rust:stop_capture") @@ -607,7 +594,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) 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 9a6ce011d..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 20cb4f904..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 b77c65d15..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 638a672f9..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 de6dc58b4..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/build.gradle b/flutter/android/build.gradle index e34fecc69..f7ab9782c 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.10' + ext.kotlin_version = '1.7.10' repositories { google() jcenter() @@ -7,9 +7,9 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.0.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.3' + classpath 'com.google.gms:google-services:4.3.14' } } diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0..cc5527d78 100644 --- a/flutter/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 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/Google.svg b/flutter/assets/Google.svg new file mode 100644 index 000000000..df394a84f --- /dev/null +++ b/flutter/assets/Google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg new file mode 100644 index 000000000..931e72844 --- /dev/null +++ b/flutter/assets/Okta.svg @@ -0,0 +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.png b/flutter/assets/android.png deleted file mode 100644 index 323100330..000000000 Binary files a/flutter/assets/android.png and /dev/null differ diff --git a/flutter/assets/android.svg b/flutter/assets/android.svg new file mode 100644 index 000000000..6fd89c9ab --- /dev/null +++ b/flutter/assets/android.svg @@ -0,0 +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.png b/flutter/assets/insecure.png deleted file mode 100644 index 0c954468d..000000000 Binary files a/flutter/assets/insecure.png and /dev/null differ diff --git a/flutter/assets/insecure.svg b/flutter/assets/insecure.svg new file mode 100644 index 000000000..5a344dd04 --- /dev/null +++ b/flutter/assets/insecure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/insecure_relay.png b/flutter/assets/insecure_relay.png deleted file mode 100644 index 878d57467..000000000 Binary files a/flutter/assets/insecure_relay.png and /dev/null differ diff --git a/flutter/assets/insecure_relay.svg b/flutter/assets/insecure_relay.svg new file mode 100644 index 000000000..17b474e6e --- /dev/null +++ b/flutter/assets/insecure_relay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg new file mode 100644 index 000000000..163e045e1 --- /dev/null +++ b/flutter/assets/kb_layout_iso.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg new file mode 100644 index 000000000..cfbb046ca --- /dev/null +++ b/flutter/assets/kb_layout_not_iso.svg @@ -0,0 +1 @@ + \ 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.png b/flutter/assets/linux.png deleted file mode 100644 index 456e58675..000000000 Binary files a/flutter/assets/linux.png and /dev/null differ diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg new file mode 100644 index 000000000..2c3697be9 --- /dev/null +++ b/flutter/assets/linux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg new file mode 100644 index 000000000..4d43f8bcd --- /dev/null +++ b/flutter/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/mac.png b/flutter/assets/mac.png deleted file mode 100644 index 4be16f369..000000000 Binary files a/flutter/assets/mac.png and /dev/null differ diff --git a/flutter/assets/mac.svg b/flutter/assets/mac.svg new file mode 100644 index 000000000..ccf9c7aab --- /dev/null +++ b/flutter/assets/mac.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/peer_searchbar.ttf b/flutter/assets/peer_searchbar.ttf new file mode 100644 index 000000000..7f87e48ce Binary files /dev/null and b/flutter/assets/peer_searchbar.ttf differ 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.png b/flutter/assets/secure.png deleted file mode 100644 index 01dcb2a8a..000000000 Binary files a/flutter/assets/secure.png and /dev/null differ diff --git a/flutter/assets/secure.svg b/flutter/assets/secure.svg new file mode 100644 index 000000000..fcd99f2f5 --- /dev/null +++ b/flutter/assets/secure.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/secure_relay.png b/flutter/assets/secure_relay.png deleted file mode 100644 index 4119f05ba..000000000 Binary files a/flutter/assets/secure_relay.png and /dev/null differ diff --git a/flutter/assets/secure_relay.svg b/flutter/assets/secure_relay.svg new file mode 100644 index 000000000..af54808a8 --- /dev/null +++ b/flutter/assets/secure_relay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/tabbar.ttf b/flutter/assets/tabbar.ttf new file mode 100644 index 000000000..a9220f348 Binary files /dev/null and b/flutter/assets/tabbar.ttf differ 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.png b/flutter/assets/win.png deleted file mode 100644 index 5ce86a257..000000000 Binary files a/flutter/assets/win.png and /dev/null differ diff --git a/flutter/assets/win.svg b/flutter/assets/win.svg new file mode 100644 index 000000000..a0f7e3def --- /dev/null +++ b/flutter/assets/win.svg @@ -0,0 +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/build_android_deps.sh b/flutter/build_android_deps.sh index f120346cf..a30abd154 100755 --- a/flutter/build_android_deps.sh +++ b/flutter/build_android_deps.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Build libyuv / opus / libvpx / oboe for Android -# Required: +# Build libyuv / opus / libvpx / oboe for Android +# Required: # 1. set VCPKG_ROOT / ANDROID_NDK path environment variables # 2. vcpkg initialized # 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`) @@ -23,7 +23,7 @@ HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/l TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG function build { - ANDROID_ABI=$1 + ANDROID_ABI=$1 VCPKG_TARGET=$2 NDK_LLVM_TARGET=$3 LIBVPX_TARGET=$4 @@ -111,15 +111,15 @@ patch -N -d build/oboe -p1 < ../src/oboe.patch # x86_64-linux-android # i686-linux-android -# LIBVPX_TARGET : -# arm64-android-gcc -# armv7-android-gcc +# LIBVPX_TARGET : +# arm64-android-gcc +# armv7-android-gcc # x86_64-android-gcc -# x86-android-gcc +# x86-android-gcc # args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc -build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc +build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc # rm -rf build/libvpx # rm -rf build/oboe \ No newline at end of file diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fab2..eabd8512d 100644 --- a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1,122 @@ { - "images" : [ + "images": [ { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "filename": "Icon-App-20x20@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" }, { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" + "filename": "Icon-App-20x20@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "filename": "Icon-App-29x29@1x.png", + "idiom": "iphone", + "scale": "1x", + "size": "29x29" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "filename": "Icon-App-29x29@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" }, { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" + "filename": "Icon-App-29x29@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "filename": "Icon-App-40x40@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" }, { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "filename": "Icon-App-40x40@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" + "filename": "Icon-App-60x60@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" }, { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "filename": "Icon-App-60x60@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "filename": "Icon-App-20x20@1x.png", + "idiom": "ipad", + "scale": "1x", + "size": "20x20" }, { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "filename": "Icon-App-20x20@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "20x20" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "filename": "Icon-App-29x29@1x.png", + "idiom": "ipad", + "scale": "1x", + "size": "29x29" }, { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "filename": "Icon-App-29x29@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "29x29" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "filename": "Icon-App-40x40@1x.png", + "idiom": "ipad", + "scale": "1x", + "size": "40x40" }, { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "filename": "Icon-App-40x40@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "40x40" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "filename": "Icon-App-76x76@1x.png", + "idiom": "ipad", + "scale": "1x", + "size": "76x76" }, { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "filename": "Icon-App-76x76@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" }, { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "filename": "Icon-App-83.5x83.5@2x.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" }, { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" + "filename": "Icon-App-1024x1024@1x.png", + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" } ], - "info" : { - "version" : 1, - "author" : "xcode" + "info": { + "author": "icons_launcher", + "version": 1 } -} +} \ No newline at end of file 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 eb0bf04fd..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 b17866fe4..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 70164e1a4..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 fa5fb621a..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 8999d5ed9..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 047d553ce..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 295741a1b..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 70164e1a4..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 6207bba1b..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 df9cb7d0a..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 df9cb7d0a..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 12ffdab24..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 563187384..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 834c4d8e8..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 4a3ffa808..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 dbd15d436..ff373cc9c 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,25 +1,150 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi' hide Size; +import 'dart:io'; +import 'dart:math'; + +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:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'dart:async'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +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: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'; +import 'models/input_model.dart'; import 'models/model.dart'; +import 'models/platform_model.dart'; final globalKey = GlobalKey(); final navigationBarKey = GlobalKey(); -var isAndroid = false; -var isIOS = false; +final isAndroid = Platform.isAndroid; +final isIOS = Platform.isIOS; +final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; var isWeb = false; -var isDesktop = false; +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; + typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); -class Translator { - static late F call; +typedef StreamEventHandler = Future Function(Map); + +final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( + "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII="))); +final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='))); +final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='))); +final iconFile = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); +final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); +final iconRecording = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAANpJREFUWEftltENAiEMhtsJ1NcynG6gI+gGugEOR591gppeQoIYSDBILxEeydH/57u2FMF4obE+TAOTwLoIhBDOAHBExG2n6rgR0akW640AM0sn4SWMiDycc7s8JjN7Ijro/k8NqAAR5RoeAPZxv2ggP9hCJiWZxtGbq3hqbJiBVHy4gVx8qAER8Yi4JFy6huVAKXemgb8icI+1b5KEitq0DOO/Nm1EEX1TK27p/bVvv36MOhl4EtHHbFF7jq8AoG1z08OAiFycczrkFNe6RrIet26NMQlMAuYEXiayryF/QQktAAAAAElFTkSuQmCC'))); +final iconHardDrive = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAMAAACahl6sAAAAmVBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjHWqVAAAAMnRSTlMAv0BmzLJNXlhiUu2fxXDgu7WuSUUe29LJvpqUjX53VTstD7ilNujCqTEk5IYH+vEoFjKvAagAAAPpSURBVHja7d0JbhpBEIXhB3jYzb5vBgzYgO04df/DJXGUKMwU9ECmZ6pQfSfw028LCXW3YYwxxhhjjDHGGGOM0eZ9VV1MckdKWLM1bRQ/35GW/WxHHu1me6ShuyHvNl34VhlTKsYVeDWj1EzgUZ1S1DrAk/UDparZgxd9Sl0BHnxSBhpI3jfKQG2FpLUpE69I2ILikv1nsvygjBwPSNKYMlNHggqUoSKS80AZCnwHqQ1zCRvW+CRegwRFeFAMKKrtM8gTPJlzSfwFgT9dJom3IDN4VGaSeAryAK8m0SSeghTg1ZYiql6CjBDhO8mzlyAVhKhIwgXxrh5NojGIhyRckEdwpCdhgpSQgiWTRGMQNonGIGySp0SDvMDBX5KWxiB8Eo1BgE00SYJBykhNnkmSWJAcLpGaJNMgfJKyxiDAK4WNEwryhMtkJsk8CJtEYxA+icYgQIfCcgkEqcJNXhIRQdgkGoPwSTQG+e8khdu/7JOVREwQIKCwF41B2CQljUH4JLcH6SI+OUlEBQHa0SQag/BJNAbhkjxqDMIn0RgEeI4muSlID9eSkERgEKAVTaIxCJ9EYxA2ydVB8hCASVLRGAQYR5NoDMIn0RgEyFHYSGMQPonGII4kziCNvBgNJonEk4u3GAk8Sprk6eYaqbMDY0oKvUm5jfC/viGiSypV7+M3i2iDsAGpNEDYjlTa3W8RdR/r544g50ilnA0RxoZIE2NIXqQbhkAkGyKNDZHGhkhjQ6SxIdLYEGlsiDQ2JGTVeD0264U9zipPh7XOooffpA6pfNCXjxl4/c3pUzlChwzor53zwYYVfpI5pOV6LWFF/2jiJ5FDSs5jdY/0rwUAkUMeXWdBqnSqD0DikBqdqCHsjTvELm9In0IOri/0pwAEDtlSyNaRjAIAAoesKWTtuusxByBwCJp0oomwBXcYUuCQgE50ENajE4OvZAKHLB1/68Br5NqiyCGYOY8YRd77kTkEb64n7lZN+mOIX4QOwb5FX0ZVx3uOxwW+SB0CbBubemWP8/rlaaeRX+M3uUOuZENsiA25zIbYkPsZElBIHwL13U/PTjJ/cyOOEoVM3I+hziDQlELm7pPxw3eI8/7gPh1fpLA6xGnEeDDgO0UcIAzzM35HxLPIq5SXe9BLzOsj9eUaQqyXzxS1QFSfWM2cCANiHcAISJ0AnCKpUwTuIkkA3EeSInAXSQKcs1V18e24wlllUmQp9v9zXKeHi+akRAMOPVKhAqdPBZeUmnnEsO6QcJ0+4qmOSbBxFfGVRiTUqITrdKcCbyYO3/K4wX4+aQ+FfNjXhu3JfAVjjDHGGGOMMcYYY4xIPwCgfqT6TbhCLAAAAABJRU5ErkJggg=='))); + +enum DesktopType { + main, + remote, + fileTransfer, + cm, + portForward, +} + +class IconFont { + static const _family1 = 'Tabbar'; + static const _family2 = 'PeerSearchbar'; + IconFont._(); + + static const IconData max = IconData(0xe606, fontFamily: _family1); + static const IconData restore = IconData(0xe607, fontFamily: _family1); + static const IconData close = IconData(0xe668, fontFamily: _family1); + static const IconData min = IconData(0xe609, fontFamily: _family1); + static const IconData add = IconData(0xe664, fontFamily: _family1); + static const IconData menu = IconData(0xe628, fontFamily: _family1); + static const IconData search = IconData(0xe6a4, fontFamily: _family2); + static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2); +} + +class ColorThemeExtension extends ThemeExtension { + const ColorThemeExtension({ + required this.border, + required this.highlight, + }); + + final Color? border; + final Color? highlight; + + static const light = ColorThemeExtension( + border: Color(0xFFCCCCCC), + highlight: Color(0xFFE5E5E5), + ); + + static const dark = ColorThemeExtension( + border: Color(0xFF555555), + highlight: Color(0xFF3F3F3F), + ); + + @override + ThemeExtension copyWith( + {Color? border, Color? highlight}) { + return ColorThemeExtension( + border: border ?? this.border, + highlight: highlight ?? this.highlight, + ); + } + + @override + ThemeExtension lerp( + ThemeExtension? other, double t) { + if (other is! ColorThemeExtension) { + return this; + } + return ColorThemeExtension( + border: Color.lerp(border, other.border, t), + highlight: Color.lerp(highlight, other.highlight, t), + ); + } } class MyTheme { @@ -33,7 +158,144 @@ 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); + static const Color hoverBorder = Color(0xFF999999); + + static ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + hoverColor: Color.fromARGB(255, 224, 224, 224), + scaffoldBackgroundColor: Color(0xFFFFFFFF), + textTheme: const TextTheme( + titleLarge: TextStyle(fontSize: 19, color: Colors.black87), + titleSmall: TextStyle(fontSize: 14, color: Colors.black87), + bodySmall: TextStyle(fontSize: 12, color: Colors.black87, height: 1.25), + bodyMedium: + TextStyle(fontSize: 14, color: Colors.black87, height: 1.25), + labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)), + cardColor: Color(0xFFEEEEEE), + hintColor: Color(0xFFAAAAAA), + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: const TabBarTheme( + labelColor: Colors.black87, + ), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + splashFactory: isDesktop ? NoSplash.splashFactory : null, + textButtonTheme: isDesktop + ? TextButtonThemeData( + style: ButtonStyle(splashFactory: NoSplash.splashFactory), + ) + : null, + colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blue).copyWith( + brightness: Brightness.light, + background: Color(0xFFEEEEEE), + ), + ).copyWith( + extensions: >[ + ColorThemeExtension.light, + TabbarTheme.light, + ], + ); + static ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + hoverColor: Color.fromARGB(255, 45, 46, 53), + scaffoldBackgroundColor: Color(0xFF18191E), + textTheme: const TextTheme( + titleLarge: TextStyle(fontSize: 19), + titleSmall: TextStyle(fontSize: 14), + bodySmall: TextStyle(fontSize: 12, height: 1.25), + bodyMedium: TextStyle(fontSize: 14, height: 1.25), + labelLarge: TextStyle( + fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)), + cardColor: Color(0xFF24252B), + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: const TabBarTheme( + labelColor: Colors.white70, + ), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + splashFactory: isDesktop ? NoSplash.splashFactory : null, + outlinedButtonTheme: OutlinedButtonThemeData( + style: + OutlinedButton.styleFrom(side: BorderSide(color: Colors.white38))), + textButtonTheme: isDesktop + ? TextButtonThemeData( + style: ButtonStyle(splashFactory: NoSplash.splashFactory), + ) + : null, + checkboxTheme: + const CheckboxThemeData(checkColor: MaterialStatePropertyAll(dark)), + colorScheme: ColorScheme.fromSwatch( + brightness: Brightness.dark, + primarySwatch: Colors.blue, + ).copyWith(background: Color(0xFF24252B)), + ).copyWith( + extensions: >[ + ColorThemeExtension.dark, + TabbarTheme.dark, + ], + ); + + static ThemeMode getThemeModePreference() { + return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); + } + + static void changeDarkMode(ThemeMode mode) async { + Get.changeThemeMode(mode); + if (desktopType == DesktopType.main) { + if (mode == ThemeMode.system) { + await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); + } else { + await bind.mainSetLocalOption( + key: kCommConfKeyTheme, value: mode.toShortString()); + } + await bind.mainChangeTheme(dark: mode.toShortString()); + // Synchronize the window theme of the system. + updateSystemWindowTheme(); + } + } + + static ThemeMode currentThemeMode() { + final preference = getThemeModePreference(); + if (preference == ThemeMode.system) { + if (WidgetsBinding.instance.platformDispatcher.platformBrightness == + Brightness.light) { + return ThemeMode.light; + } else { + return ThemeMode.dark; + } + } else { + return preference; + } + } + + static ColorThemeExtension color(BuildContext context) { + return Theme.of(context).extension()!; + } + + static TabbarTheme tabbar(BuildContext context) { + return Theme.of(context).extension()!; + } + + static ThemeMode themeModeFromString(String v) { + switch (v) { + case "light": + return ThemeMode.light; + case "dark": + return ThemeMode.dark; + default: + return ThemeMode.system; + } + } +} + +extension ParseToString on ThemeMode { + String toShortString() { + return toString().split('.').last; + } } final ButtonStyle flatButtonStyle = TextButton.styleFrom( @@ -44,144 +306,530 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom( ), ); -void showToast(String text, {Duration? duration}) { - SmartDialog.showToast(text, displayTime: duration); +List supportedLocales = const [ + // specify CN/TW to fix CJK issue in flutter + Locale('zh', 'CN'), + Locale('zh', 'TW'), + Locale('zh', 'SG'), + Locale('fr'), + Locale('de'), + Locale('it'), + Locale('ja'), + Locale('cs'), + Locale('pl'), + Locale('ko'), + Locale('hu'), + Locale('pt'), + Locale('ru'), + Locale('sk'), + Locale('id'), + Locale('da'), + Locale('eo'), + Locale('tr'), + Locale('vi'), + Locale('pl'), + Locale('kz'), + Locale('en', 'US'), +]; + +String formatDurationToTime(Duration duration) { + var totalTime = duration.inSeconds; + final secs = totalTime % 60; + totalTime = (totalTime - secs) ~/ 60; + final mins = totalTime % 60; + totalTime = (totalTime - mins) ~/ 60; + return "${totalTime.toString().padLeft(2, "0")}:${mins.toString().padLeft(2, "0")}:${secs.toString().padLeft(2, "0")}"; } -void showLoading(String text, {bool clickMaskDismiss = false}) { - SmartDialog.dismiss(); - SmartDialog.showLoading( - clickMaskDismiss: false, - builder: (context) { - return Container( - color: MyTheme.white, - constraints: BoxConstraints(maxWidth: 240), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 30), - Center(child: CircularProgressIndicator()), - SizedBox(height: 20), - Center( - child: Text(Translator.call(text), - style: TextStyle(fontSize: 15))), - SizedBox(height: 20), - Center( - child: TextButton( - style: flatButtonStyle, - onPressed: () { - SmartDialog.dismiss(); - backToHome(); - }, - child: Text(Translator.call('Cancel'), - style: TextStyle(color: MyTheme.accent)))) - ])); - }); +closeConnection({String? id}) { + if (isAndroid || isIOS) { + Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + } else { + final controller = Get.find(); + controller.closeBy(id); + } } -backToHome() { - Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); +void window_on_top(int? id) { + if (!isDesktop) { + return; + } + if (id == null) { + // main window + windowManager.restore(); + windowManager.show(); + windowManager.focus(); + rustDeskWinManager.registerActiveWindow(kWindowMainId); + } else { + WindowController.fromWindowId(id) + ..focus() + ..show(); + rustDeskWinManager.call(WindowType.Main, kWindowEventShow, {"id": id}); + } } typedef DialogBuilder = CustomAlertDialog Function( StateSetter setState, void Function([dynamic]) close); -class DialogManager { - static int _tag = 0; +class Dialog { + OverlayEntry? entry; + Completer completer = Completer(); - static dismissByTag(String tag, [result]) { - SmartDialog.dismiss(tag: tag, result: result); - } + Dialog(); - static Future show(DialogBuilder builder, - {bool clickMaskDismiss = false, - bool backDismiss = false, - String? tag, - bool useAnimation = true}) async { - final t; - if (tag != null) { - t = tag; - } else { - _tag += 1; - t = _tag.toString(); + void complete(T? res) { + try { + if (!completer.isCompleted) { + completer.complete(res); + } + } catch (e) { + debugPrint("Dialog complete catch error: $e"); + } finally { + entry?.remove(); } - SmartDialog.dismiss(status: SmartStatus.allToast); - SmartDialog.dismiss(status: SmartStatus.loading); - final close = ([res]) { - SmartDialog.dismiss(tag: t, result: res); - }; - final res = await SmartDialog.show( - tag: t, - clickMaskDismiss: clickMaskDismiss, - backDismiss: backDismiss, - useAnimation: useAnimation, - builder: (_) => StatefulBuilder( - builder: (_, setState) => builder(setState, close))); - return res; } } -class CustomAlertDialog extends StatelessWidget { - CustomAlertDialog( - {required this.title, - required this.content, - required this.actions, - this.contentPadding}); +class OverlayKeyState { + final _overlayKey = GlobalKey(); - final Widget title; + /// use global overlay by default + OverlayState? get state => + _overlayKey.currentState ?? globalKey.currentState?.overlay; + + GlobalKey? get key => _overlayKey; +} + +class OverlayDialogManager { + final Map _dialogs = {}; + var _overlayKeyState = OverlayKeyState(); + int _tagCount = 0; + + OverlayEntry? _mobileActionsOverlayEntry; + + void setOverlayState(OverlayKeyState overlayKeyState) { + _overlayKeyState = overlayKeyState; + } + + void dismissAll() { + _dialogs.forEach((key, value) { + value.complete(null); + BackButtonInterceptor.removeByName(key); + }); + _dialogs.clear(); + } + + void dismissByTag(String tag) { + _dialogs[tag]?.complete(null); + _dialogs.remove(tag); + BackButtonInterceptor.removeByName(tag); + } + + Future show(DialogBuilder builder, + {bool clickMaskDismiss = false, + bool backDismiss = false, + String? tag, + bool useAnimation = true, + bool forceGlobal = false}) { + final overlayState = + forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state; + + if (overlayState == null) { + return Future.error( + "[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first"); + } + + final String dialogTag; + if (tag != null) { + dialogTag = tag; + } else { + dialogTag = _tagCount.toString(); + _tagCount++; + } + + final dialog = Dialog(); + _dialogs[dialogTag] = dialog; + + close([res]) { + _dialogs.remove(dialogTag); + dialog.complete(res); + BackButtonInterceptor.removeByName(dialogTag); + } + + dialog.entry = OverlayEntry(builder: (_) { + bool innerClicked = false; + return Listener( + onPointerUp: (_) { + if (!innerClicked && clickMaskDismiss) { + close(); + } + innerClicked = false; + }, + child: Container( + color: Colors.black12, + child: StatefulBuilder(builder: (context, setState) { + return Listener( + onPointerUp: (_) => innerClicked = true, + child: builder(setState, close), + ); + }))); + }); + overlayState.insert(dialog.entry!); + BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) { + if (backDismiss) { + close(); + } + return true; + }, name: dialogTag); + return dialog.completer.future; + } + + String showLoading(String text, + {bool clickMaskDismiss = false, + bool showCancel = true, + VoidCallback? onCancel}) { + final tag = _tagCount.toString(); + _tagCount++; + show((setState, close) { + cancel() { + dismissAll(); + if (onCancel != null) { + onCancel(); + } + } + + return CustomAlertDialog( + content: Container( + constraints: const BoxConstraints(maxWidth: 240), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 30), + const Center(child: CircularProgressIndicator()), + const SizedBox(height: 20), + Center( + child: Text(translate(text), + style: const TextStyle(fontSize: 15))), + const SizedBox(height: 20), + Offstage( + offstage: !showCancel, + child: Center( + 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, + ); + }, tag: tag); + return tag; + } + + void resetMobileActionsOverlay({FFI? ffi}) { + if (_mobileActionsOverlayEntry == null) return; + hideMobileActionsOverlay(); + showMobileActionsOverlay(ffi: ffi); + } + + void showMobileActionsOverlay({FFI? ffi}) { + if (_mobileActionsOverlayEntry != null) return; + final overlayState = _overlayKeyState.state; + if (overlayState == null) return; + + // compute overlay position + final screenW = MediaQuery.of(globalKey.currentContext!).size.width; + final screenH = MediaQuery.of(globalKey.currentContext!).size.height; + const double overlayW = 200; + const double overlayH = 45; + final left = (screenW - overlayW) / 2; + final top = screenH - overlayH - 80; + + final overlay = OverlayEntry(builder: (context) { + final session = ffi ?? gFFI; + return DraggableMobileActions( + position: Offset(left, top), + width: overlayW, + height: overlayH, + onBackPressed: () => session.inputModel.tap(MouseButtons.right), + onHomePressed: () => session.inputModel.tap(MouseButtons.wheel), + onRecentPressed: () async { + session.inputModel.sendMouse('down', MouseButtons.wheel); + await Future.delayed(const Duration(milliseconds: 500)); + session.inputModel.sendMouse('up', MouseButtons.wheel); + }, + onHidePressed: () => hideMobileActionsOverlay(), + ); + }); + overlayState.insert(overlay); + _mobileActionsOverlayEntry = overlay; + } + + void hideMobileActionsOverlay() { + if (_mobileActionsOverlayEntry != null) { + _mobileActionsOverlayEntry!.remove(); + _mobileActionsOverlayEntry = null; + return; + } + } + + void toggleMobileActionsOverlay({FFI? ffi}) { + if (_mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(ffi: ffi); + } else { + hideMobileActionsOverlay(); + } + } + + bool existing(String tag) { + return _dialogs.keys.contains(tag); + } +} + +void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { + final overlayState = globalKey.currentState?.overlay; + if (overlayState == null) return; + final entry = OverlayEntry(builder: (_) { + return IgnorePointer( + child: Align( + alignment: const Alignment(0.0, 0.8), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + borderRadius: const BorderRadius.all( + Radius.circular(20), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), + child: Text( + text, + style: const TextStyle( + decoration: TextDecoration.none, + fontWeight: FontWeight.w300, + fontSize: 18, + color: Colors.white), + ), + ))); + }); + overlayState.insert(entry); + Future.delayed(timeout, () { + entry.remove(); + }); +} + +class CustomAlertDialog extends StatelessWidget { + const CustomAlertDialog( + {Key? key, + this.title, + required this.content, + this.actions, + this.contentPadding, + this.contentBoxConstraints = const BoxConstraints(maxWidth: 500), + this.onSubmit, + this.onCancel}) + : super(key: key); + + final Widget? title; final Widget content; - final List actions; + final List? actions; final double? contentPadding; + final BoxConstraints contentBoxConstraints; + final Function()? onSubmit; + final Function()? onCancel; @override Widget build(BuildContext context) { - return AlertDialog( - scrollable: true, - title: title, - contentPadding: - EdgeInsets.symmetric(horizontal: contentPadding ?? 25, vertical: 10), - content: content, - actions: actions, + // request focus + FocusScopeNode scopeNode = FocusScopeNode(); + Future.delayed(Duration.zero, () { + if (!scopeNode.hasFocus) scopeNode.requestFocus(); + }); + const double padding = 16; + bool tabTapped = false; + return FocusScope( + node: scopeNode, + autofocus: true, + onKey: (node, key) { + if (key.logicalKey == LogicalKeyboardKey.escape) { + if (key is RawKeyDownEvent) { + onCancel?.call(); + } + return KeyEventResult.handled; // avoid TextField exception on escape + } 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, + 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: Theme( + data: Theme.of(context).copyWith( + inputDecorationTheme: InputDecorationTheme( + isDense: true, contentPadding: EdgeInsets.all(15))), + child: content), + ), + actions: actions, + actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding), + ), ); } } -void msgBox(String type, String title, String text, {bool? hasCancel}) { - var wrap = (String text, void Function() onPressed) => 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(Translator.call(text), - style: TextStyle(color: MyTheme.accent)))); - - SmartDialog.dismiss(); - final buttons = [ - wrap(Translator.call('OK'), () { - SmartDialog.dismiss(); - backToHome(); - }) - ]; - if (hasCancel == null) { - hasCancel = type != 'error'; +void msgBox(String id, String type, String title, String text, String link, + OverlayDialogManager dialogManager, + {bool? hasCancel, ReconnectHandle? reconnect}) { + dialogManager.dismissAll(); + List buttons = []; + bool hasOk = false; + submit() { + dialogManager.dismissAll(); + // https://github.com/fufesou/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (!type.contains("custom") && desktopType != DesktopType.portForward) { + closeConnection(); + } } + + cancel() { + dialogManager.dismissAll(); + } + + jumplink() { + if (link.startsWith('http')) { + launchUrl(Uri.parse(link)); + } + } + + if (type != "connecting" && type != "success" && !type.contains("nook")) { + hasOk = true; + buttons.insert(0, dialogButton('OK', onPressed: submit)); + } + hasCancel ??= !type.contains("error") && + !type.contains("nocancel") && + type != "restarting"; if (hasCancel) { + buttons.insert( + 0, dialogButton('Cancel', onPressed: cancel, isOutline: true)); + } + if (type.contains("hasclose")) { buttons.insert( 0, - wrap(Translator.call('Cancel'), () { - SmartDialog.dismiss(); + dialogButton('Close', onPressed: () { + dialogManager.dismissAll(); })); } - DialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate(title), style: TextStyle(fontSize: 21)), - content: Text(Translator.call(text), style: TextStyle(fontSize: 15)), - actions: buttons)); + if (reconnect != null && title == "Connection Error") { + buttons.insert( + 0, + dialogButton('Reconnect', isOutline: true, onPressed: () { + reconnect(dialogManager, id, false); + })); + } + if (link.isNotEmpty) { + buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); + } + dialogManager.show( + (setState, close) => CustomAlertDialog( + title: null, + content: SelectionArea(child: msgboxContent(type, title, text)), + actions: buttons, + onSubmit: hasOk ? submit : null, + onCancel: hasCancel == true ? cancel : null, + ), + tag: '$id-$type-$title-$text-$link', + ); +} + +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 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: Text( + translate(title), + style: TextStyle(fontSize: 21), + ), + content: content, + actions: buttons, + onCancel: hasCancel ? close : null, + )); } Color str2color(String str, [alpha = 0xFF]) { @@ -199,13 +847,13 @@ const G = M * K; String readableFileSize(double size) { if (size < K) { - return size.toStringAsFixed(2) + " B"; + return "${size.toStringAsFixed(2)} B"; } else if (size < M) { - return (size / K).toStringAsFixed(2) + " KB"; + return "${(size / K).toStringAsFixed(2)} KB"; } else if (size < G) { - return (size / M).toStringAsFixed(2) + " MB"; + return "${(size / M).toStringAsFixed(2)} MB"; } else { - return (size / G).toStringAsFixed(2) + " GB"; + return "${(size / G).toStringAsFixed(2)} GB"; } } @@ -275,21 +923,30 @@ class PermissionManager { } static Future check(String type) { - if (!permissions.contains(type)) + if (isDesktop) { + return Future.value(true); + } + if (!permissions.contains(type)) { return Future.error("Wrong permission!$type"); - return FFI.invokeMethod("check_permission", type); + } + return gFFI.invokeMethod("check_permission", type); } static Future request(String type) { - if (!permissions.contains(type)) + if (isDesktop) { + return Future.value(true); + } + if (!permissions.contains(type)) { return Future.error("Wrong permission!$type"); + } - FFI.invokeMethod("request_permission", type); + gFFI.invokeMethod("request_permission", type); if (type == "ignore_battery_optimizations") { return Future.value(false); } _current = type; _completer = Completer(); + gFFI.invokeMethod("request_permission", type); // timeout _timer?.cancel(); @@ -315,8 +972,10 @@ class PermissionManager { } RadioListTile getRadio( - String name, T toValue, T curValue, void Function(T?) onChange) { + String name, T toValue, T curValue, void Function(T?) onChange, + {EdgeInsetsGeometry? contentPadding}) { return RadioListTile( + contentPadding: contentPadding, controlAffinity: ListTileControlAffinity.trailing, title: Text(translate(name)), value: toValue, @@ -325,3 +984,854 @@ RadioListTile getRadio( dense: true, ); } + +CheckboxListTile getToggle( + String id, void Function(void Function()) setState, option, name, + {FFI? ffi}) { + final opt = bind.sessionGetToggleOptionSync(id: id, arg: option); + return CheckboxListTile( + value: opt, + onChanged: (v) { + setState(() { + bind.sessionToggleOption(id: id, value: option); + }); + if (option == "show-quality-monitor") { + (ffi ?? gFFI).qualityMonitorModel.checkShowQualityMonitor(id); + } + }, + dense: true, + title: Text(translate(name))); +} + +/// find ffi, tag is Remote ID +/// for session specific usage +FFI ffi(String? tag) { + return Get.find(tag: tag); +} + +/// Global FFI object +late FFI _globalFFI; + +FFI get gFFI => _globalFFI; + +Future initGlobalFFI() async { + debugPrint("_globalFFI init"); + _globalFFI = FFI(); + debugPrint("_globalFFI init end"); + // after `put`, can also be globally found by Get.find(); + Get.put(_globalFFI, permanent: true); +} + +String translate(String name) { + if (name.startsWith('Failed to') && name.contains(': ')) { + return name.split(': ').map((x) => translate(x)).join(': '); + } + return platformFFI.translate(name, localeName); +} + +bool option2bool(String option, String value) { + bool res; + if (option.startsWith("enable-")) { + res = value != "N"; + } else if (option.startsWith("allow-") || + option == "stop-service" || + option == "direct-server" || + option == "stop-rendezvous-service" || + option == "force-always-relay") { + res = value == "Y"; + } else { + assert(false); + res = value != "N"; + } + return res; +} + +String bool2option(String option, bool b) { + String res; + if (option.startsWith('enable-')) { + res = b ? '' : 'N'; + } else if (option.startsWith('allow-') || + option == "stop-service" || + option == "direct-server" || + option == "stop-rendezvous-service" || + option == "force-always-relay") { + res = b ? 'Y' : ''; + } else { + assert(false); + res = b ? 'Y' : 'N'; + } + return res; +} + +Future matchPeer(String searchText, Peer peer) async { + if (searchText.isEmpty) { + return true; + } + if (peer.id.toLowerCase().contains(searchText)) { + return true; + } + if (peer.hostname.toLowerCase().contains(searchText) || + peer.username.toLowerCase().contains(searchText)) { + return true; + } + final alias = await bind.mainGetPeerOption(id: peer.id, key: 'alias'); + if (alias.isEmpty) { + return false; + } + return alias.toLowerCase().contains(searchText); +} + +/// Get the image for the current [platform]. +Widget getPlatformImage(String platform, {double size = 50}) { + if (platform == kPeerPlatformMacOS) { + platform = 'mac'; + } else if (platform != kPeerPlatformLinux && + platform != kPeerPlatformAndroid) { + platform = 'win'; + } else { + platform = platform.toLowerCase(); + } + return SvgPicture.asset('assets/$platform.svg', height: size, width: size); +} + +class LastWindowPosition { + double? width; + double? height; + double? offsetWidth; + double? offsetHeight; + bool? isMaximized; + + LastWindowPosition(this.width, this.height, this.offsetWidth, + this.offsetHeight, this.isMaximized); + + Map toJson() { + return { + "width": width, + "height": height, + "offsetWidth": offsetWidth, + "offsetHeight": offsetHeight, + "isMaximized": isMaximized, + }; + } + + @override + String toString() { + return jsonEncode(toJson()); + } + + static LastWindowPosition? loadFromString(String content) { + if (content.isEmpty) { + return null; + } + try { + final m = jsonDecode(content); + return LastWindowPosition(m["width"], m["height"], m["offsetWidth"], + m["offsetHeight"], m["isMaximized"]); + } catch (e) { + debugPrintStack(label: e.toString()); + return null; + } + } +} + +/// Save window position and size on exit +/// Note that windowId must be provided if it's subwindow +Future saveWindowPosition(WindowType type, {int? windowId}) async { + if (type != WindowType.Main && windowId == null) { + debugPrint( + "Error: windowId cannot be null when saving positions for sub window"); + } + switch (type) { + case WindowType.Main: + final position = await windowManager.getPosition(); + final sz = await windowManager.getSize(); + final isMaximized = await windowManager.isMaximized(); + final pos = LastWindowPosition( + sz.width, sz.height, position.dx, position.dy, isMaximized); + await bind.setLocalFlutterConfig( + k: kWindowPrefix + type.name, v: pos.toString()); + break; + default: + final wc = WindowController.fromWindowId(windowId!); + final frame = await wc.getFrame(); + final position = frame.topLeft; + final sz = frame.size; + final isMaximized = await wc.isMaximized(); + final pos = LastWindowPosition( + sz.width, sz.height, position.dx, position.dy, isMaximized); + debugPrint( + "saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}"); + await bind.setLocalFlutterConfig( + k: kWindowPrefix + type.name, v: pos.toString()); + break; + } +} + +Future _adjustRestoreMainWindowSize(double? width, double? height) async { + const double minWidth = 600; + const double minHeight = 100; + double maxWidth = (((isDesktop || isWebDesktop) + ? kDesktopMaxDisplayWidth + : kMobileMaxDisplayWidth)) + .toDouble(); + double maxHeight = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplayHeight + : kMobileMaxDisplayHeight) + .toDouble(); + + if (isDesktop || isWebDesktop) { + final screen = (await window_size.getWindowInfo()).screen; + if (screen != null) { + maxWidth = screen.visibleFrame.width; + maxHeight = screen.visibleFrame.height; + } + } + + final defaultWidth = + ((isDesktop || isWebDesktop) ? 1280 : kMobileDefaultDisplayWidth) + .toDouble(); + final defaultHeight = + ((isDesktop || isWebDesktop) ? 720 : kMobileDefaultDisplayHeight) + .toDouble(); + double restoreWidth = width ?? defaultWidth; + double restoreHeight = height ?? defaultHeight; + + if (restoreWidth < minWidth) { + restoreWidth = minWidth; + } + if (restoreHeight < minHeight) { + restoreHeight = minHeight; + } + if (restoreWidth > maxWidth) { + restoreWidth = maxWidth; + } + if (restoreHeight > maxHeight) { + restoreHeight = maxHeight; + } + return Size(restoreWidth, restoreHeight); +} + +/// return null means center +Future _adjustRestoreMainWindowOffset( + double? left, double? top) async { + if (left == null || top == null) { + await windowManager.center(); + } else { + double windowLeft = max(0.0, left); + double windowTop = max(0.0, top); + + double frameLeft = double.infinity; + double frameTop = double.infinity; + double frameRight = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplayWidth + : kMobileMaxDisplayWidth) + .toDouble(); + double frameBottom = ((isDesktop || isWebDesktop) + ? kDesktopMaxDisplayHeight + : kMobileMaxDisplayHeight) + .toDouble(); + + if (isDesktop || isWebDesktop) { + for (final screen in await window_size.getScreenList()) { + frameLeft = min(screen.visibleFrame.left, frameLeft); + frameTop = min(screen.visibleFrame.top, frameTop); + frameRight = max(screen.visibleFrame.right, frameRight); + frameBottom = max(screen.visibleFrame.bottom, frameBottom); + } + } + + if (windowLeft < frameLeft || + windowLeft > frameRight || + windowTop < frameTop || + windowTop > frameBottom) { + return null; + } else { + return Offset(windowLeft, windowTop); + } + } + return null; +} + +/// Restore window position and size on start +/// Note that windowId must be provided if it's subwindow +Future restoreWindowPosition(WindowType type, {int? windowId}) async { + if (type != WindowType.Main && windowId == null) { + debugPrint( + "Error: windowId cannot be null when saving positions for sub window"); + } + final pos = bind.getLocalFlutterConfig(k: kWindowPrefix + type.name); + var lpos = LastWindowPosition.loadFromString(pos); + if (lpos == null) { + debugPrint("no window position saved, ignoring position restoration"); + return false; + } + + switch (type) { + case WindowType.Main: + if (lpos.isMaximized == true) { + await windowManager.maximize(); + } else { + final size = + await _adjustRestoreMainWindowSize(lpos.width, lpos.height); + final offset = await _adjustRestoreMainWindowOffset( + lpos.offsetWidth, lpos.offsetHeight); + await windowManager.setSize(size); + if (offset == null) { + await windowManager.center(); + } else { + await windowManager.setPosition(offset); + } + } + return true; + default: + final wc = WindowController.fromWindowId(windowId!); + if (lpos.isMaximized == true) { + await wc.maximize(); + } else { + final size = + await _adjustRestoreMainWindowSize(lpos.width, lpos.height); + final offset = await _adjustRestoreMainWindowOffset( + lpos.offsetWidth, lpos.offsetHeight); + debugPrint( + "restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}"); + if (offset == null) { + await wc.center(); + } else { + final frame = + Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height); + await wc.setFrame(frame); + } + } + break; + } + return false; +} + +/// Initialize uni links for macos/windows +/// +/// [Availability] +/// initUniLinks should only be used on macos/windows. +/// we use dbus for linux currently. +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'); + } + // check cold boot + try { + final initialLink = await getInitialLink(); + if (initialLink == null) { + return false; + } + return parseRustdeskUri(initialLink); + } catch (err) { + debugPrintStack(label: "$err"); + return false; + } +} + +/// 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) { + if (handleByFlutter) { + callUniLinksUriHandler(uri); + } else { + bind.sendUrlScheme(url: uri.toString()); + } + } else { + print("uni listen error: uri is empty."); + } + }, onError: (err) { + print("uni links error: $err"); + }); + return sub; +} + +/// 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 + var connectIndex = kBootArgs.indexOf("--connect"); + if (connectIndex == -1) { + return false; + } + 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 + kBootArgs.removeAt(connectIndex); + kBootArgs.removeAt(connectIndex); + // fallback to peer id + Future.delayed(Duration.zero, () { + rustDeskWinManager.newRemoteDesktop(id, switch_uuid: switchUuid); + }); + return true; + } + } + return false; +} + +/// Parse `rustdesk://` unilinks +/// +/// Returns true if we successfully handle the uri provided. +/// [Functions] +/// 1. New Connection: rustdesk://connection/new/your_peer_id +bool parseRustdeskUri(String uriPath) { + final uri = Uri.tryParse(uriPath); + if (uri == null) { + debugPrint("uri is not valid: $uriPath"); + return false; + } + return callUniLinksUriHandler(uri); +} + +/// uri handler +/// +/// Returns true if we successfully handle the uri provided. +bool callUniLinksUriHandler(Uri uri) { + debugPrint("uni links called: $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, switch_uuid: switch_uuid); + }); + return true; + } + return false; +} + +connectMainDesktop(String id, + {required bool isFileTransfer, + required bool isTcpTunneling, + required bool isRDP, + bool? forceRelay}) async { + if (isFileTransfer) { + await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay); + } else { + await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay); + } +} + +/// Connect to a peer with [id]. +/// If [isFileTransfer], starts a session only for file transfer. +/// If [isTcpTunneling], starts a session only for tcp tunneling. +/// If [isRDP], starts a session only for rdp. +connect(BuildContext context, String id, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false, + bool forceRelay = false}) async { + if (id == '') return; + id = id.replaceAll(' ', ''); + assert(!(isFileTransfer && isTcpTunneling && isRDP), + "more than one connect type"); + + if (isDesktop) { + if (desktopType == DesktopType.main) { + 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")) { + return; + } + } + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FileManagerPage(id: id), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => RemotePage(id: id), + ), + ); + } + } + + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } +} + +Map getHttpHeaders() { + return { + 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}' + }; +} + +// Simple wrapper of built-in types for reference use. +class SimpleWrapper { + T value; + SimpleWrapper(this.value); +} + +/// call this to reload current window. +/// +/// [Note] +/// Must have [RefreshWrapper] on the top of widget tree. +void reloadCurrentWindow() { + if (Get.context != null) { + // reload self window + RefreshWrapper.of(Get.context!)?.rebuild(); + } else { + debugPrint( + "reload current window failed, global BuildContext does not exist"); + } +} + +/// call this to reload all windows, including main + all sub windows. +Future reloadAllWindows() async { + reloadCurrentWindow(); + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + for (final id in ids) { + DesktopMultiWindow.invokeMethod(id, kWindowActionRebuild); + } + } on AssertionError { + // ignore + } +} + +/// Indicate the flutter app is running in portable mode. +/// +/// [Note] +/// Portable build is only available on Windows. +bool isRunningInPortableMode() { + if (!Platform.isWindows) { + return false; + } + return bool.hasEnvironment(kEnvPortableExecutable); +} + +/// Window status callback +Future onActiveWindowChanged() async { + print( + "[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}"); + if (rustDeskWinManager.getActiveWindows().isEmpty) { + // close all sub windows + try { + await Future.wait([ + saveWindowPosition(WindowType.Main), + rustDeskWinManager.closeAllSubWindows() + ]); + } catch (err) { + debugPrintStack(label: "$err"); + } finally { + debugPrint("Start closing RustDesk..."); + await windowManager.setPreventClose(false); + await windowManager.close(); + if (Platform.isMacOS) { + RdPlatformChannel.instance.terminate(); + } + } + } +} + +Timer periodic_immediate(Duration duration, Future Function() callback) { + Future.delayed(Duration.zero, callback); + return Timer.periodic(duration, (timer) async { + await callback(); + }); +} + +/// return a human readable windows version +WindowsTarget getWindowsTarget(int buildNumber) { + if (!Platform.isWindows) { + return WindowsTarget.naw; + } + if (buildNumber >= 22000) { + return WindowsTarget.w11; + } else if (buildNumber >= 10240) { + return WindowsTarget.w10; + } else if (buildNumber >= 9600) { + return WindowsTarget.w8_1; + } else if (buildNumber >= 9200) { + return WindowsTarget.w8; + } else if (buildNumber >= 7601) { + return WindowsTarget.w7; + } else if (buildNumber >= 6002) { + return WindowsTarget.vista; + } else { + // minimum support + return WindowsTarget.xp; + } +} + +/// Get windows target build number. +/// +/// [Note] +/// Please use this function wrapped with `Platform.isWindows`. +int getWindowsTargetBuildNumber() { + final rtlGetVersion = DynamicLibrary.open('ntdll.dll').lookupFunction< + Void Function(Pointer), + void Function(Pointer)>('RtlGetVersion'); + final osVersionInfo = getOSVERSIONINFOEXPointer(); + rtlGetVersion(osVersionInfo); + int buildNumber = osVersionInfo.ref.dwBuildNumber; + calloc.free(osVersionInfo); + return buildNumber; +} + +/// Get Windows OS version pointer +/// +/// [Note] +/// Please use this function wrapped with `Platform.isWindows`. +Pointer getOSVERSIONINFOEXPointer() { + final pointer = calloc(); + pointer.ref + ..dwOSVersionInfoSize = sizeOf() + ..dwBuildNumber = 0 + ..dwMajorVersion = 0 + ..dwMinorVersion = 0 + ..dwPlatformId = 0 + ..szCSDVersion = '' + ..wServicePackMajor = 0 + ..wServicePackMinor = 0 + ..wSuiteMask = 0 + ..wProductType = 0 + ..wReserved = 0; + return pointer; +} + +/// Indicating we need to use compatible ui mode. +/// +/// [Conditions] +/// - Windows 7, window will overflow when we use frameless ui. +bool get kUseCompatibleUiMode => + Platform.isWindows && + const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion); + +class ServerConfig { + late String idServer; + late String relayServer; + late String apiServer; + late String key; + + ServerConfig( + {String? idServer, String? relayServer, String? apiServer, String? key}) { + this.idServer = idServer?.trim() ?? ''; + this.relayServer = relayServer?.trim() ?? ''; + this.apiServer = apiServer?.trim() ?? ''; + this.key = key?.trim() ?? ''; + } + + /// decode from shared string (from user shared or rustdesk-server generated) + /// also see [encode] + /// throw when decoding failure + ServerConfig.decode(String msg) { + final input = msg.split('').reversed.join(''); + final bytes = base64Decode(base64.normalize(input)); + final json = jsonDecode(utf8.decode(bytes)); + + idServer = json['host'] ?? ''; + relayServer = json['relay'] ?? ''; + apiServer = json['api'] ?? ''; + key = json['key'] ?? ''; + } + + /// encode to shared string + /// also see [ServerConfig.decode] + String encode() { + Map config = {}; + config['host'] = idServer.trim(); + config['relay'] = relayServer.trim(); + config['api'] = apiServer.trim(); + config['key'] = key.trim(); + return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits)) + .split('') + .reversed + .join(); + } + + /// from local options + ServerConfig.fromOptions(Map options) + : idServer = options['custom-rendezvous-server'] ?? "", + relayServer = options['relay-server'] ?? "", + apiServer = options['api-server'] ?? "", + key = options['key'] ?? ""; +} + +Widget dialogButton(String text, + {required VoidCallback? onPressed, + bool isOutline = false, + TextStyle? style, + ButtonStyle? buttonStyle}) { + if (isDesktop) { + if (isOutline) { + return OutlinedButton( + onPressed: onPressed, + child: Text(translate(text), style: style), + ); + } else { + return ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), + onPressed: onPressed, + child: 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/formatter/id_formatter.dart b/flutter/lib/common/formatter/id_formatter.dart new file mode 100644 index 000000000..a9e4893a6 --- /dev/null +++ b/flutter/lib/common/formatter/id_formatter.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class IDTextEditingController extends TextEditingController { + IDTextEditingController({String? text}) : super(text: text); + + String get id => trimID(value.text); + + set id(String newID) => text = formatID(newID); +} + +class IDTextInputFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, TextEditingValue newValue) { + if (newValue.text.isEmpty) { + return newValue.copyWith(text: ''); + } else if (newValue.text.compareTo(oldValue.text) == 0) { + return newValue; + } else { + int selectionIndexFromTheRight = + newValue.text.length - newValue.selection.extentOffset; + String newID = formatID(newValue.text); + return TextEditingValue( + text: newID, + selection: TextSelection.collapsed( + offset: newID.length - selectionIndexFromTheRight, + ), + ); + } + } +} + +String formatID(String id) { + String id2 = id.replaceAll(' ', ''); + if (int.tryParse(id2) == null) return id; + String newID = ''; + if (id2.length <= 3) { + newID = id2; + } else { + var n = id2.length; + var a = n % 3 != 0 ? n % 3 : 3; + newID = id2.substring(0, a); + for (var i = a; i < n; i += 3) { + newID += " ${id2.substring(i, i + 3)}"; + } + } + return newID; +} + +String trimID(String id) { + return id.replaceAll(' ', ''); +} diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart new file mode 100644 index 000000000..4717143fd --- /dev/null +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -0,0 +1,121 @@ +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"; + static const kAuthReqTypeSMSCode = "sms_code"; + static const kAuthReqTypeEmailCode = "email_code"; + + static const kAuthResTypeToken = "access_token"; + static const kAuthResTypeEmailCheck = "email_check"; +} + +class UserPayload { + String name = ''; + String email = ''; + String note = ''; + int? status; + String grp = ''; + bool isAdmin = false; + + UserPayload.fromJson(Map json) + : name = json['name'] ?? '', + email = json['email'] ?? '', + note = json['note'] ?? '', + status = json['status'], + grp = json['grp'] ?? '', + isAdmin = json['is_admin'] == true; +} + +class PeerPayload { + String id = ''; + String info = ''; + int? status; + String user = ''; + String user_name = ''; + String note = ''; + + PeerPayload.fromJson(Map json) + : id = json['id'] ?? '', + info = json['info'] ?? '', + status = json['status'], + user = json['user'] ?? '', + user_name = json['user_name'] ?? '', + note = json['note'] ?? ''; + + static Peer toPeer(PeerPayload p) { + return Peer.fromJson({"id": p.id}); + } +} + +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; + String? id; + String? uuid; + bool? autoLogin; + String? type; + String? verificationCode; + Map deviceInfo = DeviceInfo.toJson(); + + LoginRequest( + {this.username, + this.password, + this.id, + this.uuid, + this.autoLogin, + this.type, + this.verificationCode}); + + Map toJson() { + final Map data = {}; + data['username'] = username ?? ''; + data['password'] = password ?? ''; + data['id'] = id ?? ''; + data['uuid'] = uuid ?? ''; + data['autoLogin'] = autoLogin ?? ''; + data['type'] = type ?? ''; + data['verificationCode'] = verificationCode ?? ''; + data['deviceInfo'] = deviceInfo; + return data; + } +} + +class LoginResponse { + String? access_token; + String? type; + UserPayload? user; + + LoginResponse({this.access_token, this.type, this.user}); + + LoginResponse.fromJson(Map json) { + access_token = json['access_token']; + type = json['type']; + user = json['user'] != null ? UserPayload.fromJson(json['user']) : null; + } +} + +class RequestException implements Exception { + int statusCode; + String cause; + RequestException(this.statusCode, this.cause); + + @override + String toString() { + return "RequestException, statusCode: $statusCode, error: $cause"; + } +} diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart new file mode 100644 index 000000000..ebac18dac --- /dev/null +++ b/flutter/lib/common/shared_state.dart @@ -0,0 +1,247 @@ +import 'package:get/get.dart'; + +import '../consts.dart'; + +// TODO: A lot of dup code. + +class PrivacyModeState { + static String tag(String id) => 'privacy_mode_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class BlockInputState { + static String tag(String id) => 'block_input_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class CurrentDisplayState { + static String tag(String id) => 'current_display_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxInt state = RxInt(0); + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxInt find(String id) => Get.find(tag: tag(id)); +} + +class ConnectionType { + final Rx _secure = kInvalidValueStr.obs; + final Rx _direct = kInvalidValueStr.obs; + + Rx get secure => _secure; + Rx get direct => _direct; + + static String get strSecure => 'secure'; + static String get strInsecure => 'insecure'; + static String get strDirect => ''; + static String get strIndirect => '_relay'; + + void setSecure(bool v) { + _secure.value = v ? strSecure : strInsecure; + } + + void setDirect(bool v) { + _direct.value = v ? strDirect : strIndirect; + } + + bool isValid() { + return _secure.value != kInvalidValueStr && + _direct.value != kInvalidValueStr; + } +} + +class ConnectionTypeState { + static String tag(String id) => 'connection_type_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final ConnectionType collectionType = ConnectionType(); + Get.put(collectionType, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static ConnectionType find(String id) => + Get.find(tag: tag(id)); +} + +class ShowRemoteCursorState { + static String tag(String id) => 'show_remote_cursor_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class KeyboardEnabledState { + static String tag(String id) => 'keyboard_enabled_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + // Server side, default true + final RxBool state = true.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class RemoteCursorMovedState { + static String tag(String id) => 'remote_cursor_moved_$id'; + + static void init(String id) { + final key = tag(id); + if (!Get.isRegistered(tag: key)) { + // Server side, default true + final RxBool state = false.obs; + Get.put(state, tag: key); + } + } + + static void delete(String id) { + final key = tag(id); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id) => Get.find(tag: tag(id)); +} + +class RemoteCountState { + static String tag() => 'remote_count_'; + + static void init() { + final key = tag(); + if (!Get.isRegistered(tag: key)) { + // Server side, default true + final RxInt state = 1.obs; + Get.put(state, tag: key); + } + } + + static void delete() { + final key = tag(); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxInt find() => Get.find(tag: tag()); +} + +class PeerBoolOption { + static String tag(String id, String opt) => 'peer_{$opt}_$id'; + + static void init(String id, String opt, bool Function() init_getter) { + final key = tag(id, opt); + if (!Get.isRegistered(tag: key)) { + final RxBool value = RxBool(init_getter()); + Get.put(value, tag: key); + } + } + + static void delete(String id, String opt) { + final key = tag(id, opt); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxBool find(String id, String opt) => + Get.find(tag: tag(id, opt)); +} + +class PeerStringOption { + static String tag(String id, String opt) => 'peer_{$opt}_$id'; + + static void init(String id, String opt, String Function() init_getter) { + final key = tag(id, opt); + if (!Get.isRegistered(tag: key)) { + final RxString value = RxString(init_getter()); + Get.put(value, tag: key); + } + } + + static void delete(String id, String opt) { + final key = tag(id, opt); + if (Get.isRegistered(tag: key)) { + Get.delete(tag: key); + } + } + + static RxString find(String id, String opt) => + Get.find(tag: tag(id, opt)); +} diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart new file mode 100644 index 000000000..88a5aaaa3 --- /dev/null +++ b/flutter/lib/common/widgets/address_book.dart @@ -0,0 +1,491 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/formatter/id_formatter.dart'; +import 'package:flutter_hbb/common/widgets/peer_card.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import '../../consts.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import 'package:get/get.dart'; + +import '../../common.dart'; +import 'login.dart'; + +class AddressBook extends StatefulWidget { + final EdgeInsets? menuPadding; + const AddressBook({Key? key, this.menuPadding}) : super(key: key); + + @override + State createState() { + return _AddressBookState(); + } +} + +class _AddressBookState extends State { + var menuPos = RelativeRect.fill; + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => FutureBuilder( + future: buildBody(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const Offstage(); + } + }); + + Future buildBody(BuildContext context) async { + return Obx(() { + if (gFFI.userModel.userName.value.isEmpty) { + return Center( + child: ElevatedButton( + onPressed: loginDialog, child: Text(translate("Login")))); + } else { + if (gFFI.abModel.abLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (gFFI.abModel.abError.isNotEmpty) { + return _buildShowError(gFFI.abModel.abError.value); + } + return isDesktop + ? _buildAddressBookDesktop() + : _buildAddressBookMobile(); + } + }); + } + + Widget _buildShowError(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate(error)), + TextButton( + onPressed: () { + gFFI.abModel.pullAb(); + }, + child: Text(translate("Retry"))) + ], + )); + } + + Widget _buildAddressBookDesktop() { + return Row( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: + BorderSide(color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + width: 200, + height: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + _buildTagHeader(), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(2)), + child: _buildTags(), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + _buildPeersViews() + ], + ); + } + + Widget _buildAddressBookMobile() { + return Column( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 1.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: + BorderSide(color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildTagHeader(), + Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(4)), + child: _buildTags(), + ).marginSymmetric(vertical: 8.0), + ], + ), + ), + ), + Divider(), + _buildPeersViews() + ], + ); + } + + Widget _buildTagHeader() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(translate('Tags')), + Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onPointerUp: (_) => _showMenu(menuPos), + child: ActionMore()), + ], + ); + } + + Widget _buildTags() { + return Obx( + () => Wrap( + children: gFFI.abModel.tags + .map((e) => AddressBookTag( + name: e, + tags: gFFI.abModel.selectedTags, + onTap: () { + if (gFFI.abModel.selectedTags.contains(e)) { + gFFI.abModel.selectedTags.remove(e); + } else { + gFFI.abModel.selectedTags.add(e); + } + })) + .toList(), + ), + ); + } + + Widget _buildPeersViews() { + return Expanded( + child: Align( + alignment: Alignment.topLeft, + child: Obx(() => AddressBookPeersView( + menuPadding: widget.menuPadding, + initPeers: gFFI.abModel.peers.value, + ))), + ); + } + + void _showMenu(RelativeRect pos) { + final items = [ + getEntry(translate("Add ID"), abAddId), + getEntry(translate("Add Tag"), abAddTag), + getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags), + ]; + + mod_menu.showMenu( + context: context, + position: pos, + items: items + .map((e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ); + } + + void abAddId() async { + var isInProgress = false; + IDTextEditingController idController = IDTextEditingController(text: ''); + TextEditingController aliasController = TextEditingController(text: ''); + final tags = List.of(gFFI.abModel.tags); + var selectedTag = List.empty(growable: true).obs; + final style = TextStyle(fontSize: 14.0); + String? errorMsg; + + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + isInProgress = true; + errorMsg = null; + }); + String id = idController.id; + if (id.isEmpty) { + // pass + } else { + if (gFFI.abModel.idContainBy(id)) { + setState(() { + isInProgress = false; + errorMsg = translate('ID already exists'); + }); + return; + } + gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag); + await gFFI.abModel.pushAb(); + this.setState(() {}); + // final currentPeers + } + close(); + } + + return CustomAlertDialog( + title: Text(translate("Add ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + Text( + '*', + style: TextStyle(color: Colors.red, fontSize: 14), + ), + Text( + 'ID', + style: style, + ), + ], + ), + ), + TextField( + controller: idController, + inputFormatters: [IDTextInputFormatter()], + decoration: InputDecoration( + isDense: true, + border: OutlineInputBorder(), + errorText: errorMsg), + style: style, + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate('Alias'), + style: style, + ), + ).marginOnly(top: 8, bottom: 2), + TextField( + controller: aliasController, + decoration: InputDecoration( + border: OutlineInputBorder(), + isDense: true, + ), + style: style, + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate('Tags'), + style: style, + ), + ).marginOnly(top: 8), + Container( + child: Wrap( + children: tags + .map((e) => AddressBookTag( + name: e, + tags: selectedTag, + onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + }, + showActionMenu: false)) + .toList(growable: false), + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + void abAddTag() async { + var field = ""; + var msg = ""; + var isInProgress = false; + TextEditingController controller = TextEditingController(text: field); + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = controller.text.trim(); + if (field.isEmpty) { + // pass + } else { + final tags = field.trim().split(RegExp(r"[\s,;\n]+")); + field = tags.join(','); + for (final tag in tags) { + gFFI.abModel.addTag(tag); + } + await gFFI.abModel.pushAb(); + // final currentPeers + } + close(); + } + + return CustomAlertDialog( + title: Text(translate("Add Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + autofocus: true, + ), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } +} + +class AddressBookTag extends StatelessWidget { + final String name; + final RxList tags; + final Function()? onTap; + final bool showActionMenu; + + const AddressBookTag( + {Key? key, + required this.name, + required this.tags, + this.onTap, + this.showActionMenu = true}) + : super(key: key); + + @override + Widget build(BuildContext context) { + var pos = RelativeRect.fill; + + void setPosition(TapDownDetails e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + pos = RelativeRect.fromLTRB(x, y, x, y); + } + + return GestureDetector( + onTap: onTap, + onTapDown: showActionMenu ? setPosition : null, + onSecondaryTapDown: showActionMenu ? setPosition : null, + onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null, + onLongPress: showActionMenu ? () => _showMenu(context, pos) : null, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: tags.contains(name) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(6)), + margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text(name, + style: + TextStyle(color: tags.contains(name) ? Colors.white : null)), + ), + ), + ); + } + + void _showMenu(BuildContext context, RelativeRect pos) { + final items = [ + getEntry(translate("Delete"), () { + gFFI.abModel.deleteTag(name); + gFFI.abModel.pushAb(); + Future.delayed(Duration.zero, () => Get.back()); + }), + ]; + + mod_menu.showMenu( + context: context, + position: pos, + items: items + .map((e) => e.build( + context, + MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ); + } +} + +MenuEntryButton getEntry(String title, VoidCallback proc) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + title, + style: style, + ), + proc: proc, + padding: kDesktopMenuPadding, + dismissOnClicked: true, + ); +} diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart new file mode 100644 index 000000000..c1991633a --- /dev/null +++ b/flutter/lib/common/widgets/chat_page.dart @@ -0,0 +1,156 @@ +import 'package:dash_chat_2/dash_chat_2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:provider/provider.dart'; + +import '../../mobile/pages/home_page.dart'; + +class ChatPage extends StatelessWidget implements PageShape { + late final ChatModel chatModel; + + ChatPage({ChatModel? chatModel}) { + this.chatModel = chatModel ?? gFFI.chatModel; + } + + @override + final title = translate("Chat"); + + @override + final icon = Icon(Icons.chat); + + @override + final appBarActions = [ + PopupMenuButton( + icon: Icon(Icons.group), + itemBuilder: (context) { + // only mobile need [appBarActions], just bind gFFI.chatModel + final chatModel = gFFI.chatModel; + return chatModel.messages.entries.map((entry) { + final id = entry.key; + final user = entry.value.chatUser; + return PopupMenuItem( + child: Text("${user.firstName} ${user.id}"), + value: id, + ); + }).toList(); + }, + onSelected: (id) { + gFFI.chatModel.changeCurrentID(id); + }) + ]; + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: chatModel, + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Consumer(builder: (context, chatModel, child) { + final currentUser = chatModel.currentUser; + return Stack( + children: [ + LayoutBuilder(builder: (context, constraints) { + final chat = DashChat( + onSend: (chatMsg) { + chatModel.send(chatMsg); + chatModel.inputNode.requestFocus(); + }, + currentUser: chatModel.me, + messages: chatModel + .messages[chatModel.currentID]?.chatMessages ?? + [], + inputOptions: InputOptions( + sendOnEnter: true, + focusNode: chatModel.inputNode, + inputTextStyle: TextStyle( + fontSize: 14, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color), + inputDecoration: isDesktop + ? InputDecoration( + isDense: true, + hintText: + "${translate('Write a message')}...", + filled: true, + fillColor: + Theme.of(context).colorScheme.background, + contentPadding: EdgeInsets.all(10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: const BorderSide( + width: 0, + style: BorderStyle.none, + ), + ), + ) + : defaultInputDecoration( + hintText: + "${translate('Write a message')}...", + 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, + 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, + borderTopLeft: 8, + borderTopRight: 8, + borderBottomRight: 8, + borderBottomLeft: 8, + )), + ); + return SelectionArea(child: chat); + }), + desktopType == DesktopType.cm || + chatModel.currentID == ChatModel.clientModeID + ? SizedBox.shrink() + : Padding( + padding: EdgeInsets.all(12), + child: Row( + children: [ + Icon(Icons.account_circle, + color: MyTheme.accent80), + SizedBox(width: 5), + Text( + "${currentUser.firstName} ${currentUser.id}", + style: TextStyle(color: MyTheme.accent50), + ), + ], + )), + ], + ); + }))); + } +} 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 new file mode 100644 index 000000000..cdce6f12a --- /dev/null +++ b/flutter/lib/common/widgets/dialog.dart @@ -0,0 +1,292 @@ +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; + bind.mainChangeId(newId: newId); + }); + + var status = await bind.mainGetAsyncStatus(); + while (status == " ") { + await Future.delayed(const Duration(milliseconds: 100)); + status = await bind.mainGetAsyncStatus(); + } + if (status.isEmpty) { + // ok + close(); + return; + } + setState(() { + isInProgress = false; + msg = '${translate('Prompt')}: ${translate(status)}'; + }); + } + + return CustomAlertDialog( + title: Text(translate("Change ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("id_change_tip")), + const SizedBox( + height: 12.0, + ), + TextField( + decoration: InputDecoration( + labelText: translate('Your new ID'), + border: const OutlineInputBorder(), + 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) + ], + controller: controller, + autofocus: true, + onChanged: (value) { + setState(() { + rxId.value = value.trim(); + msg = ''; + }); + }, + ), + const SizedBox( + 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: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void changeWhiteList({Function()? callback}) async { + var newWhiteList = (await bind.mainGetOption(key: 'whitelist')).split(','); + var newWhiteListField = newWhiteList.join('\n'); + var controller = TextEditingController(text: newWhiteListField); + var msg = ""; + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("IP Whitelisting")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + const SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + decoration: InputDecoration( + border: const OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + autofocus: true), + ), + ], + ), + const SizedBox( + height: 4.0, + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + 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(); + }, + ), + ], + onCancel: close, + ); + }); +} + +Future changeDirectAccessPort( + String currentIP, String currentPort) async { + final controller = TextEditingController(text: currentPort); + await gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Change Local Port")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 8.0), + Row( + children: [ + Expanded( + child: TextField( + maxLines: null, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: '21118', + isCollapsed: true, + prefix: Text('$currentIP : '), + suffix: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.clear, size: 16), + onPressed: () => controller.clear())), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + 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, + autofocus: true), + ), + ], + ), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: () async { + await bind.mainSetOption( + key: 'direct-access-port', value: controller.text); + close(); + }), + ], + onCancel: close, + ); + }); + return controller.text; +} diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart new file mode 100644 index 000000000..43dc3a658 --- /dev/null +++ b/flutter/lib/common/widgets/login.dart @@ -0,0 +1,679 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../common.dart'; + +class _IconOP extends StatelessWidget { + final String icon; + final double iconWidth; + final EdgeInsets margin; + const _IconOP( + {Key? key, + required this.icon, + required this.iconWidth, + this.margin = const EdgeInsets.symmetric(horizontal: 4.0)}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: margin, + child: SvgPicture.asset( + 'assets/$icon.svg', + width: iconWidth, + ), + ); + } +} + +class ButtonOP extends StatelessWidget { + final String op; + final RxString curOP; + final double iconWidth; + final Color primaryColor; + final double height; + final Function() onTap; + + const ButtonOP({ + Key? key, + required this.op, + required this.curOP, + required this.iconWidth, + required this.primaryColor, + required this.height, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row(children: [ + Container( + height: height, + width: 200, + child: Obx(() => ElevatedButton( + style: ElevatedButton.styleFrom( + primary: curOP.value.isEmpty || curOP.value == op + ? primaryColor + : Colors.grey, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null, + child: Row( + children: [ + SizedBox( + width: 30, + child: _IconOP( + icon: op, + iconWidth: iconWidth, + margin: EdgeInsets.only(right: 5), + )), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Center( + child: Text('${translate("Continue with")} $op')))), + ], + ))), + ), + ]); + } +} + +class ConfigOP { + final String op; + final double iconWidth; + ConfigOP({required this.op, required this.iconWidth}); +} + +class WidgetOP extends StatefulWidget { + final ConfigOP config; + final RxString curOP; + final Function(String) cbLogin; + const WidgetOP({ + Key? key, + required this.config, + required this.curOP, + required this.cbLogin, + }) : super(key: key); + + @override + State createState() { + return _WidgetOPState(); + } +} + +class _WidgetOPState extends State { + Timer? _updateTimer; + String _stateMsg = ''; + String _failedMsg = ''; + String _url = ''; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + _updateTimer?.cancel(); + } + + _beginQueryState() { + _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + _updateState(); + }); + } + + _updateState() { + bind.mainAccountAuthResult().then((result) { + if (result.isEmpty) { + return; + } + final resultMap = jsonDecode(result); + if (resultMap == null) { + return; + } + final String stateMsg = resultMap['state_msg']; + String failedMsg = resultMap['failed_msg']; + final String? url = resultMap['url']; + final authBody = resultMap['auth_body']; + if (_stateMsg != stateMsg || _failedMsg != failedMsg) { + if (_url.isEmpty && url != null && url.isNotEmpty) { + launchUrl(Uri.parse(url)); + _url = url; + } + if (authBody != null) { + _updateTimer?.cancel(); + final String username = authBody['user']['name']; + widget.curOP.value = ''; + widget.cbLogin(username); + } + + setState(() { + _stateMsg = stateMsg; + _failedMsg = failedMsg; + if (failedMsg.isNotEmpty) { + widget.curOP.value = ''; + _updateTimer?.cancel(); + } + }); + } + }); + } + + _resetState() { + _stateMsg = ''; + _failedMsg = ''; + _url = ''; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ButtonOP( + op: widget.config.op, + curOP: widget.curOP, + iconWidth: widget.config.iconWidth, + primaryColor: str2color(widget.config.op, 0x7f), + height: 36, + onTap: () async { + _resetState(); + widget.curOP.value = widget.config.op; + await bind.mainAccountAuth(op: widget.config.op); + _beginQueryState(); + }, + ), + Obx(() { + if (widget.curOP.isNotEmpty && + widget.curOP.value != widget.config.op) { + _failedMsg = ''; + } + return Offstage( + 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( + offstage: widget.curOP.value != widget.config.op, + child: const SizedBox( + height: 5.0, + ), + ), + ), + Obx( + () => Offstage( + offstage: widget.curOP.value != widget.config.op, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 20), + child: ElevatedButton( + onPressed: () { + widget.curOP.value = ''; + _updateTimer?.cancel(); + _resetState(); + bind.mainAccountAuthCancel(); + }, + child: Text( + translate('Cancel'), + style: TextStyle(fontSize: 15), + ), + ), + ), + ), + ), + ], + ); + } +} + +class LoginWidgetOP extends StatelessWidget { + final List ops; + final RxString curOP; + final Function(String) cbLogin; + + LoginWidgetOP({ + Key? key, + required this.ops, + required this.curOP, + required this.cbLogin, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var children = ops + .map((op) => [ + WidgetOP( + config: op, + curOP: curOP, + cbLogin: cbLogin, + ), + const Divider( + indent: 5, + endIndent: 5, + ) + ]) + .expand((i) => i) + .toList(); + if (children.isNotEmpty) { + children.removeLast(); + } + return SingleChildScrollView( + child: Container( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + ))); + } +} + +class LoginWidgetUserPass extends StatelessWidget { + final TextEditingController username; + final TextEditingController pass; + final String? usernameMsg; + final String? passMsg; + final bool isInProgress; + final RxString curOP; + final RxBool autoLogin; + final Function() onLogin; + final FocusNode? userFocusNode; + const LoginWidgetUserPass({ + Key? key, + this.userFocusNode, + required this.username, + required this.pass, + required this.usernameMsg, + required this.passMsg, + required this.isInProgress, + required this.curOP, + required this.autoLogin, + required this.onLogin, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8.0), + DialogTextField( + title: translate("Username"), + controller: username, + focusNode: userFocusNode, + prefixIcon: Icon(Icons.account_circle_outlined), + errorText: usernameMsg), + DialogTextField( + title: translate("Password"), + obscureText: true, + controller: pass, + prefixIcon: Icon(Icons.lock_outline), + errorText: passMsg), + Obx(() => CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Remember me"), + ), + value: autoLogin.value, + onChanged: (v) { + if (v == null) return; + autoLogin.value = v; + }, + )), + Offstage( + offstage: !isInProgress, + child: const LinearProgressIndicator()), + const SizedBox(height: 12.0), + FittedBox( + child: + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + height: 38, + width: 200, + child: Obx(() => ElevatedButton( + child: Text( + translate('Login'), + style: TextStyle(fontSize: 16), + ), + onPressed: + curOP.value.isEmpty || curOP.value == 'rustdesk' + ? () { + onLogin(); + } + : null, + )), + ), + ])), + ], + )); + } +} + +class DialogTextField extends StatelessWidget { + final String title; + final bool obscureText; + final String? errorText; + final String? helperText; + final Widget? prefixIcon; + final TextEditingController controller; + final FocusNode? focusNode; + + DialogTextField( + {Key? key, + this.focusNode, + this.obscureText = false, + this.errorText, + this.helperText, + this.prefixIcon, + required this.title, + required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: title, + border: const OutlineInputBorder(), + prefixIcon: prefixIcon, + helperText: helperText, + helperMaxLines: 8, + errorText: errorText), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, + ), + ), + ], + ).paddingSymmetric(vertical: 4.0); + } +} + +/// common login dialog for desktop +/// call this directly +Future loginDialog() async { + var username = TextEditingController(); + var password = TextEditingController(); + final userFocusNode = FocusNode()..requestFocus(); + Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus()); + + String? usernameMsg; + String? passwordMsg; + var isInProgress = false; + final autoLogin = true.obs; + final RxString curOP = ''.obs; + + final res = await gFFI.dialogManager.show((setState, close) { + username.addListener(() { + if (usernameMsg != null) { + setState(() => usernameMsg = null); + } + }); + + password.addListener(() { + if (passwordMsg != null) { + setState(() => passwordMsg = null); + } + }); + + onDialogCancel() { + isInProgress = false; + close(false); + } + + onLogin() async { + // validate + if (username.text.isEmpty) { + setState(() => usernameMsg = translate('Username missed')); + return; + } + if (password.text.isEmpty) { + setState(() => passwordMsg = translate('Password missed')); + return; + } + curOP.value = 'rustdesk'; + setState(() => isInProgress = true); + try { + final resp = await gFFI.userModel.login(LoginRequest( + username: username.text, + password: password.text, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin.value, + type: HttpType.kAuthReqTypeAccount)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + await bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + case HttpType.kAuthResTypeEmailCheck: + setState(() => isInProgress = false); + final res = await verificationCodeDialog(resp.user); + if (res == true) { + close(true); + return; + } + break; + default: + passwordMsg = "Failed, bad response from server"; + break; + } + } on RequestException catch (err) { + passwordMsg = translate(err.cause); + debugPrintStack(label: err.toString()); + } catch (err) { + passwordMsg = "Unknown Error: $err"; + debugPrintStack(label: err.toString()); + } + curOP.value = ''; + setState(() => isInProgress = false); + } + + return CustomAlertDialog( + title: Text(translate('Login')), + contentBoxConstraints: BoxConstraints(minWidth: 400), + content: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 8.0, + ), + LoginWidgetUserPass( + username: username, + pass: password, + usernameMsg: usernameMsg, + passMsg: passwordMsg, + isInProgress: isInProgress, + curOP: curOP, + autoLogin: autoLogin, + onLogin: onLogin, + userFocusNode: userFocusNode, + ), + const SizedBox( + height: 8.0, + ), + Center( + child: Text( + translate('or'), + style: TextStyle(fontSize: 16), + )), + const SizedBox( + height: 8.0, + ), + LoginWidgetOP( + ops: [ + ConfigOP(op: 'GitHub', iconWidth: 20), + ConfigOP(op: 'Google', iconWidth: 20), + ConfigOP(op: 'Okta', iconWidth: 38), + ], + curOP: curOP, + cbLogin: (String username) { + gFFI.userModel.userName.value = username; + close(true); + }, + ), + ], + ), + actions: [dialogButton('Close', onPressed: onDialogCancel)], + onCancel: onDialogCancel, + ); + }); + + if (res != null) { + // update ab and group status + await gFFI.abModel.pullAb(); + await gFFI.groupModel.pull(); + } + + return res; +} + +Future verificationCodeDialog(UserPayload? user) async { + var autoLogin = true; + var isInProgress = false; + String? errorText; + + final code = TextEditingController(); + final focusNode = FocusNode()..requestFocus(); + Timer(Duration(milliseconds: 100), () => focusNode..requestFocus()); + + final res = await gFFI.dialogManager.show((setState, close) { + bool validate() { + return code.text.length >= 6; + } + + code.addListener(() { + if (errorText != null) { + setState(() => errorText = null); + } + }); + + void onVerify() async { + if (!validate()) { + setState( + () => errorText = translate('Too short, at least 6 characters.')); + return; + } + setState(() => isInProgress = true); + + try { + final resp = await gFFI.userModel.login(LoginRequest( + verificationCode: code.text, + username: user?.name, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin, + type: HttpType.kAuthReqTypeEmailCode)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + await bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + default: + errorText = "Failed, bad response from server"; + break; + } + } on RequestException catch (err) { + errorText = translate(err.cause); + debugPrintStack(label: err.toString()); + } catch (err) { + errorText = "Unknown Error: $err"; + debugPrintStack(label: err.toString()); + } + + setState(() => isInProgress = false); + } + + return CustomAlertDialog( + title: Text(translate("Verification code")), + contentBoxConstraints: BoxConstraints(maxWidth: 300), + content: Column( + children: [ + Offstage( + offstage: user?.email == null, + child: TextField( + decoration: InputDecoration( + labelText: "Email", + prefixIcon: Icon(Icons.email), + border: InputBorder.none), + readOnly: true, + controller: TextEditingController(text: user?.email), + )), + const SizedBox(height: 8), + DialogTextField( + title: '${translate("Verification code")}:', + controller: code, + errorText: errorText, + focusNode: focusNode, + helperText: translate('verification_tip'), + ), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Row(children: [ + Expanded(child: Text(translate("Trust this device"))) + ]), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = !autoLogin); + }, + ), + Offstage( + offstage: !isInProgress, + child: const LinearProgressIndicator()), + ], + ), + onCancel: close, + onSubmit: onVerify, + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Verify", onPressed: onVerify), + ]); + }); + + return res; +} diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart new file mode 100644 index 000000000..65eaba40f --- /dev/null +++ b/flutter/lib/common/widgets/my_group.dart @@ -0,0 +1,225 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class MyGroup extends StatefulWidget { + final EdgeInsets? menuPadding; + const MyGroup({Key? key, this.menuPadding}) : super(key: key); + + @override + State createState() { + return _MyGroupState(); + } +} + +class _MyGroupState extends State { + static final RxString selectedUser = ''.obs; + static final RxString searchUserText = ''.obs; + static TextEditingController searchUserController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => FutureBuilder( + future: buildBody(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const Offstage(); + } + }); + + Future buildBody(BuildContext context) async { + return Obx(() { + if (gFFI.groupModel.userLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (gFFI.groupModel.userLoadError.isNotEmpty) { + return _buildShowError(gFFI.groupModel.userLoadError.value); + } + if (isDesktop) { + return _buildDesktop(); + } else { + return _buildMobile(); + } + }); + } + + Widget _buildShowError(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate(error)), + TextButton( + onPressed: () { + gFFI.groupModel.pull(); + }, + child: Text(translate("Retry"))) + ], + )); + } + + Widget _buildDesktop() { + return Obx( + () => Row( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + width: 200, + height: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + _buildLeftHeader(), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(2)), + child: _buildUserContacts(), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ), + ); + } + + Widget _buildMobile() { + return Obx( + () => Column( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildLeftHeader(), + Container( + width: double.infinity, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(4)), + child: _buildUserContacts(), + ).marginSymmetric(vertical: 8.0) + ], + ), + ), + ), + Divider(), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ), + ); + } + + Widget _buildLeftHeader() { + return Row( + children: [ + Expanded( + child: TextField( + controller: searchUserController, + onChanged: (value) { + searchUserText.value = value; + }, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 10), + hintText: translate("Search"), + hintStyle: + TextStyle(fontSize: 14, color: Theme.of(context).hintColor), + border: InputBorder.none, + isDense: true, + ), + )), + ], + ); + } + + Widget _buildUserContacts() { + return Obx(() { + return Column( + children: gFFI.groupModel.users + .where((p0) { + if (searchUserText.isNotEmpty) { + return p0.name.contains(searchUserText.value); + } + return true; + }) + .map((e) => _buildUserItem(e.name)) + .toList()); + }); + } + + Widget _buildUserItem(String username) { + return InkWell(onTap: () { + if (selectedUser.value != username) { + selectedUser.value = username; + gFFI.groupModel.pullUserPeers(username); + } + }, child: Obx( + () { + bool selected = selectedUser.value == username; + return Container( + decoration: BoxDecoration( + color: selected ? MyTheme.color(context).highlight : null, + border: Border( + bottom: BorderSide( + width: 0.7, + color: Theme.of(context).dividerColor.withOpacity(0.1))), + ), + child: Container( + child: Row( + children: [ + Icon(Icons.person_outline_rounded, color: Colors.grey, size: 16) + .marginOnly(right: 4), + Expanded(child: Text(username)), + ], + ).paddingSymmetric(vertical: 4), + ), + ); + }, + )).marginSymmetric(horizontal: 12); + } +} diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart new file mode 100644 index 000000000..ba7b8a059 --- /dev/null +++ b/flutter/lib/common/widgets/overlay.dart @@ -0,0 +1,425 @@ +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'; +import '../../desktop/widgets/tabbar_widget.dart'; +import '../../models/chat_model.dart'; +import '../../models/model.dart'; +import 'chat_page.dart'; + +class DraggableChatWindow extends StatelessWidget { + const DraggableChatWindow( + {Key? key, + this.position = Offset.zero, + required this.width, + required this.height, + required this.chatModel}) + : super(key: key); + + final Offset position; + final double width; + final double height; + final ChatModel chatModel; + + @override + Widget build(BuildContext context) { + return Draggable( + checkKeyboard: true, + position: position, + width: width, + height: height, + builder: (context, onPanUpdate) { + return isIOS + ? ChatPage(chatModel: chatModel) + : Scaffold( + resizeToAvoidBottomInset: false, + appBar: CustomAppBar( + onPanUpdate: onPanUpdate, + appBar: isDesktop + ? _buildDesktopAppBar(context) + : _buildMobileAppBar(context), + ), + body: ChatPage(chatModel: chatModel), + ); + }); + } + + Widget _buildMobileAppBar(BuildContext context) { + return Container( + color: Theme.of(context).colorScheme.primary, + height: 50, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Text( + translate("Chat"), + style: const TextStyle( + color: Colors.white, + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 20), + )), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: () { + chatModel.hideChatWindowOverlay(); + }, + icon: const Icon(Icons.keyboard_arrow_down)), + IconButton( + onPressed: () { + chatModel.hideChatWindowOverlay(); + chatModel.hideChatIconOverlay(); + }, + icon: const Icon(Icons.close)) + ], + ) + ], + ), + ); + } + + Widget _buildDesktopAppBar(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).hintColor.withOpacity(0.4)))), + height: 38, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), + 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( + message: 'Close', + icon: IconFont.close, + onTap: chatModel.hideChatWindowOverlay, + isClose: true, + boxSize: 32, + )) + ], + ), + ); + } +} + +class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { + final GestureDragUpdateCallback onPanUpdate; + final Widget appBar; + + const CustomAppBar( + {Key? key, required this.onPanUpdate, required this.appBar}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return GestureDetector(onPanUpdate: onPanUpdate, child: appBar); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} + +/// floating buttons of back/home/recent actions for android +class DraggableMobileActions extends StatelessWidget { + DraggableMobileActions( + {this.position = Offset.zero, + this.onBackPressed, + this.onRecentPressed, + this.onHomePressed, + this.onHidePressed, + required this.width, + required this.height}); + + final Offset position; + final double width; + final double height; + final VoidCallback? onBackPressed; + final VoidCallback? onHomePressed; + final VoidCallback? onRecentPressed; + final VoidCallback? onHidePressed; + + @override + Widget build(BuildContext context) { + return Draggable( + position: position, + width: width, + height: height, + builder: (_, onPanUpdate) { + return GestureDetector( + onPanUpdate: onPanUpdate, + child: Card( + color: Colors.transparent, + shadowColor: Colors.transparent, + child: Container( + decoration: BoxDecoration( + color: MyTheme.accent.withOpacity(0.4), + borderRadius: BorderRadius.all(Radius.circular(15))), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + color: Colors.white, + onPressed: onBackPressed, + splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.arrow_back)), + IconButton( + color: Colors.white, + onPressed: onHomePressed, + splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.home)), + IconButton( + color: Colors.white, + onPressed: onRecentPressed, + splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.more_horiz)), + const VerticalDivider( + width: 0, + thickness: 2, + indent: 10, + endIndent: 10, + ), + IconButton( + color: Colors.white, + onPressed: onHidePressed, + splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.keyboard_arrow_down)), + ], + ), + ))); + }); + } +} + +class Draggable extends StatefulWidget { + const Draggable( + {Key? key, + this.checkKeyboard = false, + this.checkScreenSize = false, + this.position = Offset.zero, + required this.width, + required this.height, + required this.builder}) + : super(key: key); + + final bool checkKeyboard; + final bool checkScreenSize; + final Offset position; + final double width; + final double height; + final Widget Function(BuildContext, GestureDragUpdateCallback) builder; + + @override + State createState() => _DraggableState(); +} + +class _DraggableState extends State { + late Offset _position; + bool _keyboardVisible = false; + double _saveHeight = 0; + double _lastBottomHeight = 0; + + @override + void initState() { + super.initState(); + _position = widget.position; + } + + void onPanUpdate(DragUpdateDetails d) { + final offset = d.delta; + final size = MediaQuery.of(context).size; + double x = 0; + double y = 0; + + if (_position.dx + offset.dx + widget.width > size.width) { + x = size.width - widget.width; + } else if (_position.dx + offset.dx < 0) { + x = 0; + } else { + x = _position.dx + offset.dx; + } + + if (_position.dy + offset.dy + widget.height > size.height) { + y = size.height - widget.height; + } else if (_position.dy + offset.dy < 0) { + y = 0; + } else { + y = _position.dy + offset.dy; + } + setState(() { + _position = Offset(x, y); + }); + } + + checkScreenSize() {} + + checkKeyboard() { + final bottomHeight = MediaQuery.of(context).viewInsets.bottom; + final currentVisible = bottomHeight != 0; + + // save + if (!_keyboardVisible && currentVisible) { + _saveHeight = _position.dy; + } + + // reset + if (_lastBottomHeight > 0 && bottomHeight == 0) { + setState(() { + _position = Offset(_position.dx, _saveHeight); + }); + } + + // onKeyboardVisible + if (_keyboardVisible && currentVisible) { + final sumHeight = bottomHeight + widget.height; + final contextHeight = MediaQuery.of(context).size.height; + if (sumHeight + _position.dy > contextHeight) { + final y = contextHeight - sumHeight; + setState(() { + _position = Offset(_position.dx, y); + }); + } + } + + _keyboardVisible = currentVisible; + _lastBottomHeight = bottomHeight; + } + + @override + Widget build(BuildContext context) { + if (widget.checkKeyboard) { + checkKeyboard(); + } + if (widget.checkScreenSize) { + checkScreenSize(); + } + 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 { + 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) => 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 new file mode 100644 index 000000000..a69fc3bbe --- /dev/null +++ b/flutter/lib/common/widgets/peer_card.dart @@ -0,0 +1,1185 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/address_book.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; +import '../../common/formatter/id_formatter.dart'; +import '../../models/model.dart'; +import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; +import '../../desktop/widgets/popup_menu.dart'; + +typedef PopupMenuEntryBuilder = Future>> + Function(BuildContext); + +enum PeerUiType { grid, list } + +final peerCardUiType = PeerUiType.grid.obs; + +class _PeerCard extends StatefulWidget { + final Peer peer; + final Function(BuildContext, String) connect; + final PopupMenuEntryBuilder popupMenuEntryBuilder; + + const _PeerCard( + {required this.peer, + required this.connect, + required this.popupMenuEntryBuilder, + Key? key}) + : super(key: key); + + @override + _PeerCardState createState() => _PeerCardState(); +} + +/// State for the connection page. +class _PeerCardState extends State<_PeerCard> + with AutomaticKeepAliveClientMixin { + var _menuPos = RelativeRect.fill; + final double _cardRadius = 16; + final double _borderWidth = 2; + + @override + Widget build(BuildContext context) { + super.build(context); + if (isDesktop) { + return _buildDesktop(); + } else { + return _buildMobile(); + } + } + + Widget _buildMobile() { + final peer = super.widget.peer; + final name = + '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; + + return Card( + margin: EdgeInsets.symmetric(horizontal: 2), + child: GestureDetector( + onTap: !isWebDesktop ? () => connect(context, peer.id) : null, + onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null, + onLongPressStart: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + _showPeerMenu(peer.id); + }, + child: Container( + padding: EdgeInsets.only(left: 12, top: 8, bottom: 8), + child: Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + borderRadius: BorderRadius.circular(4), + ), + padding: const EdgeInsets.all(6), + child: getPlatformImage(peer.platform)), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + getOnline(4, peer.online), + Text(peer.alias.isEmpty + ? formatID(peer.id) + : peer.alias) + ]), + Text(name) + ], + ).paddingOnly(left: 8.0), + ), + InkWell( + child: const Padding( + padding: EdgeInsets.all(12), + child: Icon(Icons.more_vert)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + _showPeerMenu(peer.id); + }) + ], + ), + ))); + } + + Widget _buildDesktop() { + final peer = super.widget.peer; + var deco = Rx(BoxDecoration( + border: Border.all(color: Colors.transparent, width: _borderWidth), + borderRadius: peerCardUiType.value == PeerUiType.grid + ? BorderRadius.circular(_cardRadius) + : null)); + return MouseRegion( + onEnter: (evt) { + deco.value = BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: _borderWidth), + borderRadius: peerCardUiType.value == PeerUiType.grid + ? BorderRadius.circular(_cardRadius) + : null); + }, + onExit: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.transparent, width: _borderWidth), + borderRadius: peerCardUiType.value == PeerUiType.grid + ? BorderRadius.circular(_cardRadius) + : null); + }, + child: GestureDetector( + onDoubleTap: () => widget.connect(context, peer.id), + child: Obx(() => peerCardUiType.value == PeerUiType.grid + ? _buildPeerCard(context, peer, deco) + : _buildPeerTile(context, peer, deco))), + ); + } + + Widget _buildPeerTile( + BuildContext context, Peer peer, Rx deco) { + final name = + '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; + final greyStyle = TextStyle( + fontSize: 11, + color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); + final alias = bind.mainGetPeerOptionSync(id: peer.id, key: 'alias'); + return Obx( + () => Container( + foregroundDecoration: deco.value, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + ), + alignment: Alignment.center, + width: 42, + child: getPlatformImage(peer.platform, size: 30).paddingAll(6), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background), + child: Row( + children: [ + Expanded( + child: Column( + children: [ + Row(children: [ + getOnline(8, peer.online), + Expanded( + child: Text( + alias.isEmpty ? formatID(peer.id) : alias, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall, + )), + ]).marginOnly(bottom: 2), + Align( + alignment: Alignment.centerLeft, + child: Text( + name, + style: greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ).marginOnly(top: 2), + ), + _actionMore(peer), + ], + ).paddingOnly(left: 10.0, top: 3.0), + ), + ) + ], + ), + ), + ); + } + + Widget _buildPeerCard( + BuildContext context, Peer peer, Rx deco) { + final name = + '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; + return Card( + color: Colors.transparent, + elevation: 0, + margin: EdgeInsets.zero, + child: Obx( + () => Container( + foregroundDecoration: deco.value, + child: ClipRRect( + borderRadius: BorderRadius.circular(_cardRadius - _borderWidth), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + color: str2color('${peer.id}${peer.platform}', 0x7f), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6), + child: + getPlatformImage(peer.platform, size: 60), + ), + Row( + children: [ + Expanded( + child: Tooltip( + message: name, + waitDuration: const Duration(seconds: 1), + child: Text( + name, + style: const TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ], + ).paddingAll(4.0), + ), + ], + ), + ), + ), + Container( + color: Theme.of(context).colorScheme.background, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Row(children: [ + getOnline(8, peer.online), + Expanded( + child: Text( + peer.alias.isEmpty ? formatID(peer.id) : peer.alias, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall, + )), + ]).paddingSymmetric(vertical: 8)), + _actionMore(peer), + ], + ).paddingSymmetric(horizontal: 12.0), + ) + ], + ), + ), + ), + ), + ); + } + + Widget _actionMore(Peer peer) => Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onPointerUp: (_) => _showPeerMenu(peer.id), + child: ActionMore()); + + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. + void _showPeerMenu(String id) async { + await mod_menu.showMenu( + context: context, + position: _menuPos, + items: await super.widget.popupMenuEntryBuilder(context), + elevation: 8, + ); + } + + @override + bool get wantKeepAlive => true; +} + +abstract class BasePeerCard extends StatelessWidget { + final Peer peer; + final EdgeInsets? menuPadding; + + BasePeerCard({required this.peer, this.menuPadding, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _PeerCard( + peer: peer, + connect: (BuildContext context, String id) => connect(context, id), + popupMenuEntryBuilder: _buildPopupMenuEntry, + ); + } + + Future>> _buildPopupMenuEntry( + BuildContext context) async => + (await _buildMenuItems(context)) + .map((e) => e.build( + context, + const MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight))) + .expand((i) => i) + .toList(); + + @protected + Future>> _buildMenuItems(BuildContext context); + + MenuEntryBase _connectCommonAction( + BuildContext context, String id, String title, + {bool isFileTransfer = false, + bool isTcpTunneling = false, + bool isRDP = false}) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + title, + style: style, + ), + proc: () { + connect( + context, + peer.id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + ); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _connectAction(BuildContext context, Peer peer) { + return _connectCommonAction( + context, + peer.id, + peer.alias.isEmpty + ? translate('Connect') + : "${translate('Connect')} ${peer.id}"); + } + + @protected + MenuEntryBase _transferFileAction(BuildContext context, String id) { + return _connectCommonAction( + context, + id, + translate('Transfer File'), + isFileTransfer: true, + ); + } + + @protected + MenuEntryBase _tcpTunnelingAction(BuildContext context, String id) { + return _connectCommonAction( + context, + id, + translate('TCP Tunneling'), + isTcpTunneling: true, + ); + } + + @protected + MenuEntryBase _rdpAction(BuildContext context, String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Container( + alignment: AlignmentDirectional.center, + height: CustomPopupMenuTheme.height, + child: Row( + children: [ + Text( + translate('RDP'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: IconButton( + icon: const Icon(Icons.edit), + padding: EdgeInsets.zero, + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + _rdpDialog(id); + }, + )), + )) + ], + )), + proc: () { + connect(context, id, isRDP: true); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _wolAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('WOL'), + style: style, + ), + proc: () { + bind.mainWol(id: id); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + /// Only available on Windows. + @protected + MenuEntryBase _createShortCutAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Create Desktop Shortcut'), + style: style, + ), + proc: () { + bind.mainCreateShortcut(id: id); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @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'; + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate('Always connect via relay'), + getter: () async { + return await _isForceAlwaysRelay(id); + }, + setter: (bool v) async { + gFFI.abModel.setPeerForceAlwaysRelay(id, v); + await bind.mainSetPeerOption( + id: id, key: option, value: bool2option(option, v)); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _renameAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Rename'), + style: style, + ), + proc: () { + _rename(id); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _removeAction( + String id, Future Function() reloadFunc, + {bool isLan = false}) { + return MenuEntryButton( + 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) { + 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(); + }(); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _unrememberPasswordAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Unremember Password'), + style: style, + ), + proc: () { + bind.mainForgetPassword(id: id); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _addFavAction(String id) { + return MenuEntryButton( + 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 { + final favs = (await bind.mainGetFav()).toList(); + if (!favs.contains(id)) { + favs.add(id); + await bind.mainStoreFav(favs: favs); + } + }(); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _rmFavAction( + String id, Future Function() reloadFunc) { + return MenuEntryButton( + 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 { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + await bind.mainStoreFav(favs: favs); + await reloadFunc(); + } + }(); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _addToAb(Peer peer) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Add to Address Book'), + style: style, + ), + proc: () { + () async { + if (!gFFI.abModel.idContainBy(peer.id)) { + gFFI.abModel.addPeer(peer); + await gFFI.abModel.pushAb(); + } + }(); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + Future _getAlias(String id) async => + await bind.mainGetPeerOption(id: id, key: 'alias'); + + void _rename(String id) async { + RxBool isInProgress = false.obs; + String name = await _getAlias(id); + var controller = TextEditingController(text: name); + gFFI.dialogManager.show((setState, close) { + submit() async { + isInProgress.value = true; + String name = controller.text.trim(); + await bind.mainSetPeerAlias(id: id, alias: name); + gFFI.abModel.setPeerAlias(id, name); + _update(); + close(); + isInProgress.value = false; + } + + return CustomAlertDialog( + title: Text(translate('Rename')), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + child: Form( + child: TextFormField( + controller: controller, + autofocus: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: translate('Name')), + ), + ), + ), + Obx(() => Offstage( + offstage: isInProgress.isFalse, + child: const LinearProgressIndicator())), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + @protected + void _update(); +} + +class RecentPeerCard extends BasePeerCard { + RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _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)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (Platform.isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + menuItems.add(_renameAction(peer.id)); + if (await bind.mainPeerHasPassword(id: peer.id)) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + + 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, menuPadding: menuPadding, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer), + _transferFileAction(context, peer.id), + ]; + if (isDesktop && peer.platform != 'Android') { + menuItems.add(_tcpTunnelingAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (Platform.isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + menuItems.add(_renameAction(peer.id)); + if (await bind.mainPeerHasPassword(id: peer.id)) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + menuItems.add(_rmFavAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); + + 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, menuPadding: menuPadding, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _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)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (Platform.isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + + 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, menuPadding: menuPadding, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer), + _transferFileAction(context, peer.id), + ]; + if (isDesktop && peer.platform != 'Android') { + menuItems.add(_tcpTunnelingAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (Platform.isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + menuItems.add(_renameAction(peer.id)); + 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( + String id, Future Function() reloadFunc, + {bool isLan = false}) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Remove'), + style: style, + ), + proc: () { + () async { + gFFI.abModel.deletePeer(id); + await gFFI.abModel.pushAb(); + }(); + }, + padding: super.menuPadding, + dismissOnClicked: true, + ); + } + + @protected + MenuEntryBase _editTagAction(String id) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Edit Tag'), + style: style, + ), + proc: () { + _abEditTag(id); + }, + padding: super.menuPadding, + dismissOnClicked: true, + ); + } + + void _abEditTag(String id) { + var isInProgress = false; + + final tags = List.of(gFFI.abModel.tags); + var selectedTag = gFFI.abModel.getPeerTags(id).obs; + + gFFI.dialogManager.show((setState, close) { + submit() async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.pushAb(); + close(); + } + + return CustomAlertDialog( + title: Text(translate("Edit Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Wrap( + children: tags + .map((e) => AddressBookTag( + name: e, + tags: selectedTag, + onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + }, + showActionMenu: false)) + .toList(growable: false), + ), + ), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } +} + +class MyGroupPeerCard extends BasePeerCard { + MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer), + _transferFileAction(context, peer.id), + ]; + if (isDesktop && peer.platform != 'Android') { + menuItems.add(_tcpTunnelingAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (Platform.isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + menuItems.add(_renameAction(peer.id)); + if (await bind.mainPeerHasPassword(id: peer.id)) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + return menuItems; + } + + @protected + @override + void _update() => gFFI.groupModel.pull(); +} + +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( + text: await bind.mainGetPeerOption(id: id, key: 'rdp_password')); + RxBool secure = true.obs; + + gFFI.dialogManager.show((setState, close) { + submit() async { + String port = portController.text.trim(); + String username = userController.text; + String password = passwordController.text; + await bind.mainSetPeerOption(id: id, key: 'rdp_port', value: port); + await bind.mainSetPeerOption( + id: id, key: 'rdp_username', value: username); + await bind.mainSetPeerOption( + id: id, key: 'rdp_password', value: password); + gFFI.abModel.setRdp(id, port, username); + close(); + } + + return CustomAlertDialog( + title: Text('RDP ${translate('Settings')}'), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Port')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)), + Expanded( + child: TextField( + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + 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])$')) + ], + decoration: const InputDecoration( + border: OutlineInputBorder(), hintText: '3389'), + controller: portController, + autofocus: true, + ), + ), + ], + ).marginOnly(bottom: 8), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)), + Expanded( + child: TextField( + decoration: + const InputDecoration(border: OutlineInputBorder()), + controller: userController, + ), + ), + ], + ).marginOnly(bottom: 8), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)), + Expanded( + child: Obx(() => TextField( + obscureText: secure.value, + decoration: InputDecoration( + border: const OutlineInputBorder(), + suffixIcon: IconButton( + onPressed: () => secure.value = !secure.value, + icon: Icon(secure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: passwordController, + )), + ), + ], + ).marginOnly(bottom: 8), + ], + ), + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +Widget getOnline(double rightPadding, bool online) { + return Tooltip( + message: translate(online ? 'Online' : 'Offline'), + waitDuration: const Duration(seconds: 1), + child: Padding( + padding: EdgeInsets.fromLTRB(0, 4, rightPadding, 4), + child: CircleAvatar( + radius: 3, backgroundColor: online ? Colors.green : kColorWarn))); +} + +class ActionMore extends StatelessWidget { + final RxBool _hover = false.obs; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + onHover: (value) => _hover.value = value, + child: Obx(() => CircleAvatar( + radius: 14, + backgroundColor: _hover.value + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).colorScheme.background, + child: Icon(Icons.more_vert, + size: 18, + color: _hover.value + ? Theme.of(context).textTheme.titleLarge?.color + : Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5))))); + } +} diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart new file mode 100644 index 000000000..da7e37e6b --- /dev/null +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -0,0 +1,419 @@ +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'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:flutter_hbb/common/widgets/peer_card.dart'; +import 'package:flutter_hbb/consts.dart'; +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'; + +class PeerTabPage extends StatefulWidget { + const PeerTabPage({Key? key}) : super(key: key); + @override + State createState() => _PeerTabPageState(); +} + +class _TabEntry { + final Widget widget; + final Function() load; + _TabEntry(this.widget, this.load); +} + +EdgeInsets? _menuPadding() { + return isDesktop ? kDesktopMenuPadding : null; +} + +class _PeerTabPageState extends State + with SingleTickerProviderStateMixin { + final List<_TabEntry> entries = [ + _TabEntry( + RecentPeersView( + menuPadding: _menuPadding(), + ), + bind.mainLoadRecentPeers), + _TabEntry( + FavoritePeersView( + menuPadding: _menuPadding(), + ), + bind.mainLoadFavPeers), + _TabEntry( + DiscoveredPeersView( + menuPadding: _menuPadding(), + ), + bind.mainDiscover), + _TabEntry( + AddressBook( + menuPadding: _menuPadding(), + ), + () => {}), + _TabEntry( + MyGroup( + menuPadding: _menuPadding(), + ), + () => {}), + ]; + final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50)); + + @override + void initState() { + final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); + if (uiType != '') { + peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index + ? PeerUiType.list + : PeerUiType.grid; + } + super.initState(); + } + + Future handleTabSelection(int tabIndex) async { + if (tabIndex < entries.length) { + gFFI.peerTabModel.setCurrentTab(tabIndex); + entries[tabIndex].load(); + } + } + + @override + Widget build(BuildContext context) { + return Column( + textBaseline: TextBaseline.ideographic, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 28, + child: Container( + padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2), + constraints: isDesktop ? null : kMobilePageConstraints, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: visibleContextMenuListener( + _createSwitchBar(context))), + buildScrollJumper(), + const PeerSearchBar(), + Offstage( + offstage: !isDesktop, + child: _createPeerViewTypeSwitch(context) + .marginOnly(left: 13)), + ], + )), + ), + _createPeersView(), + ], + ); + } + + Widget _createSwitchBar(BuildContext context) { + 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); + }, + 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()); + } + + 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 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 { + model.setCurrentTab(model.visibleOrderedTabs[0]); + child = entries[0].widget; + } + } + 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).colorScheme.background); + return Row( + children: [PeerUiType.grid, PeerUiType.list] + .map((type) => Obx( + () => Container( + padding: EdgeInsets.all(4.0), + decoration: peerCardUiType.value == type ? activeDeco : null, + child: InkWell( + onTap: () async { + await bind.setLocalFlutterConfig( + k: 'peer-card-ui-type', v: type.index.toString()); + peerCardUiType.value = type; + }, + child: Icon( + type == PeerUiType.grid + ? Icons.grid_view_rounded + : Icons.list, + size: 18, + color: + peerCardUiType.value == type ? textColor : textColor + ?..withOpacity(0.5), + )), + ), + )) + .toList(), + ); + } + + Widget visibleContextMenuListener(Widget child) { + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + if (e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return visibleContextMenu(cancelFunc); + }, + target: e.position, + ); + } + }, + child: child); + } + + Widget visibleContextMenu(CancelFunc cancelFunc) { + 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()); + } +} + +class PeerSearchBar extends StatefulWidget { + const PeerSearchBar({Key? key}) : super(key: key); + + @override + State createState() => _PeerSearchBarState(); +} + +class _PeerSearchBarState extends State { + var drawer = false; + + @override + Widget build(BuildContext context) { + return drawer + ? _buildSearchBar() + : IconButton( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 2), + onPressed: () { + setState(() { + drawer = true; + }); + }, + icon: Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + )); + } + + Widget _buildSearchBar() { + RxBool focused = false.obs; + FocusNode focusNode = FocusNode(); + 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).colorScheme.background, + borderRadius: BorderRadius.circular(6), + ), + child: Obx(() => Row( + children: [ + Expanded( + child: Row( + children: [ + Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + ).marginSymmetric(horizontal: 4), + Expanded( + child: TextField( + autofocus: true, + controller: peerSearchTextController, + onChanged: (searchText) { + peerSearchText.value = searchText; + }, + focusNode: focusNode, + textAlign: TextAlign.start, + maxLines: 1, + cursorColor: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5), + cursorHeight: 18, + cursorWidth: 1, + style: const TextStyle(fontSize: 14), + decoration: InputDecoration( + contentPadding: + const EdgeInsets.symmetric(vertical: 6), + hintText: + focused.value ? null : translate("Search ID"), + hintStyle: TextStyle( + fontSize: 14, color: Theme.of(context).hintColor), + border: InputBorder.none, + isDense: true, + ), + ), + ), + // Icon(Icons.close), + IconButton( + alignment: Alignment.centerRight, + padding: const EdgeInsets.only(right: 2), + onPressed: () { + setState(() { + peerSearchTextController.clear(); + peerSearchText.value = ""; + drawer = false; + }); + }, + icon: Icon( + Icons.close, + color: Theme.of(context).hintColor, + )), + ], + ), + ) + ], + )), + ); + } +} diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart new file mode 100644 index 000000000..9c98f24b8 --- /dev/null +++ b/flutter/lib/common/widgets/peers_view.dart @@ -0,0 +1,346 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:visibility_detector/visibility_detector.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../../common.dart'; +import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; +import 'peer_card.dart'; + +typedef PeerFilter = bool Function(Peer peer); +typedef PeerCardBuilder = Widget Function(Peer peer); + +/// for peer search text, global obs value +final peerSearchText = "".obs; +final peerSearchTextController = + TextEditingController(text: peerSearchText.value); + +class _PeersView extends StatefulWidget { + final Peers peers; + final PeerFilter? peerFilter; + final PeerCardBuilder peerCardBuilder; + + const _PeersView( + {required this.peers, + required this.peerCardBuilder, + this.peerFilter, + Key? key}) + : super(key: key); + + @override + _PeersViewState createState() => _PeersViewState(); +} + +/// State for the peer widget. +class _PeersViewState extends State<_PeersView> with WindowListener { + static const int _maxQueryCount = 3; + final space = isDesktop ? 12.0 : 8.0; + final _curPeers = {}; + var _lastChangeTime = DateTime.now(); + var _lastQueryPeers = {}; + var _lastQueryTime = DateTime.now().subtract(const Duration(hours: 1)); + var _queryCoun = 0; + var _exit = false; + + late final mobileWidth = () { + const minWidth = 320.0; + final windowWidth = MediaQuery.of(context).size.width; + var width = windowWidth - 2 * space; + if (windowWidth > minWidth + 2 * space) { + final n = (windowWidth / (minWidth + 2 * space)).floor(); + width = windowWidth / n - 2 * space; + } + return width; + }(); + + _PeersViewState() { + _startCheckOnlines(); + } + + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + _exit = true; + super.dispose(); + } + + @override + void onWindowFocus() { + _queryCoun = 0; + } + + @override + void onWindowMinimize() { + _queryCoun = _maxQueryCount; + } + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => widget.peers, + child: Consumer( + builder: (context, peers, child) => peers.peers.isEmpty + ? Container( + margin: EdgeInsets.only(top: kEmptyMarginTop), + alignment: Alignment.topCenter, + child: Text(translate("Empty"))) + : _buildPeersView(peers)), + ); + } + + Widget _buildPeersView(Peers peers) { + final body = ObxValue((searchText) { + return FutureBuilder>( + builder: (context, snapshot) { + if (snapshot.hasData) { + final peers = snapshot.data!; + final cards = []; + for (final peer in peers) { + final visibilityChild = VisibilityDetector( + key: ValueKey(peer.id), + onVisibilityChanged: (info) { + final peerId = (info.key as ValueKey).value; + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + }, + child: widget.peerCardBuilder(peer), + ); + cards.add(isDesktop + ? Obx( + () => SizedBox( + width: 220, + height: + peerCardUiType.value == PeerUiType.grid ? 140 : 42, + child: visibilityChild, + ), + ) + : SizedBox(width: mobileWidth, child: visibilityChild)); + } + return Wrap(spacing: space, runSpacing: space, children: cards); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + future: matchPeers(searchText.value, peers.peers), + ); + }, peerSearchText); + + return body; + } + + // ignore: todo + // TODO: variables walk through async tasks? + void _startCheckOnlines() { + () async { + while (!_exit) { + final now = DateTime.now(); + if (!setEquals(_curPeers, _lastQueryPeers)) { + if (now.difference(_lastChangeTime) > const Duration(seconds: 1)) { + if (_curPeers.isNotEmpty) { + platformFFI.ffiBind + .queryOnlines(ids: _curPeers.toList(growable: false)); + _lastQueryPeers = {..._curPeers}; + _lastQueryTime = DateTime.now(); + _queryCoun = 0; + } + } + } else { + if (_queryCoun < _maxQueryCount) { + if (now.difference(_lastQueryTime) > const Duration(seconds: 20)) { + if (_curPeers.isNotEmpty) { + platformFFI.ffiBind + .queryOnlines(ids: _curPeers.toList(growable: false)); + _lastQueryTime = DateTime.now(); + _queryCoun += 1; + } + } + } + } + await Future.delayed(const Duration(milliseconds: 300)); + } + }(); + } + + Future>? matchPeers(String searchText, List peers) async { + if (widget.peerFilter != null) { + peers = peers.where((peer) => widget.peerFilter!(peer)).toList(); + } + + searchText = searchText.trim(); + if (searchText.isEmpty) { + return peers; + } + searchText = searchText.toLowerCase(); + final matches = + await Future.wait(peers.map((peer) => matchPeer(searchText, peer))); + final filteredList = List.empty(growable: true); + for (var i = 0; i < peers.length; i++) { + if (matches[i]) { + filteredList.add(peers[i]); + } + } + return filteredList; + } +} + +abstract class BasePeersView extends StatelessWidget { + final String name; + final String loadEvent; + final PeerFilter? peerFilter; + final PeerCardBuilder peerCardBuilder; + final List initPeers; + + const BasePeersView({ + Key? key, + required this.name, + required this.loadEvent, + this.peerFilter, + required this.peerCardBuilder, + required this.initPeers, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return _PeersView( + peers: Peers(name: name, loadEvent: loadEvent, peers: initPeers), + peerFilter: peerFilter, + peerCardBuilder: peerCardBuilder); + } +} + +class RecentPeersView extends BasePeersView { + RecentPeersView( + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) + : super( + key: key, + name: 'recent peer', + loadEvent: 'load_recent_peers', + peerCardBuilder: (Peer peer) => RecentPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + initPeers: [], + ); + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + bind.mainLoadRecentPeers(); + return widget; + } +} + +class FavoritePeersView extends BasePeersView { + FavoritePeersView( + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) + : super( + key: key, + name: 'favorite peer', + loadEvent: 'load_fav_peers', + peerCardBuilder: (Peer peer) => FavoritePeerCard( + peer: peer, + menuPadding: menuPadding, + ), + initPeers: [], + ); + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + bind.mainLoadFavPeers(); + return widget; + } +} + +class DiscoveredPeersView extends BasePeersView { + DiscoveredPeersView( + {Key? key, EdgeInsets? menuPadding, ScrollController? scrollController}) + : super( + key: key, + name: 'discovered peer', + loadEvent: 'load_lan_peers', + peerCardBuilder: (Peer peer) => DiscoveredPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + initPeers: [], + ); + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + bind.mainLoadLanPeers(); + return widget; + } +} + +class AddressBookPeersView extends BasePeersView { + AddressBookPeersView( + {Key? key, + EdgeInsets? menuPadding, + ScrollController? scrollController, + required List initPeers}) + : super( + key: key, + name: 'address book peer', + loadEvent: 'load_address_book_peers', + peerFilter: (Peer peer) => + _hitTag(gFFI.abModel.selectedTags, peer.tags), + peerCardBuilder: (Peer peer) => AddressBookPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + initPeers: initPeers, + ); + + static bool _hitTag(List selectedTags, List idents) { + if (selectedTags.isEmpty) { + return true; + } + if (idents.isEmpty) { + return false; + } + for (final tag in selectedTags) { + if (!idents.contains(tag)) { + return false; + } + } + return true; + } +} + +class MyGroupPeerView extends BasePeersView { + MyGroupPeerView( + {Key? key, + EdgeInsets? menuPadding, + ScrollController? scrollController, + required List initPeers}) + : super( + key: key, + name: 'my group peer', + loadEvent: 'load_my_group_peers', + peerCardBuilder: (Peer peer) => MyGroupPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + initPeers: initPeers, + ); +} diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart new file mode 100644 index 000000000..dd39cbdfd --- /dev/null +++ b/flutter/lib/common/widgets/remote_input.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../models/input_model.dart'; + +class RawKeyFocusScope extends StatelessWidget { + final FocusNode? focusNode; + final ValueChanged? onFocusChange; + final InputModel inputModel; + final Widget child; + + RawKeyFocusScope({ + this.focusNode, + this.onFocusChange, + required this.inputModel, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return FocusScope( + autofocus: true, + child: Focus( + autofocus: true, + canRequestFocus: true, + focusNode: focusNode, + onFocusChange: onFocusChange, + onKey: inputModel.handleRawKeyEvent, + child: child)); + } +} + +class RawPointerMouseRegion extends StatelessWidget { + final InputModel inputModel; + final Widget child; + final MouseCursor? cursor; + final PointerEnterEventListener? onEnter; + final PointerExitEventListener? onExit; + final PointerDownEventListener? onPointerDown; + final PointerUpEventListener? onPointerUp; + + RawPointerMouseRegion( + {this.onEnter, + this.onExit, + this.cursor, + this.onPointerDown, + this.onPointerUp, + required this.inputModel, + required this.child}); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerHover: inputModel.onPointHoverImage, + onPointerDown: (evt) { + onPointerDown?.call(evt); + inputModel.onPointDownImage(evt); + }, + onPointerUp: (evt) { + onPointerUp?.call(evt); + inputModel.onPointUpImage(evt); + }, + onPointerMove: inputModel.onPointMoveImage, + onPointerSignal: inputModel.onPointerSignalImage, + onPointerPanZoomStart: inputModel.onPointerPanZoomStart, + onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate, + onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd, + child: MouseRegion( + cursor: cursor ?? MouseCursor.defer, + onEnter: onEnter, + onExit: onExit, + child: child)); + } +} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart new file mode 100644 index 000000000..537784918 --- /dev/null +++ b/flutter/lib/consts.dart @@ -0,0 +1,377 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; + +const double kDesktopRemoteTabBarHeight = 28.0; +const int kMainWindowId = 0; + +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 kTabLabelHomePage = "Home"; +const String kTabLabelSettingPage = "Settings"; + +const String kWindowPrefix = "wm_"; +const int kWindowMainId = 0; + +// the executable name of the portable version +const String kEnvPortableExecutable = "RUSTDESK_APPNAME"; + +const Color kColorWarn = Color.fromARGB(255, 245, 133, 59); + +const int kMobileDefaultDisplayWidth = 720; +const int kMobileDefaultDisplayHeight = 1280; + +const int kDesktopDefaultDisplayWidth = 1080; +const int kDesktopDefaultDisplayHeight = 720; + +const int kMobileMaxDisplayWidth = 720; +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; + +// 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; +const double kEmptyMarginTop = 50; +const double kDesktopIconButtonSplashRadius = 20; + +/// [kMinCursorSize] indicates min cursor (w, h) +const int kMinCursorSize = 12; + +/// [kDefaultScrollAmountMultiplier] indicates how many rows can be scrolled after a minimum scroll action of mouse +const kDefaultScrollAmountMultiplier = 5.0; +const kDefaultScrollDuration = Duration(milliseconds: 50); +const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); +const kFullScreenEdgeSize = 0.0; +var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0; +const kWindowBorderWidth = 1.0; +const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); + +const kInvalidValueStr = 'InvalidValueStr'; + +// Config key shared by flutter and other ui. +const kCommConfKeyTheme = 'theme'; +const kCommConfKeyLang = 'lang'; + +const kMobilePageConstraints = BoxConstraints(maxWidth: 600); + +/// [kMouseControlDistance] indicates the distance that self-side move to get control of mouse. +const kMouseControlDistance = 12; + +/// [kMouseControlTimeoutMSec] indicates the timeout (in milliseconds) that self-side can get control of mouse. +const kMouseControlTimeoutMSec = 1000; + +/// [kRemoteViewStyleOriginal] Show remote image without scaling. +const kRemoteViewStyleOriginal = 'original'; + +/// [kRemoteViewStyleAdaptive] Show remote image scaling by ratio factor. +const kRemoteViewStyleAdaptive = 'adaptive'; + +/// [kRemoteScrollStyleAuto] Scroll image auto by position. +const kRemoteScrollStyleAuto = 'scrollauto'; + +/// [kRemoteScrollStyleBar] Scroll image with scroll bar. +const kRemoteScrollStyleBar = 'scrollbar'; + +/// [kRemoteImageQualityBest] Best image quality. +const kRemoteImageQualityBest = 'best'; + +/// [kRemoteImageQualityBalanced] Balanced image quality, mid performance. +const kRemoteImageQualityBalanced = 'balanced'; + +/// [kRemoteImageQualityLow] Low image quality, better performance. +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; + +/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels +/// see [LogicalKeyboardKey.keyLabel] +const Map logicalKeyMap = { + 0x00000000020: 'VK_SPACE', + 0x00000000022: 'VK_QUOTE', + 0x0000000002c: 'VK_COMMA', + 0x0000000002d: 'VK_MINUS', + 0x0000000002f: 'VK_SLASH', + 0x00000000030: 'VK_0', + 0x00000000031: 'VK_1', + 0x00000000032: 'VK_2', + 0x00000000033: 'VK_3', + 0x00000000034: 'VK_4', + 0x00000000035: 'VK_5', + 0x00000000036: 'VK_6', + 0x00000000037: 'VK_7', + 0x00000000038: 'VK_8', + 0x00000000039: 'VK_9', + 0x0000000003b: 'VK_SEMICOLON', + 0x0000000003d: 'VK_PLUS', // it is = + 0x0000000005b: 'VK_LBRACKET', + 0x0000000005c: 'VK_BACKSLASH', + 0x0000000005d: 'VK_RBRACKET', + 0x00000000061: 'VK_A', + 0x00000000062: 'VK_B', + 0x00000000063: 'VK_C', + 0x00000000064: 'VK_D', + 0x00000000065: 'VK_E', + 0x00000000066: 'VK_F', + 0x00000000067: 'VK_G', + 0x00000000068: 'VK_H', + 0x00000000069: 'VK_I', + 0x0000000006a: 'VK_J', + 0x0000000006b: 'VK_K', + 0x0000000006c: 'VK_L', + 0x0000000006d: 'VK_M', + 0x0000000006e: 'VK_N', + 0x0000000006f: 'VK_O', + 0x00000000070: 'VK_P', + 0x00000000071: 'VK_Q', + 0x00000000072: 'VK_R', + 0x00000000073: 'VK_S', + 0x00000000074: 'VK_T', + 0x00000000075: 'VK_U', + 0x00000000076: 'VK_V', + 0x00000000077: 'VK_W', + 0x00000000078: 'VK_X', + 0x00000000079: 'VK_Y', + 0x0000000007a: 'VK_Z', + 0x00100000008: 'VK_BACK', + 0x00100000009: 'VK_TAB', + 0x0010000000d: 'VK_ENTER', + 0x0010000001b: 'VK_ESCAPE', + 0x0010000007f: 'VK_DELETE', + 0x00100000104: 'VK_CAPITAL', + 0x00100000301: 'VK_DOWN', + 0x00100000302: 'VK_LEFT', + 0x00100000303: 'VK_RIGHT', + 0x00100000304: 'VK_UP', + 0x00100000305: 'VK_END', + 0x00100000306: 'VK_HOME', + 0x00100000307: 'VK_NEXT', + 0x00100000308: 'VK_PRIOR', + 0x00100000401: 'VK_CLEAR', + 0x00100000407: 'VK_INSERT', + 0x00100000504: 'VK_CANCEL', + 0x00100000506: 'VK_EXECUTE', + 0x00100000508: 'VK_HELP', + 0x00100000509: 'VK_PAUSE', + 0x0010000050c: 'VK_SELECT', + 0x00100000608: 'VK_PRINT', + 0x00100000705: 'VK_CONVERT', + 0x00100000706: 'VK_FINAL', + 0x00100000711: 'VK_HANGUL', + 0x00100000712: 'VK_HANJA', + 0x00100000713: 'VK_JUNJA', + 0x00100000718: 'VK_KANA', + 0x00100000719: 'VK_KANJI', + 0x00100000801: 'VK_F1', + 0x00100000802: 'VK_F2', + 0x00100000803: 'VK_F3', + 0x00100000804: 'VK_F4', + 0x00100000805: 'VK_F5', + 0x00100000806: 'VK_F6', + 0x00100000807: 'VK_F7', + 0x00100000808: 'VK_F8', + 0x00100000809: 'VK_F9', + 0x0010000080a: 'VK_F10', + 0x0010000080b: 'VK_F11', + 0x0010000080c: 'VK_F12', + 0x00100000d2b: 'Apps', + 0x00200000002: 'VK_SLEEP', + 0x00200000100: 'VK_CONTROL', + 0x00200000101: 'RControl', + 0x00200000102: 'VK_SHIFT', + 0x00200000103: 'RShift', + 0x00200000104: 'VK_MENU', + 0x00200000105: 'RAlt', + 0x002000001f0: 'VK_CONTROL', + 0x002000001f2: 'VK_SHIFT', + 0x002000001f4: 'VK_MENU', + 0x002000001f6: 'Meta', + 0x0020000022a: 'VK_MULTIPLY', + 0x0020000022b: 'VK_ADD', + 0x0020000022d: 'VK_SUBTRACT', + 0x0020000022e: 'VK_DECIMAL', + 0x0020000022f: 'VK_DIVIDE', + 0x00200000230: 'VK_NUMPAD0', + 0x00200000231: 'VK_NUMPAD1', + 0x00200000232: 'VK_NUMPAD2', + 0x00200000233: 'VK_NUMPAD3', + 0x00200000234: 'VK_NUMPAD4', + 0x00200000235: 'VK_NUMPAD5', + 0x00200000236: 'VK_NUMPAD6', + 0x00200000237: 'VK_NUMPAD7', + 0x00200000238: 'VK_NUMPAD8', + 0x00200000239: 'VK_NUMPAD9', +}; + +/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _debugName +/// see [PhysicalKeyboardKey.debugName] -> _debugName +const Map physicalKeyMap = { + 0x00010082: 'VK_SLEEP', + 0x00070004: 'VK_A', + 0x00070005: 'VK_B', + 0x00070006: 'VK_C', + 0x00070007: 'VK_D', + 0x00070008: 'VK_E', + 0x00070009: 'VK_F', + 0x0007000a: 'VK_G', + 0x0007000b: 'VK_H', + 0x0007000c: 'VK_I', + 0x0007000d: 'VK_J', + 0x0007000e: 'VK_K', + 0x0007000f: 'VK_L', + 0x00070010: 'VK_M', + 0x00070011: 'VK_N', + 0x00070012: 'VK_O', + 0x00070013: 'VK_P', + 0x00070014: 'VK_Q', + 0x00070015: 'VK_R', + 0x00070016: 'VK_S', + 0x00070017: 'VK_T', + 0x00070018: 'VK_U', + 0x00070019: 'VK_V', + 0x0007001a: 'VK_W', + 0x0007001b: 'VK_X', + 0x0007001c: 'VK_Y', + 0x0007001d: 'VK_Z', + 0x0007001e: 'VK_1', + 0x0007001f: 'VK_2', + 0x00070020: 'VK_3', + 0x00070021: 'VK_4', + 0x00070022: 'VK_5', + 0x00070023: 'VK_6', + 0x00070024: 'VK_7', + 0x00070025: 'VK_8', + 0x00070026: 'VK_9', + 0x00070027: 'VK_0', + 0x00070028: 'VK_ENTER', + 0x00070029: 'VK_ESCAPE', + 0x0007002a: 'VK_BACK', + 0x0007002b: 'VK_TAB', + 0x0007002c: 'VK_SPACE', + 0x0007002d: 'VK_MINUS', + 0x0007002e: 'VK_PLUS', // it is = + 0x0007002f: 'VK_LBRACKET', + 0x00070030: 'VK_RBRACKET', + 0x00070033: 'VK_SEMICOLON', + 0x00070034: 'VK_QUOTE', + 0x00070036: 'VK_COMMA', + 0x00070038: 'VK_SLASH', + 0x00070039: 'VK_CAPITAL', + 0x0007003a: 'VK_F1', + 0x0007003b: 'VK_F2', + 0x0007003c: 'VK_F3', + 0x0007003d: 'VK_F4', + 0x0007003e: 'VK_F5', + 0x0007003f: 'VK_F6', + 0x00070040: 'VK_F7', + 0x00070041: 'VK_F8', + 0x00070042: 'VK_F9', + 0x00070043: 'VK_F10', + 0x00070044: 'VK_F11', + 0x00070045: 'VK_F12', + 0x00070049: 'VK_INSERT', + 0x0007004a: 'VK_HOME', + 0x0007004b: 'VK_PRIOR', // Page Up + 0x0007004c: 'VK_DELETE', + 0x0007004d: 'VK_END', + 0x0007004e: 'VK_NEXT', // Page Down + 0x0007004f: 'VK_RIGHT', + 0x00070050: 'VK_LEFT', + 0x00070051: 'VK_DOWN', + 0x00070052: 'VK_UP', + 0x00070053: 'Num Lock', // TODO rust not impl + 0x00070054: 'VK_DIVIDE', // numpad + 0x00070055: 'VK_MULTIPLY', + 0x00070056: 'VK_SUBTRACT', + 0x00070057: 'VK_ADD', + 0x00070058: 'VK_ENTER', // num enter + 0x00070059: 'VK_NUMPAD1', + 0x0007005a: 'VK_NUMPAD2', + 0x0007005b: 'VK_NUMPAD3', + 0x0007005c: 'VK_NUMPAD4', + 0x0007005d: 'VK_NUMPAD5', + 0x0007005e: 'VK_NUMPAD6', + 0x0007005f: 'VK_NUMPAD7', + 0x00070060: 'VK_NUMPAD8', + 0x00070061: 'VK_NUMPAD9', + 0x00070062: 'VK_NUMPAD0', + 0x00070063: 'VK_DECIMAL', + 0x00070075: 'VK_HELP', + 0x00070077: 'VK_SELECT', + 0x00070088: 'VK_KANA', + 0x0007008a: 'VK_CONVERT', + 0x000700e0: 'VK_CONTROL', + 0x000700e1: 'VK_SHIFT', + 0x000700e2: 'VK_MENU', + 0x000700e3: 'Meta', + 0x000700e4: 'RControl', + 0x000700e5: 'RShift', + 0x000700e6: 'RAlt', + 0x000700e7: 'RWin', + 0x000c00b1: 'VK_PAUSE', + 0x000c00cd: 'VK_PAUSE', + 0x000c019e: 'LOCK_SCREEN', + 0x000c0208: 'VK_PRINT', +}; + +/// The windows targets in the publish time order. +enum WindowsTarget { + naw, // not a windows target + xp, + vista, + w7, + w8, + w8_1, + w10, + w11 +} + +/// A convenient method to transform a build number to the corresponding windows version. +extension WindowsTargetExt on int { + WindowsTarget get windowsVersion => getWindowsTarget(this); +} diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart new file mode 100644 index 000000000..4aad66eee --- /dev/null +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -0,0 +1,360 @@ +// main window right pane + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +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'; + +import '../../common.dart'; +import '../../common/formatter/id_formatter.dart'; +import '../../common/widgets/peer_tab_page.dart'; +import '../../models/platform_model.dart'; +import '../widgets/button.dart'; + +/// Connection page for connecting to a remote peer. +class ConnectionPage extends StatefulWidget { + const ConnectionPage({Key? key}) : super(key: key); + + @override + State createState() => _ConnectionPageState(); +} + +/// State for the connection page. +class _ConnectionPageState extends State + with SingleTickerProviderStateMixin, WindowListener { + /// Controller for the id input bar. + final _idController = IDTextEditingController(); + + /// Nested scroll controller + final _scrollController = ScrollController(); + + Timer? _updateTimer; + + final RxBool _idInputFocused = false.obs; + final FocusNode _idFocusNode = FocusNode(); + + var svcStopped = Get.find(tag: 'stop-service'); + var svcStatusCode = 0.obs; + var svcIsUsingPublicServer = true.obs; + + bool isWindowMinimized = false; + + @override + void initState() { + super.initState(); + if (_idController.text.isEmpty) { + () async { + final lastRemoteId = await bind.mainGetLastRemoteId(); + if (lastRemoteId != _idController.id) { + setState(() { + _idController.id = lastRemoteId; + }); + } + }(); + } + _updateTimer = periodic_immediate(Duration(seconds: 1), () async { + updateStatus(); + }); + _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); + } + + @override + void dispose() { + _idController.dispose(); + _updateTimer?.cancel(); + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowEvent(String eventName) { + super.onWindowEvent(eventName); + if (eventName == 'minimize') { + isWindowMinimized = true; + } else if (eventName == 'maximize' || eventName == 'restore') { + if (isWindowMinimized && Platform.isWindows) { + // windows can't update when minimized. + Get.forceAppUpdate(); + } + 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(); + bind.mainOnMainWindowClose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: DesktopScrollWrapper( + scrollController: _scrollController, + child: CustomScrollView( + controller: _scrollController, + physics: DraggableNeverScrollableScrollPhysics(), + slivers: [ + SliverList( + delegate: SliverChildListDelegate([ + Row( + children: [ + Flexible(child: _buildRemoteIDTextField(context)), + ], + ).marginOnly(top: 22), + SizedBox(height: 12), + Divider().paddingOnly(right: 12), + ])), + SliverFillRemaining( + hasScrollBody: false, + child: PeerTabPage().paddingOnly(right: 12.0), + ) + ], + ).paddingOnly(left: 12.0), + ), + ), + const Divider(height: 1), + buildStatus() + ], + ); + } + + /// Callback for the connect button. + /// Connects to the selected peer. + void onConnect({bool isFileTransfer = false}) { + var id = _idController.id; + var forceRelay = id.endsWith(r'/r'); + if (forceRelay) id = id.substring(0, id.length - 2); + connect(context, id, + isFileTransfer: isFileTransfer, forceRelay: forceRelay); + } + + /// UI for the remote ID TextField. + /// Search for a peer and connect to it if the id exists. + Widget _buildRemoteIDTextField(BuildContext context) { + var w = Container( + width: 320 + 20 * 2, + padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: const BorderRadius.all(Radius.circular(13)), + ), + child: Ink( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: AutoSizeText( + translate('Control Remote Desktop'), + maxLines: 1, + style: Theme.of(context) + .textTheme + .titleLarge + ?.merge(TextStyle(height: 1)), + ), + ), + ], + ).marginOnly(bottom: 15), + Row( + children: [ + Expanded( + child: Obx( + () => TextField( + maxLength: 90, + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + focusNode: _idFocusNode, + style: const TextStyle( + fontFamily: 'WorkSans', + fontSize: 22, + height: 1.25, + ), + maxLines: 1, + cursorColor: + Theme.of(context).textTheme.titleLarge?.color, + decoration: InputDecoration( + counterText: '', + hintText: _idInputFocused.value + ? null + : translate('Enter Remote ID'), + border: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide( + color: MyTheme.color(context).border!)), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide( + color: MyTheme.color(context).border!)), + focusedBorder: const OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: + BorderSide(color: MyTheme.button, width: 3), + ), + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 12)), + controller: _idController, + inputFormatters: [IDTextInputFormatter()], + onSubmitted: (s) { + onConnect(); + }, + ), + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 13.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Button( + isOutline: true, + onTap: () { + onConnect(isFileTransfer: true); + }, + text: "Transfer File", + ), + const SizedBox( + width: 17, + ), + Button(onTap: onConnect, text: "Connect"), + ], + ), + ) + ], + ), + ), + ); + return Container( + constraints: const BoxConstraints(maxWidth: 600), child: w); + } + + Widget buildStatus() { + final em = 14.0; + return ConstrainedBox( + constraints: BoxConstraints.tightFor(height: 3 * em), + child: Obx(() => Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: svcStopped.value || svcStatusCode.value == 0 + ? kColorWarn + : (svcStatusCode.value == 1 + ? Color.fromARGB(255, 50, 190, 166) + : Color.fromARGB(255, 224, 79, 95)), + ), + ).marginSymmetric(horizontal: em), + Text( + svcStopped.value + ? translate("Service is not running") + : svcStatusCode.value == 0 + ? translate("connecting_status") + : svcStatusCode.value == -1 + ? translate("not_ready_status") + : translate('Ready'), + style: TextStyle(fontSize: em)), + // stop + Offstage( + offstage: !svcStopped.value, + child: InkWell( + onTap: () async { + bool checked = !bind.mainIsInstalled() || + await bind.mainCheckSuperUserPermission(); + if (checked) { + bind.mainSetOption(key: "stop-service", value: ""); + bind.mainSetOption(key: "access-mode", value: ""); + } + }, + child: Text(translate("Start Service"), + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: em))) + .marginOnly(left: em), + ), + // ready && public + Flexible( + child: Offstage( + offstage: !(!svcStopped.value && + svcStatusCode.value == 1 && + svcIsUsingPublicServer.value), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(', ', style: TextStyle(fontSize: em)), + Flexible( + child: InkWell( + onTap: onUsePublicServerGuide, + child: Row( + children: [ + Flexible( + child: Text( + translate('setup_server_tip'), + style: TextStyle( + decoration: TextDecoration.underline, + fontSize: em), + ), + ), + ], + ), + ), + ) + ], + ), + ), + ) + ], + )), + ); + } + + void onUsePublicServerGuide() { + const url = "https://rustdesk.com/blog/id-relay-set/"; + canLaunchUrlString(url).then((can) { + if (can) { + launchUrlString(url); + } + }); + } + + updateStatus() async { + final status = + jsonDecode(await bind.mainGetConnectStatus()) as Map; + svcStatusCode.value = status["status_num"]; + svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer(); + } +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart new file mode 100644 index 000000000..dfa5762b0 --- /dev/null +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -0,0 +1,714 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:convert'; + +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'; +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'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:window_size/window_size.dart' as window_size; + +import '../widgets/button.dart'; + +class DesktopHomePage extends StatefulWidget { + const DesktopHomePage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopHomePageState(); +} + +const borderColor = Color(0xFF2F65BA); + +class _DesktopHomePageState extends State + with AutomaticKeepAliveClientMixin { + final _leftPaneScrollController = ScrollController(); + + @override + bool get wantKeepAlive => true; + var updateUrl = ''; + var systemError = ''; + StreamSubscription? _uniLinksSubscription; + var svcStopped = false.obs; + var watchIsCanScreenRecording = false; + var watchIsProcessTrust = false; + var watchIsInputMonitoring = false; + var watchIsCanRecordAudio = false; + Timer? _updateTimer; + + @override + Widget build(BuildContext context) { + super.build(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildLeftPane(context), + const VerticalDivider( + width: 1, + thickness: 1, + ), + Expanded( + child: buildRightPane(context), + ), + ], + ); + } + + Widget buildLeftPane(BuildContext context) { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Container( + width: 200, + 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), + FutureBuilder( + future: buildHelpCards(), + builder: (_, data) { + if (data.hasData) { + return data.data!; + } else { + return const Offstage(); + } + }, + ), + ], + ), + ), + ), + ), + ); + } + + buildRightPane(BuildContext context) { + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: ConnectionPage(), + ); + } + + buildIDBoard(BuildContext context) { + final model = gFFI.serverModel; + return Container( + margin: const EdgeInsets.only(left: 20, right: 11), + height: 57, + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Container( + width: 2, + decoration: const BoxDecoration(color: MyTheme.accent), + ).marginOnly(top: 5), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 7), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("ID"), + style: TextStyle( + fontSize: 14, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.5)), + ).marginOnly(top: 5), + buildPopupMenu(context) + ], + ), + ), + Flexible( + child: GestureDetector( + onDoubleTap: () { + Clipboard.setData( + ClipboardData(text: model.serverId.text)); + showToast(translate("Copied")); + }, + child: TextFormField( + controller: model.serverId, + readOnly: true, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.only(bottom: 20), + ), + style: TextStyle( + fontSize: 22, + ), + ), + ), + ) + ], + ), + ), + ), + ], + ), + ); + } + + Widget buildPopupMenu(BuildContext context) { + final textColor = Theme.of(context).textTheme.titleLarge?.color; + RxBool hover = false.obs; + return InkWell( + onTap: DesktopTabPage.onAddSetting, + child: Obx( + () => CircleAvatar( + radius: 15, + backgroundColor: hover.value + ? Theme.of(context).scaffoldBackgroundColor + : Theme.of(context).colorScheme.background, + child: Icon( + Icons.more_vert_outlined, + size: 20, + color: hover.value ? textColor : textColor?.withOpacity(0.5), + ), + ), + ), + onHover: (value) => hover.value = value, + ); + } + + buildPasswordBoard(BuildContext context) { + final model = gFFI.serverModel; + RxBool refreshHover = false.obs; + RxBool editHover = false.obs; + final textColor = Theme.of(context).textTheme.titleLarge?.color; + return Container( + margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Container( + width: 2, + height: 52, + decoration: BoxDecoration(color: MyTheme.accent), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 7), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AutoSizeText( + translate("One-time Password"), + style: TextStyle( + fontSize: 14, color: textColor?.withOpacity(0.5)), + maxLines: 1, + ), + Row( + children: [ + Expanded( + child: GestureDetector( + onDoubleTap: () { + if (model.verificationMethod != + kUsePermanentPassword) { + Clipboard.setData( + ClipboardData(text: model.serverPasswd.text)); + showToast(translate("Copied")); + } + }, + child: TextFormField( + controller: model.serverPasswd, + readOnly: true, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.only(bottom: 2), + ), + style: TextStyle(fontSize: 15), + ), + ), + ), + InkWell( + child: Obx( + () => Icon( + Icons.refresh, + color: refreshHover.value + ? textColor + : Color(0xFFDDDDDD), // TODO + size: 22, + ).marginOnly(right: 8, bottom: 2), + ), + onTap: () => bind.mainUpdateTemporaryPassword(), + onHover: (value) => refreshHover.value = value, + ), + InkWell( + child: Obx( + () => Icon( + Icons.edit, + color: editHover.value + ? textColor + : Color(0xFFDDDDDD), // TODO + size: 22, + ).marginOnly(right: 8, bottom: 2), + ), + onTap: () => DesktopSettingPage.switch2page(1), + onHover: (value) => editHover.value = value, + ), + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + + buildTip(BuildContext context) { + return Padding( + padding: + const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 5), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("Your Desktop"), + style: Theme.of(context).textTheme.titleLarge, + // style: TextStyle( + // // color: MyTheme.color(context).text, + // fontWeight: FontWeight.normal, + // fontSize: 19), + ), + SizedBox( + height: 10.0, + ), + Text( + translate("desk_tip"), + overflow: TextOverflow.clip, + style: Theme.of(context).textTheme.bodySmall, + ) + ], + ), + ); + } + + Future buildHelpCards() async { + if (updateUrl.isNotEmpty) { + return buildInstallCard( + "Status", + "There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.", + "Click to download", () async { + final Uri url = Uri.parse('https://rustdesk.com'); + await launchUrl(url); + }); + } + if (systemError.isNotEmpty) { + return buildInstallCard("", systemError, "", () {}); + } + if (Platform.isWindows) { + if (!bind.mainIsInstalled()) { + return buildInstallCard( + "", "install_tip", "Install", bind.mainGotoInstall); + } else if (bind.mainIsInstalledLowerVersion()) { + return buildInstallCard("Status", "Your installation is lower version.", + "Click to upgrade", bind.mainUpdateMe); + } + } else if (Platform.isMacOS) { + if (!bind.mainIsCanScreenRecording(prompt: false)) { + return buildInstallCard("Permissions", "config_screen", "Configure", + () async { + bind.mainIsCanScreenRecording(prompt: true); + watchIsCanScreenRecording = true; + }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!bind.mainIsProcessTrusted(prompt: false)) { + return buildInstallCard("Permissions", "config_acc", "Configure", + () async { + bind.mainIsProcessTrusted(prompt: true); + watchIsProcessTrust = true; + }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!bind.mainIsCanInputMonitoring(prompt: false)) { + return buildInstallCard("Permissions", "config_input", "Configure", + () async { + bind.mainIsCanInputMonitoring(prompt: true); + watchIsInputMonitoring = true; + }, help: 'Help', link: translate("doc_mac_permission")); + } else if (!svcStopped.value && + bind.mainIsInstalled() && + !bind.mainIsInstalledDaemon(prompt: false)) { + return buildInstallCard("", "install_daemon_tip", "Install", () async { + 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( + "Warning", translate("wayland_experiment_tip"), "", () async {}, + help: 'Help', + link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required'); + } else if (bind.mainIsLoginWayland()) { + return buildInstallCard("Warning", + "Login screen using Wayland is not supported", "", () async {}, + help: 'Help', + link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen'); + } + } + return Container(); + } + + Widget buildInstallCard(String title, String content, String btnText, + GestureTapCallback onPressed, + {String? help, String? link}) { + return Container( + margin: EdgeInsets.only(top: 20), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [ + Color.fromARGB(255, 226, 66, 188), + Color.fromARGB(255, 244, 114, 124), + ], + )), + padding: EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: (title.isNotEmpty + ? [ + Center( + child: Text( + translate(title), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15), + ).marginOnly(bottom: 6)), + ] + : []) + + [ + Text( + translate(content), + style: TextStyle( + height: 1.5, + color: Colors.white, + fontWeight: FontWeight.normal, + fontSize: 13), + ).marginOnly(bottom: 20) + ] + + (btnText.isNotEmpty + ? [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FixedWidthButton( + width: 150, + padding: 8, + isOutline: true, + text: translate(btnText), + textColor: Colors.white, + borderColor: Colors.white, + textSize: 20, + radius: 10, + onTap: onPressed, + ) + ]) + ] + : []) + + (help != null + ? [ + Center( + child: InkWell( + onTap: () async => + await launchUrl(Uri.parse(link!)), + child: Text( + translate(help), + style: TextStyle( + decoration: TextDecoration.underline, + color: Colors.white, + fontSize: 12), + )).marginOnly(top: 6)), + ] + : []))), + ); + } + + @override + void initState() { + super.initState(); + _updateTimer = periodic_immediate(const Duration(seconds: 1), () async { + await gFFI.serverModel.fetchID(); + final url = await bind.mainGetSoftwareUpdateUrl(); + if (updateUrl != url) { + updateUrl = url; + setState(() {}); + } + final error = await bind.mainGetError(); + if (systemError != error) { + systemError = error; + setState(() {}); + } + final v = await bind.mainGetOption(key: "stop-service") == "Y"; + if (v != svcStopped.value) { + svcStopped.value = v; + setState(() {}); + } + if (watchIsCanScreenRecording) { + if (bind.mainIsCanScreenRecording(prompt: false)) { + watchIsCanScreenRecording = false; + setState(() {}); + } + } + if (watchIsProcessTrust) { + if (bind.mainIsProcessTrusted(prompt: false)) { + watchIsProcessTrust = false; + setState(() {}); + } + } + 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(() {}); + } + } + }); + Get.put(svcStopped, tag: 'stop-service'); + rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + debugPrint( + "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + if (call.method == kWindowMainWindowOnTop) { + window_on_top(null); + } else if (call.method == kWindowGetWindowInfo) { + final screen = (await window_size.getWindowInfo()).screen; + if (screen == null) { + return ""; + } else { + return jsonEncode({ + 'frame': { + 'l': screen.frame.left, + 't': screen.frame.top, + 'r': screen.frame.right, + 'b': screen.frame.bottom, + }, + 'visibleFrame': { + 'l': screen.visibleFrame.left, + 't': screen.visibleFrame.top, + 'r': screen.visibleFrame.right, + 'b': screen.visibleFrame.bottom, + }, + 'scaleFactor': screen.scaleFactor, + }); + } + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventShow) { + await rustDeskWinManager.registerActiveWindow(call.arguments["id"]); + } else if (call.method == kWindowEventHide) { + await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]); + } else if (call.method == kWindowConnect) { + await connectMainDesktop( + call.arguments['id'], + isFileTransfer: call.arguments['isFileTransfer'], + isTcpTunneling: call.arguments['isTcpTunneling'], + isRDP: call.arguments['isRDP'], + forceRelay: call.arguments['forceRelay'], + ); + } + }); + _uniLinksSubscription = listenUniLinks(); + } + + @override + void dispose() { + _uniLinksSubscription?.cancel(); + Get.delete(tag: 'stop-service'); + _updateTimer?.cancel(); + super.dispose(); + } +} + +void setPasswordDialog() async { + final pw = await bind.mainGetPermanentPassword(); + final p0 = TextEditingController(text: pw); + 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() { + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final pass = p0.text.trim(); + 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('Prompt')}: ${translate("The confirmation is not identical.")}'; + }); + return; + } + bind.mainSetPermanentPassword(password: pass); + close(); + } + + return CustomAlertDialog( + title: Text(translate("Set Password")), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + Row( + children: [ + 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(), + labelText: translate('Confirmation'), + errorText: errMsg1.isNotEmpty ? errMsg1 : null), + controller: p1, + onChanged: (value) { + setState(() { + errMsg1 = ''; + }); + }, + ), + ), + ], + ), + const SizedBox( + height: 8.0, + ), + 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: [ + 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 new file mode 100644 index 000000000..e041b591d --- /dev/null +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -0,0 +1,1912 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/server_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +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 _kTabHeight = 42; +const double _kCardFixedWidth = 540; +const double _kCardLeftMargin = 15; +const double _kContentHMargin = 15; +const double _kContentHSubMargin = _kContentHMargin + 33; +const double _kCheckBoxLeftMargin = 10; +const double _kRadioLeftMargin = 10; +const double _kListViewBottomMargin = 15; +const double _kTitleFontSize = 20; +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; + late final IconData unselected; + late final IconData selected; + _TabInfo(this.label, this.unselected, this.selected); +} + +class DesktopSettingPage extends StatefulWidget { + final int initialPage; + + const DesktopSettingPage({Key? key, required this.initialPage}) + : super(key: key); + + @override + State createState() => _DesktopSettingPageState(); + + static void switch2page(int page) { + if (page >= _kPageCount) return; + try { + if (Get.isRegistered(tag: _kSettingPageControllerTag)) { + DesktopTabPage.onAddSetting(initialPage: page); + PageController controller = Get.find(tag: _kSettingPageControllerTag); + RxInt selectedIndex = Get.find(tag: _kSettingPageIndexTag); + selectedIndex.value = page; + controller.jumpToPage(page); + } else { + DesktopTabPage.onAddSetting(initialPage: page); + } + } catch (e) { + debugPrintStack(label: '$e'); + } + } +} + +class _DesktopSettingPageState extends State + with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + final List<_TabInfo> settingTabs = <_TabInfo>[ + _TabInfo('General', Icons.settings_outlined, Icons.settings), + _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) + ]; + + late PageController controller; + late RxInt selectedIndex; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + selectedIndex = + (widget.initialPage < _kPageCount ? widget.initialPage : 0).obs; + Get.put(selectedIndex, tag: _kSettingPageIndexTag); + controller = PageController(initialPage: widget.initialPage); + Get.put(controller, tag: _kSettingPageControllerTag); + } + + @override + void dispose() { + super.dispose(); + Get.delete(tag: _kSettingPageControllerTag); + Get.delete(tag: _kSettingPageIndexTag); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: Row( + children: [ + SizedBox( + width: _kTabWidth, + child: Column( + children: [ + _header(), + Flexible(child: _listView(tabs: settingTabs)), + ], + ), + ), + const VerticalDivider(thickness: 1, width: 1), + Expanded( + child: Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: DesktopScrollWrapper( + scrollController: controller, + child: PageView( + controller: controller, + physics: DraggableNeverScrollableScrollPhysics(), + children: const [ + _General(), + _Safety(), + _Network(), + _Display(), + _Account(), + _About(), + ], + )), + ), + ) + ], + ), + ); + } + + Widget _header() { + return Row( + children: [ + SizedBox( + height: 62, + child: Text( + translate('Settings'), + textAlign: TextAlign.left, + style: const TextStyle( + color: _accentColor, + fontSize: _kTitleFontSize, + fontWeight: FontWeight.w400, + ), + ), + ).marginOnly(left: 20, top: 10), + const Spacer(), + ], + ); + } + + Widget _listView({required List<_TabInfo> tabs}) { + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + physics: DraggableNeverScrollableScrollPhysics(), + controller: scrollController, + children: tabs + .asMap() + .entries + .map((tab) => _listItem(tab: tab.value, index: tab.key)) + .toList(), + )); + } + + Widget _listItem({required _TabInfo tab, required int index}) { + return Obx(() { + bool selected = index == selectedIndex.value; + return SizedBox( + width: _kTabWidth, + height: _kTabHeight, + child: InkWell( + onTap: () { + if (selectedIndex.value != index) { + controller.jumpToPage(index); + } + selectedIndex.value = index; + }, + child: Row(children: [ + Container( + width: 4, + height: _kTabHeight * 0.7, + color: selected ? _accentColor : null, + ), + Icon( + selected ? tab.selected : tab.unselected, + color: selected ? _accentColor : null, + size: 20, + ).marginOnly(left: 13, right: 10), + Text( + translate(tab.label), + style: TextStyle( + color: selected ? _accentColor : null, + fontWeight: FontWeight.w400, + fontSize: _kContentFontSize), + ), + ]), + ), + ); + }); + } +} + +//#region pages + +class _General extends StatefulWidget { + const _General({Key? key}) : super(key: key); + + @override + State<_General> createState() => _GeneralState(); +} + +class _GeneralState extends State<_General> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + physics: DraggableNeverScrollableScrollPhysics(), + controller: scrollController, + children: [ + theme(), + hwcodec(), + audio(context), + record(context), + _Card(title: 'Language', children: [language()]), + other() + ], + ).marginOnly(bottom: _kListViewBottomMargin)); + } + + Widget theme() { + final current = MyTheme.getThemeModePreference().toShortString(); + onChanged(String value) { + MyTheme.changeDarkMode(MyTheme.themeModeFromString(value)); + setState(() {}); + } + + return _Card(title: 'Theme', children: [ + _Radio(context, + value: 'light', + groupValue: current, + label: 'Light', + onChanged: onChanged), + _Radio(context, + value: 'dark', + groupValue: current, + label: 'Dark', + onChanged: onChanged), + _Radio(context, + value: 'system', + groupValue: current, + label: 'Follow System', + onChanged: onChanged), + ]); + } + + Widget other() { + return _Card(title: 'Other', children: [ + _OptionCheckBox(context, 'Confirm before closing multiple tabs', + 'enable-confirm-closing-tabs'), + _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), + if (Platform.isLinux) + Tooltip( + message: translate('software_render_tip'), + child: _OptionCheckBox( + context, + "Always use software rendering", + 'allow-always-software-render', + ), + ) + ]); + } + + Widget hwcodec() { + return Offstage( + offstage: !bind.mainHasHwcodec(), + child: _Card(title: 'Hardware Codec', children: [ + _OptionCheckBox(context, 'Enable hardware codec', 'enable-hwcodec'), + ]), + ); + } + + Widget audio(BuildContext context) { + String getDefault() { + if (Platform.isWindows) return 'System Sound'; + return ''; + } + + Future getValue() async { + String device = await bind.mainGetOption(key: 'audio-input'); + if (device.isNotEmpty) { + return device; + } else { + return getDefault(); + } + } + + setDevice(String device) { + if (device == getDefault()) device = ''; + bind.mainSetOption(key: 'audio-input', value: device); + } + + return futureBuilder(future: () async { + List devices = (await bind.mainGetSoundInputs()).toList(); + if (Platform.isWindows) { + devices.insert(0, 'System Sound'); + } + String current = await getValue(); + return {'devices': devices, 'current': current}; + }(), hasData: (data) { + String currentDevice = data['current']; + List devices = data['devices'] as List; + if (devices.isEmpty) { + return const Offstage(); + } + return _Card(title: 'Audio Input Device', children: [ + ...devices.map((device) => _Radio(context, + value: device, + groupValue: currentDevice, + autoNewLine: false, + label: device, onChanged: (value) { + setDevice(value); + setState(() {}); + })) + ]); + }); + } + + Widget record(BuildContext context) { + return futureBuilder(future: () async { + String customDirectory = + await bind.mainGetOption(key: 'video-save-directory'); + String defaultDirectory = await bind.mainDefaultVideoSaveDirectory(); + String dir; + if (customDirectory.isNotEmpty) { + dir = customDirectory; + } else { + dir = defaultDirectory; + } + // canLaunchUrl blocked on windows portable, user SYSTEM + return {'dir': dir, 'canlaunch': true}; + }(), hasData: (data) { + Map map = data as Map; + String dir = map['dir']!; + bool canlaunch = map['canlaunch']! as bool; + + return _Card(title: 'Recording', children: [ + _OptionCheckBox(context, 'Automatically record incoming sessions', + 'allow-auto-record-incoming'), + Row( + children: [ + Text('${translate("Directory")}:'), + Expanded( + child: GestureDetector( + onTap: canlaunch ? () => launchUrl(Uri.file(dir)) : null, + child: Text( + dir, + softWrap: true, + style: + const TextStyle(decoration: TextDecoration.underline), + )).marginOnly(left: 10), + ), + ElevatedButton( + onPressed: () async { + String? selectedDirectory = await FilePicker.platform + .getDirectoryPath(initialDirectory: dir); + if (selectedDirectory != null) { + await bind.mainSetOption( + key: 'video-save-directory', + value: selectedDirectory); + setState(() {}); + } + }, + child: Text(translate('Change'))) + .marginOnly(left: 5), + ], + ).marginOnly(left: _kContentHMargin), + ]); + }); + } + + Widget language() { + return futureBuilder(future: () async { + String langs = await bind.mainGetLangs(); + String lang = bind.mainGetLocalOption(key: kCommConfKeyLang); + return {'langs': langs, 'lang': lang}; + }(), hasData: (res) { + Map data = res as Map; + List langsList = jsonDecode(data['langs']!); + Map langsMap = {for (var v in langsList) v[0]: v[1]}; + List keys = langsMap.keys.toList(); + List values = langsMap.values.toList(); + keys.insert(0, ''); + values.insert(0, 'Default'); + String currentKey = data['lang']!; + if (!keys.contains(currentKey)) { + currentKey = ''; + } + return _ComboBox( + keys: keys, + values: values, + initialKey: currentKey, + onChanged: (key) async { + await bind.mainSetLocalOption(key: kCommConfKeyLang, value: key); + reloadAllWindows(); + bind.mainChangeLanguage(lang: key); + }, + ).marginOnly(left: _kContentHMargin); + }); + } +} + +enum _AccessMode { + custom, + full, + view, + deny, +} + +class _Safety extends StatefulWidget { + const _Safety({Key? key}) : super(key: key); + + @override + State<_Safety> createState() => _SafetyState(); +} + +class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + bool locked = bind.mainIsInstalled(); + final scrollController = ScrollController(); + final RxBool serviceStop = Get.find(tag: 'stop-service'); + + @override + Widget build(BuildContext context) { + super.build(context); + return DesktopScrollWrapper( + scrollController: scrollController, + child: SingleChildScrollView( + physics: DraggableNeverScrollableScrollPhysics(), + controller: scrollController, + child: Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + permissions(context), + password(context), + _Card(title: 'ID', children: [changeId()]), + more(context), + ]), + ), + ], + )).marginOnly(bottom: _kListViewBottomMargin)); + } + + Widget changeId() { + return _Button('Change ID', changeIdDialog, enabled: !locked); + } + + Widget permissions(context) { + return Obx(() => _permissions(context, serviceStop.value)); + } + + Widget _permissions(context, bool stopService) { + bool enabled = !locked; + return futureBuilder(future: () async { + return await bind.mainGetOption(key: 'access-mode'); + }(), hasData: (data) { + String accessMode = data! as String; + _AccessMode mode; + if (stopService) { + mode = _AccessMode.deny; + } else { + if (accessMode == 'full') { + mode = _AccessMode.full; + } else if (accessMode == 'view') { + mode = _AccessMode.view; + } else { + mode = _AccessMode.custom; + } + } + String initialKey; + bool? fakeValue; + switch (mode) { + case _AccessMode.custom: + initialKey = ''; + fakeValue = null; + break; + case _AccessMode.full: + initialKey = 'full'; + fakeValue = true; + break; + case _AccessMode.view: + initialKey = 'view'; + fakeValue = false; + break; + case _AccessMode.deny: + initialKey = 'deny'; + fakeValue = false; + break; + } + + return _Card(title: 'Permissions', children: [ + _ComboBox( + keys: [ + '', + 'full', + 'view', + 'deny' + ], + values: [ + translate('Custom'), + translate('Full Access'), + translate('Screen Share'), + translate('Deny remote access'), + ], + initialKey: initialKey, + onChanged: (mode) async { + String modeValue; + bool stopService; + if (mode == 'deny') { + modeValue = ''; + stopService = true; + } else { + modeValue = mode; + stopService = false; + } + await bind.mainSetOption(key: 'access-mode', value: modeValue); + await bind.mainSetOption( + key: 'stop-service', + value: bool2option('stop-service', stopService)); + setState(() {}); + }).marginOnly(left: _kContentHMargin), + Offstage( + offstage: mode == _AccessMode.deny, + child: Column( + children: [ + _OptionCheckBox( + context, 'Enable Keyboard/Mouse', 'enable-keyboard', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable File Transfer', 'enable-file-transfer', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable Audio', 'enable-audio', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable TCP Tunneling', 'enable-tunnel', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable Remote Restart', 'enable-remote-restart', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, 'Enable Recording Session', 'enable-record-session', + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox( + context, + 'Enable remote configuration modification', + 'allow-remote-config-modification', + enabled: enabled, + fakeValue: fakeValue), + ], + ), + ) + ]); + }); + } + + Widget password(BuildContext context) { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer(builder: ((context, model, child) { + List passwordKeys = [ + kUseTemporaryPassword, + kUsePermanentPassword, + kUseBothPasswords, + ]; + List passwordValues = [ + translate('Use one-time password'), + translate('Use permanent password'), + translate('Use both passwords'), + ]; + bool tmpEnabled = model.verificationMethod != kUsePermanentPassword; + bool permEnabled = model.verificationMethod != kUseTemporaryPassword; + String currentValue = + passwordValues[passwordKeys.indexOf(model.verificationMethod)]; + List radios = passwordValues + .map((value) => _Radio( + context, + value: value, + groupValue: currentValue, + label: value, + onChanged: ((value) { + () async { + await model.setVerificationMethod( + passwordKeys[passwordValues.indexOf(value)]); + await model.updatePasswordModel(); + }(); + }), + enabled: !locked, + )) + .toList(); + + var onChanged = tmpEnabled && !locked + ? (value) { + if (value != null) { + () async { + await model.setTemporaryPasswordLength(value.toString()); + await model.updatePasswordModel(); + }(); + } + } + : null; + List lengthRadios = ['6', '8', '10'] + .map((value) => GestureDetector( + child: Row( + children: [ + Radio( + value: value, + groupValue: model.temporaryPasswordLength, + onChanged: onChanged), + Text( + value, + style: TextStyle( + color: _disabledTextColor( + context, onChanged != null)), + ), + ], + ).paddingOnly(right: 10), + onTap: () => onChanged?.call(value), + )) + .toList(); + + final modeKeys = ['password', 'click', '']; + final modeValues = [ + translate('Accept sessions via password'), + translate('Accept sessions via click'), + translate('Accept sessions via both'), + ]; + var modeInitialKey = model.approveMode; + if (!modeKeys.contains(modeInitialKey)) modeInitialKey = ''; + final usePassword = model.approveMode != 'click'; + + return _Card(title: 'Password', children: [ + _ComboBox( + keys: modeKeys, + values: modeValues, + initialKey: modeInitialKey, + onChanged: (key) => model.setApproveMode(key), + ).marginOnly(left: _kContentHMargin), + if (usePassword) radios[0], + if (usePassword) + _SubLabeledWidget( + context, + 'One-time password length', + Row( + children: [ + ...lengthRadios, + ], + ), + enabled: tmpEnabled && !locked), + if (usePassword) radios[1], + if (usePassword) + _SubButton('Set permanent password', setPasswordDialog, + permEnabled && !locked), + if (usePassword) + hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6), + if (usePassword) radios[2], + ]); + }))); + } + + Widget more(BuildContext context) { + bool enabled = !locked; + return _Card(title: 'Security', children: [ + Offstage( + offstage: !Platform.isWindows, + 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), + whitelist(), + ]); + } + + 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(() {}); + RxBool applyEnabled = false.obs; + return [ + _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', + update: update, enabled: !locked), + futureBuilder( + future: () async { + String enabled = await bind.mainGetOption(key: 'direct-server'); + String port = await bind.mainGetOption(key: 'direct-access-port'); + return {'enabled': enabled, 'port': port}; + }(), + hasData: (data) { + bool enabled = + option2bool('direct-server', data['enabled'].toString()); + if (!enabled) applyEnabled.value = false; + controller.text = data['port'].toString(); + return Offstage( + offstage: !enabled, + child: _SubLabeledWidget( + context, + 'Port', + Row(children: [ + SizedBox( + width: 80, + child: TextField( + controller: controller, + enabled: enabled && !locked, + onChanged: (_) => applyEnabled.value = true, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + 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])$')), + ], + textAlign: TextAlign.end, + decoration: const InputDecoration( + hintText: '21118', + border: OutlineInputBorder(), + contentPadding: + EdgeInsets.only(bottom: 10, top: 10, right: 10), + isCollapsed: true, + ), + ).marginOnly(right: 15), + ), + 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, + ), + ); + }, + ), + ]; + } + + Widget whitelist() { + bool enabled = !locked; + return futureBuilder(future: () async { + return await bind.mainGetOption(key: 'whitelist'); + }(), hasData: (data) { + RxBool hasWhitelist = (data as String).isNotEmpty.obs; + update() async { + hasWhitelist.value = + (await bind.mainGetOption(key: 'whitelist')).isNotEmpty; + } + + onChanged(bool? checked) async { + changeWhiteList(callback: update); + } + + return GestureDetector( + child: Tooltip( + message: translate('whitelist_tip'), + child: Obx(() => Row( + children: [ + Checkbox( + value: hasWhitelist.value, + onChanged: enabled ? onChanged : null) + .marginOnly(right: 5), + Offstage( + offstage: !hasWhitelist.value, + child: const Icon(Icons.warning_amber_rounded, + color: Color.fromARGB(255, 255, 204, 0)) + .marginOnly(right: 5), + ), + Expanded( + child: Text( + translate('Use IP Whitelisting'), + style: + TextStyle(color: _disabledTextColor(context, enabled)), + )) + ], + )), + ), + onTap: () { + onChanged(!hasWhitelist.value); + }, + ).marginOnly(left: _kCheckBoxLeftMargin); + }); + } + + Widget hide_cm(bool enabled) { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer(builder: (context, model, child) { + final enableHideCm = model.approveMode == 'password' && + model.verificationMethod == kUsePermanentPassword; + onHideCmChanged(bool? b) { + if (b != null) { + bind.mainSetOption( + key: 'allow-hide-cm', value: bool2option('allow-hide-cm', b)); + } + } + + return Tooltip( + message: enableHideCm ? "" : translate('hide_cm_tip'), + child: GestureDetector( + onTap: + enableHideCm ? () => onHideCmChanged(!model.hideCm) : null, + child: Row( + children: [ + Checkbox( + value: model.hideCm, + onChanged: enabled && enableHideCm + ? onHideCmChanged + : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('Hide connection management window'), + style: TextStyle( + color: _disabledTextColor( + context, enabled && enableHideCm)), + ), + ), + ], + ), + )); + })); + } +} + +class _Network extends StatefulWidget { + const _Network({Key? key}) : super(key: key); + + @override + State<_Network> createState() => _NetworkState(); +} + +class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + bool locked = bind.mainIsInstalled(); + + @override + Widget build(BuildContext context) { + super.build(context); + bool enabled = !locked; + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + controller: scrollController, + physics: DraggableNeverScrollableScrollPhysics(), + children: [ + _lock(locked, 'Unlock Network Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + server(enabled), + _Card(title: 'Proxy', children: [ + _Button('Socks5 Proxy', changeSocks5Proxy, + enabled: enabled), + ]), + ]), + ), + ]).marginOnly(bottom: _kListViewBottomMargin)); + } + + server(bool enabled) { + 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. + Map oldOptions = jsonDecode(data! as String); + old(String key) { + return (oldOptions[key] ?? '').trim(); + } + + RxString idErrMsg = ''.obs; + RxString relayErrMsg = ''.obs; + RxString apiErrMsg = ''.obs; + var idController = + TextEditingController(text: old('custom-rendezvous-server')); + var relayController = TextEditingController(text: old('relay-server')); + var apiController = TextEditingController(text: old('api-server')); + var keyController = TextEditingController(text: old('key')); + + set(String idServer, String relayServer, String apiServer, + String key) async { + idServer = idServer.trim(); + relayServer = relayServer.trim(); + apiServer = apiServer.trim(); + key = key.trim(); + if (idServer.isNotEmpty) { + idErrMsg.value = + translate(await bind.mainTestIfValidServer(server: idServer)); + if (idErrMsg.isNotEmpty) { + return false; + } + } + if (relayServer.isNotEmpty) { + relayErrMsg.value = + translate(await bind.mainTestIfValidServer(server: relayServer)); + if (relayErrMsg.isNotEmpty) { + return false; + } + } + if (apiServer.isNotEmpty) { + if (!apiServer.startsWith('http://') && + !apiServer.startsWith('https://')) { + apiErrMsg.value = + '${translate("API Server")}: ${translate("invalid_http")}'; + return false; + } + } + final old = await bind.mainGetOption(key: 'custom-rendezvous-server'); + if (old.isNotEmpty && old != idServer) { + await gFFI.userModel.logOut(); + } + // should set one by one + await bind.mainSetOption( + key: 'custom-rendezvous-server', value: idServer); + await bind.mainSetOption(key: 'relay-server', value: relayServer); + await bind.mainSetOption(key: 'api-server', value: apiServer); + await bind.mainSetOption(key: 'key', value: key); + return true; + } + + submit() async { + bool result = await set(idController.text, relayController.text, + apiController.text, keyController.text); + if (result) { + setState(() {}); + showToast(translate('Successful')); + } else { + showToast(translate('Failed')); + } + } + + import() { + Clipboard.getData(Clipboard.kTextPlain).then((value) { + final text = value?.text; + if (text != null && text.isNotEmpty) { + try { + final sc = ServerConfig.decode(text); + if (sc.idServer.isNotEmpty) { + idController.text = sc.idServer; + relayController.text = sc.relayServer; + apiController.text = sc.apiServer; + keyController.text = sc.key; + Future success = + set(sc.idServer, sc.relayServer, sc.apiServer, sc.key); + success.then((value) { + if (value) { + showToast( + translate('Import server configuration successfully')); + } else { + showToast(translate('Invalid server configuration')); + } + }); + } else { + showToast(translate('Invalid server configuration')); + } + } catch (e) { + showToast(translate('Invalid server configuration')); + } + } else { + showToast(translate('Clipboard is empty')); + } + }); + } + + export() { + final text = ServerConfig( + idServer: idController.text, + relayServer: relayController.text, + apiServer: apiController.text, + key: keyController.text) + .encode(); + debugPrint("ServerConfig export: $text"); + + Clipboard.setData(ClipboardData(text: text)); + showToast(translate('Export server configuration successfully')); + } + + bool secure = !enabled; + return _Card(title: 'ID/Relay Server', title_suffix: [ + Tooltip( + message: translate('Import Server Config'), + child: IconButton( + icon: Icon(Icons.paste, color: Colors.grey), + onPressed: enabled ? import : null), + ), + Tooltip( + message: translate('Export Server Config'), + child: IconButton( + icon: Icon(Icons.copy, color: Colors.grey), + onPressed: enabled ? export : null)), + ], children: [ + Column( + children: [ + Obx(() => _LabeledTextField(context, 'ID Server', idController, + idErrMsg.value, enabled, secure)), + Obx(() => _LabeledTextField(context, 'Relay Server', + relayController, relayErrMsg.value, enabled, secure)), + Obx(() => _LabeledTextField(context, 'API Server', apiController, + apiErrMsg.value, enabled, secure)), + _LabeledTextField( + context, 'Key', keyController, '', enabled, secure), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [_Button('Apply', submit, enabled: enabled)], + ).marginOnly(top: 10), + ], + ) + ]); + }); + } +} + +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); + + @override + State<_Account> createState() => _AccountState(); +} + +class _AccountState extends State<_Account> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + physics: DraggableNeverScrollableScrollPhysics(), + controller: scrollController, + children: [ + _Card(title: 'Account', children: [accountAction()]), + ], + ).marginOnly(bottom: _kListViewBottomMargin)); + } + + Widget accountAction() { + return Obx(() => _Button( + gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + () => { + gFFI.userModel.userName.value.isEmpty + ? loginDialog() + : gFFI.userModel.logOut() + })); + } +} + +class _About extends StatefulWidget { + const _About({Key? key}) : super(key: key); + + @override + State<_About> createState() => _AboutState(); +} + +class _AboutState extends State<_About> { + @override + Widget build(BuildContext context) { + return futureBuilder(future: () async { + final license = await bind.mainGetLicense(); + final version = await bind.mainGetVersion(); + final buildDate = await bind.mainGetBuildDate(); + return {'license': license, 'version': version, 'buildDate': buildDate}; + }(), hasData: (data) { + final license = data['license'].toString(); + final version = data['version'].toString(); + final buildDate = data['buildDate'].toString(); + const linkStyle = TextStyle(decoration: TextDecoration.underline); + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: SingleChildScrollView( + controller: scrollController, + physics: DraggableNeverScrollableScrollPhysics(), + child: _Card(title: '${translate('About')} RustDesk', children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.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'); + }, + child: Text( + translate('Privacy Statement'), + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString('https://rustdesk.com'); + }, + child: Text( + translate('Website'), + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + Container( + decoration: const BoxDecoration(color: Color(0xFF2c8cff)), + padding: + const EdgeInsets.symmetric(vertical: 24, horizontal: 8), + child: SelectionArea( + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Copyright © 2022 Purslane Ltd.\n$license', + style: const TextStyle(color: Colors.white), + ), + Text( + translate('Slogan_tip'), + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) + ], + ), + ), + ], + )), + ).marginSymmetric(vertical: 4.0) + ], + ).marginOnly(left: _kContentHMargin) + ]), + )); + }); + } +} + +//#endregion + +//#region components + +// ignore: non_constant_identifier_names +Widget _Card( + {required String title, + required List children, + List? title_suffix}) { + return Row( + children: [ + Flexible( + child: SizedBox( + width: _kCardFixedWidth, + child: Card( + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Text( + translate(title), + textAlign: TextAlign.start, + style: const TextStyle( + fontSize: _kTitleFontSize, + ), + )), + ...?title_suffix + ], + ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10), + ...children + .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)), + ], + ).marginOnly(bottom: 10), + ).marginOnly(left: _kCardLeftMargin, top: 15), + ), + ), + ], + ); +} + +Color? _disabledTextColor(BuildContext context, bool enabled) { + return enabled + ? null + : Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6); +} + +// ignore: non_constant_identifier_names +Widget _OptionCheckBox(BuildContext context, String label, String key, + {Function()? update, + bool reverse = false, + bool enabled = true, + Icon? checkedIcon, + bool? fakeValue}) { + return futureBuilder( + future: bind.mainGetOption(key: key), + hasData: (data) { + bool value = option2bool(key, data.toString()); + if (reverse) value = !value; + var ref = value.obs; + onChanged(option) async { + if (option != null) { + ref.value = option; + if (reverse) option = !option; + String value = bool2option(key, option); + await bind.mainSetOption(key: key, value: value); + update?.call(); + } + } + + if (fakeValue != null) { + ref.value = fakeValue; + enabled = false; + } + + return GestureDetector( + child: Obx( + () => Row( + children: [ + Checkbox( + value: ref.value, onChanged: enabled ? onChanged : null) + .marginOnly(right: 5), + Offstage( + offstage: !ref.value || checkedIcon == null, + child: checkedIcon?.marginOnly(right: 5), + ), + Expanded( + child: Text( + translate(label), + style: TextStyle(color: _disabledTextColor(context, enabled)), + )) + ], + ), + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: enabled + ? () { + onChanged(!ref.value); + } + : null, + ); + }); +} + +// ignore: non_constant_identifier_names +Widget _Radio(BuildContext context, + {required T value, + required T groupValue, + required String label, + required Function(T value) onChanged, + bool autoNewLine = true, + bool enabled = true}) { + var onChange = enabled + ? (T? value) { + if (value != null) { + onChanged(value); + } + } + : null; + return GestureDetector( + child: Row( + children: [ + Radio(value: value, groupValue: groupValue, onChanged: onChange), + Expanded( + child: Text(translate(label), + overflow: autoNewLine ? null : TextOverflow.ellipsis, + style: TextStyle( + fontSize: _kContentFontSize, + color: _disabledTextColor(context, enabled))) + .marginOnly(left: 5), + ), + ], + ).marginOnly(left: _kRadioLeftMargin), + onTap: () => onChange?.call(value), + ); +} + +// ignore: non_constant_identifier_names +Widget _Button(String label, Function() onPressed, + {bool enabled = true, String? tip}) { + var button = ElevatedButton( + onPressed: enabled ? onPressed : null, + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + ); + StatefulWidget child; + if (tip == null) { + child = button; + } else { + child = Tooltip(message: translate(tip), child: button); + } + return Row(children: [ + child, + ]).marginOnly(left: _kContentHMargin); +} + +// ignore: non_constant_identifier_names +Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { + return Row( + children: [ + ElevatedButton( + onPressed: enabled ? onPressed : null, + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + ), + ], + ).marginOnly(left: _kContentHSubMargin); +} + +// ignore: non_constant_identifier_names +Widget _SubLabeledWidget(BuildContext context, String label, Widget child, + {bool enabled = true}) { + return Row( + children: [ + Text( + '${translate(label)}: ', + style: TextStyle(color: _disabledTextColor(context, enabled)), + ), + SizedBox( + width: 10, + ), + child, + ], + ).marginOnly(left: _kContentHSubMargin); +} + +Widget _lock( + bool locked, + String label, + Function() onUnlock, +) { + return Offstage( + offstage: !locked, + child: Row( + children: [ + Flexible( + child: SizedBox( + width: _kCardFixedWidth, + child: Card( + child: ElevatedButton( + child: SizedBox( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.security_sharp, + size: 20, + ), + Text(translate(label)).marginOnly(left: 5), + ]).marginSymmetric(vertical: 2)), + onPressed: () async { + bool checked = await bind.mainCheckSuperUserPermission(); + if (checked) { + onUnlock(); + } + }, + ).marginSymmetric(horizontal: 2, vertical: 4), + ).marginOnly(left: _kCardLeftMargin), + ).marginOnly(top: 10), + ), + ], + )); +} + +_LabeledTextField( + BuildContext context, + String label, + TextEditingController controller, + String errorText, + bool enabled, + bool secure) { + return Row( + children: [ + 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( + child: TextField( + controller: controller, + enabled: enabled, + obscureText: secure, + decoration: InputDecoration( + isDense: true, + border: OutlineInputBorder(), + contentPadding: EdgeInsets.fromLTRB(14, 15, 14, 15), + errorText: errorText.isNotEmpty ? errorText : null), + style: TextStyle( + color: _disabledTextColor(context, enabled), + )), + ), + ], + ).marginOnly(bottom: 8); +} + +// ignore: must_be_immutable +class _ComboBox extends StatelessWidget { + late final List keys; + late final List values; + late final String initialKey; + late final Function(String key) onChanged; + late final bool enabled; + late String current; + + _ComboBox({ + Key? key, + required this.keys, + required this.values, + required this.initialKey, + required this.onChanged, + // ignore: unused_element + this.enabled = true, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var index = keys.indexOf(initialKey); + if (index < 0) { + index = 0; + } + var ref = values[index].obs; + current = keys[index]; + return Container( + decoration: BoxDecoration(border: Border.all(color: MyTheme.border)), + height: 30, + child: Obx(() => DropdownButton( + isExpanded: true, + value: ref.value, + elevation: 16, + underline: Container( + height: 25, + ), + icon: const Icon( + Icons.expand_more_sharp, + size: 20, + ), + onChanged: enabled + ? (String? newValue) { + if (newValue != null && newValue != ref.value) { + ref.value = newValue; + current = newValue; + onChanged(keys[values.indexOf(newValue)]); + } + } + : null, + items: values.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: const TextStyle(fontSize: _kContentFontSize), + overflow: TextOverflow.ellipsis, + ).marginOnly(left: 5), + ); + }).toList(), + )), + ); + } +} + +//#endregion + +//#region dialogs + +void changeSocks5Proxy() async { + var socks = await bind.mainGetSocks(); + + String proxy = ''; + String proxyMsg = ''; + String username = ''; + String password = ''; + if (socks.length == 3) { + proxy = socks[0]; + username = socks[1]; + password = socks[2]; + } + 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) { + submit() async { + setState(() { + proxyMsg = ''; + isInProgress = true; + }); + cancel() { + setState(() { + isInProgress = false; + }); + } + + proxy = proxyController.text.trim(); + username = userController.text.trim(); + password = pwdController.text.trim(); + + if (proxy.isNotEmpty) { + proxyMsg = translate(await bind.mainTestIfValidServer(server: proxy)); + if (proxyMsg.isEmpty) { + // ignore + } else { + cancel(); + return; + } + } + await bind.mainSetSocks( + proxy: proxy, username: username, password: password); + close(); + } + + return CustomAlertDialog( + title: Text(translate('Socks5 Proxy')), + content: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + 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, + autofocus: true, + ), + ), + ], + ).marginOnly(bottom: 8), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), + Expanded( + child: TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + ), + controller: userController, + ), + ), + ], + ).marginOnly(bottom: 8), + Row( + children: [ + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Password")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), + Expanded( + 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, + )), + ), + ], + ).marginOnly(bottom: 8), + Offstage( + offstage: !isInProgress, child: const LinearProgressIndicator()) + ], + ), + ), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +//#endregion diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart new file mode 100644 index 000000000..053a2d8a2 --- /dev/null +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -0,0 +1,87 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../../common/shared_state.dart'; + +class DesktopTabPage extends StatefulWidget { + const DesktopTabPage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopTabPageState(); + + static void onAddSetting({int initialPage = 0}) { + try { + DesktopTabController tabController = Get.find(); + tabController.add(TabInfo( + key: kTabLabelSettingPage, + label: translate(kTabLabelSettingPage), + selectedIcon: Icons.build_sharp, + unselectedIcon: Icons.build_outlined, + page: DesktopSettingPage( + key: const ValueKey(kTabLabelSettingPage), + initialPage: initialPage, + ))); + } catch (e) { + debugPrintStack(label: '$e'); + } + } +} + +class _DesktopTabPageState extends State { + final tabController = DesktopTabController(tabType: DesktopTabType.main); + + @override + void initState() { + super.initState(); + Get.put(tabController); + RemoteCountState.init(); + tabController.add(TabInfo( + key: kTabLabelHomePage, + label: translate(kTabLabelHomePage), + selectedIcon: Icons.home_sharp, + unselectedIcon: Icons.home_outlined, + closable: false, + page: DesktopHomePage( + key: const ValueKey(kTabLabelHomePage), + ))); + } + + @override + void dispose() { + super.dispose(); + Get.delete(); + } + + @override + Widget build(BuildContext context) { + final tabWidget = Container( + 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 + ? tabWidget + : Obx( + () => DragToResizeArea( + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + child: tabWidget, + ), + ); + } +} diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart new file mode 100644 index 000000000..c8cb7c935 --- /dev/null +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -0,0 +1,1464 @@ +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'; +import '../widgets/popup_menu.dart'; + +/// status of location bar +enum LocationStatus { + /// normal bread crumb bar + bread, + + /// show path text field + pathLocation, + + /// show file search bar text field + fileSearchBar +} + +/// The status of currently focused scope of the mouse +enum MouseFocusScope { + /// Mouse is in local field. + local, + + /// Mouse is in remote field. + remote, + + /// Mouse is not in local field, remote neither. + none +} + +class FileManagerPage extends StatefulWidget { + const FileManagerPage({Key? key, required this.id, this.forceRelay}) + : super(key: key); + final String id; + final bool? forceRelay; + + @override + State createState() => _FileManagerPageState(); +} + +class _FileManagerPageState extends State + with AutomaticKeepAliveClientMixin { + final _localSelectedItems = SelectedItems(); + final _remoteSelectedItems = SelectedItems(); + + final _locationStatusLocal = LocationStatus.bread.obs; + final _locationStatusRemote = LocationStatus.bread.obs; + final _locationNodeLocal = FocusNode(debugLabel: "locationNodeLocal"); + final _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote"); + final _locationBarKeyLocal = GlobalKey(debugLabel: "locationBarKeyLocal"); + final _locationBarKeyRemote = GlobalKey(debugLabel: "locationBarKeyRemote"); + final _searchTextLocal = "".obs; + final _searchTextRemote = "".obs; + final _breadCrumbScrollerLocal = ScrollController(); + final _breadCrumbScrollerRemote = ScrollController(); + final _mouseFocusScope = Rx(MouseFocusScope.none); + final _keyboardNodeLocal = FocusNode(debugLabel: "keyboardNodeLocal"); + 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 = + DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000; + Entry? _lastClickEntry; + + final _dropMaskVisible = false.obs; // TODO impl drop mask + final _overlayKeyState = OverlayKeyState(); + + ScrollController getBreadCrumbScrollController(bool isLocal) { + return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; + } + + GlobalKey getLocationBarKey(bool isLocal) { + return isLocal ? _locationBarKeyLocal : _locationBarKeyRemote; + } + + late FFI _ffi; + + FileModel get model => _ffi.fileModel; + + SelectedItems getSelectedItems(bool isLocal) { + return isLocal ? _localSelectedItems : _remoteSelectedItems; + } + + @override + void initState() { + super.initState(); + _ffi = FFI(); + _ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay); + WidgetsBinding.instance.addPostFrameCallback((_) { + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + Get.put(_ffi, tag: 'ft_${widget.id}'); + if (!Platform.isLinux) { + Wakelock.enable(); + } + debugPrint("File manager page init success with id ${widget.id}"); + model.onDirChanged = breadCrumbScrollToEnd; + // register location listener + _locationNodeLocal.addListener(onLocalLocationFocusChanged); + _locationNodeRemote.addListener(onRemoteLocationFocusChanged); + _ffi.dialogManager.setOverlayState(_overlayKeyState); + } + + @override + void dispose() { + model.onClose().whenComplete(() { + _ffi.close(); + _ffi.dialogManager.dismissAll(); + if (!Platform.isLinux) { + Wakelock.disable(); + } + Get.delete(tag: 'ft_${widget.id}'); + _locationNodeLocal.removeListener(onLocalLocationFocusChanged); + _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); + _locationNodeLocal.dispose(); + _locationNodeRemote.dispose(); + }); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(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).scaffoldBackgroundColor, + body: Row( + children: [ + Flexible(flex: 3, child: body(isLocal: true)), + Flexible(flex: 3, child: body(isLocal: false)), + Flexible(flex: 2, child: statusList()) + ], + ), + ); + })); + }) + ]); + } + + Widget menu({bool isLocal = false}) { + var menuPos = RelativeRect.fill; + + final List> items = [ + MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate("Show Hidden Files"), + getter: () async { + return model.getCurrentShowHidden(isLocal); + }, + setter: (bool v) async { + model.toggleShowHidden(local: isLocal); + }, + padding: kDesktopMenuPadding, + dismissOnClicked: true, + ), + MenuEntryButton( + childBuilder: (style) => Text(translate("Select All"), style: style), + proc: () => setState(() => getSelectedItems(isLocal) + .selectAll(model.getCurrentDir(isLocal).entries)), + padding: kDesktopMenuPadding, + dismissOnClicked: true), + MenuEntryButton( + childBuilder: (style) => + Text(translate("Unselect All"), style: style), + proc: () => setState(() => getSelectedItems(isLocal).clear()), + padding: kDesktopMenuPadding, + dismissOnClicked: true) + ]; + + return Listener( + 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( + margin: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(8.0), + child: DropTarget( + onDragDone: (detail) => handleDragDone(detail, isLocal), + onDragEntered: (enter) { + _dropMaskVisible.value = true; + }, + onDragExited: (exit) { + _dropMaskVisible.value = false; + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + headTools(isLocal), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: _buildFileList(context, isLocal, scrollController), + ) + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildFileList( + BuildContext context, bool isLocal, ScrollController scrollController) { + final fd = model.getCurrentDir(isLocal); + final entries = fd.entries; + final selectedEntries = getSelectedItems(isLocal); + + return MouseRegion( + onEnter: (evt) { + _mouseFocusScope.value = + isLocal ? MouseFocusScope.local : MouseFocusScope.remote; + if (isLocal) { + _keyboardNodeLocal.requestFocus(); + } else { + _keyboardNodeRemote.requestFocus(); + } + }, + onExit: (evt) { + _mouseFocusScope.value = MouseFocusScope.none; + }, + child: ListSearchActionListener( + node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote, + buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote, + onNext: (buffer) { + debugPrint("searching next for $buffer"); + assert(buffer.length == 1); + assert(selectedEntries.length <= 1); + var skipCount = 0; + if (selectedEntries.items.isNotEmpty) { + final index = entries.indexOf(selectedEntries.items.first); + if (index < 0) { + return; + } + skipCount = index + 1; + } + var searchResult = entries + .skip(skipCount) + .where((element) => element.name.toLowerCase().startsWith(buffer)); + if (searchResult.isEmpty) { + // cannot find next, lets restart search from head + debugPrint("restart search from head"); + searchResult = + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); + } + if (searchResult.isEmpty) { + setState(() { + getSelectedItems(isLocal).clear(); + }); + return; + } + _jumpToEntry(isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight); + }, + onSearch: (buffer) { + debugPrint("searching for $buffer"); + final selectedEntries = getSelectedItems(isLocal); + final searchResult = + entries.where((element) => element.name.toLowerCase().startsWith(buffer)); + selectedEntries.clear(); + if (searchResult.isEmpty) { + setState(() { + getSelectedItems(isLocal).clear(); + }); + return; + } + _jumpToEntry(isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight); + }, + child: ObxValue( + (searchText) { + final filteredEntries = searchText.isNotEmpty + ? entries.where((element) { + return element.name.contains(searchText.value); + }).toList(growable: false) + : entries; + 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), + ), + ), + 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, + ), + ), + ); + } + + void _jumpToEntry(bool isLocal, Entry entry, + 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 == entry); + selectedEntries.clear(); + if (searchResult.isEmpty) { + return; + } + final offset = min( + max(scrollController.position.minScrollExtent, + entries.indexOf(searchResult.first) * rowHeight), + scrollController.position.maxScrollExtent); + scrollController.jumpTo(offset); + setState(() { + selectedEntries.add(isLocal, searchResult.first); + debugPrint("focused on ${searchResult.first.name}"); + }); + } + + void _onSelectedChanged(SelectedItems selectedItems, List entries, + Entry entry, bool isLocal) { + final isCtrlDown = RawKeyboard.instance.keysPressed + .contains(LogicalKeyboardKey.controlLeft); + final isShiftDown = + RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft); + if (isCtrlDown) { + if (selectedItems.contains(entry)) { + selectedItems.remove(entry); + } else { + selectedItems.add(isLocal, entry); + } + } else if (isShiftDown) { + final List indexGroup = []; + for (var selected in selectedItems.items) { + indexGroup.add(entries.indexOf(selected)); + } + indexGroup.add(entries.indexOf(entry)); + indexGroup.removeWhere((e) => e == -1); + final maxIndex = indexGroup.reduce(max); + final minIndex = indexGroup.reduce(min); + selectedItems.clear(); + entries + .getRange(minIndex, maxIndex + 1) + .forEach((e) => selectedItems.add(isLocal, e)); + } else { + selectedItems.clear(); + selectedItems.add(isLocal, entry); + } + setState(() {}); + } + + bool _checkDoubleClick(Entry entry) { + final current = DateTime.now().millisecondsSinceEpoch; + final elapsed = current - _lastClickTime; + _lastClickTime = current; + if (_lastClickEntry == entry) { + if (elapsed < bind.getDoubleClickTime()) { + return true; + } + } else { + _lastClickEntry = entry; + } + return false; + } + + /// transfer status list + /// watch transfer status + Widget statusList() { + return PreferredSize( + preferredSize: const Size(200, double.infinity), + child: model.jobTable.isEmpty + ? Center(child: Text(translate("Empty"))) + : Container( + margin: + const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), + padding: const EdgeInsets.all(8.0), + child: Obx( + () => ListView.builder( + controller: ScrollController(), + itemBuilder: (BuildContext context, int index) { + final item = model.jobTable[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + 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, + ), + ], + ), + ], + ), + ], + ).paddingSymmetric(vertical: 10), + ), + ); + }, + itemCount: model.jobTable.length, + ), + ), + )); + } + + Widget headTools(bool isLocal) { + final locationStatus = + isLocal ? _locationStatusLocal : _locationStatusRemote; + 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( + 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) + ], + ), + 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 = + isLocal ? _locationNodeLocal : _locationNodeRemote; + Future.delayed( + Duration.zero, () => focusNode.requestFocus()); + }, + 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, + 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; + }, + 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); + }, + 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) { + submit() { + if (name.value.text.isNotEmpty) { + model.createDir( + PathUtil.join( + model.getCurrentDir(isLocal).path, + name.value.text, + model.getCurrentIsWindows(isLocal)), + isLocal: isLocal); + close(); + } + } + + cancel() => close(false); + return CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), + ), + controller: name, + autofocus: true, + ), + ], + ), + actions: [ + dialogButton("Cancel", + onPressed: cancel, isOutline: true), + dialogButton("OK", onPressed: submit) + ], + onSubmit: submit, + onCancel: cancel, + ); + }); + }, + 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, + isLocal: isLocal)); + selectedItems.clear(); + } + : null, + 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), + ], + ), + ), + 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, + ), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), + ), + onPressed: validItems(selectedItems) + ? () { + model.sendFiles(selectedItems, isRemote: !isLocal); + selectedItems.clear(); + } + : null, + icon: isLocal + ? Text( + translate('Send'), + textAlign: TextAlign.right, + style: TextStyle( + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + ), + ) + : RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + alignment: Alignment.bottomRight, + ), + ), + label: isLocal + ? SvgPicture.asset( + "assets/arrow.svg", + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + ) + : Text( + translate('Receive'), + style: TextStyle( + color: selectedItems.length == 0 + ? MyTheme.darkGray + : Colors.white, + ), + ), + ), + ], + ).marginOnly(top: 8.0) + ], + ), + ); + } + + bool validItems(SelectedItems items) { + if (items.length > 0) { + // exclude DirDrive type + return items.items.any((item) => !item.isDrive); + } + return false; + } + + @override + bool get wantKeepAlive => true; + + void onLocalLocationFocusChanged() { + debugPrint("focus changed on local"); + if (_locationNodeLocal.hasFocus) { + // ignore + } else { + // lost focus, change to bread + if (_locationStatusLocal.value != LocationStatus.fileSearchBar) { + _locationStatusLocal.value = LocationStatus.bread; + } + } + } + + void onRemoteLocationFocusChanged() { + debugPrint("focus changed on remote"); + if (_locationNodeRemote.hasFocus) { + // ignore + } else { + // lost focus, change to bread + if (_locationStatusRemote.value != LocationStatus.fileSearchBar) { + _locationStatusRemote.value = LocationStatus.bread; + } + } + } + + Widget buildBread(bool isLocal) { + final items = getPathBreadCrumbItems(isLocal, (list) { + var path = ""; + for (var item in list) { + path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); + } + openDirectory(path, isLocal: isLocal); + }); + final locationBarKey = getLocationBarKey(isLocal); + + return items.isEmpty + ? Offstage() + : Row( + key: locationBarKey, + 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: const Icon(Icons.keyboard_arrow_right_rounded), + overflow: ScrollableOverflow( + controller: getBreadCrumbScrollController(isLocal), + ), + ), + ), + ), + ActionIcon( + message: "", + icon: Icons.keyboard_arrow_down_rounded, + onTap: () async { + final renderBox = locationBarKey.currentContext + ?.findRenderObject() as RenderBox; + locationBarKey.currentContext?.size; + + final size = renderBox.size; + final offset = renderBox.localToGlobal(Offset.zero); + + final x = offset.dx; + final y = offset.dy + size.height + 1; + + final isPeerWindows = model.getCurrentIsWindows(isLocal); + final List menuItems = [ + MenuEntryButton( + childBuilder: (TextStyle? style) => isPeerWindows + ? buildWindowsThisPC(style) + : Text( + '/', + style: style, + ), + proc: () { + openDirectory('/', isLocal: isLocal); + }, + dismissOnClicked: true), + MenuEntryDivider() + ]; + if (isPeerWindows) { + var loadingTag = ""; + if (!isLocal) { + loadingTag = _ffi.dialogManager.showLoading("Waiting"); + } + try { + final fd = + await model.fetchDirectory("/", isLocal, isLocal); + for (var entry in fd.entries) { + menuItems.add(MenuEntryButton( + childBuilder: (TextStyle? style) => + Row(children: [ + Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7)), + SizedBox(width: 10), + Text( + entry.name, + style: style, + ) + ]), + proc: () { + openDirectory('${entry.name}\\', + isLocal: isLocal); + }, + dismissOnClicked: true)); + } + } catch (e) { + debugPrint("buildBread fetchDirectory err=$e"); + } finally { + if (!isLocal) { + _ffi.dialogManager.dismissByTag(loadingTag); + } + } + } + menuItems.add(MenuEntryDivider()); + mod_menu.showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + elevation: 4, + items: menuItems + .map((e) => e.build( + context, + MenuConfig( + commonColor: + CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: + CustomPopupMenuTheme.dividerHeight, + boxWidth: size.width))) + .expand((i) => i) + .toList()); + }, + iconSize: 20, + ) + ]); + } + + Widget buildWindowsThisPC([TextStyle? textStyle]) { + final color = Theme.of(context).iconTheme.color?.withOpacity(0.7); + return Row(children: [ + Icon(Icons.computer, size: 20, color: color), + SizedBox(width: 10), + Text(translate('This PC'), style: textStyle) + ]); + } + + List getPathBreadCrumbItems( + bool isLocal, void Function(List) onPressed) { + final path = model.getCurrentDir(isLocal).path; + final breadCrumbList = List.empty(growable: true); + final isWindows = model.getCurrentIsWindows(isLocal); + if (isWindows && path == '/') { + breadCrumbList.add(BreadCrumbItem( + content: TextButton( + child: buildWindowsThisPC(), + style: ButtonStyle( + minimumSize: MaterialStateProperty.all(Size(0, 0))), + onPressed: () => onPressed(['/'])) + .marginSymmetric(horizontal: 4))); + } else { + final list = PathUtil.split(path, isWindows); + 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), + ), + ), + ); + } + return breadCrumbList; + } + + breadCrumbScrollToEnd(bool isLocal) { + Future.delayed(Duration(milliseconds: 200), () { + final breadCrumbScroller = getBreadCrumbScrollController(isLocal); + if (breadCrumbScroller.hasClients) { + breadCrumbScroller.animateTo( + breadCrumbScroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + } + }); + } + + Widget buildPathLocation(bool isLocal) { + final searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote; + final locationStatus = + isLocal ? _locationStatusLocal : _locationStatusRemote; + final focusNode = isLocal ? _locationNodeLocal : _locationNodeRemote; + final text = locationStatus.value == LocationStatus.pathLocation + ? model.getCurrentDir(isLocal).path + : searchTextObs.value; + final textController = TextEditingController(text: text) + ..selection = TextSelection.collapsed(offset: text.length); + 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, + ), + ) + ], + ); + } + + onSearchText(String searchText, bool isLocal) { + if (isLocal) { + _localSelectedItems.clear(); + _searchTextLocal.value = searchText; + } else { + _remoteSelectedItems.clear(); + _searchTextRemote.value = searchText; + } + } + + openDirectory(String path, {bool isLocal = false}) { + model.openDirectory(path, isLocal: isLocal); + } + + void handleDragDone(DropDoneDetails details, bool isLocal) { + if (isLocal) { + // ignore local + return; + } + var items = SelectedItems(); + for (var file in details.files) { + final f = File(file.path); + items.add( + true, + Entry() + ..path = file.path + ..name = file.name + ..size = + FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync()); + } + model.sendFiles(items, isRemote: false); + } + + void refocusKeyboardListener(bool isLocal) { + Future.delayed(Duration.zero, () { + if (isLocal) { + _keyboardNodeLocal.requestFocus(); + } else { + _keyboardNodeRemote.requestFocus(); + } + }); + } + + 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 new file mode 100644 index 000000000..148d928d9 --- /dev/null +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -0,0 +1,139 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +import '../../models/platform_model.dart'; + +/// File Transfer for multi tabs +class FileManagerTabPage extends StatefulWidget { + final Map params; + + const FileManagerTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _FileManagerTabPageState(params); +} + +class _FileManagerTabPageState extends State { + DesktopTabController get tabController => Get.find(); + + static const IconData selectedIcon = Icons.file_copy_sharp; + static const IconData unselectedIcon = Icons.file_copy_outlined; + + _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'], + forceRelay: params['forceRelay'], + ))); + } + + @override + void initState() { + super.initState(); + + tabController.onRemoved = (_, id) => onRemoveId(id); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}"); + // for simplify, just replace connectionId + if (call.method == "new_file_transfer") { + final args = jsonDecode(call.arguments); + final id = args['id']; + window_on_top(windowId()); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(id), + page: FileManagerPage( + key: ValueKey(id), + id: id, + forceRelay: args['forceRelay'], + ))); + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } + }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.FileTransfer, windowId: windowId()); + }); + } + + @override + Widget build(BuildContext context) { + final tabWidget = Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton().paddingOnly(left: 10), + labelGetter: DesktopTab.labelGetterAlias, + )), + ); + return Platform.isMacOS + ? tabWidget + : SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + windowId: stateGlobal.windowId, + ); + } + + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } + + Future handleWindowCloseButton() async { + final connLength = tabController.state.value.tabs.length; + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final opt = "enable-confirm-closing-tabs"; + final bool res; + if (!option2bool(opt, await bind.mainGetOption(key: opt))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } +} diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart new file mode 100644 index 000000000..e7bb28813 --- /dev/null +++ b/flutter/lib/desktop/pages/install_page.dart @@ -0,0 +1,198 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; + +class InstallPage extends StatefulWidget { + const InstallPage({Key? key}) : super(key: key); + + @override + State createState() => _InstallPageState(); +} + +class _InstallPageState extends State with WindowListener { + late final TextEditingController controller; + final RxBool startmenu = true.obs; + final RxBool desktopicon = true.obs; + final RxBool showProgress = false.obs; + final RxBool btnEnabled = true.obs; + + @override + void initState() { + windowManager.addListener(this); + controller = TextEditingController(text: bind.installInstallPath()); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowClose() { + gFFI.close(); + super.onWindowClose(); + windowManager.setPreventClose(false); + windowManager.close(); + } + + @override + Widget build(BuildContext context) { + final double em = 13; + final btnFontSize = 0.9 * em; + final double button_radius = 6; + final buttonStyle = OutlinedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(button_radius)), + )); + final inputBorder = OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide(color: Colors.black12)); + return Scaffold( + backgroundColor: Colors.white, + body: SingleChildScrollView( + child: Column( + children: [ + Row( + children: [ + Text( + translate('Installation'), + style: TextStyle( + fontSize: 2 * em, fontWeight: FontWeight.w500), + ), + ], + ), + Row( + children: [ + Text('${translate('Installation Path')}: '), + Expanded( + child: TextField( + controller: controller, + readOnly: true, + style: TextStyle( + fontSize: 1.5 * em, fontWeight: FontWeight.w400), + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.all(0.75 * em), + enabledBorder: inputBorder, + border: inputBorder, + focusedBorder: inputBorder, + constraints: BoxConstraints(maxHeight: 3 * em), + ), + )), + Obx(() => OutlinedButton( + onPressed: + btnEnabled.value ? selectInstallPath : null, + style: buttonStyle, + child: Text(translate('Change Path'), + style: TextStyle( + color: Colors.black87, + fontSize: btnFontSize))) + .marginOnly(left: em)) + ], + ).marginSymmetric(vertical: 2 * em), + Row( + children: [ + Obx(() => Checkbox( + value: startmenu.value, + onChanged: (b) { + if (b != null) startmenu.value = b; + })), + Text(translate('Create start menu shortcuts')) + ], + ), + Row( + children: [ + Obx(() => Checkbox( + value: desktopicon.value, + onChanged: (b) { + if (b != null) desktopicon.value = b; + })), + Text(translate('Create desktop icon')) + ], + ), + GestureDetector( + onTap: () => launchUrlString('http://rustdesk.com/privacy'), + child: Row( + children: [ + Text(translate('End-user license agreement'), + style: const TextStyle( + decoration: TextDecoration.underline)) + ], + )).marginOnly(top: 2 * em), + Row(children: [Text(translate('agreement_tip'))]) + .marginOnly(top: em), + Divider(color: Colors.black87) + .marginSymmetric(vertical: 0.5 * em), + Row( + children: [ + Expanded( + child: Obx(() => Offstage( + offstage: !showProgress.value, + child: LinearProgressIndicator(), + ))), + Obx(() => OutlinedButton( + onPressed: btnEnabled.value + ? () => windowManager.close() + : null, + style: buttonStyle, + child: Text(translate('Cancel'), + style: TextStyle( + color: Colors.black87, + fontSize: btnFontSize))) + .marginOnly(right: 2 * em)), + Obx(() => ElevatedButton( + onPressed: btnEnabled.value ? install : null, + style: ElevatedButton.styleFrom( + primary: MyTheme.button, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(button_radius)), + )), + child: Text( + translate('Accept and Install'), + style: TextStyle(fontSize: btnFontSize), + ))), + Offstage( + offstage: bind.installShowRunWithoutInstall(), + child: Obx(() => OutlinedButton( + onPressed: btnEnabled.value + ? () => bind.installRunWithoutInstall() + : null, + style: buttonStyle, + child: Text(translate('Run without install'), + style: TextStyle( + color: Colors.black87, + fontSize: btnFontSize))) + .marginOnly(left: 2 * em)), + ), + ], + ) + ], + ).paddingSymmetric(horizontal: 8 * em, vertical: 2 * em), + )); + } + + void install() { + btnEnabled.value = false; + showProgress.value = true; + String args = ''; + if (startmenu.value) args += ' startmenu'; + if (desktopicon.value) args += ' desktopicon'; + bind.installInstallMe(options: args, path: controller.text); + } + + void selectInstallPath() async { + String? install_path = await FilePicker.platform + .getDirectoryPath(initialDirectory: controller.text); + if (install_path != null) { + install_path = '$install_path\\${await bind.mainGetAppName()}'; + controller.text = install_path; + } + } +} diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart new file mode 100644 index 000000000..ae070b47b --- /dev/null +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -0,0 +1,352 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:wakelock/wakelock.dart'; + +const double _kColumn1Width = 30; +const double _kColumn4Width = 100; +const double _kRowHeight = 50; +const double _kTextLeftMargin = 20; + +class _PortForward { + int localPort; + String remoteHost; + int remotePort; + + _PortForward.fromJson(List json) + : localPort = json[0] as int, + remoteHost = json[1] as String, + remotePort = json[2] as int; +} + +class PortForwardPage extends StatefulWidget { + 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(); +} + +class _PortForwardPageState extends State + with AutomaticKeepAliveClientMixin { + final TextEditingController localPortController = TextEditingController(); + final TextEditingController remoteHostController = TextEditingController(); + final TextEditingController remotePortController = TextEditingController(); + RxList<_PortForward> pfs = RxList.empty(growable: true); + late FFI _ffi; + + @override + void initState() { + super.initState(); + _ffi = FFI(); + _ffi.start(widget.id, isPortForward: true, forceRelay: widget.forceRelay); + Get.put(_ffi, tag: 'pf_${widget.id}'); + if (!Platform.isLinux) { + Wakelock.enable(); + } + debugPrint("Port forward page init success with id ${widget.id}"); + } + + @override + void dispose() { + _ffi.close(); + _ffi.dialogManager.dismissAll(); + if (!Platform.isLinux) { + Wakelock.disable(); + } + Get.delete(tag: 'pf_${widget.id}'); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: FutureBuilder(future: () async { + if (!widget.isRDP) { + refreshTunnelConfig(); + } + }(), builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return Container( + decoration: BoxDecoration( + border: Border.all( + width: 20, + color: Theme.of(context).scaffoldBackgroundColor)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildPrompt(context), + Flexible( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + border: Border.all(width: 1, color: MyTheme.border)), + child: + widget.isRDP ? buildRdp(context) : buildTunnel(context), + ), + ), + ], + ), + ); + } + return const Offstage(); + }), + ); + } + + buildPrompt(BuildContext context) { + return Obx(() => Offstage( + offstage: pfs.isEmpty && !widget.isRDP, + child: Container( + height: 45, + color: const Color(0xFF007F00), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate('Listening ...'), + style: const TextStyle(fontSize: 16, color: Colors.white), + ), + Text( + translate('not_close_tcp_tip'), + style: const TextStyle( + fontSize: 10, color: Color(0xFFDDDDDD), height: 1.2), + ) + ])).marginOnly(bottom: 8), + )); + } + + buildTunnel(BuildContext context) { + text(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); + + return Theme( + data: Theme.of(context) + .copyWith(backgroundColor: Theme.of(context).colorScheme.background), + child: Obx(() => ListView.builder( + controller: ScrollController(), + itemCount: pfs.length + 2, + itemBuilder: ((context, index) { + if (index == 0) { + return Container( + height: 25, + color: Theme.of(context).scaffoldBackgroundColor, + child: Row(children: [ + text('Local Port'), + const SizedBox(width: _kColumn1Width), + text('Remote Host'), + text('Remote Port'), + SizedBox( + width: _kColumn4Width, child: Text(translate('Action'))) + ]), + ); + } else if (index == 1) { + return buildTunnelAddRow(context); + } else { + return buildTunnelDataRow(context, pfs[index - 2], index - 2); + } + }))), + ); + } + + buildTunnelAddRow(BuildContext context) { + var portInputFormatter = [ + FilteringTextInputFormatter.allow(RegExp( + 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])$')) + ]; + + return Container( + height: _kRowHeight, + decoration: + BoxDecoration(color: Theme.of(context).colorScheme.background), + child: Row(children: [ + buildTunnelInputCell(context, + controller: localPortController, + inputFormatters: portInputFormatter), + const SizedBox( + width: _kColumn1Width, child: Icon(Icons.arrow_forward_sharp)), + buildTunnelInputCell(context, + controller: remoteHostController, hint: 'localhost'), + buildTunnelInputCell(context, + controller: remotePortController, + inputFormatters: portInputFormatter), + 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), + ]), + ); + } + + buildTunnelInputCell(BuildContext context, + {required TextEditingController controller, + List? inputFormatters, + String? hint}) { + final textColor = Theme.of(context).textTheme.titleLarge?.color; + return Expanded( + child: TextField( + controller: controller, + inputFormatters: inputFormatters, + cursorColor: textColor, + cursorHeight: 20, + cursorWidth: 1, + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: MyTheme.color(context).border!)), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: MyTheme.color(context).border!)), + fillColor: Theme.of(context).colorScheme.background, + contentPadding: const EdgeInsets.all(10), + hintText: hint, + hintStyle: + TextStyle(color: Theme.of(context).hintColor, fontSize: 16)), + style: TextStyle(color: textColor, fontSize: 16), + ).marginAll(10), + ); + } + + Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) { + text(String label) => Expanded( + child: Text(label, style: const TextStyle(fontSize: 20)) + .marginOnly(left: _kTextLeftMargin)); + + return Container( + height: _kRowHeight, + decoration: BoxDecoration( + color: index % 2 == 0 + ? MyTheme.currentThemeMode() == ThemeMode.dark + ? const Color(0xFF202020) + : const Color(0xFFF4F5F6) + : Theme.of(context).colorScheme.background), + child: Row(children: [ + text(pf.localPort.toString()), + const SizedBox(width: _kColumn1Width), + text(pf.remoteHost), + text(pf.remotePort.toString()), + SizedBox( + width: _kColumn4Width, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + await bind.sessionRemovePortForward( + id: 'pf_${widget.id}', localPort: pf.localPort); + refreshTunnelConfig(); + }, + ), + ), + ]), + ); + } + + void refreshTunnelConfig() async { + String peer = await bind.mainGetPeer(id: widget.id); + Map config = jsonDecode(peer); + List infos = config['port_forwards'] as List; + List<_PortForward> result = List.empty(growable: true); + for (var e in infos) { + result.add(_PortForward.fromJson(e)); + } + pfs.value = result; + } + + buildRdp(BuildContext context) { + text1(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); + text2(String label) => Expanded( + child: Text( + label, + style: const TextStyle(fontSize: 20), + ).marginOnly(left: _kTextLeftMargin)); + return Theme( + data: Theme.of(context) + .copyWith(backgroundColor: Theme.of(context).colorScheme.background), + child: ListView.builder( + controller: ScrollController(), + itemCount: 2, + itemBuilder: ((context, index) { + if (index == 0) { + return Container( + height: 25, + color: Theme.of(context).scaffoldBackgroundColor, + child: Row(children: [ + text1('Local Port'), + const SizedBox(width: _kColumn1Width), + text1('Remote Host'), + text1('Remote Port'), + ]), + ); + } else { + return Container( + height: _kRowHeight, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background), + child: Row(children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: SizedBox( + width: 120, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + elevation: 0, + side: const BorderSide(color: MyTheme.border)), + onPressed: () => bind.sessionNewRdp(id: widget.id), + child: Text( + translate('New RDP'), + style: const TextStyle( + fontWeight: FontWeight.w300, fontSize: 14), + ), + ).marginSymmetric(vertical: 10), + ).marginOnly(left: 20), + ), + ), + const SizedBox( + width: _kColumn1Width, + child: Icon(Icons.arrow_forward_sharp)), + text2('localhost'), + text2('RDP'), + ]), + ); + } + })), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart new file mode 100644 index 000000000..f2d75d00f --- /dev/null +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -0,0 +1,128 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/port_forward_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +class PortForwardTabPage extends StatefulWidget { + final Map params; + + const PortForwardTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _PortForwardTabPageState(params); +} + +class _PortForwardTabPageState extends State { + late final DesktopTabController tabController; + late final bool isRDP; + + static const IconData selectedIcon = Icons.forward_sharp; + static const IconData unselectedIcon = Icons.forward_outlined; + + _PortForwardTabPageState(Map params) { + 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'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: PortForwardPage( + key: ValueKey(params['id']), + id: params['id'], + isRDP: isRDP, + forceRelay: params['forceRelay'], + ))); + } + + @override + void initState() { + super.initState(); + + tabController.onRemoved = (_, id) => onRemoveId(id); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + debugPrint( + "[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + // for simplify, just replace connectionId + if (call.method == "new_port_forward") { + final args = jsonDecode(call.arguments); + final id = args['id']; + final isRDP = args['isRDP']; + window_on_top(windowId()); + if (tabController.state.value.tabs.indexWhere((e) => e.key == id) >= + 0) { + debugPrint("port forward $id exists"); + return; + } + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + 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) { + reloadCurrentWindow(); + } + }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.PortForward, windowId: windowId()); + }); + } + + @override + Widget build(BuildContext context) { + final tabWidget = Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: () async { + tabController.clear(); + return true; + }, + tail: AddButton().paddingOnly(left: 10), + labelGetter: DesktopTab.labelGetterAlias, + )), + ); + return Platform.isMacOS + ? tabWidget + : SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + windowId: stateGlobal.windowId, + ); + } + + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart new file mode 100644 index 000000000..ab0daece7 --- /dev/null +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -0,0 +1,747 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_custom_cursor/cursor_manager.dart' + as custom_cursor_manager; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock/wakelock.dart'; +import 'package:flutter_custom_cursor/flutter_custom_cursor.dart'; +import 'package:flutter_improved_scrolling/flutter_improved_scrolling.dart'; + +import '../../consts.dart'; +import '../../common/widgets/overlay.dart'; +import '../../common/widgets/remote_input.dart'; +import '../../common.dart'; +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'; + +bool _isCustomCursorInited = false; +final SimpleWrapper _firstEnterImage = SimpleWrapper(false); + +class RemotePage extends StatefulWidget { + RemotePage({ + 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; + + @override + State createState() { + final state = _RemotePageState(); + _lastState.value = state; + return state; + } +} + +class _RemotePageState extends State + with AutomaticKeepAliveClientMixin, MultiWindowListener { + Timer? _timer; + String keyboardMode = "legacy"; + bool _isWindowBlur = false; + final _cursorOverImage = false.obs; + late RxBool _showRemoteCursor; + 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"); + + Function(bool)? _onEnterOrLeaveImage4Menubar; + + late FFI _ffi; + + void _initStates(String id) { + PrivacyModeState.init(id); + BlockInputState.init(id); + CurrentDisplayState.init(id); + KeyboardEnabledState.init(id); + ShowRemoteCursorState.init(id); + RemoteCursorMovedState.init(id); + final optZoomCursor = 'zoom-cursor'; + PeerBoolOption.init(id, optZoomCursor, () => false); + _zoomCursor = PeerBoolOption.find(id, optZoomCursor); + _showRemoteCursor = ShowRemoteCursorState.find(id); + _keyboardEnabled = KeyboardEnabledState.find(id); + _remoteCursorMoved = RemoteCursorMovedState.find(id); + _textureKey = newTextureId; + _textureId = RxInt(-1); + } + + void _removeStates(String id) { + PrivacyModeState.delete(id); + BlockInputState.delete(id); + CurrentDisplayState.delete(id); + ShowRemoteCursorState.delete(id); + KeyboardEnabledState.delete(id); + RemoteCursorMovedState.delete(id); + } + + @override + void initState() { + super.initState(); + _initStates(widget.id); + _ffi = FFI(); + Get.put(_ffi, tag: widget.id); + _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + showKBLayoutTypeChooserIfNeeded( + _ffi.ffiModel.pi.platform, _ffi.dialogManager); + }); + _ffi.start( + widget.id, + switchUuid: widget.switchUuid, + forceRelay: widget.forceRelay, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + 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 + _showRemoteCursor.value = bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); + _zoomCursor.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor'); + DesktopMultiWindow.addListener(this); + // if (!_isCustomCursorInited) { + // customCursorController.registerNeedUpdateCursorCallback( + // (String? lastKey, String? currentKey) async { + // if (_firstEnterImage.value) { + // _firstEnterImage.value = false; + // return true; + // } + // return lastKey == null || lastKey != currentKey; + // }); + // _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 + void onWindowBlur() { + super.onWindowBlur(); + // On windows, we use `focus` way to handle keyboard better. + // Now on Linux, there's some rdev issues which will break the input. + // We disable the `focus` way for non-Windows temporarily. + if (Platform.isWindows) { + _isWindowBlur = true; + // unfocus the primary-focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } + } + + @override + void onWindowFocus() { + super.onWindowFocus(); + // See [onWindowBlur]. + if (Platform.isWindows) { + _isWindowBlur = false; + } + } + + @override + void onWindowRestore() { + super.onWindowRestore(); + // On windows, we use `onWindowRestore` way to handle window restore from + // a minimized state. + if (Platform.isWindows) { + _isWindowBlur = false; + } + } + + @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); + _ffi.dialogManager.hideMobileActionsOverlay(); + _ffi.recordingModel.onClose(); + _rawKeyFocusNode.dispose(); + _ffi.close(); + _timer?.cancel(); + _ffi.dialogManager.dismissAll(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + if (!Platform.isLinux) { + Wakelock.disable(); + } + Get.delete(tag: widget.id); + super.dispose(); + _removeStates(widget.id); + } + + Widget buildBody(BuildContext context) { + return Scaffold( + 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 + Widget build(BuildContext context) { + super.build(context); + return WillPopScope( + onWillPop: () async { + clientClose(widget.id, _ffi.dialogManager); + return false; + }, + child: MultiProvider(providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ChangeNotifierProvider.value(value: _ffi.recordingModel), + ], child: buildBody(context))); + } + + void enterView(PointerEnterEvent evt) { + _cursorOverImage.value = true; + _firstEnterImage.value = true; + if (_onEnterOrLeaveImage4Menubar != null) { + try { + _onEnterOrLeaveImage4Menubar!(true); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!Platform.isWindows) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + bind.sessionEnterOrLeave(id: widget.id, enter: true); + } + } + + void leaveView(PointerExitEvent evt) { + _cursorOverImage.value = false; + _firstEnterImage.value = false; + if (_onEnterOrLeaveImage4Menubar != null) { + try { + _onEnterOrLeaveImage4Menubar!(false); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!Platform.isWindows) { + bind.sessionEnterOrLeave(id: widget.id, enter: false); + } + } + + 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) { + bind.hostStopSystemKeyPropagate(stopped: false); + }, onExit: (evt) { + bind.hostStopSystemKeyPropagate(stopped: true); + }, child: LayoutBuilder(builder: (context, constraints) { + Future.delayed(Duration.zero, () { + Provider.of(context, listen: false).updateViewStyle(); + }); + return ImagePaint( + id: widget.id, + zoomCursor: _zoomCursor, + cursorOverImage: _cursorOverImage, + keyboardEnabled: _keyboardEnabled, + remoteCursorMoved: _remoteCursorMoved, + textureId: _textureId, + useTextureRender: useTextureRender, + listenerBuilder: (child) => + _buildRawPointerMouseRegion(child, enterView, leaveView), + ); + })) + ]; + + if (!_ffi.canvasModel.cursorEmbedded) { + paints.add(Obx(() => Offstage( + offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse, + child: CursorPaint( + id: widget.id, + zoomCursor: _zoomCursor, + )))); + } + paints.add( + Positioned( + top: 10, + right: 10, + child: _buildRawPointerMouseRegion( + QualityMonitor(_ffi.qualityMonitorModel), null, null), + ), + ); + return Stack( + children: paints, + ); + } + + @override + bool get wantKeepAlive => true; +} + +class ImagePaint extends StatefulWidget { + final String id; + final RxBool zoomCursor; + final RxBool cursorOverImage; + final RxBool keyboardEnabled; + final RxBool remoteCursorMoved; + final RxInt textureId; + final bool useTextureRender; + final Widget Function(Widget)? listenerBuilder; + + ImagePaint( + {Key? key, + required this.id, + required this.zoomCursor, + required this.cursorOverImage, + required this.keyboardEnabled, + required this.remoteCursorMoved, + required this.textureId, + required this.useTextureRender, + this.listenerBuilder}) + : super(key: key); + + @override + State createState() => _ImagePaintState(); +} + +class _ImagePaintState extends State { + bool _lastRemoteCursorMoved = false; + final ScrollController _horizontal = ScrollController(); + final ScrollController _vertical = ScrollController(); + + String get id => widget.id; + 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 + Widget build(BuildContext context) { + final m = Provider.of(context); + var c = Provider.of(context); + final s = c.scale; + + 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); + 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) { + final percentX = _horizontal.hasClients + ? _horizontal.position.extentBefore / + (_horizontal.position.extentBefore + + _horizontal.position.extentInside + + _horizontal.position.extentAfter) + : 0.0; + final percentY = _vertical.hasClients + ? _vertical.position.extentBefore / + (_vertical.position.extentBefore + + _vertical.position.extentInside + + _vertical.position.extentAfter) + : 0.0; + c.setScrollPercent(percentX, percentY); + return false; + }, + child: mouseRegion( + child: Obx(() => _buildCrossScrollbarFromLayout( + context, _buildListener(imageWidget), c.size, imageSize)), + )); + } else { + 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(); + } + } + } + + MouseCursor _buildCursorOfCache( + CursorModel cursor, double scale, CursorData? cache) { + if (cache == null) { + return MouseCursor.defer; + } else { + final key = cache.updateGetKey(scale); + if (!cursor.cachedKeys.contains(key)) { + debugPrint("Register custom cursor with key $key"); + // [Safety] + // It's ok to call async registerCursor in current synchronous context, + // because activating the cursor is also an async call and will always + // be executed after this. + custom_cursor_manager.CursorManager.instance + .registerCursor(custom_cursor_manager.CursorData() + ..buffer = cache.data! + ..height = (cache.height * cache.scale).toInt() + ..width = (cache.width * cache.scale).toInt() + ..hotX = cache.hotx + ..hotY = cache.hoty + ..name = key); + cursor.addKey(key); + } + return FlutterCustomMemoryImageCursor(key: key); + } + } + + MouseCursor _buildCustomCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = cursor.cache ?? preDefaultCursor.cache; + return _buildCursorOfCache(cursor, scale, cache); + } + + MouseCursor _buildDisabledCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = preForbiddenCursor.cache; + return _buildCursorOfCache(cursor, scale, cache); + } + + Widget _buildCrossScrollbarFromLayout( + BuildContext context, Widget child, Size layoutSize, Size size) { + final scrollConfig = CustomMouseWheelScrollConfig( + scrollDuration: kDefaultScrollDuration, + scrollCurve: Curves.linearToEaseOut, + mouseWheelTurnsThrottleTimeMs: + kDefaultMouseWheelThrottleDuration.inMilliseconds, + scrollAmountMultiplier: kDefaultScrollAmountMultiplier); + var widget = child; + if (layoutSize.width < size.width) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: _horizontal, + scrollDirection: Axis.horizontal, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Row( + children: [ + Container( + width: ((layoutSize.width - size.width) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.height < size.height) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: _vertical, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Column( + children: [ + Container( + height: ((layoutSize.height - size.height) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.width < size.width) { + widget = ImprovedScrolling( + scrollController: _horizontal, + enableCustomMouseWheelScrolling: cursorOverImage.isFalse, + customMouseWheelScrollConfig: scrollConfig, + child: RawScrollbar( + thumbColor: Colors.grey, + controller: _horizontal, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: layoutSize.height < size.height + ? (notification) => notification.depth == 1 + : defaultScrollNotificationPredicate, + child: widget, + ), + ); + } + if (layoutSize.height < size.height) { + widget = ImprovedScrolling( + scrollController: _vertical, + enableCustomMouseWheelScrolling: cursorOverImage.isFalse, + customMouseWheelScrollConfig: scrollConfig, + child: RawScrollbar( + thumbColor: Colors.grey, + controller: _vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, + ), + ); + } + + return widget; + } + + Widget _buildListener(Widget child) { + if (listenerBuilder != null) { + return listenerBuilder!(child); + } else { + return child; + } + } +} + +class CursorPaint extends StatelessWidget { + final String id; + final RxBool zoomCursor; + + const CursorPaint({ + Key? key, + required this.id, + required this.zoomCursor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + final c = Provider.of(context); + double hotx = m.hotx; + double hoty = m.hoty; + if (m.image == null) { + if (preDefaultCursor.image != null) { + hotx = preDefaultCursor.image!.width / 2; + hoty = preDefaultCursor.image!.height / 2; + } + } + + double cx = c.x; + double cy = c.y; + if (c.viewStyle.style == kRemoteViewStyleOriginal && + c.scrollStyle == ScrollStyle.scrollbar) { + final d = c.parent.target!.ffiModel.display; + final imageWidth = d.width * c.scale; + final imageHeight = d.height * c.scale; + cx = -imageWidth * c.scrollX; + cy = -imageHeight * c.scrollY; + } + + double x = (m.x - hotx) * c.scale + cx; + double y = (m.y - hoty) * c.scale + cy; + double scale = 1.0; + 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; + } + + return CustomPaint( + painter: ImagePainter( + image: m.image ?? preDefaultCursor.image, + x: x, + y: y, + scale: scale, + ), + ); + } +} diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart new file mode 100644 index 000000000..0deb646c0 --- /dev/null +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -0,0 +1,333 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/remote_page.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_menubar.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/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:bot_toast/bot_toast.dart'; + +import '../../models/platform_model.dart'; + +class _MenuTheme { + 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; +} + +class ConnectionTabPage extends StatefulWidget { + final Map params; + + const ConnectionTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _ConnectionTabPageState(params); +} + +class _ConnectionTabPageState extends State { + 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; + + late MenubarState _menubarState; + + var connectionMap = RxList.empty(growable: true); + + _ConnectionTabPageState(Map params) { + _menubarState = MenubarState(); + RemoteCountState.init(); + 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, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(peerId), + page: RemotePage( + key: ValueKey(peerId), + id: peerId, + menubarState: _menubarState, + switchUuid: params['switch_uuid'], + forceRelay: params['forceRelay'], + ), + )); + _update_remote_count(); + } + } + + @override + void initState() { + super.initState(); + + tabController.onRemoved = (_, id) => onRemoveId(id); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + + // for simplify, just replace connectionId + 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( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(id), + page: RemotePage( + 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) { + reloadCurrentWindow(); + } + _update_remote_count(); + }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId()); + }); + } + + @override + void dispose() { + super.dispose(); + _menubarState.save(); + } + + @override + Widget build(BuildContext context) { + final tabWidget = Obx( + () => Container( + decoration: BoxDecoration( + border: Border.all( + color: MyTheme.color(context).border!, + width: stateGlobal.windowBorderWidth.value), + ), + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton().paddingOnly(left: 10), + pageViewBuilder: (pageView) => pageView, + labelGetter: DesktopTab.labelGetterAlias, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + final msgDirect = translate( + connectionType.direct.value == ConnectionType.strDirect + ? 'Direct Connection' + : 'Relay Connection'); + final msgSecure = translate( + connectionType.secure.value == ConnectionType.strSecure + ? 'Secure Connection' + : 'Insecure Connection'); + final tab = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgDirect\n$msgSecure', + child: SvgPicture.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + ], + ); + + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + if (e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return _tabMenuBuilder(key, cancelFunc); + }, + target: e.position, + ); + } + }, + child: tab, + ); + } + }), + ), + ), + ), + ); + return Platform.isMacOS + ? tabWidget + : Obx(() => SubWindowDragToResizeArea( + key: contentKey, + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + windowId: stateGlobal.windowId, + )); + } + + // Note: Some dup code to ../widgets/remote_menubar + Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + final remotePage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as RemotePage; + final ffi = remotePage.ffi; + final pi = ffi.ffiModel.pi; + final perms = ffi.ffiModel.permissions; + menu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Close'), + style: style, + ), + proc: () { + tabController.closeBy(key); + cancelFunc(); + }, + padding: padding, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Obx(() => Text( + translate( + _menubarState.show.isTrue ? 'Hide Menubar' : 'Show Menubar'), + style: style, + )), + proc: () { + _menubarState.switchShow(); + cancelFunc(); + }, + padding: padding, + ), + MenuEntryDivider(), + RemoteMenuEntry.viewStyle( + key, + ffi, + padding, + dismissFunc: cancelFunc, + ), + ]); + + if (!ffi.canvasModel.cursorEmbedded) { + menu.add(MenuEntryDivider()); + menu.add(RemoteMenuEntry.showRemoteCursor( + key, + padding, + dismissFunc: cancelFunc, + )); + } + + if (perms['keyboard'] != false) { + if (perms['clipboard'] != false) { + menu.add(RemoteMenuEntry.disableClipboard(key, padding, + dismissFunc: cancelFunc)); + } + + menu.add( + RemoteMenuEntry.insertLock(key, padding, dismissFunc: cancelFunc)); + + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { + menu.add(RemoteMenuEntry.insertCtrlAltDel(key, padding, + dismissFunc: cancelFunc)); + } + } + + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenuTheme.blueColor, + height: _MenuTheme.height, + dividerHeight: _MenuTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + void onRemoveId(String id) async { + if (tabController.state.value.tabs.isEmpty) { + await WindowController.fromWindowId(windowId()).close(); + } + ConnectionTypeState.delete(id); + _update_remote_count(); + } + + int windowId() { + return widget.params["windowId"]; + } + + Future handleWindowCloseButton() async { + final connLength = tabController.length; + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final opt = "enable-confirm-closing-tabs"; + final bool res; + if (!option2bool(opt, await bind.mainGetOption(key: opt))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } + + _update_remote_count() => + RemoteCountState.find().value = tabController.length; +} diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart new file mode 100644 index 000000000..45591b79b --- /dev/null +++ b/flutter/lib/desktop/pages/server_page.dart @@ -0,0 +1,746 @@ +// 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'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../../common.dart'; +import '../../common/widgets/chat_page.dart'; +import '../../models/platform_model.dart'; +import '../../models/server_model.dart'; + +class DesktopServerPage extends StatefulWidget { + const DesktopServerPage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopServerPageState(); +} + +class _DesktopServerPageState extends State + with WindowListener, AutomaticKeepAliveClientMixin { + final tabController = gFFI.serverModel.tabController; + @override + void initState() { + gFFI.ffiModel.updateEventListener(""); + windowManager.addListener(this); + tabController.onRemoved = (_, id) { + onRemoveId(id); + }; + tabController.onSelected = (_, id) { + windowManager.setTitle(getWindowNameWithId(id)); + }; + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowClose() { + Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) { + if (Platform.isMacOS) { + RdPlatformChannel.instance.terminate(); + } else { + windowManager.setPreventClose(false); + windowManager.close(); + } + }); + super.onWindowClose(); + } + + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + windowManager.close(); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.serverModel), + ChangeNotifierProvider.value(value: gFFI.chatModel), + ], + child: Consumer( + builder: (context, serverModel, child) => Container( + 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 + bool get wantKeepAlive => true; +} + +class ConnectionManager extends StatefulWidget { + @override + State createState() => ConnectionManagerState(); +} + +class ConnectionManagerState extends State { + @override + void initState() { + gFFI.serverModel.updateClientState(); + gFFI.serverModel.tabController.onSelected = (index, _) => + gFFI.chatModel.changeCurrentID(gFFI.serverModel.clients[index].id); + gFFI.chatModel.isConnManager = true; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final serverModel = Provider.of(context); + pointerHandler(PointerEvent e) { + if (serverModel.cmHiddenTimer != null) { + serverModel.cmHiddenTimer!.cancel(); + serverModel.cmHiddenTimer = null; + debugPrint("CM hidden timer has been canceled"); + } + } + + return serverModel.clients.isEmpty + ? Column( + children: [ + buildTitleBar(), + Expanded( + child: Center( + child: Text(translate("Waiting")), + ), + ), + ], + ) + : Listener( + onPointerDown: pointerHandler, + onPointerMove: pointerHandler, + child: DesktopTab( + showTitle: false, + showMaximize: false, + showMinimize: true, + showClose: true, + onWindowCloseButton: handleWindowCloseButton, + controller: serverModel.tabController, + maxLabelWidth: 100, + tail: buildScrollJumper(), + selectedTabBackgroundColor: + Theme.of(context).hintColor.withOpacity(0.2), + tabBuilder: (key, icon, label, themeConf) { + final client = serverModel.clients.firstWhereOrNull( + (client) => client.id.toString() == key); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Tooltip( + message: key, + waitDuration: Duration(seconds: 1), + child: label), + Obx(() => Offstage( + offstage: + !(client?.hasUnreadChatMessage.value ?? false), + child: + Icon(Icons.circle, color: Colors.red, size: 10))) + ], + ); + }, + pageViewBuilder: (pageView) => Row(children: [ + Expanded(child: pageView), + Consumer( + builder: (_, model, child) => model.isShowCMChatPage + ? Expanded(child: Scaffold(body: ChatPage())) + : Offstage()) + ]))); + } + + Widget buildTitleBar() { + return SizedBox( + height: kDesktopRemoteTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const _AppIcon(), + Expanded( + child: GestureDetector( + onPanStart: (d) { + windowManager.startDragging(); + }, + child: Container( + color: Theme.of(context).colorScheme.background, + ), + ), + ), + const SizedBox( + width: 4.0, + ), + const _CloseButton() + ], + ), + ); + } + + Widget buildScrollJumper() { + final offstage = gFFI.serverModel.clients.length < 2; + final sc = gFFI.serverModel.tabController.state.value.scrollController; + return Offstage( + offstage: offstage, + child: Row( + children: [ + ActionIcon( + icon: Icons.arrow_left, iconSize: 22, onTap: sc.backward), + ActionIcon( + icon: Icons.arrow_right, iconSize: 22, onTap: sc.forward), + ], + )); + } + + Future handleWindowCloseButton() async { + var tabController = gFFI.serverModel.tabController; + final connLength = tabController.length; + if (connLength <= 1) { + windowManager.close(); + return true; + } else { + final opt = "enable-confirm-closing-tabs"; + final bool res; + if (!option2bool(opt, await bind.mainGetOption(key: opt))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + windowManager.close(); + } + return res; + } + } +} + +Widget buildConnectionCard(Client client) { + return Consumer( + builder: (context, value, child) => Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + key: ValueKey(client.id), + children: [ + _CmHeader(client: client), + client.type_() != ClientType.remote || client.disconnected + ? Offstage() + : _PrivilegeBoard(client: client), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: _CmControlPanel(client: client), + )) + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 8.0)); +} + +class _AppIcon extends StatelessWidget { + const _AppIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.0), + child: SvgPicture.asset( + 'assets/logo.svg', + width: 30, + height: 30, + ), + ); + } +} + +class _CloseButton extends StatelessWidget { + const _CloseButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () { + windowManager.close(); + }, + icon: const Icon( + IconFont.close, + size: 18, + ), + splashColor: Colors.transparent, + hoverColor: Colors.transparent, + ); + } +} + +class _CmHeader extends StatefulWidget { + final Client client; + + const _CmHeader({Key? key, required this.client}) : super(key: key); + + @override + State<_CmHeader> createState() => _CmHeaderState(); +} + +class _CmHeaderState extends State<_CmHeader> + with AutomaticKeepAliveClientMixin { + Client get client => widget.client; + + final _time = 0.obs; + Timer? _timer; + + @override + void initState() { + super.initState(); + _timer = Timer.periodic(Duration(seconds: 1), (_) { + if (client.authorized && !client.disconnected) { + _time.value = _time.value + 1; + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // icon + Container( + width: 90, + height: 90, + alignment: Alignment.center, + decoration: BoxDecoration(color: str2color(client.name)), + child: Text( + client.name[0], + style: TextStyle( + fontWeight: FontWeight.bold, color: Colors.white, fontSize: 65), + ), + ).marginOnly(left: 4.0, right: 8.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + child: Text( + client.name, + style: TextStyle( + color: MyTheme.cmIdColor, + fontWeight: FontWeight.bold, + fontSize: 20, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + )), + FittedBox( + child: Text("(${client.peerId})", + style: + TextStyle(color: MyTheme.cmIdColor, fontSize: 14))), + SizedBox( + height: 16.0, + ), + FittedBox( + child: Row( + children: [ + Text(client.authorized + ? client.disconnected + ? translate("Disconnected") + : translate("Connected") + : "${translate("Request access to your device")}...") + .marginOnly(right: 8.0), + if (client.authorized) + Obx(() => Text( + formatDurationToTime(Duration(seconds: _time.value)))) + ], + )) + ], + ), + ), + Offstage( + offstage: !client.authorized || client.type_() != ClientType.remote, + child: IconButton( + onPressed: () => checkClickTime( + client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)), + icon: Icon(Icons.message_outlined), + splashRadius: kDesktopIconButtonSplashRadius), + ) + ], + ); + } + + @override + bool get wantKeepAlive => true; +} + +class _PrivilegeBoard extends StatefulWidget { + final Client client; + + const _PrivilegeBoard({Key? key, required this.client}) : super(key: key); + + @override + State createState() => _PrivilegeBoardState(); +} + +class _PrivilegeBoardState extends State<_PrivilegeBoard> { + late final client = widget.client; + Widget buildPermissionIcon( + bool enabled, ImageProvider icon, Function(bool)? onTap, String tooltip) { + return Tooltip( + message: tooltip, + child: Ink( + decoration: + BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), + padding: EdgeInsets.all(4.0), + child: InkWell( + onTap: () => + checkClickTime(widget.client.id, () => onTap?.call(!enabled)), + child: Image( + image: icon, + width: 50, + height: 50, + fit: BoxFit.scaleDown, + ), + ), + ).marginSymmetric(horizontal: 4.0), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("Permissions"), + style: TextStyle(fontSize: 16), + ).marginOnly(left: 4.0), + SizedBox( + height: 8.0, + ), + FittedBox( + child: Row( + children: [ + buildPermissionIcon(client.keyboard, iconKeyboard, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "keyboard", enabled: enabled); + setState(() { + client.keyboard = enabled; + }); + }, translate('Allow using keyboard and mouse')), + buildPermissionIcon(client.clipboard, iconClipboard, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "clipboard", enabled: enabled); + setState(() { + client.clipboard = enabled; + }); + }, translate('Allow using clipboard')), + buildPermissionIcon(client.audio, iconAudio, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "audio", enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, translate('Allow hearing sound')), + buildPermissionIcon(client.file, iconFile, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "file", enabled: enabled); + setState(() { + client.file = enabled; + }); + }, translate('Allow file copy and paste')), + buildPermissionIcon(client.restart, iconRestart, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "restart", enabled: enabled); + setState(() { + client.restart = enabled; + }); + }, translate('Allow remote restart')), + buildPermissionIcon(client.recording, iconRecording, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "recording", enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, translate('Allow recording session')) + ], + )), + ], + ), + ); + } +} + +const double bigMargin = 15; + +class _CmControlPanel extends StatelessWidget { + final Client client; + + const _CmControlPanel({Key? key, required this.client}) : super(key: key); + + @override + Widget build(BuildContext context) { + return client.authorized + ? client.disconnected + ? buildDisconnected(context) + : buildAuthorized(context) + : buildUnAuthorized(context); + } + + buildAuthorized(BuildContext context) { + final bool canElevate = bind.cmCanElevate(); + final model = Provider.of(context); + final showElevation = canElevate && + model.showElevation && + client.type_() == ClientType.remote; + 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: () { + handleElevate(context); + windowManager.minimize(); + }, + icon: Icon( + Icons.security_sharp, + color: Colors.white, + ), + text: 'Elevate', + textColor: Colors.white), + ), + Row( + children: [ + Expanded( + child: buildButton(context, + color: Colors.redAccent, + onClick: handleDisconnect, + text: 'Disconnect', + textColor: Colors.white)), + ], + ) + ], + ) + .marginOnly(bottom: showElevation ? 0 : bigMargin) + .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); + } + + buildDisconnected(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: buildButton(context, + color: MyTheme.accent, + onClick: handleClose, + text: 'Close', + textColor: Colors.white)), + ], + ).marginOnly(bottom: 15).marginSymmetric(horizontal: bigMargin); + } + + buildUnAuthorized(BuildContext context) { + final bool canElevate = bind.cmCanElevate(); + final model = Provider.of(context); + final showElevation = canElevate && + model.showElevation && + client.type_() == ClientType.remote; + final showAccept = model.approveMode != 'password'; + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Offstage( + offstage: !showElevation || !showAccept, + child: buildButton(context, color: Colors.green[700], onClick: () { + handleAccept(context); + handleElevate(context); + windowManager.minimize(); + }, + text: 'Accept', + icon: Icon( + Icons.security_sharp, + color: Colors.white, + ), + textColor: Colors.white), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (showAccept) + Expanded( + child: Column( + children: [ + buildButton(context, color: MyTheme.accent, onClick: () { + handleAccept(context); + windowManager.minimize(); + }, text: 'Accept', textColor: Colors.white), + ], + ), + ), + Expanded( + child: buildButton(context, + color: Colors.transparent, + border: Border.all(color: Colors.grey), + onClick: handleDisconnect, + text: 'Cancel', + textColor: null)), + ], + ), + ], + ) + .marginOnly(bottom: showElevation ? 0 : bigMargin) + .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); + } + + Widget buildButton( + BuildContext context, { + required Color? color, + required Function() onClick, + Icon? icon, + BoxBorder? border, + required String text, + required Color? textColor, + }) { + Widget textWidget; + if (icon != null) { + textWidget = Text( + translate(text), + style: TextStyle(color: textColor), + textAlign: TextAlign.center, + ); + } else { + textWidget = Expanded( + child: Text( + translate(text), + style: TextStyle(color: textColor), + textAlign: TextAlign.center, + ), + ); + } + return Container( + height: 30, + decoration: BoxDecoration( + color: color, borderRadius: BorderRadius.circular(4), border: border), + child: InkWell( + onTap: () => checkClickTime(client.id, onClick), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Offstage(offstage: icon == null, child: icon), + textWidget, + ], + )), + ).marginAll(4); + } + + void handleDisconnect() { + bind.cmCloseConnection(connId: client.id); + } + + void handleAccept(BuildContext context) { + final model = Provider.of(context, listen: false); + model.sendLoginResponse(client, true); + } + + void handleElevate(BuildContext context) { + final model = Provider.of(context, listen: false); + model.setShowElevation(false); + bind.cmElevatePortable(connId: client.id); + } + + void handleClose() async { + await bind.cmRemoveDisconnectedConnection(connId: client.id); + if (await bind.cmGetClientsLength() == 0) { + 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 { + var clickCallbackTime = DateTime.now().millisecondsSinceEpoch; + await bind.cmCheckClickTime(connId: id); + Timer(const Duration(milliseconds: 120), () async { + var d = clickCallbackTime - await bind.cmGetClickTime(); + if (d > 120) callback(); + }); +} diff --git a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart new file mode 100644 index 000000000..74764a803 --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/file_manager_tab_page.dart'; +import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab file transfer remote screen +class DesktopFileTransferScreen extends StatelessWidget { + final Map params; + + const DesktopFileTransferScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ], + child: Scaffold( + body: FileManagerTabPage( + params: params, + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/screen/desktop_port_forward_screen.dart b/flutter/lib/desktop/screen/desktop_port_forward_screen.dart new file mode 100644 index 000000000..5cec10f86 --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_port_forward_screen.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/port_forward_tab_page.dart'; +import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab file port forward screen +class DesktopPortForwardScreen extends StatelessWidget { + final Map params; + + const DesktopPortForwardScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ], + child: Scaffold( + body: PortForwardTabPage( + params: params, + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart new file mode 100644 index 000000000..bb6bc431b --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -0,0 +1,34 @@ +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 +class DesktopRemoteScreen extends StatelessWidget { + final Map params; + + DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) { + if (!bind.mainStartGrabKeyboard()) { + stateGlobal.grabKeyboard = true; + } + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ], + child: Scaffold( + body: ConnectionTabPage( + params: params, + ), + )); + } +} diff --git a/flutter/lib/desktop/widgets/button.dart b/flutter/lib/desktop/widgets/button.dart new file mode 100644 index 000000000..0c09f7c77 --- /dev/null +++ b/flutter/lib/desktop/widgets/button.dart @@ -0,0 +1,171 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class Button extends StatefulWidget { + final GestureTapCallback onTap; + final String text; + final double? textSize; + final double? minWidth; + final bool isOutline; + final double? padding; + final Color? textColor; + final double? radius; + final Color? borderColor; + + Button({ + Key? key, + this.minWidth, + this.isOutline = false, + this.textSize, + this.padding, + this.textColor, + this.radius, + this.borderColor, + required this.onTap, + required this.text, + }) : super(key: key); + + @override + State} - {auth ? "" : } - {auth ? : ""} +
+ {!auth && !disconnected && show_elevation_btn && show_accept_btn ? : "" } + {auth && !disconnected && show_elevation_btn ? : "" } +
+ {!auth && show_accept_btn ? : "" } + {!auth ? : "" } +
+ {auth && !disconnected ? : "" } + {auth && disconnected ? : "" }
{c.is_file_transfer || c.port_forward ? "" :
{svg_chat}
} @@ -118,6 +134,15 @@ class Body: Reactor.Component }); } + event click $(icon.recording) { + var { cid, connection } = this; + checkClickTime(function() { + connection.recording = !connection.recording; + body.update(); + handler.switch_permission(cid, "recording", connection.recording); + }); + } + event click $(button#accept) { var { cid, connection } = this; checkClickTime(function() { @@ -130,6 +155,32 @@ class Body: Reactor.Component }); } + event click $(button#elevate_accept) { + var { cid, connection } = this; + checkClickTime(function() { + connection.authorized = true; + show_elevation = false; + body.update(); + handler.elevate_portable(cid); + handler.authorize(cid); + self.timer(30ms, function() { + view.windowState = View.WINDOW_MINIMIZED; + }); + }); + } + + event click $(button#elevate) { + var { cid, connection } = this; + checkClickTime(function() { + show_elevation = false; + body.update(); + handler.elevate_portable(cid); + self.timer(30ms, function() { + view.windowState = View.WINDOW_MINIMIZED; + }); + }); + } + event click $(button#dismiss) { var cid = this.cid; checkClickTime(function() { @@ -143,6 +194,25 @@ class Body: Reactor.Component handler.close(cid); }); } + + event click $(button#close) { + var cid = this.cid; + if (this.cur >= 0 && this.cur < connections.length){ + handler.remove_disconnected_connection(cid); + connections.splice(this.cur, 1); + if (connections.length > 0) { + if (this.cur > 0) + this.cur -= 1; + else + this.cur = connections.length - 1; + header.update(); + body.update(); + } else { + handler.quit(); + } + } + + } } $(body).content(); @@ -276,7 +346,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart) { +handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -287,15 +357,26 @@ handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, na update(); return; } + var idx = -1; + connections.map(function(c, i) { + if (c.disconnected && c.peer_id == peer_id) idx = i; + }); if (!name) name = "NA"; - connections.push({ + conn = { id: id, is_file_transfer: is_file_transfer, peer_id: peer_id, port_forward: port_forward, - name: name, authorized: authorized, time: new Date(), + name: name, authorized: authorized, time: new Date(), now: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, - audio: audio, file: file, restart: restart - }); - body.cur = connections.length - 1; + audio: audio, file: file, restart: restart, recording: recording, + disconnected: false + }; + if (idx < 0) { + connections.push(conn); + body.cur = connections.length - 1; + } else { + connections[idx] = conn; + body.cur = idx; + } bring_to_top(); update(); self.timer(1ms, adjustHeader); @@ -306,15 +387,20 @@ handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, na } } -handler.removeConnection = function(id) { +handler.removeConnection = function(id, close) { var i = -1; connections.map(function(c, idx) { if (c.id == id) i = idx; }); if (i < 0) return; - connections.splice(i, 1); + if (close) { + connections.splice(i, 1); + } else { + var conn = connections[i]; + conn.disconnected = true; + } if (connections.length > 0) { - if (body.cur >= i && body.cur > 0) body.cur -= 1; + if (body.cur >= i && body.cur > 0 && close) body.cur -= 1; update(); } } @@ -337,6 +423,13 @@ handler.newMessage = function(id, text) { update(); } +handler.showElevation = function(show) { + if (show != show_elevation) { + show_elevation = show; + update(); + } +} + view << event statechange { adjustBorder(); } @@ -349,8 +442,7 @@ function self.ready() { view.move(sw - w, 0, w, h); } -function getElaspsed(time) { - var now = new Date(); +function getElapsed(time, now) { var seconds = Date.diff(time, now, #seconds); var hours = seconds / 3600; var days = hours / 24; @@ -364,13 +456,33 @@ function getElaspsed(time) { return out; } +var ui_status_cache = [""]; +function check_update_ui() { + self.timer(1s, function() { + var approve_mode = handler.get_option('approve-mode'); + var changed = false; + if (ui_status_cache[0] != approve_mode) { + ui_status_cache[0] = approve_mode; + changed = true; + } + if (changed) update(); + check_update_ui(); + }); +} +check_update_ui(); + function updateTime() { self.timer(1s, function() { + var now = new Date(); + connections.map(function(c) { + if (!c.authorized) c.time = now; + if (!c.disconnected) c.now = now; + }); var el = $(#time); if (el) { var c = connections[body.cur]; - if (c) { - el.text = getElaspsed(c.time); + if (c && c.authorized && !c.disconnected) { + el.text = getElapsed(c.time, c.now); } } updateTime(); diff --git a/src/ui/common.css b/src/ui/common.css index c3f3706ef..0fb9afcb1 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -56,7 +56,7 @@ button[type=checkbox], button[type=checkbox]:active { button.outline { border: color(border) solid 1px; - background: transparent; + background: transparent; color: color(text); } @@ -70,6 +70,15 @@ button.button:hover, button.outline:hover { border-color: color(hover-border); } +button.link { + background: none !important; + border: none; + padding: 0 !important; + color: color(button); + text-decoration: underline; + cursor: pointer; +} + input[type=text], input[type=password], input[type=number] { width: *; font-size: 1.5em; @@ -106,7 +115,7 @@ textarea:empty { .base:disabled { background: transparent; } .slider:hover { background: grey; } .slider:active { background: grey; } - .base { size: 16px; } + .base { size: 16px; } .corner { background: white; } } @@ -176,7 +185,7 @@ header div.window-icon icon { header caption { size: *; -} +} @media platform != "OSX" { button.window { diff --git a/src/ui/common.tis b/src/ui/common.tis index aae950c2d..b6723b131 100644 --- a/src/ui/common.tis +++ b/src/ui/common.tis @@ -155,6 +155,7 @@ var svg_send = var svg_chat = ; +var svg_keyboard = ; function scrollToBottom(el) { var y = el.box(#height, #content) - el.box(#height, #client); @@ -231,7 +232,8 @@ class ChatBox: Reactor.Component { /******************** start of msgbox ****************************************/ var remember_password = false; -function msgbox(type, title, content, callback=null, height=180, width=500, hasRetry=false, contentStyle="") { +var last_msgbox_tag = ""; +function msgbox(type, title, content, link="", callback=null, height=180, width=500, hasRetry=false, contentStyle="") { $(body).scrollTo(0, 0); if (!type) { closeMsgbox(); @@ -262,22 +264,36 @@ function msgbox(type, title, content, callback=null, height=180, width=500, hasR }; } else if (type.indexOf("custom") < 0 && !is_port_forward && !callback) { callback = function() { view.close(); } + } else if (type == 'wait-remote-accept-nook') { + callback = function (res) { + if (!res) { + view.close(); + return; + } + }; } - $(#msgbox).content(); + last_msgbox_tag = type + "-" + title + "-" + content + "-" + link; + $(#msgbox).content(); } function connecting() { handler.msgbox("connecting", "Connecting...", "Connection in progress. Please wait."); } -handler.msgbox = function(type, title, text, hasRetry=false) { +handler.msgbox = function(type, title, text, link = "", hasRetry=false) { // crash somehow (when input wrong password), even with small time, for example, 1ms - self.timer(60ms, function() { msgbox(type, title, text, null, 180, 500, hasRetry); }); + self.timer(60ms, function() { msgbox(type, title, text, link, null, 180, 500, hasRetry); }); +} + +handler.cancel_msgbox = function(tag) { + if (last_msgbox_tag == tag) { + closeMsgbox(); + } } var reconnectTimeout = 1000; -handler.msgbox_retry = function(type, title, text, hasRetry) { - handler.msgbox(type, title, text, hasRetry); +handler.msgbox_retry = function(type, title, text, link, hasRetry) { + handler.msgbox(type, title, text, link, hasRetry); if (hasRetry) { self.timer(0, retryConnect); self.timer(reconnectTimeout, retryConnect); diff --git a/src/ui/file_transfer.css b/src/ui/file_transfer.css index 9b45ea2b7..7fd4ac7a8 100644 --- a/src/ui/file_transfer.css +++ b/src/ui/file_transfer.css @@ -12,22 +12,22 @@ div#file-transfer { } table -{ +{ font: system; border: 1px solid color(border); flow: table-fixed; prototype: Grid; size: *; padding:0; - border-spacing: 0; + border-spacing: 0; overflow-x: auto; overflow-y: hidden; } - -table > thead { + +table > thead { behavior: column-resizer; border-bottom: color(border) solid 1px; -} +} table > tbody { behavior: select-multiple; @@ -41,20 +41,20 @@ table th { } table th -{ +{ padding: 4px; foreground-repeat: no-repeat; foreground-position: 50% 3px auto auto; border-left: color(border) solid 1px; -} +} -table th.sortable[sort=asc] -{ +table th.sortable[sort=asc] +{ foreground-image: url(stock:arrow-down); -} +} table th.sortable[sort=desc] -{ +{ foreground-image: url(stock:arrow-up); } @@ -81,10 +81,10 @@ table.has_current thead th:current { table tr:nth-child(odd) { background-color: white; } /* each odd row */ table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */ -table.has_current tr:current /* current row */ -{ - background-color: color(accent); -} +table.has_current tr:current /* current row */ +{ + background-color: color(accent); +} table.has_current tbody tr:checked { @@ -95,9 +95,9 @@ table.has_current tbody tr:checked td { color: highlighttext; } -table td -{ - padding: 4px; +table td +{ + padding: 4px; text-align: left; font-size: 1em; height: 1.4em; @@ -124,11 +124,11 @@ table td:nth-child(4) { section { size: *; margin: 1em; - border-spacing: 0.5em; + border-spacing: 0.5em; } table td:nth-child(1) { - foreground-repeat: no-repeat; + foreground-repeat: no-repeat; foreground-position: 50% 50% } @@ -160,11 +160,11 @@ div.toolbar > div.button:hover { div.toolbar > div.send { flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; } div.remote > div.send svg { - transform: scale(-1, 1); + transform: scale(-1, 1); } div.navbar { @@ -207,7 +207,7 @@ table.job-table tr td { padding: 0.5em 1em; border-bottom: color(border) 1px solid; flow: horizontal; - border-spacing: 1em; + border-spacing: 1em; height: 3em; overflow-x: hidden; } @@ -217,11 +217,11 @@ table.job-table tr svg { } table.job-table tr.is_remote svg { - transform: scale(-1, 1); + transform: scale(-1, 1); } table.job-table tr.is_remote div.svg_continue svg { - transform: scale(1, 1); + transform: scale(1, 1); } table.job-table tr td div.text { @@ -246,7 +246,7 @@ table#port-forward thead tr th { table#port-forward tr td { height: 3em; - text-align: left; + text-align: left; } table#port-forward input[type=text], table#port-forward input[type=number] { diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 7d50bdf7a..f69f6d323 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -188,7 +188,8 @@ class JobTable: Reactor.Component { job.confirmed = true; return; }else if (job.type == "del-dir"){ - handler.remove_dir_all(job.id, job.path, job.is_remote); + // TODO: include_hidden is always true + handler.remove_dir_all(job.id, job.path, job.is_remote, true); job.confirmed = true; return; } @@ -244,7 +245,13 @@ class JobTable: Reactor.Component { var percent = job.total_size == 0 ? 100 : (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger(); if (job.finished) percent = '100'; if (percent) res += ", " + percent + "%"; - if (job.finished) res = translate("Finished") + " " + res; + if (job.finished) { + if (job.err == "skipped") { + res = translate("Skipped") + " " + res; + } else { + res = translate("Finished") + " " + res; + } + } if (job.speed) res += ", " + getSize(0, job.speed) + "/s"; return res; } @@ -267,9 +274,10 @@ class JobTable: Reactor.Component { if (file_num < job.file_num) return; job.file_num = file_num; var n = job.num_entries || job.entries.length; - job.finished = job.file_num >= n - 1 || err == "cancel"; + job.finished = job.file_num >= n - 1 || err == "cancel" || err == "skipped"; job.finished_size = finished_size; job.speed = speed || 0; + job.err = err; this.updateJob(job); if (job.type == "del-dir") { if (job.finished) { @@ -534,7 +542,7 @@ class FolderView : Reactor.Component { msgbox("custom", translate("Create Folder"), "
\
" + translate("Please enter the folder name") + ":
\
\ -
", function(res=null) { + ", "", function(res=null) { if (!res) return; if (!res.name) return; var name = res.name.trim(); @@ -694,7 +702,7 @@ handler.clearAllJobs = function() { file_transfer.job_table.clearAllJobs(); } -handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { +handler.addJob = function (id, path, to, file_num, show_hidden, is_remote) { // load last job // stdout.println("restore job: " + is_remote); file_transfer.job_table.addJob(id,path,to,file_num,show_hidden,is_remote); } @@ -715,7 +723,7 @@ function confirmDelete(id ,path, is_remote) { msgbox("custom-skip", "Confirm Delete", "
\
" + translate('Are you sure you want to delete this file?') + "
\ " + path + "
\ - ", function(res=null) { + ", "", function(res=null) { if (!res) { file_transfer.job_table.updateJobStatus(id, -1, "cancel"); file_transfer.job_table.cancelDeletePolling(); @@ -745,7 +753,7 @@ handler.confirmDeleteFiles = function(id, i, name) {
" + translate('Are you sure you want to delete this file?') + "
\ " + file_path + " \
" + translate('Do this for all conflicts') + "
\ - ", function(res=null) { + ", "", function(res=null) { if (!res) { jt.updateJobStatus(id, i - 1, "cancel"); file_transfer.job_table.cancelDeletePolling(); @@ -777,7 +785,7 @@ handler.overrideFileConfirm = function(id, file_num, to, is_upload) {
" + translate('This file exists, skip or overwrite this file?') + "
\ " + to + " \
" + translate('Do this for all conflicts') + "
\ - ", function(res=null) { + ", "", function(res=null) { if (!res) { jt.updateJobStatus(id, -1, "cancel"); handler.cancel_job(id); diff --git a/src/ui/header.css b/src/ui/header.css index e248b46d5..8fe408612 100644 --- a/src/ui/header.css +++ b/src/ui/header.css @@ -8,7 +8,7 @@ header #screens { height: 22px; border-radius: 4px; flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; padding-right: 1em; position: relative; } diff --git a/src/ui/header.tis b/src/ui/header.tis index 7ff160e6d..e25c0d544 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -14,6 +14,8 @@ var svg_secure = var svg_insecure = ; var svg_insecure_relay = ; var svg_secure_relay = ; +var svg_recording_off = ; +var svg_recording_on = ; var cur_window_state = view.windowState; function check_state_change() { @@ -76,7 +78,7 @@ class EditOsPassword: Reactor.Component { function editOSPassword(login=false) { var p0 = handler.get_option('os-password'); - msgbox("custom-os-password", 'OS Password', p0, function(res=null) { + msgbox("custom-os-password", 'OS Password', p0, "", function(res=null) { if (!res) return; var a0 = handler.get_option('auto-login') != ''; var p = (res.password || '').trim(); @@ -90,6 +92,8 @@ function editOSPassword(login=false) { }); } +var recording = false; + class Header: Reactor.Component { this var conn_note = ""; @@ -139,15 +143,34 @@ class Header: Reactor.Component { {svg_chat} {svg_action} {svg_display} + {svg_keyboard} + {recording_enabled ? {recording ? svg_recording_on : svg_recording_off} : ""} + {this.renderKeyboardPop()} {this.renderDisplayPop()} {this.renderActionPop()} ; } + function renderKeyboardPop(){ + return + +
  • {svg_checkmark}{translate('Legacy mode')}
  • +
  • {svg_checkmark}{translate('Map mode')}
  • + +
    ; + } + function renderDisplayPop() { var codecs = handler.supported_hwcodec(); var show_codec = handler.has_hwcodec() && (codecs[0] || codecs[1]); + var cursor_embedded = false; + if ((pi.displays || []).length > 0) { + if (pi.displays.length > pi.current_display) { + cursor_embedded = pi.displays[pi.current_display].cursor_embedded; + } + } + return
  • {translate('Adjust Window')}
  • @@ -168,7 +191,7 @@ class Header: Reactor.Component { {codecs[1] ?
  • {svg_checkmark}H265
  • : ""} : ""}
    -
  • {svg_checkmark}{translate('Show remote cursor')}
  • + {!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} {is_win && pi.platform == 'Windows' && file_enabled ?
  • {svg_checkmark}{translate('Allow file copy and paste')}
  • : ""} @@ -185,7 +208,7 @@ class Header: Reactor.Component { {keyboard_enabled ?
  • {translate('OS Password')}
  • : ""}
  • {translate('Transfer File')}
  • {translate('TCP Tunneling')}
  • - {handler.get_audit_server() &&
  • {translate('Note')}
  • } + {handler.get_audit_server("conn") &&
  • {translate('Note')}
  • }
    {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""} {restart_enabled && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS") ?
  • {translate('Restart Remote Device')}
  • : ""} @@ -263,6 +286,20 @@ class Header: Reactor.Component { me.popup(menu); } + event click $(#keyboard) (_, me) { + var menu = $(menu#keyboard-options); + me.popup(menu); + } + + event click $(span#recording) (_, me) { + recording = !recording; + header.update(); + if (recording) + handler.refresh_video(); + else + handler.record_screen(false, display_width, display_height); + } + event click $(#screen) (_, me) { if (pi.current_display == me.index) return; handler.switch_display(me.index); @@ -290,7 +327,7 @@ class Header: Reactor.Component { var self = this; msgbox("custom", "Note",
    -
    , function(res=null) { +
    , "", function(res=null) { if (!res) return; if (!res.text) return; self.conn_note = res.text; @@ -303,9 +340,15 @@ class Header: Reactor.Component { } event click $(#restart_remote_device) { - msgbox("restart-confirmation", translate("Restart Remote Device"), translate("Are you sure you want to restart") + " " + pi.username + "@" + pi.hostname + "(" + get_id() + ") ?", function(res=null) { - if (res != null) handler.restart_remote_device(); - }); + msgbox( + "restart-confirmation", + translate("Restart Remote Device"), + translate("Are you sure you want to restart") + " " + pi.username + "@" + pi.hostname + "(" + get_id() + ") ?", + "", + function(res=null) { + if (res != null) handler.restart_remote_device(); + } + ); } event click $(#lock-screen) { @@ -335,7 +378,7 @@ class Header: Reactor.Component { togglePrivacyMode(me.id); } else if (me.id == "show-quality-monitor") { toggleQualityMonitor(me.id); - }else if (me.attributes.hasClass("toggle-option")) { + } else if (me.attributes.hasClass("toggle-option")) { handler.toggle_option(me.id); toggleMenuState(); } else if (!me.attributes.hasClass("selected")) { @@ -352,6 +395,17 @@ class Header: Reactor.Component { toggleMenuState(); } } + + event click $(menu#keyboard-options>li) (_, me) { + if (me.id == "legacy") { + handler.save_keyboard_mode("legacy"); + } else if (me.id == "map") { + handler.save_keyboard_mode("map"); + } else if (me.id == "translate") { + handler.save_keyboard_mode("translate"); + } + toggleMenuState() + } } function handle_custom_image_quality() { @@ -359,7 +413,7 @@ function handle_custom_image_quality() { var bitrate = (tmp[0] || 50); msgbox("custom", "Custom Image Quality", "
    \
    x% Bitrate
    \ -
    ", function(res=null) { +
    ", "", function(res=null) { if (!res) return; if (!res.bitrate) return; handler.save_custom_image_quality(res.bitrate); @@ -375,12 +429,17 @@ function toggleMenuState() { var s = handler.get_view_style(); if (!s) s = "original"; values.push(s); + var k = handler.get_keyboard_mode(); + values.push(k); var c = handler.get_option("codec-preference"); if (!c) c = "auto"; values.push(c); for (var el in $$(menu#display-options li)) { el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); } + for (var el in $$(menu#keyboard-options>li)) { + el.attributes.toggleClass("selected", values.indexOf(el.id) >= 0); + } for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end"]) { var el = self.select('#' + id); if (el) { @@ -421,6 +480,14 @@ handler.updatePi = function(v) { } } +handler.updateDisplays = function(v) { + pi.displays = v; + header.update(); + if (is_port_forward) { + view.windowState = View.WINDOW_MINIMIZED; + } +} + function updatePrivacyMode() { var el = $(li#privacy-mode); if (el) { @@ -443,7 +510,7 @@ handler.updatePrivacyMode = updatePrivacyMode; function togglePrivacyMode(privacy_id) { var supported = handler.is_privacy_mode_supported(); if (!supported) { - msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), function() { }); + msgbox("nocancel", translate("Privacy mode"), translate("Unsupported"), "", function() { }); } else { handler.toggle_option(privacy_id); } diff --git a/src/ui/index.tis b/src/ui/index.tis index 256f00c44..ec2e0a748 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -9,7 +9,6 @@ var app; var tmp = handler.get_connect_status(); var connect_status = tmp[0]; var service_stopped = handler.get_option("stop-service") == "Y"; -var rendezvous_service_stopped = false; var using_public_server = handler.using_public_server(); var software_update_url = ""; var key_confirmed = tmp[1]; @@ -214,6 +213,7 @@ class Enhancements: Reactor.Component { {has_hwcodec ?
  • {svg_checkmark}{translate("Hardware Codec")} (beta)
  • : ""}
  • {svg_checkmark}{translate("Adaptive Bitrate")} (beta)
  • +
  • {translate("Recording")}
  • ; } @@ -232,6 +232,26 @@ class Enhancements: Reactor.Component { var v = me.id; if (v.indexOf("enable-") == 0) { handler.set_option(v, handler.get_option(v) != 'N' ? 'N' : ''); + } else if (v == 'screen-recording') { + var dir = handler.get_option("video-save-directory"); + if (!dir) dir = handler.default_video_save_directory(); + var ts0 = handler.get_option("enable-record-session") == '' ? { checked: true } : {}; + var ts1 = handler.get_option("allow-auto-record-incoming") == 'Y' ? { checked: true } : {}; + msgbox("custom-recording", translate('Recording'), +
    +
    {translate('Enable Recording Session')}
    +
    {translate('Automatically record incoming sessions')}
    +
    +
    {translate("Directory")}:  {dir}
    +
    +
    +
    + , "", function(res=null) { + if (!res) return; + handler.set_option("enable-record-session", res.enable_record_session ? '' : 'N'); + handler.set_option("allow-auto-record-incoming", res.auto_record_incoming ? 'Y' : ''); + handler.set_option("video-save-directory", $(#folderPath).text); + }); } this.toggleMenuState(); } @@ -276,6 +296,7 @@ class MyIdMenu: Reactor.Component {
  • {svg_checkmark}{translate('Enable File Transfer')}
  • {svg_checkmark}{translate('Enable Remote Restart')}
  • {svg_checkmark}{translate('Enable TCP Tunneling')}
  • +
  • {svg_checkmark}{translate('Enable LAN Discovery')}
  • {svg_checkmark}{translate('Enable remote configuration modification')}
  • @@ -288,7 +309,6 @@ class MyIdMenu: Reactor.Component { {handler.is_rdp_service_open() ? : ""} {false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connected via relay')}
  • } - {handler.has_rendezvous_service() ?
  • {translate(rendezvous_service_stopped ? "Start ID/relay service" : "Stop ID/relay service")}
  • : ""} {handler.is_ok_change_id() ?
    : ""} {username ?
  • {translate('Logout')} ({username})
  • : @@ -339,7 +359,7 @@ class MyIdMenu: Reactor.Component { function showAbout() { var name = handler.get_app_name(); - msgbox("custom-nocancel-nook-hasclose", "About " + name, "
    \ + msgbox("custom-nocancel-nook-hasclose", translate("About") + " " + name, "
    \
    Version: " + handler.get_version() + " \
    Privacy Statement
    \
    Website
    \ @@ -347,7 +367,7 @@ class MyIdMenu: Reactor.Component {
    " + handler.get_license() + " \

    Made with heart in this chaotic world!

    \
    \ -
    ", function(el) { +
    ", "", function(el) { if (el && el.attributes) { handler.open_url(el.attributes['url']); }; @@ -367,13 +387,14 @@ class MyIdMenu: Reactor.Component {
    " + translate("whitelist_sep") + "
    \ \
    \ - ", function(res=null) { + ", "", function(res=null) { if (!res) return; var value = (res.text || "").trim(); if (value) { var values = value.split(/[\s,;\n]+/g); for (var ip in values) { - if (!ip.match(/^\d+\.\d+\.\d+\.\d+$/)) { + if (!ip.match(/^(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}$/) + && !ip.match(/^(((?:[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}$/)) { return translate("Invalid IP") + ": " + ip; } } @@ -395,7 +416,7 @@ class MyIdMenu: Reactor.Component {
    " + translate("API Server") + ":
    \
    " + translate("Key") + ":
    \ \ - ", function(res=null) { + ", "", function(res=null) { if (!res) return; var id = (res.id || "").trim(); var relay = (res.relay || "").trim(); @@ -431,7 +452,7 @@ class MyIdMenu: Reactor.Component {
    {translate("Username")}:
    {translate("Password")}:
    - , function(res=null) { + , "", function(res=null) { if (!res) return; var proxy = (res.proxy || "").trim(); var username = (res.username || "").trim(); @@ -445,14 +466,12 @@ class MyIdMenu: Reactor.Component { }, 240); } else if (me.id == "stop-service") { handler.set_option("stop-service", service_stopped ? "" : "Y"); - } else if (me.id == "stop-rendezvous-service") { - handler.set_option("stop-rendezvous-service", rendezvous_service_stopped ? "" : "Y"); } else if (me.id == "change-id") { msgbox("custom-id", translate("Change ID"), "
    \
    " + translate('id_change_tip') + "
    \
    ID:
    \
    \ - ", function(res=null, show_progress) { + ", "", function(res=null, show_progress) { if (!res) return; show_progress(); var id = (res.id || "").trim(); @@ -498,7 +517,7 @@ function editDirectAccessPort() { ; msgbox("custom-direct-access-port", translate('Direct IP Access Settings'),
    {translate('Port')}:{port}
    -
    , function(res=null) { + , "", function(res=null) { if (!res) return; var p = (res.port || '').trim(); if (p) { @@ -736,11 +755,6 @@ class FixWayland: Reactor.Component { ; } - event click $(#fix-wayland) { - handler.fix_login_wayland(); - app.update(); - } - event click $(#help-me) { handler.open_url(translate("doc_fix_wayland")); } @@ -750,19 +764,11 @@ class ModifyDefaultLogin: Reactor.Component { function render() { return
    {translate('Warning')}
    -
    {translate('Current Wayland display server is not supported')}
    +
    {translate('wayland_experiment_tip')}
    {translate('Help')}
    ; } - event click $(#modify-default-login) { - if (var r = handler.modify_default_login()) { - // without handler, will fail, fucking stupid sciter - handler.msgbox("custom-error", "Error", r); - } - app.update(); - } - event click $(#help-me) { handler.open_url(translate("doc_fix_wayland")); } @@ -802,7 +808,8 @@ function watch_screen_recording() { class PasswordEyeArea : Reactor.Component { render() { var method = handler.get_option('verification-method'); - var value = method != 'use-permanent-password' ? password_cache[0] : "-"; + var mode= handler.get_option('approve-mode'); + var value = mode == 'click' || method == 'use-permanent-password' ? "-" : password_cache[0]; return
    @@ -827,7 +834,7 @@ class TemporaryPasswordLengthMenu: Reactor.Component { var me = this; var method = handler.get_option('verification-method'); self.timer(1ms, function() { me.toggleMenuState() }); - return
  • {translate("Set temporary password length")} + return
  • {translate("One-time password length")}
  • {svg_checkmark}6
  • {svg_checkmark}8
  • @@ -868,7 +875,7 @@ class PasswordArea: Reactor.Component { self.timer(1ms, function() { me.toggleMenuState() }); return
    -
    {translate('Password')}
    +
    {translate('One-time Password')}
    {this.renderPop()} @@ -879,27 +886,46 @@ class PasswordArea: Reactor.Component { function renderPop() { var method = handler.get_option('verification-method'); + var approve_mode= handler.get_option('approve-mode'); + var show_password = approve_mode != 'click'; return -
  • {svg_checkmark}{translate('Use temporary password')}
  • -
  • {svg_checkmark}{translate('Use permanent password')}
  • -
  • {svg_checkmark}{translate('Use both passwords')}
  • -
    -
  • {translate('Set permanent password')}
  • - +
  • {svg_checkmark}{translate('Accept sessions via password')}
  • +
  • {svg_checkmark}{translate('Accept sessions via click')}
  • +
  • {svg_checkmark}{translate('Accept sessions via both')}
  • + { !show_password ? '' :
    } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use one-time password')}
  • } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use permanent password')}
  • } + { !show_password ? '' :
  • {svg_checkmark}{translate('Use both passwords')}
  • } + { !show_password ? '' :
    } + { !show_password ? '' :
  • {translate('Set permanent password')}
  • } + { !show_password ? '' : }
    ; } function toggleMenuState() { - var id = handler.get_option('verification-method'); - if (id != 'use-temporary-password' && id != 'use-permanent-password') - id = 'use-both-passwords'; - for (var el in [this.$(li#use-temporary-password), this.$(li#use-permanent-password), this.$(li#use-both-passwords)]) { - el.attributes.toggleClass("selected", el.id == id); + var mode= handler.get_option('approve-mode'); + var mode_id; + if (mode == 'password') + mode_id = 'approve-mode-password'; + else if (mode == 'click') + mode_id = 'approve-mode-click'; + else + mode_id = 'approve-mode-both'; + var pwd_id = handler.get_option('verification-method'); + if (pwd_id != 'use-temporary-password' && pwd_id != 'use-permanent-password') + pwd_id = 'use-both-passwords'; + for (var el in this.$$(menu#edit-password-context>li)) { + if (el.id.indexOf("approve-mode-") == 0) + el.attributes.toggleClass("selected", el.id == mode_id); + if (el.id.indexOf("use-") == 0) + el.attributes.toggleClass("selected", el.id == pwd_id); } } event click $(svg#edit) (_, me) { - temporaryPasswordLengthMenu.update({show: true }); + var approve_mode= handler.get_option('approve-mode'); + var show_password = approve_mode != 'click'; + if(show_password && temporaryPasswordLengthMenu) temporaryPasswordLengthMenu.update({show: true }); var menu = $(menu#edit-password-context); me.popup(menu); } @@ -912,11 +938,11 @@ class PasswordArea: Reactor.Component {
    " + translate('Password') + ":
    \
    " + translate('Confirmation') + ":
    \
  • \ - ", function(res=null) { + ", "", function(res=null) { if (!res) return; var p0 = (res.password || "").trim(); var p1 = (res.confirmation || "").trim(); - if (p0.length < 6) { + if (p0.length < 6 && p0.length != 0) { return translate("Too short, at least 6 characters."); } if (p0 != p1) { @@ -932,16 +958,28 @@ class PasswordArea: Reactor.Component { handler.set_option('verification-method', me.id); this.toggleMenuState(); passwordArea.update(); + } else if (me.id.indexOf('approve-mode') == 0) { + var approve_mode; + if (me.id == 'approve-mode-password') + approve_mode = 'password'; + else if (me.id == 'approve-mode-click') + approve_mode = 'click'; + else + approve_mode = ''; + handler.set_option('approve-mode', approve_mode); + this.toggleMenuState(); + passwordArea.update(); } } } -var password_cache = ["","",""]; +var password_cache = ["","","",""]; function updatePasswordArea() { self.timer(1s, function() { var temporary_password = handler.temporary_password(); var verification_method = handler.get_option('verification-method'); var temporary_password_length = handler.get_option('temporary-password-length'); + var approve_mode = handler.get_option('approve-mode'); var update = false; if (password_cache[0] != temporary_password) { password_cache[0] = temporary_password; @@ -955,6 +993,10 @@ function updatePasswordArea() { password_cache[2] = temporary_password_length; update = true; } + if (password_cache[3] != approve_mode) { + password_cache[3] = approve_mode; + update = true; + } if (update) passwordArea.update(); updatePasswordArea(); }); @@ -1055,17 +1097,13 @@ function showSettings() { } function checkConnectStatus() { + handler.check_mouse_time(); // trigger connection status updater self.timer(1s, function() { var tmp = !!handler.get_option("stop-service"); if (tmp != service_stopped) { service_stopped = tmp; app.update(); } - tmp = !!handler.get_option("stop-rendezvous-service"); - if (tmp != rendezvous_service_stopped) { - rendezvous_service_stopped = tmp; - myIdMenu.update(); - } tmp = handler.using_public_server(); if (tmp != using_public_server) { using_public_server = tmp; @@ -1138,7 +1176,7 @@ function login() { msgbox("custom-login", translate('Login'),
    {translate('Username')}:
    {translate('Password')}:
    -
    , function(res=null, show_progress) { + , "", function(res=null, show_progress) { if (!res) return; show_progress(); var name = (res.username || '').trim(); diff --git a/src/ui/install.tis b/src/ui/install.tis index 39301fd02..3a7920bcf 100644 --- a/src/ui/install.tis +++ b/src/ui/install.tis @@ -13,7 +13,7 @@ class Install: Reactor.Component {
    {translate('Create start menu shortcuts')}
    {translate('Create desktop icon')}
    -
    {translate('End-user license agreement')}
    +
    {translate('End-user license agreement')}
    {translate('agreement_tip')}
    @@ -46,7 +46,7 @@ class Install: Reactor.Component { } } - event click $(#aggrement) { + event click $(#agreement) { view.open_url("http://rustdesk.com/privacy"); } diff --git a/src/ui/macos.rs b/src/ui/macos.rs index 188fbb603..cd0e5871b 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -1,9 +1,12 @@ +use std::{ffi::c_void, rc::Rc}; + #[cfg(target_os = "macos")] use cocoa::{ appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*, NSMenu, NSMenuItem}, base::{id, nil, YES}, foundation::{NSAutoreleasePool, NSString}, }; +use objc::runtime::Class; use objc::{ class, declare::ClassDecl, @@ -12,7 +15,8 @@ use objc::{ sel, sel_impl, }; use sciter::{make_args, Host}; -use std::{ffi::c_void, rc::Rc}; + +use hbb_common::log; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -42,7 +46,7 @@ impl DelegateState { } } -static mut LAUCHED: bool = false; +static mut LAUNCHED: bool = false; impl AppHandler for Rc { fn command(&mut self, cmd: u32) { @@ -98,18 +102,30 @@ unsafe fn set_delegate(handler: Option>) { sel!(handleMenuItem:), handle_menu_item as extern "C" fn(&mut Object, Sel, id), ); + decl.add_method( + sel!(handleEvent:withReplyEvent:), + handle_apple_event as extern "C" fn(&Object, Sel, u64, u64), + ); let decl = decl.register(); let delegate: id = msg_send![decl, alloc]; let () = msg_send![delegate, init]; let state = DelegateState { handler }; let handler_ptr = Box::into_raw(Box::new(state)); (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void); + // Set the url scheme handler + let cls = Class::get("NSAppleEventManager").unwrap(); + let manager: *mut Object = msg_send![cls, sharedAppleEventManager]; + let _: () = msg_send![manager, + setEventHandler: delegate + andSelector: sel!(handleEvent:withReplyEvent:) + forEventClass: fruitbasket::kInternetEventClass + andEventID: fruitbasket::kAEGetURL]; let () = msg_send![NSApp(), setDelegate: delegate]; } extern "C" fn application_did_finish_launching(_this: &mut Object, _: Sel, _notification: id) { unsafe { - LAUCHED = true; + LAUNCHED = true; } unsafe { let () = msg_send![NSApp(), activateIgnoringOtherApps: YES]; @@ -122,13 +138,10 @@ extern "C" fn application_should_handle_open_untitled_file( _sender: id, ) -> BOOL { unsafe { - if !LAUCHED { + if !LAUNCHED { return YES; } - hbb_common::log::debug!("icon clicked on finder"); - if std::env::args().nth(1) == Some("--server".to_owned()) { - check_main_window(); - } + crate::platform::macos::handle_application_should_open_untitled_file(); let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); (*inner).command(AWAKE); @@ -167,6 +180,13 @@ extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { } } +extern "C" fn handle_apple_event(_this: &Object, _cmd: Sel, event: u64, _reply: u64) { + let event = event as *mut Object; + let url = fruitbasket::parse_url_event(event); + log::debug!("an event was received: {}", url); + std::thread::spawn(move || crate::handle_url_scheme(url)); +} + unsafe fn make_menu_item(title: &str, key: &str, tag: u32) -> *mut Object { let title = NSString::alloc(nil).init_str(title); let action = sel!(handleMenuItem:); @@ -227,46 +247,3 @@ pub fn show_dock() { NSApp().setActivationPolicy_(NSApplicationActivationPolicyRegular); } } - -pub fn make_tray() { - unsafe { - set_delegate(None); - } - use tray_item::TrayItem; - if let Ok(mut tray) = TrayItem::new(&crate::get_app_name(), "mac-tray.png") { - tray.add_label(&format!( - "{} {}", - crate::get_app_name(), - crate::lang::translate("Service is running".to_owned()) - )) - .ok(); - - let inner = tray.inner_mut(); - inner.add_quit_item(&crate::lang::translate("Quit".to_owned())); - inner.display(); - } else { - loop { - std::thread::sleep(std::time::Duration::from_secs(3)); - } - } -} - -pub fn check_main_window() { - use sysinfo::{ProcessExt, System, SystemExt}; - let mut sys = System::new(); - sys.refresh_processes(); - let app = format!("/Applications/{}.app", crate::get_app_name()); - let my_uid = sys - .process((std::process::id() as i32).into()) - .map(|x| x.user_id()) - .unwrap_or_default(); - for (_, p) in sys.processes().iter() { - if p.cmd().len() == 1 && p.user_id() == my_uid && p.cmd()[0].contains(&app) { - return; - } - } - std::process::Command::new("open") - .args(["-n", &app]) - .status() - .ok(); -} diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index a622a45b8..d5c60d95c 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -22,6 +22,7 @@ class MsgboxComponent: Reactor.Component { this.type = params.type; this.title = params.title; this.content = params.content; + this.link = params.link; this.remember = params.remember; this.callback = params.callback; this.hasRetry = params.hasRetry; @@ -63,7 +64,7 @@ class MsgboxComponent: Reactor.Component { var ts = this.auto_login ? { checked: true } : {}; return
    -
    {translate('Auto Login')}
    +
    {translate('Auto Login')}
    ; } return this.content; @@ -93,6 +94,7 @@ class MsgboxComponent: Reactor.Component { var content = this.getContent(); var hasCancel = this.type.indexOf("error") < 0 && this.type.indexOf("nocancel") < 0 && this.type != "restarting"; var hasOk = this.type != "connecting" && this.type != "success" && this.type.indexOf("nook") < 0; + var hasLink = this.link != ""; var hasClose = this.type.indexOf("hasclose") >= 0; var show_progress = this.type == "connecting"; var me = this; @@ -121,6 +123,7 @@ class MsgboxComponent: Reactor.Component { {hasCancel || this.hasRetry ? : ""} {this.hasSkip() ? : ""} {hasOk || this.hasRetry ? : ""} + {hasLink ? : ""} {hasClose ? : ""}
    @@ -155,6 +158,12 @@ class MsgboxComponent: Reactor.Component { if (this.callback) this.callback(values); if (this.close) this.close(); } + + event click $(button#jumplink) { + if (this.link.indexOf("http") == 0) { + Sciter.launch(this.link); + } + } event click $(button#submit) { if (this.type == "error") { @@ -192,6 +201,14 @@ class MsgboxComponent: Reactor.Component { } } } + + event click $(button#select_directory) { + var folder = view.selectFolder(translate("Change"), $(#folderPath).text); + if (folder) { + if (folder.indexOf("file://") == 0) folder = folder.substring(7); + $(#folderPath).text = folder; + } + } function show_progress(show=1, err="") { if (show == -1) { diff --git a/src/ui/remote.css b/src/ui/remote.css index 66c5ce80f..71b2c1682 100644 --- a/src/ui/remote.css +++ b/src/ui/remote.css @@ -16,7 +16,7 @@ div#quality-monitor { padding: 5px; min-width: 150px; color: azure; - border: solid azure; + border: 0.5px solid azure; } video#handler { diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 1a446317d..c6e0229b2 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,10 +1,7 @@ use std::{ collections::HashMap, - ops::Deref, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex, RwLock, - }, + ops::{Deref, DerefMut}, + sync::{Arc, Mutex, RwLock}, }; use sciter::{ @@ -17,106 +14,322 @@ use sciter::{ Value, }; -#[cfg(windows)] -use clipboard::{ - cliprdr::CliprdrClientContext, create_cliprdr_context as create_clipboard_file_context, - get_rx_clip_client, server_clip_file, -}; -use enigo::{self, Enigo, KeyboardControllable}; use hbb_common::{ - allow_err, - config::{Config, LocalConfig, PeerConfig}, - fs, log, - message_proto::{permission_info::Permission, *}, - protobuf::Message as _, - rendezvous_proto::ConnType, - sleep, - tokio::{ - self, - sync::mpsc, - time::{self, Duration, Instant, Interval}, - }, - Stream, -}; -use hbb_common::{config::TransferSerde, fs::TransferJobMeta}; -use hbb_common::{ - fs::{ - can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, - RemoveJobMeta, - }, - get_version_number, + allow_err, fs::TransferJobMeta, log, message_proto::*, rendezvous_proto::ConnType, }; -#[cfg(windows)] -use crate::clipboard_file::*; use crate::{ client::*, - common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, + ui_interface::has_hwcodec, + ui_session_interface::{InvokeUiSession, Session}, }; type Video = AssetPtr; lazy_static::lazy_static! { - static ref ENIGO: Arc> = Arc::new(Mutex::new(Enigo::new())); static ref VIDEO: Arc>> = Default::default(); } -fn get_key_state(key: enigo::Key) -> bool { - #[cfg(target_os = "macos")] - if key == enigo::Key::NumLock { - return true; - } - ENIGO.lock().unwrap().get_key_state(key) -} - -static IS_IN: AtomicBool = AtomicBool::new(false); -static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); -static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -#[cfg(windows)] -static mut IS_ALT_GR: bool = false; - -#[derive(Default)] -pub struct HandlerInner { - element: Option, - sender: Option>, - thread: Option>, +/// SciterHandler +/// * element +/// * close_state for file path when close +#[derive(Clone, Default)] +pub struct SciterHandler { + element: Arc>>, close_state: HashMap, } -#[derive(Clone, Default)] -pub struct Handler { - inner: Arc>, - cmd: String, - id: String, - password: String, - args: Vec, - lc: Arc>, -} +impl SciterHandler { + #[inline] + fn call(&self, func: &str, args: &[Value]) { + if let Some(ref e) = self.element.lock().unwrap().as_ref() { + allow_err!(e.call_method(func, args)); + } + } -impl Deref for Handler { - type Target = Arc>; + #[inline] + fn call2(&self, func: &str, args: &[Value]) { + if let Some(ref e) = self.element.lock().unwrap().as_ref() { + allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); + } + } - fn deref(&self) -> &Self::Target { - &self.inner + fn make_displays_array(displays: &Vec) -> Value { + let mut displays_value = Value::array(0); + for d in displays.iter() { + let mut display = Value::map(); + display.set_item("x", d.x); + display.set_item("y", d.y); + display.set_item("width", d.width); + display.set_item("height", d.height); + display.set_item("cursor_embedded", d.cursor_embedded); + displays_value.push(display); + } + displays_value } } -impl FileManager for Handler {} +impl InvokeUiSession for SciterHandler { + fn set_cursor_data(&self, cd: CursorData) { + let mut colors = hbb_common::compress::decompress(&cd.colors); + if colors.iter().filter(|x| **x != 0).next().is_none() { + log::info!("Fix transparent"); + // somehow all 0 images shows black rect, here is a workaround + colors[3] = 1; + } + let mut png = Vec::new(); + if let Ok(()) = repng::encode(&mut png, cd.width as _, cd.height as _, &colors) { + self.call( + "setCursorData", + &make_args!( + cd.id.to_string(), + cd.hotx, + cd.hoty, + cd.width, + cd.height, + &png[..] + ), + ); + } + } -impl sciter::EventHandler for Handler { + fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool) { + self.call("setDisplay", &make_args!(x, y, w, h, cursor_embedded)); + // https://sciter.com/forums/topic/color_spaceiyuv-crash + // Nothing spectacular in decoder – done on CPU side. + // So if you can do BGRA translation on your side – the better. + // BGRA is used as internal image format so it will not require additional transformations. + VIDEO.lock().unwrap().as_mut().map(|v| { + v.stop_streaming().ok(); + let ok = v.start_streaming((w, h), COLOR_SPACE::Rgb32, None); + log::info!("[video] reinitialized: {:?}", ok); + }); + } + + fn update_privacy_mode(&self) { + self.call("updatePrivacyMode", &[]); + } + + fn set_permission(&self, name: &str, value: bool) { + self.call2("setPermission", &make_args!(name, value)); + } + + fn close_success(&self) { + self.call2("closeSuccess", &make_args!()); + } + + fn update_quality_status(&self, status: QualityStatus) { + self.call2( + "updateQualityStatus", + &make_args!( + status.speed.map_or(Value::null(), |it| it.into()), + status.fps.map_or(Value::null(), |it| it.into()), + status.delay.map_or(Value::null(), |it| it.into()), + status.target_bitrate.map_or(Value::null(), |it| it.into()), + status + .codec_format + .map_or(Value::null(), |it| it.to_string().into()) + ), + ); + } + + fn set_cursor_id(&self, id: String) { + self.call("setCursorId", &make_args!(id)); + } + + fn set_cursor_position(&self, cp: CursorPosition) { + self.call("setCursorPosition", &make_args!(cp.x, cp.y)); + } + + fn set_connection_type(&self, is_secured: bool, direct: bool) { + self.call("setConnectionType", &make_args!(is_secured, direct)); + } + + fn job_error(&self, id: i32, err: String, file_num: i32) { + self.call("jobError", &make_args!(id, err, file_num)); + } + + fn job_done(&self, id: i32, file_num: i32) { + self.call("jobDone", &make_args!(id, file_num)); + } + + fn clear_all_jobs(&self) { + self.call("clearAllJobs", &make_args!()); + } + + fn load_last_job(&self, cnt: i32, job_json: &str) { + let job: Result = serde_json::from_str(job_json); + if let Ok(job) = job { + let path; + let to; + if job.is_remote { + path = job.remote.clone(); + to = job.to.clone(); + } else { + path = job.to.clone(); + to = job.remote.clone(); + } + self.call( + "addJob", + &make_args!(cnt, path, to, job.file_num, job.show_hidden, job.is_remote), + ); + } + } + + fn update_folder_files( + &self, + id: i32, + entries: &Vec, + path: String, + _is_local: bool, + only_count: bool, + ) { + let mut m = make_fd(id, entries, only_count); + m.set_item("path", path); + self.call("updateFolderFiles", &make_args!(m)); + } + + fn update_transfer_list(&self) { + self.call("updateTransferList", &make_args!()); + } + + fn confirm_delete_files(&self, id: i32, i: i32, name: String) { + self.call("confirmDeleteFiles", &make_args!(id, i, name)); + } + + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { + self.call( + "overrideFileConfirm", + &make_args!(id, file_num, to, is_upload), + ); + } + + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64) { + self.call( + "jobProgress", + &make_args!(id, file_num, speed, finished_size), + ); + } + + fn adapt_size(&self) { + self.call("adaptSize", &make_args!()); + } + + fn on_rgba(&self, data: &mut Vec) { + VIDEO + .lock() + .unwrap() + .as_mut() + .map(|v| v.render_frame(data).ok()); + } + + fn set_peer_info(&self, pi: &PeerInfo) { + let mut pi_sciter = Value::map(); + pi_sciter.set_item("username", pi.username.clone()); + pi_sciter.set_item("hostname", pi.hostname.clone()); + pi_sciter.set_item("platform", pi.platform.clone()); + pi_sciter.set_item("sas_enabled", pi.sas_enabled); + pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays)); + pi_sciter.set_item("current_display", pi.current_display); + self.call("updatePi", &make_args!(pi_sciter)); + } + + fn set_displays(&self, displays: &Vec) { + self.call( + "updateDisplays", + &make_args!(Self::make_displays_array(displays)), + ); + } + + fn on_connected(&self, conn_type: ConnType) { + match conn_type { + ConnType::RDP => {} + ConnType::PORT_FORWARD => {} + ConnType::FILE_TRANSFER => {} + ConnType::DEFAULT_CONN => { + crate::keyboard::client::start_grab_loop(); + } + } + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { + self.call2( + "msgbox_retry", + &make_args!(msgtype, title, text, link, retry), + ); + } + + fn cancel_msgbox(&self, tag: &str) { + self.call("cancel_msgbox", &make_args!(tag)); + } + + fn new_message(&self, msg: String) { + self.call("newMessage", &make_args!(msg)); + } + + fn switch_display(&self, display: &SwitchDisplay) { + self.call("switchDisplay", &make_args!(display.display)); + } + + fn update_block_input_state(&self, on: bool) { + self.call("updateBlockInputState", &make_args!(on)); + } + + fn switch_back(&self, _id: &str) {} + + fn portable_service_running(&self, _running: bool) {} + + fn on_voice_call_started(&self) { + self.call("onVoiceCallStart", &make_args!()); + } + + fn on_voice_call_closed(&self, reason: &str) { + self.call("onVoiceCallClosed", &make_args!(reason)); + } + + fn on_voice_call_waiting(&self) { + self.call("onVoiceCallWaiting", &make_args!()); + } + + fn on_voice_call_incoming(&self) { + self.call("onVoiceCallIncoming", &make_args!()); + } + + /// RGBA is directly rendered by [on_rgba]. No need to store the rgba for the sciter ui. + fn get_rgba(&self) -> *const u8 { + std::ptr::null() + } + + fn next_rgba(&self) {} +} + +pub struct SciterSession(Session); + +impl Deref for SciterSession { + type Target = Session; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SciterSession { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl sciter::EventHandler for SciterSession { fn get_subscription(&mut self) -> Option { Some(EVENT_GROUPS::HANDLE_BEHAVIOR_EVENT) } fn attached(&mut self, root: HELEMENT) { - self.write().unwrap().element = Some(Element::from(root)); + *self.element.lock().unwrap() = Some(Element::from(root)); } fn detached(&mut self, _root: HELEMENT) { - self.write().unwrap().element = None; - self.write().unwrap().sender.take().map(|sender| { + *self.element.lock().unwrap() = None; + self.sender.write().unwrap().take().map(|sender| { sender.send(Data::Close).ok(); }); } @@ -145,7 +358,7 @@ impl sciter::EventHandler for Handler { let site = AssetPtr::adopt(ptr as *mut video_destination); log::debug!("[video] start video"); *VIDEO.lock().unwrap() = Some(site); - self.reconnect(); + self.reconnect(false); } } BEHAVIOR_EVENTS::VIDEO_INITIALIZED => { @@ -172,7 +385,7 @@ impl sciter::EventHandler for Handler { } sciter::dispatch_script_call! { - fn get_audit_server(); + fn get_audit_server(String); fn send_note(String); fn is_xfce(); fn get_id(); @@ -194,7 +407,7 @@ impl sciter::EventHandler for Handler { fn transfer_file(); fn tunnel(); fn lock_screen(); - fn reconnect(); + fn reconnect(bool); fn get_chatbox(); fn get_icon(); fn get_home_dir(); @@ -205,7 +418,7 @@ impl sciter::EventHandler for Handler { fn read_remote_dir(String, bool); fn send_chat(String); fn switch_display(i32); - fn remove_dir_all(i32, String, bool); + fn remove_dir_all(i32, String, bool, bool); fn confirm_delete_files(i32, i32); fn set_no_confirm(i32); fn cancel_job(i32); @@ -229,288 +442,57 @@ impl sciter::EventHandler for Handler { fn save_image_quality(String); fn save_custom_image_quality(i32); fn refresh_video(); + fn record_screen(bool, i32, i32); fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); fn get_remember(); fn peer_platform(); fn set_write_override(i32, i32, bool, bool, bool); + fn get_keyboard_mode(); + fn save_keyboard_mode(String); fn has_hwcodec(); fn supported_hwcodec(); fn change_prefer_codec(); fn restart_remote_device(); + fn request_voice_call(); + fn close_voice_call(); } } -impl Handler { +impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { - let me = Self { - cmd, + let session: Session = Session { id: id.clone(), password: password.clone(), args, + server_keyboard_enabled: Arc::new(RwLock::new(true)), + server_file_transfer_enabled: Arc::new(RwLock::new(true)), + server_clipboard_enabled: Arc::new(RwLock::new(true)), ..Default::default() }; - me.lc + + let conn_type = if cmd.eq("--file-transfer") { + ConnType::FILE_TRANSFER + } else if cmd.eq("--port-forward") { + ConnType::PORT_FORWARD + } else if cmd.eq("--rdp") { + ConnType::RDP + } else { + ConnType::DEFAULT_CONN + }; + + session + .lc .write() .unwrap() - .initialize(id, me.is_file_transfer(), me.is_port_forward()); - me + .initialize(id, conn_type, None, false); + + Self(session) } - fn update_quality_status(&self, status: QualityStatus) { - self.call2( - "updateQualityStatus", - &make_args!( - status.speed.map_or(Value::null(), |it| it.into()), - status.fps.map_or(Value::null(), |it| it.into()), - status.delay.map_or(Value::null(), |it| it.into()), - status.target_bitrate.map_or(Value::null(), |it| it.into()), - status - .codec_format - .map_or(Value::null(), |it| it.to_string().into()) - ), - ); - } - - fn start_keyboard_hook(&self) { - if self.is_port_forward() || self.is_file_transfer() { - return; - } - if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("keyboard hooked"); - let mut me = self.clone(); - let peer = self.peer_platform(); - let is_win = peer == "Windows"; - #[cfg(windows)] - crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); - std::thread::spawn(move || { - // This will block. - std::env::set_var("KEYBOARD_ONLY", "y"); // pass to rdev - use rdev::{EventType::*, *}; - let func = move |evt: Event| { - if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return; - } - let (key, down) = match evt.event_type { - KeyPress(k) => (k, 1), - KeyRelease(k) => (k, 0), - _ => return, - }; - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = get_key_state(enigo::Key::Control); - unsafe { - if IS_ALT_GR { - if alt || key == Key::AltGr { - if tmp { - tmp = false; - } - } else { - IS_ALT_GR = false; - } - } - } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control); - let shift = get_key_state(enigo::Key::Shift); - #[cfg(windows)] - let command = crate::platform::windows::get_win_key_state(); - #[cfg(not(windows))] - let command = get_key_state(enigo::Key::Meta); - let control_key = match key { - Key::Alt => Some(ControlKey::Alt), - Key::AltGr => Some(ControlKey::RAlt), - Key::Backspace => Some(ControlKey::Backspace), - Key::ControlLeft => { - // when pressing AltGr, an extra VK_LCONTROL with a special - // scancode with bit 9 set is sent, let's ignore this. - #[cfg(windows)] - if evt.scan_code & 0x200 != 0 { - unsafe { - IS_ALT_GR = true; - } - return; - } - Some(ControlKey::Control) - } - Key::ControlRight => Some(ControlKey::RControl), - Key::DownArrow => Some(ControlKey::DownArrow), - Key::Escape => Some(ControlKey::Escape), - Key::F1 => Some(ControlKey::F1), - Key::F10 => Some(ControlKey::F10), - Key::F11 => Some(ControlKey::F11), - Key::F12 => Some(ControlKey::F12), - Key::F2 => Some(ControlKey::F2), - Key::F3 => Some(ControlKey::F3), - Key::F4 => Some(ControlKey::F4), - Key::F5 => Some(ControlKey::F5), - Key::F6 => Some(ControlKey::F6), - Key::F7 => Some(ControlKey::F7), - Key::F8 => Some(ControlKey::F8), - Key::F9 => Some(ControlKey::F9), - Key::LeftArrow => Some(ControlKey::LeftArrow), - Key::MetaLeft => Some(ControlKey::Meta), - Key::MetaRight => Some(ControlKey::RWin), - Key::Return => Some(ControlKey::Return), - Key::RightArrow => Some(ControlKey::RightArrow), - Key::ShiftLeft => Some(ControlKey::Shift), - Key::ShiftRight => Some(ControlKey::RShift), - Key::Space => Some(ControlKey::Space), - Key::Tab => Some(ControlKey::Tab), - Key::UpArrow => Some(ControlKey::UpArrow), - Key::Delete => { - if is_win && ctrl && alt { - me.ctrl_alt_del(); - return; - } - Some(ControlKey::Delete) - } - Key::Apps => Some(ControlKey::Apps), - Key::Cancel => Some(ControlKey::Cancel), - Key::Clear => Some(ControlKey::Clear), - Key::Kana => Some(ControlKey::Kana), - Key::Hangul => Some(ControlKey::Hangul), - Key::Junja => Some(ControlKey::Junja), - Key::Final => Some(ControlKey::Final), - Key::Hanja => Some(ControlKey::Hanja), - Key::Hanji => Some(ControlKey::Hanja), - Key::Convert => Some(ControlKey::Convert), - Key::Print => Some(ControlKey::Print), - Key::Select => Some(ControlKey::Select), - Key::Execute => Some(ControlKey::Execute), - Key::PrintScreen => Some(ControlKey::Snapshot), - Key::Help => Some(ControlKey::Help), - Key::Sleep => Some(ControlKey::Sleep), - Key::Separator => Some(ControlKey::Separator), - Key::KpReturn => Some(ControlKey::NumpadEnter), - Key::Kp0 => Some(ControlKey::Numpad0), - Key::Kp1 => Some(ControlKey::Numpad1), - Key::Kp2 => Some(ControlKey::Numpad2), - Key::Kp3 => Some(ControlKey::Numpad3), - Key::Kp4 => Some(ControlKey::Numpad4), - Key::Kp5 => Some(ControlKey::Numpad5), - Key::Kp6 => Some(ControlKey::Numpad6), - Key::Kp7 => Some(ControlKey::Numpad7), - Key::Kp8 => Some(ControlKey::Numpad8), - Key::Kp9 => Some(ControlKey::Numpad9), - Key::KpDivide => Some(ControlKey::Divide), - Key::KpMultiply => Some(ControlKey::Multiply), - Key::KpDecimal => Some(ControlKey::Decimal), - Key::KpMinus => Some(ControlKey::Subtract), - Key::KpPlus => Some(ControlKey::Add), - Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return; - } - Key::Home => Some(ControlKey::Home), - Key::End => Some(ControlKey::End), - Key::Insert => Some(ControlKey::Insert), - Key::PageUp => Some(ControlKey::PageUp), - Key::PageDown => Some(ControlKey::PageDown), - Key::Pause => Some(ControlKey::Pause), - _ => None, - }; - let mut key_event = KeyEvent::new(); - if let Some(k) = control_key { - key_event.set_control_key(k); - } else { - let mut chr = match evt.name { - Some(ref s) => { - if s.len() <= 2 { - // exclude chinese characters - s.chars().next().unwrap_or('\0') - } else { - '\0' - } - } - _ => '\0', - }; - if chr == '·' { - // special for Chinese - chr = '`'; - } - if chr == '\0' { - chr = match key { - Key::Num1 => '1', - Key::Num2 => '2', - Key::Num3 => '3', - Key::Num4 => '4', - Key::Num5 => '5', - Key::Num6 => '6', - Key::Num7 => '7', - Key::Num8 => '8', - Key::Num9 => '9', - Key::Num0 => '0', - Key::KeyA => 'a', - Key::KeyB => 'b', - Key::KeyC => 'c', - Key::KeyD => 'd', - Key::KeyE => 'e', - Key::KeyF => 'f', - Key::KeyG => 'g', - Key::KeyH => 'h', - Key::KeyI => 'i', - Key::KeyJ => 'j', - Key::KeyK => 'k', - Key::KeyL => 'l', - Key::KeyM => 'm', - Key::KeyN => 'n', - Key::KeyO => 'o', - Key::KeyP => 'p', - Key::KeyQ => 'q', - Key::KeyR => 'r', - Key::KeyS => 's', - Key::KeyT => 't', - Key::KeyU => 'u', - Key::KeyV => 'v', - Key::KeyW => 'w', - Key::KeyX => 'x', - Key::KeyY => 'y', - Key::KeyZ => 'z', - Key::Comma => ',', - Key::Dot => '.', - Key::SemiColon => ';', - Key::Quote => '\'', - Key::LeftBracket => '[', - Key::RightBracket => ']', - Key::BackSlash => '\\', - Key::Minus => '-', - Key::Equal => '=', - Key::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - me.lock_screen(); - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", evt); - return; - } - } - me.key_down_or_up(down, key_event, alt, ctrl, shift, command); - }; - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); - } - }); - } - - fn get_view_style(&mut self) -> String { - return self.lc.read().unwrap().view_style.clone(); - } - - fn get_image_quality(&mut self) -> String { - return self.lc.read().unwrap().image_quality.clone(); + pub fn inner(&self) -> Session { + self.0.clone() } fn get_custom_image_quality(&mut self) -> Value { @@ -521,161 +503,31 @@ impl Handler { v } - #[inline] - pub(super) fn save_config(&self, config: PeerConfig) { - self.lc.write().unwrap().save_config(config); - } - - fn save_view_style(&mut self, value: String) { - self.lc.write().unwrap().save_view_style(value); - } - - #[inline] - pub(super) fn load_config(&self) -> PeerConfig { - load_config(&self.id) - } - - fn toggle_option(&mut self, name: String) { - let msg = self.lc.write().unwrap().toggle_option(name.clone()); - if name == "enable-file-transfer" { - self.send(Data::ToggleClipboardFile); - } - if let Some(msg) = msg { - self.send(Data::Message(msg)); - } - } - - fn get_toggle_option(&mut self, name: String) -> bool { - self.lc.read().unwrap().get_toggle_option(&name) - } - - fn is_privacy_mode_supported(&self) -> bool { - self.lc.read().unwrap().is_privacy_mode_supported() - } - - fn refresh_video(&mut self) { - self.send(Data::Message(LoginConfigHandler::refresh())); - } - - fn save_custom_image_quality(&mut self, custom_image_quality: i32) { - let msg = self - .lc - .write() - .unwrap() - .save_custom_image_quality(custom_image_quality); - self.send(Data::Message(msg)); - } - - fn save_image_quality(&mut self, value: String) { - let msg = self.lc.write().unwrap().save_image_quality(value); - if let Some(msg) = msg { - self.send(Data::Message(msg)); - } - } - - fn get_remember(&mut self) -> bool { - self.lc.read().unwrap().remember - } - - fn set_write_override( - &mut self, - job_id: i32, - file_num: i32, - is_override: bool, - remember: bool, - is_upload: bool, - ) -> bool { - self.send(Data::SetConfirmOverrideFile(( - job_id, - file_num, - is_override, - remember, - is_upload, - ))); - true - } - fn has_hwcodec(&self) -> bool { - #[cfg(not(feature = "hwcodec"))] - return false; - #[cfg(feature = "hwcodec")] - return true; + has_hwcodec() } - fn supported_hwcodec(&self) -> Value { - #[cfg(feature = "hwcodec")] - { - let mut v = Value::array(0); - let decoder = scrap::codec::Decoder::video_codec_state(&self.id); - let mut h264 = decoder.score_h264 > 0; - let mut h265 = decoder.score_h265 > 0; - if let Some((encoding_264, encoding_265)) = self.lc.read().unwrap().supported_encoding { - h264 = h264 && encoding_264; - h265 = h265 && encoding_265; - } - v.push(h264); - v.push(h265); - v - } - #[cfg(not(feature = "hwcodec"))] - { - let mut v = Value::array(0); - v.push(false); - v.push(false); - v - } - } - - fn change_prefer_codec(&self) { - let msg = self.lc.write().unwrap().change_prefer_codec(); - self.send(Data::Message(msg)); - } - - fn restart_remote_device(&mut self) { - let mut lc = self.lc.write().unwrap(); - lc.restarting_remote_device = true; - let msg = lc.restart_remote_device(); - self.send(Data::Message(msg)); - } - - pub fn is_restarting_remote_device(&self) -> bool { - self.lc.read().unwrap().restarting_remote_device - } - - fn t(&self, name: String) -> String { + pub fn t(&self, name: String) -> String { crate::client::translate(name) } - fn get_audit_server(&self) -> String { - if self.lc.read().unwrap().conn_id <= 0 - || LocalConfig::get_option("access_token").is_empty() - { - return "".to_owned(); - } - crate::get_audit_server( - Config::get_option("api-server"), - Config::get_option("custom-rendezvous-server"), - ) + pub fn get_icon(&self) -> String { + super::get_icon() } - fn send_note(&self, note: String) { - let url = self.get_audit_server(); - let id = self.id.clone(); - let conn_id = self.lc.read().unwrap().conn_id; - std::thread::spawn(move || { - send_note(url, id, conn_id, note); - }); - } - - fn is_xfce(&self) -> bool { - crate::platform::is_xfce() + fn supported_hwcodec(&self) -> Value { + let (h264, h265) = self.0.supported_hwcodec(); + let mut v = Value::array(0); + v.push(h264); + v.push(h265); + v } fn save_size(&mut self, x: i32, y: i32, w: i32, h: i32) { let size = (x, y, w, h); let mut config = self.load_config(); if self.is_file_transfer() { - let close_state = self.read().unwrap().close_state.clone(); + let close_state = self.close_state.clone(); let mut has_change = false; for (k, mut v) in close_state { if k == "remote_dir" { @@ -731,34 +583,6 @@ impl Handler { v } - fn remove_port_forward(&mut self, port: i32) { - let mut config = self.load_config(); - config.port_forwards = config - .port_forwards - .drain(..) - .filter(|x| x.0 != port) - .collect(); - self.save_config(config); - self.send(Data::RemovePortForward(port)); - } - - fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { - let mut config = self.load_config(); - if config - .port_forwards - .iter() - .filter(|x| x.0 == port) - .next() - .is_some() - { - return; - } - let pf = (port, remote_host, remote_port); - config.port_forwards.push(pf.clone()); - self.save_config(config); - self.send(Data::AddPortForward(pf)); - } - fn get_size(&mut self) -> Value { let s = if self.is_file_transfer() { self.lc.read().unwrap().size_ft @@ -775,10 +599,6 @@ impl Handler { v } - fn get_id(&mut self) -> String { - self.id.clone() - } - fn get_default_pi(&mut self) -> Value { let mut pi = Value::map(); let info = self.lc.read().unwrap().info.clone(); @@ -788,222 +608,8 @@ impl Handler { pi } - fn get_option(&self, k: String) -> String { - self.lc.read().unwrap().get_option(&k) - } - - fn set_option(&self, k: String, v: String) { - self.lc.write().unwrap().set_option(k, v); - } - - fn input_os_password(&mut self, pass: String, activate: bool) { - input_os_password(pass, activate, self.clone()); - } - - fn save_close_state(&self, k: String, v: String) { - self.write().unwrap().close_state.insert(k, v); - } - - fn get_chatbox(&mut self) -> String { - #[cfg(feature = "inline")] - return super::inline::get_chatbox(); - #[cfg(not(feature = "inline"))] - return "".to_owned(); - } - - fn get_icon(&mut self) -> String { - crate::get_icon() - } - - fn send_chat(&mut self, text: String) { - let mut misc = Misc::new(); - misc.set_chat_message(ChatMessage { - text, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send(Data::Message(msg_out)); - } - - fn switch_display(&mut self, display: i32) { - let mut misc = Misc::new(); - misc.set_switch_display(SwitchDisplay { - display, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - self.send(Data::Message(msg_out)); - } - - fn is_file_transfer(&self) -> bool { - self.cmd == "--file-transfer" - } - - fn is_port_forward(&self) -> bool { - self.cmd == "--port-forward" || self.is_rdp() - } - - fn is_rdp(&self) -> bool { - self.cmd == "--rdp" - } - - fn reconnect(&mut self) { - println!("reconnecting"); - let cloned = self.clone(); - let mut lock = self.write().unwrap(); - lock.thread.take().map(|t| t.join()); - lock.thread = Some(std::thread::spawn(move || { - io_loop(cloned); - })); - } - - #[inline] - fn peer_platform(&self) -> String { - self.lc.read().unwrap().info.platform.clone() - } - - fn get_platform(&mut self, is_remote: bool) -> String { - if is_remote { - self.peer_platform() - } else { - whoami::platform().to_string() - } - } - - fn get_path_sep(&mut self, is_remote: bool) -> &'static str { - let p = self.get_platform(is_remote); - if &p == "Windows" { - return "\\"; - } else { - return "/"; - } - } - - fn get_icon_path(&mut self, file_type: i32, ext: String) -> String { - let mut path = Config::icon_path(); - if file_type == FileType::DirLink as i32 { - let new_path = path.join("dir_link"); - if !std::fs::metadata(&new_path).is_ok() { - #[cfg(windows)] - allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); - #[cfg(not(windows))] - allow_err!(std::os::unix::fs::symlink(&path, &new_path)); - } - path = new_path; - } else if file_type == FileType::File as i32 { - if !ext.is_empty() { - path = path.join(format!("file.{}", ext)); - } else { - path = path.join("file"); - } - if !std::fs::metadata(&path).is_ok() { - allow_err!(std::fs::File::create(&path)); - } - } else if file_type == FileType::FileLink as i32 { - let new_path = path.join("file_link"); - if !std::fs::metadata(&new_path).is_ok() { - path = path.join("file"); - if !std::fs::metadata(&path).is_ok() { - allow_err!(std::fs::File::create(&path)); - } - #[cfg(windows)] - allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); - #[cfg(not(windows))] - allow_err!(std::os::unix::fs::symlink(&path, &new_path)); - } - path = new_path; - } else if file_type == FileType::DirDrive as i32 { - if cfg!(windows) { - path = fs::get_path("C:"); - } else if cfg!(target_os = "macos") { - if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { - for entry in entries { - if let Ok(entry) = entry { - path = entry.path(); - break; - } - } - } - } - } - fs::get_string(&path) - } - - fn login(&mut self, password: String, remember: bool) { - self.send(Data::Login((password, remember))); - } - - fn new_rdp(&mut self) { - self.send(Data::NewRDP); - } - - fn enter(&mut self) { - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(true); - IS_IN.store(true, Ordering::SeqCst); - } - - fn leave(&mut self) { - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(false); - IS_IN.store(false, Ordering::SeqCst); - } - - fn send_mouse( - &mut self, - mask: i32, - x: i32, - y: i32, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - #[allow(unused_mut)] - let mut command = command; - #[cfg(windows)] - { - if !command && crate::platform::windows::get_win_key_state() { - command = true; - } - } - - send_mouse(mask, x, y, alt, ctrl, shift, command, self); - // on macos, ctrl + left button down = right button down, up won't emit, so we need to - // emit up myself if peer is not macos - // to-do: how about ctrl + left from win to macos - if cfg!(target_os = "macos") { - let buttons = mask >> 3; - let evt_type = mask & 0x7; - if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" { - self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command); - } - } - } - - fn set_cursor_data(&mut self, cd: CursorData) { - let mut colors = hbb_common::compress::decompress(&cd.colors); - if colors.iter().filter(|x| **x != 0).next().is_none() { - log::info!("Fix transparent"); - // somehow all 0 images shows black rect, here is a workaround - colors[3] = 1; - } - let mut png = Vec::new(); - if let Ok(()) = repng::encode(&mut png, cd.width as _, cd.height as _, &colors) { - self.call( - "setCursorData", - &make_args!( - cd.id.to_string(), - cd.hotx, - cd.hoty, - cd.width, - cd.height, - &png[..] - ), - ); - } + fn save_close_state(&mut self, k: String, v: String) { + self.close_state.insert(k, v); } fn get_key_event(&self, down_or_up: i32, name: &str, code: i32) -> Option { @@ -1125,24 +731,6 @@ impl Handler { "".to_owned() } - fn ctrl_alt_del(&mut self) { - if self.peer_platform() == "Windows" { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::CtrlAltDel); - self.key_down_or_up(1, key_event, false, false, false, false); - } else { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::Delete); - self.key_down_or_up(3, key_event, true, true, false, false); - } - } - - fn lock_screen(&mut self) { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::LockScreen); - self.key_down_or_up(1, key_event, false, false, false, false); - } - fn transfer_file(&mut self) { let id = self.get_id(); let args = vec!["--file-transfer", &id, &self.password]; @@ -1158,1402 +746,6 @@ impl Handler { log::error!("Failed to spawn IP tunneling: {}", err); } } - - fn key_down_or_up( - &mut self, - down_or_up: i32, - evt: KeyEvent, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - let mut key_event = evt; - - if alt - && !crate::is_control_key(&key_event, &ControlKey::Alt) - && !crate::is_control_key(&key_event, &ControlKey::RAlt) - { - key_event.modifiers.push(ControlKey::Alt.into()); - } - if shift - && !crate::is_control_key(&key_event, &ControlKey::Shift) - && !crate::is_control_key(&key_event, &ControlKey::RShift) - { - key_event.modifiers.push(ControlKey::Shift.into()); - } - if ctrl - && !crate::is_control_key(&key_event, &ControlKey::Control) - && !crate::is_control_key(&key_event, &ControlKey::RControl) - { - key_event.modifiers.push(ControlKey::Control.into()); - } - if command - && !crate::is_control_key(&key_event, &ControlKey::Meta) - && !crate::is_control_key(&key_event, &ControlKey::RWin) - { - key_event.modifiers.push(ControlKey::Meta.into()); - } - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - if self.peer_platform() != "Mac OS" { - if get_key_state(enigo::Key::NumLock) && common::valid_for_numlock(&key_event) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - } - if down_or_up == 1 { - key_event.down = true; - } else if down_or_up == 3 { - key_event.press = true; - } - let mut msg_out = Message::new(); - msg_out.set_key_event(key_event); - log::debug!("{:?}", msg_out); - self.send(Data::Message(msg_out)); - } - - #[inline] - fn set_cursor_id(&mut self, id: String) { - self.call("setCursorId", &make_args!(id)); - } - - #[inline] - fn set_cursor_position(&mut self, cd: CursorPosition) { - self.call("setCursorPosition", &make_args!(cd.x, cd.y)); - } - - #[inline] - fn call(&self, func: &str, args: &[Value]) { - let r = self.read().unwrap(); - if let Some(ref e) = r.element { - allow_err!(e.call_method(func, args)); - } - } - - #[inline] - fn call2(&self, func: &str, args: &[Value]) { - let r = self.read().unwrap(); - if let Some(ref e) = r.element { - allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); - } - } - - #[inline] - fn set_display(&self, x: i32, y: i32, w: i32, h: i32) { - self.call("setDisplay", &make_args!(x, y, w, h)); - } -} - -const MILLI1: Duration = Duration::from_millis(1); - -async fn start_one_port_forward( - handler: Handler, - port: i32, - remote_host: String, - remote_port: i32, - receiver: mpsc::UnboundedReceiver, - key: &str, - token: &str, -) { - handler.lc.write().unwrap().port_forward = (remote_host, remote_port); - if let Err(err) = crate::port_forward::listen( - handler.id.clone(), - handler.password.clone(), - port, - handler.clone(), - receiver, - key, - token, - ) - .await - { - handler.on_error(&format!("Failed to listen on {}: {}", port, err)); - } - log::info!("port forward (:{}) exit", port); -} - -#[tokio::main(flavor = "current_thread")] -async fn io_loop(handler: Handler) { - let (sender, mut receiver) = mpsc::unbounded_channel::(); - handler.write().unwrap().sender = Some(sender.clone()); - let mut options = crate::ipc::get_options_async().await; - let mut key = options.remove("key").unwrap_or("".to_owned()); - let token = LocalConfig::get_option("access_token"); - if key.is_empty() { - key = crate::platform::get_license_key(); - } - if handler.is_port_forward() { - if handler.is_rdp() { - let port = handler - .get_option("rdp_port".to_owned()) - .parse::() - .unwrap_or(3389); - std::env::set_var( - "rdp_username", - handler.get_option("rdp_username".to_owned()), - ); - std::env::set_var( - "rdp_password", - handler.get_option("rdp_password".to_owned()), - ); - log::info!("Remote rdp port: {}", port); - start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; - } else if handler.args.len() == 0 { - let pfs = handler.lc.read().unwrap().port_forwards.clone(); - let mut queues = HashMap::>::new(); - for d in pfs { - sender.send(Data::AddPortForward(d)).ok(); - } - loop { - match receiver.recv().await { - Some(Data::AddPortForward((port, remote_host, remote_port))) => { - if port <= 0 || remote_port <= 0 { - continue; - } - let (sender, receiver) = mpsc::unbounded_channel::(); - queues.insert(port, sender); - let handler = handler.clone(); - let key = key.clone(); - let token = token.clone(); - tokio::spawn(async move { - start_one_port_forward( - handler, - port, - remote_host, - remote_port, - receiver, - &key, - &token, - ) - .await; - }); - } - Some(Data::RemovePortForward(port)) => { - if let Some(s) = queues.remove(&port) { - s.send(Data::Close).ok(); - } - } - Some(Data::Close) => { - break; - } - Some(d) => { - for (_, s) in queues.iter() { - s.send(d.clone()).ok(); - } - } - _ => {} - } - } - } else { - let port = handler.args[0].parse::().unwrap_or(0); - if handler.args.len() != 3 - || handler.args[2].parse::().unwrap_or(0) <= 0 - || port <= 0 - { - handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); - } - let remote_host = handler.args[1].clone(); - let remote_port = handler.args[2].parse::().unwrap_or(0); - start_one_port_forward( - handler, - port, - remote_host, - remote_port, - receiver, - &key, - &token, - ) - .await; - } - return; - } - let frame_count = Arc::new(AtomicUsize::new(0)); - let frame_count_cl = frame_count.clone(); - let (video_sender, audio_sender) = start_video_audio_threads(move |data: &[u8]| { - frame_count_cl.fetch_add(1, Ordering::Relaxed); - VIDEO - .lock() - .unwrap() - .as_mut() - .map(|v| v.render_frame(data).ok()); - }); - - let mut remote = Remote { - handler, - video_sender, - audio_sender, - receiver, - sender, - old_clipboard: Default::default(), - read_jobs: Vec::new(), - write_jobs: Vec::new(), - remove_jobs: Default::default(), - timer: time::interval(SEC30), - last_update_jobs_status: (Instant::now(), Default::default()), - first_frame: false, - #[cfg(windows)] - clipboard_file_context: None, - data_count: Arc::new(AtomicUsize::new(0)), - frame_count, - video_format: CodecFormat::Unknown, - }; - remote.io_loop(&key, &token).await; - remote.sync_jobs_status_to_local().await; -} - -struct RemoveJob { - files: Vec, - path: String, - sep: &'static str, - is_remote: bool, - no_confirm: bool, - last_update_job_status: Instant, -} - -impl RemoveJob { - fn new(files: Vec, path: String, sep: &'static str, is_remote: bool) -> Self { - Self { - files, - path, - sep, - is_remote, - no_confirm: false, - last_update_job_status: Instant::now(), - } - } - - pub fn _gen_meta(&self) -> RemoveJobMeta { - RemoveJobMeta { - path: self.path.clone(), - is_remote: self.is_remote, - no_confirm: self.no_confirm, - } - } -} - -struct Remote { - handler: Handler, - video_sender: MediaSender, - audio_sender: MediaSender, - receiver: mpsc::UnboundedReceiver, - sender: mpsc::UnboundedSender, - old_clipboard: Arc>, - read_jobs: Vec, - write_jobs: Vec, - remove_jobs: HashMap, - timer: Interval, - last_update_jobs_status: (Instant, HashMap), - first_frame: bool, - #[cfg(windows)] - clipboard_file_context: Option>, - data_count: Arc, - frame_count: Arc, - video_format: CodecFormat, -} - -impl Remote { - async fn io_loop(&mut self, key: &str, token: &str) { - let stop_clipboard = self.start_clipboard(); - let mut last_recv_time = Instant::now(); - let conn_type = if self.handler.is_file_transfer() { - ConnType::FILE_TRANSFER - } else { - ConnType::default() - }; - match Client::start(&self.handler.id, key, token, conn_type).await { - Ok((mut peer, direct)) => { - SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(true, Ordering::SeqCst); - self.handler - .call("setConnectionType", &make_args!(peer.is_secured(), direct)); - - // just build for now - #[cfg(not(windows))] - let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); - #[cfg(windows)] - let mut rx_clip_client = get_rx_clip_client().lock().await; - - let mut status_timer = time::interval(Duration::new(1, 0)); - - loop { - tokio::select! { - res = peer.next() => { - if let Some(res) = res { - match res { - Err(err) => { - log::error!("Connection closed: {}", err); - self.handler.msgbox("error", "Connection Error", &err.to_string()); - break; - } - Ok(ref bytes) => { - last_recv_time = Instant::now(); - self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); - if !self.handle_msg_from_peer(bytes, &mut peer).await { - break - } - } - } - } else { - if self.handler.is_restarting_remote_device() { - log::info!("Restart remote device"); - self.handler.msgbox("restarting", "Restarting Remote Device", "remote_restarting_tip"); - } else { - log::info!("Reset by the peer"); - self.handler.msgbox("error", "Connection Error", "Reset by the peer"); - } - break; - } - } - d = self.receiver.recv() => { - if let Some(d) = d { - if !self.handle_msg_from_ui(d, &mut peer).await { - break; - } - } - } - _msg = rx_clip_client.recv() => { - #[cfg(windows)] - match _msg { - Some((_, clip)) => { - allow_err!(peer.send(&clip_2_msg(clip)).await); - } - None => { - // unreachable!() - } - } - } - _ = self.timer.tick() => { - if last_recv_time.elapsed() >= SEC30 { - self.handler.msgbox("error", "Connection Error", "Timeout"); - break; - } - if !self.read_jobs.is_empty() { - if let Err(err) = fs::handle_read_jobs(&mut self.read_jobs, &mut peer).await { - self.handler.msgbox("error", "Connection Error", &err.to_string()); - break; - } - self.update_jobs_status(); - } else { - self.timer = time::interval_at(Instant::now() + SEC30, SEC30); - } - } - _ = status_timer.tick() => { - let speed = self.data_count.swap(0, Ordering::Relaxed); - let speed = format!("{:.2}kB/s", speed as f32 / 1024 as f32); - let fps = self.frame_count.swap(0, Ordering::Relaxed) as _; - self.handler.update_quality_status(QualityStatus { - speed:Some(speed), - fps:Some(fps), - ..Default::default() - }); - } - } - } - log::debug!("Exit io_loop of id={}", self.handler.id); - } - Err(err) => { - self.handler - .msgbox("error", "Connection Error", &err.to_string()); - } - } - if let Some(stop) = stop_clipboard { - stop.send(()).ok(); - } - SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); - SERVER_FILE_TRANSFER_ENABLED.store(false, Ordering::SeqCst); - } - - fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { - if let Some(job) = self.remove_jobs.get_mut(&id) { - if job.no_confirm { - let file_num = (file_num + 1) as usize; - if file_num < job.files.len() { - let path = format!("{}{}{}", job.path, job.sep, job.files[file_num].name); - self.sender - .send(Data::RemoveFile((id, path, file_num as i32, job.is_remote))) - .ok(); - let elapsed = job.last_update_job_status.elapsed().as_millis() as i32; - if elapsed >= 1000 { - job.last_update_job_status = Instant::now(); - } else { - return; - } - } else { - self.remove_jobs.remove(&id); - } - } - } - if let Some(err) = err { - self.handler - .call("jobError", &make_args!(id, err, file_num)); - } else { - self.handler.call("jobDone", &make_args!(id, file_num)); - } - } - - fn start_clipboard(&mut self) -> Option> { - if self.handler.is_file_transfer() || self.handler.is_port_forward() { - return None; - } - let (tx, rx) = std::sync::mpsc::channel(); - let old_clipboard = self.old_clipboard.clone(); - let tx_protobuf = self.sender.clone(); - let lc = self.handler.lc.clone(); - match ClipboardContext::new() { - Ok(mut ctx) => { - // ignore clipboard update before service start - check_clipboard(&mut ctx, Some(&old_clipboard)); - std::thread::spawn(move || loop { - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); - match rx.try_recv() { - Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { - log::debug!("Exit clipboard service of client"); - break; - } - _ => {} - } - if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || lc.read().unwrap().disable_clipboard - { - continue; - } - if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { - tx_protobuf.send(Data::Message(msg)).ok(); - } - }); - } - Err(err) => { - log::error!("Failed to start clipboard service of client: {}", err); - } - } - Some(tx) - } - - async fn load_last_jobs(&mut self) { - log::info!("start load last jobs"); - self.handler.call("clearAllJobs", &make_args!()); - let pc = self.handler.load_config(); - if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { - // no last jobs - return; - } - // TODO: can add a confirm dialog - let mut cnt = 1; - for job_str in pc.transfer.read_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.call( - "addJob", - &make_args!( - cnt, - job.to.clone(), - job.remote.clone(), - job.file_num, - job.show_hidden, - false - ), - ); - cnt += 1; - println!("restore read_job: {:?}", job); - } - } - for job_str in pc.transfer.write_jobs.iter() { - let job: Result = serde_json::from_str(&job_str); - if let Ok(job) = job { - self.handler.call( - "addJob", - &make_args!( - cnt, - job.remote.clone(), - job.to.clone(), - job.file_num, - job.show_hidden, - true - ), - ); - cnt += 1; - println!("restore write_job: {:?}", job); - } - } - self.handler.call("updateTransferList", &make_args!()); - } - - async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { - match data { - Data::Close => { - let mut misc = Misc::new(); - misc.set_close_reason("".to_owned()); - let mut msg = Message::new(); - msg.set_misc(misc); - allow_err!(peer.send(&msg).await); - return false; - } - Data::Login((password, remember)) => { - self.handler - .handle_login_from_ui(password, remember, peer) - .await; - } - Data::ToggleClipboardFile => { - self.check_clipboard_file_context(); - } - Data::Message(msg) => { - allow_err!(peer.send(&msg).await); - } - Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { - log::info!("send files, is remote {}", is_remote); - let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); - if is_remote { - log::debug!("New job {}, write to {} from remote {}", id, to, path); - self.write_jobs.push(fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - )); - allow_err!( - peer.send(&fs::new_send(id, path, file_num, include_hidden)) - .await - ); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(job) => { - log::debug!( - "New job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - let m = make_fd(job.id(), job.files(), true); - self.handler.call("updateFolderFiles", &make_args!(m)); - #[cfg(not(windows))] - let files = job.files().clone(); - #[cfg(windows)] - let mut files = job.files().clone(); - #[cfg(windows)] - if self.handler.peer_platform() != "Windows" { - // peer is not windows, need transform \ to / - fs::transform_windows_path(&mut files); - } - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await); - } - } - } - } - Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { - let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); - if is_remote { - log::debug!( - "new write waiting job {}, write to {} from remote {}", - id, - to, - path - ); - let mut job = fs::TransferJob::new_write( - id, - path.clone(), - to, - file_num, - include_hidden, - is_remote, - Vec::new(), - od, - ); - job.is_last_job = true; - self.write_jobs.push(job); - } else { - match fs::TransferJob::new_read( - id, - to.clone(), - path.clone(), - file_num, - include_hidden, - is_remote, - od, - ) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(mut job) => { - log::debug!( - "new read waiting job {}, read {} to remote {}, {} files", - id, - path, - to, - job.files().len() - ); - let m = make_fd(job.id(), job.files(), true); - self.handler.call("updateFolderFiles", &make_args!(m)); - job.is_last_job = true; - self.read_jobs.push(job); - self.timer = time::interval(MILLI1); - } - } - } - } - Data::ResumeJob((id, is_remote)) => { - if is_remote { - if let Some(job) = get_job(id, &mut self.write_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_send( - id, - job.remote.clone(), - job.file_num, - job.show_hidden - )) - .await - ); - } - } else { - if let Some(job) = get_job(id, &mut self.read_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_receive( - id, - job.path.to_string_lossy().to_string(), - job.file_num, - job.files.clone() - )) - .await - ); - } - } - } - Data::SetNoConfirm(id) => { - if let Some(job) = self.remove_jobs.get_mut(&id) { - job.no_confirm = true; - } - } - Data::ConfirmDeleteFiles((id, file_num)) => { - if let Some(job) = self.remove_jobs.get_mut(&id) { - let i = file_num as usize; - if i < job.files.len() { - self.handler.call( - "confirmDeleteFiles", - &make_args!(id, file_num, job.files[i].name.clone()), - ); - } - } - } - Data::SetConfirmOverrideFile((id, file_num, need_override, remember, is_upload)) => { - if is_upload { - if let Some(job) = fs::get_job(id, &mut self.read_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - job.confirm(&FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - } - } else { - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - if remember { - job.set_overwrite_strategy(Some(need_override)); - } - let mut msg = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_send_confirm(FileTransferSendConfirmRequest { - id, - file_num, - union: if need_override { - Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)) - } else { - Some(file_transfer_send_confirm_request::Union::Skip(true)) - }, - ..Default::default() - }); - msg.set_file_action(file_action); - allow_err!(peer.send(&msg).await); - } - } - } - Data::RemoveDirAll((id, path, is_remote)) => { - let sep = self.handler.get_path_sep(is_remote); - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_all_files(ReadAllFiles { - id, - path: path.clone(), - include_hidden: true, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - self.remove_jobs - .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); - } else { - match fs::get_recursive_files(&path, true) { - Ok(entries) => { - let m = make_fd(id, &entries, true); - self.handler.call("updateFolderFiles", &make_args!(m)); - self.remove_jobs - .insert(id, RemoveJob::new(entries, path, sep, is_remote)); - } - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - } - } - } - Data::CancelJob(id) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_cancel(FileTransferCancel { - id: id, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { - job.remove_download_file(); - fs::remove_job(id, &mut self.write_jobs); - } - fs::remove_job(id, &mut self.read_jobs); - self.remove_jobs.remove(&id); - } - Data::RemoveDir((id, path)) => { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_dir(FileRemoveDir { - id, - path, - recursive: true, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } - Data::RemoveFile((id, path, file_num, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_remove_file(FileRemoveFile { - id, - path, - file_num, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::remove_file(&path) { - Err(err) => { - self.handle_job_status(id, file_num, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, file_num, None); - } - } - } - } - Data::CreateDir((id, path, is_remote)) => { - if is_remote { - let mut msg_out = Message::new(); - let mut file_action = FileAction::new(); - file_action.set_create(FileDirCreate { - id, - path, - ..Default::default() - }); - msg_out.set_file_action(file_action); - allow_err!(peer.send(&msg_out).await); - } else { - match fs::create_dir(&path) { - Err(err) => { - self.handle_job_status(id, -1, Some(err.to_string())); - } - Ok(()) => { - self.handle_job_status(id, -1, None); - } - } - } - } - _ => {} - } - true - } - - #[inline] - fn update_job_status( - job: &fs::TransferJob, - elapsed: i32, - last_update_jobs_status: &mut (Instant, HashMap), - handler: &mut Handler, - ) { - if elapsed <= 0 { - return; - } - let transferred = job.transferred(); - let last_transferred = { - if let Some(v) = last_update_jobs_status.1.get(&job.id()) { - v.to_owned() - } else { - 0 - } - }; - last_update_jobs_status.1.insert(job.id(), transferred); - let speed = (transferred - last_transferred) as f64 / (elapsed as f64 / 1000.); - let file_num = job.file_num() - 1; - handler.call( - "jobProgress", - &make_args!(job.id(), file_num, speed, job.finished_size() as f64), - ); - } - - fn update_jobs_status(&mut self) { - let elapsed = self.last_update_jobs_status.0.elapsed().as_millis() as i32; - if elapsed >= 1000 { - for job in self.read_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &mut self.handler, - ); - } - for job in self.write_jobs.iter() { - Self::update_job_status( - job, - elapsed, - &mut self.last_update_jobs_status, - &mut self.handler, - ); - } - self.last_update_jobs_status.0 = Instant::now(); - } - } - - async fn sync_jobs_status_to_local(&mut self) -> bool { - log::info!("sync transfer job status"); - let mut config: PeerConfig = self.handler.load_config(); - let mut transfer_metas = TransferSerde::default(); - for job in self.read_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); - transfer_metas.read_jobs.push(json_str); - } - for job in self.write_jobs.iter() { - let json_str = serde_json::to_string(&job.gen_meta()).unwrap_or_default(); - transfer_metas.write_jobs.push(json_str); - } - log::info!("meta: {:?}", transfer_metas); - config.transfer = transfer_metas; - self.handler.save_config(config); - true - } - - async fn send_opts_after_login(&self, peer: &mut Stream) { - if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } - } - - async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { - if let Ok(msg_in) = Message::parse_from_bytes(&data) { - match msg_in.union { - Some(message::Union::VideoFrame(vf)) => { - if !self.first_frame { - self.first_frame = true; - self.handler.call2("closeSuccess", &make_args!()); - self.handler.call("adaptSize", &make_args!()); - self.send_opts_after_login(peer).await; - } - let incomming_format = CodecFormat::from(&vf); - if self.video_format != incomming_format { - self.video_format = incomming_format.clone(); - self.handler.update_quality_status(QualityStatus { - codec_format: Some(incomming_format), - ..Default::default() - }) - }; - self.video_sender.send(MediaData::VideoFrame(vf)).ok(); - } - Some(message::Union::Hash(hash)) => { - self.handler - .handle_hash(&self.handler.password.clone(), hash, peer) - .await; - } - Some(message::Union::LoginResponse(lr)) => match lr.union { - Some(login_response::Union::Error(err)) => { - if !self.handler.handle_login_error(&err) { - return false; - } - } - Some(login_response::Union::PeerInfo(pi)) => { - self.handler.handle_peer_info(pi); - self.check_clipboard_file_context(); - if !(self.handler.is_file_transfer() - || self.handler.is_port_forward() - || !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - || self.handler.lc.read().unwrap().disable_clipboard) - { - let txt = self.old_clipboard.lock().unwrap().clone(); - if !txt.is_empty() { - let msg_out = crate::create_clipboard_msg(txt); - let sender = self.sender.clone(); - tokio::spawn(async move { - // due to clipboard service interval time - sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; - sender.send(Data::Message(msg_out)).ok(); - }); - } - } - - if self.handler.is_file_transfer() { - self.load_last_jobs().await; - } - } - _ => {} - }, - Some(message::Union::CursorData(cd)) => { - self.handler.set_cursor_data(cd); - } - Some(message::Union::CursorId(id)) => { - self.handler.set_cursor_id(id.to_string()); - } - Some(message::Union::CursorPosition(cp)) => { - self.handler.set_cursor_position(cp); - } - Some(message::Union::Clipboard(cb)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { - update_clipboard(cb, Some(&self.old_clipboard)); - } - } - #[cfg(windows)] - Some(message::Union::Cliprdr(clip)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { - if let Some(context) = &mut self.clipboard_file_context { - if let Some(clip) = msg_2_clip(clip) { - server_clip_file(context, 0, clip); - } - } - } - } - Some(message::Union::FileResponse(fr)) => { - match fr.union { - Some(file_response::Union::Dir(fd)) => { - #[cfg(windows)] - let entries = fd.entries.to_vec(); - #[cfg(not(windows))] - let mut entries = fd.entries.to_vec(); - #[cfg(not(windows))] - { - if self.handler.peer_platform() == "Windows" { - fs::transform_windows_path(&mut entries); - } - } - let mut m = make_fd(fd.id, &entries, fd.id > 0); - if fd.id <= 0 { - m.set_item("path", fd.path); - } - self.handler.call("updateFolderFiles", &make_args!(m)); - if let Some(job) = fs::get_job(fd.id, &mut self.write_jobs) { - log::info!("job set_files: {:?}", entries); - job.set_files(entries); - } else if let Some(job) = self.remove_jobs.get_mut(&fd.id) { - job.files = entries; - } - } - Some(file_response::Union::Digest(digest)) => { - if digest.is_upload { - if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handler.call( - "overrideFileConfirm", - &make_args!( - digest.id, - digest.file_num, - read_path, - true - ), - ); - } - } - } - } else { - if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { - if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::Skip(true)), - ..Default::default() - }); - allow_err!(peer.send(&msg).await); - } - DigestCheckResult::NeedConfirm(digest) => { - if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); - allow_err!(peer.send(&msg).await); - } else { - self.handler.call( - "overrideFileConfirm", - &make_args!( - digest.id, - digest.file_num, - write_path, - false - ), - ); - } - } - DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), - ..Default::default() - }, - ); - allow_err!(peer.send(&msg).await); - } - }, - Err(err) => { - println!("error recving digest: {}", err); - } - } - } - } - } - } - Some(file_response::Union::Block(block)) => { - log::info!( - "file response block, file id:{}, file num: {}", - block.id, - block.file_num - ); - if let Some(job) = fs::get_job(block.id, &mut self.write_jobs) { - if let Err(_err) = job.write(block, None).await { - // to-do: add "skip" for writing job - } - self.update_jobs_status(); - } - } - Some(file_response::Union::Done(d)) => { - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { - job.modify_time(); - fs::remove_job(d.id, &mut self.write_jobs); - } - self.handle_job_status(d.id, d.file_num, None); - } - Some(file_response::Union::Error(e)) => { - self.handle_job_status(e.id, e.file_num, Some(e.error)); - } - _ => {} - } - } - Some(message::Union::Misc(misc)) => match misc.union { - Some(misc::Union::AudioFormat(f)) => { - self.audio_sender.send(MediaData::AudioFormat(f)).ok(); - } - Some(misc::Union::ChatMessage(c)) => { - self.handler.call("newMessage", &make_args!(c.text)); - } - Some(misc::Union::PermissionInfo(p)) => { - log::info!("Change permission {:?} -> {}", p.permission, p.enabled); - match p.permission.enum_value_or_default() { - Permission::Keyboard => { - SERVER_KEYBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - self.handler - .call2("setPermission", &make_args!("keyboard", p.enabled)); - } - Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); - self.handler - .call2("setPermission", &make_args!("clipboard", p.enabled)); - } - Permission::Audio => { - self.handler - .call2("setPermission", &make_args!("audio", p.enabled)); - } - Permission::File => { - SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); - if !p.enabled && self.handler.is_file_transfer() { - return true; - } - self.check_clipboard_file_context(); - self.handler - .call2("setPermission", &make_args!("file", p.enabled)); - } - Permission::Restart => { - self.handler - .call2("setPermission", &make_args!("restart", p.enabled)); - } - } - } - Some(misc::Union::SwitchDisplay(s)) => { - self.handler.call("switchDisplay", &make_args!(s.display)); - self.video_sender.send(MediaData::Reset).ok(); - if s.width > 0 && s.height > 0 { - VIDEO.lock().unwrap().as_mut().map(|v| { - v.stop_streaming().ok(); - let ok = v.start_streaming( - (s.width, s.height), - COLOR_SPACE::Rgb32, - None, - ); - log::info!("[video] reinitialized: {:?}", ok); - }); - self.handler.set_display(s.x, s.y, s.width, s.height); - } - } - Some(misc::Union::CloseReason(c)) => { - self.handler.msgbox("error", "Connection Error", &c); - return false; - } - Some(misc::Union::BackNotification(notification)) => { - if !self.handle_back_notification(notification).await { - return false; - } - } - _ => {} - }, - Some(message::Union::TestDelay(t)) => { - self.handler.handle_test_delay(t, peer).await; - } - Some(message::Union::AudioFrame(frame)) => { - if !self.handler.lc.read().unwrap().disable_audio { - self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); - } - } - Some(message::Union::FileAction(action)) => match action.union { - Some(file_action::Union::SendConfirm(c)) => { - if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { - job.confirm(&c); - } - } - _ => {} - }, - _ => {} - } - } - true - } - - async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { - match notification.union { - Some(back_notification::Union::BlockInputState(state)) => { - self.handle_back_msg_block_input( - state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), - ) - .await; - } - Some(back_notification::Union::PrivacyModeState(state)) => { - if !self - .handle_back_msg_privacy_mode( - state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), - ) - .await - { - return false; - } - } - _ => {} - } - true - } - - #[inline(always)] - fn update_block_input_state(&mut self, on: bool) { - self.handler.call("updateBlockInputState", &make_args!(on)); - } - - async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { - match state { - back_notification::BlockInputState::BlkOnSucceeded => { - self.update_block_input_state(true); - } - back_notification::BlockInputState::BlkOnFailed => { - self.handler - .msgbox("custom-error", "Block user input", "Failed"); - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffSucceeded => { - self.update_block_input_state(false); - } - back_notification::BlockInputState::BlkOffFailed => { - self.handler - .msgbox("custom-error", "Unblock user input", "Failed"); - } - _ => {} - } - } - - #[inline(always)] - fn update_privacy_mode(&mut self, on: bool) { - let mut config = self.handler.load_config(); - config.privacy_mode = on; - self.handler.save_config(config); - - self.handler.call("updatePrivacyMode", &[]); - } - - async fn handle_back_msg_privacy_mode( - &mut self, - state: back_notification::PrivacyModeState, - ) -> bool { - match state { - back_notification::PrivacyModeState::PrvOnByOther => { - self.handler.msgbox( - "error", - "Connecting...", - "Someone turns on privacy mode, exit", - ); - return false; - } - back_notification::PrivacyModeState::PrvNotSupported => { - self.handler - .msgbox("custom-error", "Privacy mode", "Unsupported"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnSucceeded => { - self.handler - .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); - self.update_privacy_mode(true); - } - back_notification::PrivacyModeState::PrvOnFailedDenied => { - self.handler - .msgbox("custom-error", "Privacy mode", "Peer denied"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailedPlugin => { - self.handler - .msgbox("custom-error", "Privacy mode", "Please install plugins"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOnFailed => { - self.handler - .msgbox("custom-error", "Privacy mode", "Failed"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffSucceeded => { - self.handler - .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffByPeer => { - self.handler - .msgbox("custom-error", "Privacy mode", "Peer exit"); - self.update_privacy_mode(false); - } - back_notification::PrivacyModeState::PrvOffFailed => { - self.handler - .msgbox("custom-error", "Privacy mode", "Failed to turn off"); - } - back_notification::PrivacyModeState::PrvOffUnknown => { - self.handler - .msgbox("custom-error", "Privacy mode", "Turned off"); - // log::error!("Privacy mode is turned off with unknown reason"); - self.update_privacy_mode(false); - } - _ => {} - } - true - } - - fn check_clipboard_file_context(&mut self) { - #[cfg(windows)] - { - let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) - && self.handler.lc.read().unwrap().enable_file_transfer; - if enabled == self.clipboard_file_context.is_none() { - self.clipboard_file_context = if enabled { - match create_clipboard_file_context(true, false) { - Ok(context) => { - log::info!("clipboard context for file transfer created."); - Some(context) - } - Err(err) => { - log::error!( - "Create clipboard context for file transfer: {}", - err.to_string() - ); - None - } - } - } else { - log::info!("clipboard context for file transfer destroyed."); - None - }; - } - } - } } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { @@ -2574,137 +766,10 @@ pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { e.set_item("size", entry.size as f64); a.push(e); } - if only_count { - m.set_item("num_entries", entries.len() as i32); - } else { + if !only_count { m.set_item("entries", a); } + m.set_item("num_entries", entries.len() as i32); m.set_item("total_size", n as f64); m } - -#[async_trait] -impl Interface for Handler { - fn send(&self, data: Data) { - if let Some(ref sender) = self.read().unwrap().sender { - sender.send(data).ok(); - } - } - - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { - let retry = check_if_retry(msgtype, title, text); - self.call2("msgbox_retry", &make_args!(msgtype, title, text, retry)); - } - - fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) - } - - fn handle_peer_info(&mut self, pi: PeerInfo) { - let mut pi_sciter = Value::map(); - let username = self.lc.read().unwrap().get_username(&pi); - pi_sciter.set_item("username", username.clone()); - pi_sciter.set_item("hostname", pi.hostname.clone()); - pi_sciter.set_item("platform", pi.platform.clone()); - pi_sciter.set_item("sas_enabled", pi.sas_enabled); - if get_version_number(&pi.version) < get_version_number("1.1.10") { - self.call2("setPermission", &make_args!("restart", false)); - } - if self.is_file_transfer() { - if pi.username.is_empty() { - self.on_error("No active console user logged on, please connect and logon first."); - return; - } - } else if !self.is_port_forward() { - if pi.displays.is_empty() { - self.lc.write().unwrap().handle_peer_info(username, pi); - self.call("updatePrivacyMode", &[]); - self.msgbox("error", "Remote Error", "No Display"); - return; - } - let mut displays = Value::array(0); - for ref d in pi.displays.iter() { - let mut display = Value::map(); - display.set_item("x", d.x); - display.set_item("y", d.y); - display.set_item("width", d.width); - display.set_item("height", d.height); - displays.push(display); - } - pi_sciter.set_item("displays", displays); - let mut current = pi.current_display as usize; - if current >= pi.displays.len() { - current = 0; - } - pi_sciter.set_item("current_display", current as i32); - let current = &pi.displays[current]; - self.set_display(current.x, current.y, current.width, current.height); - // https://sciter.com/forums/topic/color_spaceiyuv-crash - // Nothing spectacular in decoder – done on CPU side. - // So if you can do BGRA translation on your side – the better. - // BGRA is used as internal image format so it will not require additional transformations. - VIDEO.lock().unwrap().as_mut().map(|v| { - let ok = v.start_streaming( - (current.width as _, current.height as _), - COLOR_SPACE::Rgb32, - None, - ); - log::info!("[video] initialized: {:?}", ok); - }); - let p = self.lc.read().unwrap().should_auto_login(); - if !p.is_empty() { - input_os_password(p, true, self.clone()); - } - } - self.lc.write().unwrap().handle_peer_info(username, pi); - self.call("updatePrivacyMode", &[]); - self.call("updatePi", &make_args!(pi_sciter)); - if self.is_file_transfer() { - self.call2("closeSuccess", &make_args!()); - } else if !self.is_port_forward() { - self.msgbox("success", "Successful", "Connected, waiting for image..."); - } - #[cfg(windows)] - { - let mut path = std::env::temp_dir(); - path.push(&self.id); - let path = path.with_extension(crate::get_app_name().to_lowercase()); - std::fs::File::create(&path).ok(); - if let Some(path) = path.to_str() { - crate::platform::windows::add_recent_document(&path); - } - } - self.start_keyboard_hook(); - } - - async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { - handle_hash(self.lc.clone(), pass, hash, self, peer).await; - } - - async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { - handle_login_from_ui(self.lc.clone(), password, remember, peer).await; - } - - async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { - if !t.from_client { - self.update_quality_status(QualityStatus { - delay: Some(t.last_delay as _), - target_bitrate: Some(t.target_bitrate as _), - ..Default::default() - }); - handle_test_delay(t, peer).await; - } - } -} - -impl Handler { - fn on_error(&self, err: &str) { - self.msgbox("error", "Error", err); - } -} - -#[tokio::main(flavor = "current_thread")] -async fn send_note(url: String, id: String, conn_id: i32, note: String) { - let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); - allow_err!(crate::post_request(url, body.to_string(), "").await); -} diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 65e7e5030..5c828689d 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -6,23 +6,27 @@ var display_width = 0; var display_height = 0; var display_origin_x = 0; var display_origin_y = 0; +var display_cursor_embedded = false; var display_scale = 1; var keyboard_enabled = true; // server side var clipboard_enabled = true; // server side var audio_enabled = true; // server side var file_enabled = true; // server side var restart_enabled = true; // server side +var recording_enabled = true; // server side var scroll_body = $(body); -handler.setDisplay = function(x, y, w, h) { +handler.setDisplay = function(x, y, w, h, cursor_embedded) { display_width = w; display_height = h; display_origin_x = x; display_origin_y = y; + display_cursor_embedded = cursor_embedded; adaptDisplay(); + if (recording) handler.record_screen(true, w, h); } -// in case toolbar not shown correclty +// in case toolbar not shown correctly view.windowMinSize = (scaleIt(500), scaleIt(300)); function adaptDisplay() { @@ -67,6 +71,7 @@ function adaptDisplay() { } } } + refreshCursor(); handler.style.set { width: w / scaleFactor + "px", height: h / scaleFactor + "px", @@ -98,6 +103,7 @@ var acc_wheel_delta_y0 = 0; var total_wheel_time = 0; var wheeling = false; var dragging = false; +var is_mouse_event_triggered = false; // https://stackoverflow.com/questions/5833399/calculating-scroll-inertia-momentum function resetWheel() { @@ -114,7 +120,7 @@ function resetWheel() { var INERTIA_ACCELERATION = 30; -// not good, precision not enough to simulate accelation effect, +// not good, precision not enough to simulate acceleration effect, // seems have to use pixel based rather line based delta function accWheel(v, is_x) { if (wheeling) return; @@ -139,6 +145,7 @@ function accWheel(v, is_x) { function handler.onMouse(evt) { + is_mouse_event_triggered = true; if (is_file_transfer || is_port_forward) return false; if (view.windowState == View.WINDOW_FULL_SCREEN && !dragging) { var dy = evt.y - scroll_body.scroll(#top); @@ -190,6 +197,9 @@ function handler.onMouse(evt) dragging = false; break; case Event.MOUSE_MOVE: + if (display_cursor_embedded) { + break; + } if (cursor_img.style#display != "none" && keyboard_enabled) { cursor_img.style#display = "none"; } @@ -317,6 +327,7 @@ function handler.onMouse(evt) return true; }; +var cur_id = -1; var cur_hotx = 0; var cur_hoty = 0; var cur_img = null; @@ -345,7 +356,7 @@ function scaleCursorImage(img) { var useSystemCursor = true; function updateCursor(system=false) { stdout.println("Update cursor, system: " + system); - useSystemCursor= system; + useSystemCursor = system; if (system) { handler.style#cursor = undefined; } else if (cur_img) { @@ -353,6 +364,16 @@ function updateCursor(system=false) { } } +function refreshCursor() { + if (display_cursor_embedded) { + cursor_img.style#display = "none"; + return; + } + if (cur_id != -1) { + handler.setCursorId(cur_id); + } +} + handler.setCursorData = function(id, hotx, hoty, width, height, colors) { cur_hotx = hotx; cur_hoty = hoty; @@ -360,8 +381,9 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) { if (img) { image_binded = true; cursors[id] = [img, hotx, hoty, width, height]; + cur_id = id; img = scaleCursorImage(img); - if (cursor_img.style#display == 'none') { + if (!first_mouse_event_triggered || cursor_img.style#display == 'none') { self.timer(1ms, updateCursor); } cur_img = img; @@ -371,11 +393,12 @@ handler.setCursorData = function(id, hotx, hoty, width, height, colors) { handler.setCursorId = function(id) { var img = cursors[id]; if (img) { + cur_id = id; image_binded = true; cur_hotx = img[1]; cur_hoty = img[2]; img = scaleCursorImage(img[0]); - if (cursor_img.style#display == 'none') { + if (!first_mouse_event_triggered || cursor_img.style#display == 'none') { self.timer(1ms, updateCursor); } cur_img = img; @@ -455,6 +478,7 @@ function self.closing() { var (x, y, w, h) = view.box(#rectw, #border, #screen); if (is_file_transfer) save_file_transfer_close_state(); if (is_file_transfer || is_port_forward || size_adapted) handler.save_size(x, y, w, h); + if (recording) handler.record_screen(false, display_width, display_height); } var qualityMonitor; @@ -507,6 +531,7 @@ handler.setPermission = function(name, enabled) { if (name == "file") file_enabled = enabled; if (name == "clipboard") clipboard_enabled = enabled; if (name == "restart") restart_enabled = enabled; + if (name == "recording") recording_enabled = enabled; input_blocked = false; header.update(); }); diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs new file mode 100644 index 000000000..f5c575d43 --- /dev/null +++ b/src/ui_cm_interface.rs @@ -0,0 +1,862 @@ +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +use std::iter::FromIterator; +#[cfg(windows)] +use std::sync::Arc; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicI64, Ordering}, + RwLock, + }, +}; + +#[cfg(windows)] +use clipboard::{cliprdr::CliprdrClientContext, empty_clipboard, set_conn_enabled, ContextSend}; +use serde_derive::Serialize; + +use crate::ipc::{self, Connection, Data}; +#[cfg(windows)] +use hbb_common::tokio::sync::Mutex as TokioMutex; +use hbb_common::{ + allow_err, + config::Config, + fs::is_write_need_confirmation, + fs::{self, get_string, new_send_confirm, DigestCheckResult}, + log, + message_proto::*, + protobuf::Message as _, + tokio::{ + self, + sync::mpsc::{self, unbounded_channel, UnboundedSender}, + task::spawn_blocking, + }, +}; + +#[derive(Serialize, Clone)] +pub struct Client { + pub id: i32, + pub authorized: bool, + pub disconnected: bool, + pub is_file_transfer: bool, + pub port_forward: String, + pub name: String, + pub peer_id: String, + pub keyboard: bool, + pub clipboard: bool, + pub audio: bool, + pub file: bool, + pub restart: bool, + pub recording: bool, + pub from_switch: bool, + pub in_voice_call: bool, + pub incoming_voice_call: bool, + #[serde(skip)] + tx: UnboundedSender, +} + +struct IpcTaskRunner { + stream: Connection, + cm: ConnectionManager, + tx: mpsc::UnboundedSender, + rx: mpsc::UnboundedReceiver, + close: bool, + running: bool, + authorized: bool, + conn_id: i32, + #[cfg(windows)] + file_transfer_enabled: bool, +} + +lazy_static::lazy_static! { + static ref CLIENTS: RwLock> = Default::default(); + static ref CLICK_TIME: AtomicI64 = AtomicI64::new(0); +} + +#[derive(Clone)] +pub struct ConnectionManager { + pub ui_handler: T, +} + +pub trait InvokeUiCM: Send + Clone + 'static + Sized { + fn add_connection(&self, client: &Client); + + fn remove_connection(&self, id: i32, close: bool); + + fn new_message(&self, id: i32, text: String); + + fn change_theme(&self, dark: String); + + fn change_language(&self); + + fn show_elevation(&self, show: bool); + + fn update_voice_call_state(&self, client: &Client); +} + +impl Deref for ConnectionManager { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.ui_handler + } +} + +impl DerefMut for ConnectionManager { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ui_handler + } +} + +impl ConnectionManager { + fn add_connection( + &self, + id: i32, + is_file_transfer: bool, + port_forward: String, + peer_id: String, + name: String, + authorized: bool, + keyboard: bool, + clipboard: bool, + audio: bool, + file: bool, + restart: bool, + recording: bool, + from_switch: bool, + tx: mpsc::UnboundedSender, + ) { + let client = Client { + id, + authorized, + disconnected: false, + is_file_transfer, + port_forward, + name: name.clone(), + peer_id: peer_id.clone(), + keyboard, + clipboard, + audio, + file, + restart, + recording, + from_switch, + tx, + in_voice_call: false, + incoming_voice_call: false + }; + CLIENTS + .write() + .unwrap() + .retain(|_, c| !(c.disconnected && c.peer_id == client.peer_id)); + CLIENTS.write().unwrap().insert(id, client.clone()); + self.ui_handler.add_connection(&client); + } + + fn remove_connection(&self, id: i32, close: bool) { + if close { + CLIENTS.write().unwrap().remove(&id); + } else { + CLIENTS + .write() + .unwrap() + .get_mut(&id) + .map(|c| c.disconnected = true); + } + + #[cfg(any(target_os = "android"))] + if CLIENTS + .read() + .unwrap() + .iter() + .filter(|(_k, v)| !v.is_file_transfer) + .next() + .is_none() + { + if let Err(e) = + scrap::android::call_main_service_set_by_name("stop_capture", None, None) + { + log::debug!("stop_capture err:{}", e); + } + } + + self.ui_handler.remove_connection(id, close); + } + + fn show_elevation(&self, show: bool) { + self.ui_handler.show_elevation(show); + } + + fn voice_call_started(&self, id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = true; + self.ui_handler.update_voice_call_state(client); + } + } + + fn voice_call_incoming(&self, id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = true; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } + } + + fn voice_call_closed(&self, id: i32, _reason: &str) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.incoming_voice_call = false; + client.in_voice_call = false; + self.ui_handler.update_voice_call_state(client); + } + } +} + +#[inline] +pub fn check_click_time(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::ClickTime(0))); + }; +} + +#[inline] +pub fn get_click_time() -> i64 { + CLICK_TIME.load(Ordering::SeqCst) +} + +#[inline] +pub fn authorize(id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + client.authorized = true; + allow_err!(client.tx.send(Data::Authorize)); + }; +} + +#[inline] +pub fn close(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::Close)); + }; +} + +#[inline] +pub fn remove(id: i32) { + CLIENTS.write().unwrap().remove(&id); +} + +// server mode send chat to peer +#[inline] +pub fn send_chat(id: i32, text: String) { + let clients = CLIENTS.read().unwrap(); + if let Some(client) = clients.get(&id) { + allow_err!(client.tx.send(Data::ChatMessage { text })); + } +} + +#[inline] +pub fn switch_permission(id: i32, name: String, enabled: bool) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); + }; +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn get_clients_state() -> String { + let clients = CLIENTS.read().unwrap(); + let res = Vec::from_iter(clients.values().cloned()); + serde_json::to_string(&res).unwrap_or("".into()) +} + +#[inline] +pub fn get_clients_length() -> usize { + let clients = CLIENTS.read().unwrap(); + clients.len() +} + +#[inline] +#[cfg(feature = "flutter")] +pub fn switch_back(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::SwitchSidesBack)); + }; +} + +impl IpcTaskRunner { + #[cfg(windows)] + async fn enable_cliprdr_file_context(&mut self, conn_id: i32, enabled: bool) { + if conn_id == 0 { + return; + } + + let pre_enabled = ContextSend::is_enabled(); + ContextSend::enable(enabled); + if !pre_enabled && ContextSend::is_enabled() { + allow_err!( + self.stream + .send(&Data::ClipboardFile(clipboard::ClipboardFile::MonitorReady)) + .await + ); + } + set_conn_enabled(conn_id, enabled); + if !enabled { + ContextSend::proc(|context: &mut Box| -> u32 { + empty_clipboard(context, conn_id); + 0 + }); + } + } + + async fn run(&mut self) { + use hbb_common::config::LocalConfig; + + // for tmp use, without real conn id + let mut write_jobs: Vec = Vec::new(); + + #[cfg(windows)] + if self.conn_id > 0 { + self.enable_cliprdr_file_context(self.conn_id, self.file_transfer_enabled) + .await; + } + + #[cfg(windows)] + let rx_clip1; + let mut rx_clip; + let _tx_clip; + #[cfg(windows)] + if self.conn_id > 0 && self.authorized { + rx_clip1 = clipboard::get_rx_cliprdr_server(self.conn_id); + rx_clip = rx_clip1.lock().await; + } else { + let rx_clip2; + (_tx_clip, rx_clip2) = unbounded_channel::(); + rx_clip1 = Arc::new(TokioMutex::new(rx_clip2)); + rx_clip = rx_clip1.lock().await; + } + #[cfg(not(windows))] + { + (_tx_clip, rx_clip) = unbounded_channel::(); + } + + self.running = false; + loop { + tokio::select! { + res = self.stream.next() => { + match res { + Err(err) => { + log::info!("cm ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, from_switch} => { + log::debug!("conn_id: {}", id); + self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, from_switch,self.tx.clone()); + self.authorized = authorized; + self.conn_id = id; + #[cfg(windows)] + { + self.file_transfer_enabled = _file_transfer_enabled; + } + self.running = true; + break; + } + Data::Close => { + #[cfg(windows)] + self.enable_cliprdr_file_context(self.conn_id, false).await; + log::info!("cm ipc connection closed from connection request"); + break; + } + Data::Disconnected => { + self.close = false; + #[cfg(windows)] + self.enable_cliprdr_file_context(self.conn_id, false).await; + log::info!("cm ipc connection disconnect"); + break; + } + Data::PrivacyModeState((_id, _)) => { + #[cfg(windows)] + cm_inner_send(_id, data); + } + Data::ClickTime(ms) => { + CLICK_TIME.store(ms, Ordering::SeqCst); + } + Data::ChatMessage { text } => { + self.cm.new_message(self.conn_id, text); + } + Data::FS(mut fs) => { + if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs { + if let Ok(bytes) = self.stream.next_raw().await { + fs = ipc::FS::WriteBlock{id, file_num, data:bytes.into(), compressed}; + handle_fs(fs, &mut write_jobs, &self.tx).await; + } + } else { + handle_fs(fs, &mut write_jobs, &self.tx).await; + } + } + #[cfg(windows)] + Data::ClipboardFile(_clip) => { + #[cfg(windows)] + { + let conn_id = self.conn_id; + ContextSend::proc(|context: &mut Box| -> u32 { + clipboard::server_clip_file(context, conn_id, _clip) + }); + } + } + #[cfg(windows)] + Data::ClipboardFileEnabled(_enabled) => { + #[cfg(windows)] + self.enable_cliprdr_file_context(self.conn_id, _enabled).await; + } + Data::Theme(dark) => { + self.cm.change_theme(dark); + } + Data::Language(lang) => { + LocalConfig::set_option("lang".to_owned(), lang); + self.cm.change_language(); + } + Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show)) => { + self.cm.show_elevation(show); + } + Data::StartVoiceCall => { + self.cm.voice_call_started(self.conn_id); + } + Data::VoiceCallIncoming => { + self.cm.voice_call_incoming(self.conn_id); + } + Data::CloseVoiceCall(reason) => { + self.cm.voice_call_closed(self.conn_id, reason.as_str()); + } + _ => { + + } + } + } + _ => {} + } + } + Some(data) = self.rx.recv() => { + if self.stream.send(&data).await.is_err() { + break; + } + } + clip_file = rx_clip.recv() => match clip_file { + Some(_clip) => { + #[cfg(windows)] + allow_err!(self.tx.send(Data::ClipboardFile(_clip))); + } + None => { + // + } + }, + } + } + } + + async fn ipc_task(stream: Connection, cm: ConnectionManager) { + log::debug!("ipc task begin"); + let (tx, rx) = mpsc::unbounded_channel::(); + let mut task_runner = Self { + stream, + cm, + tx, + rx, + close: true, + running: true, + authorized: false, + conn_id: 0, + #[cfg(windows)] + file_transfer_enabled: false, + }; + + while task_runner.running { + task_runner.run().await; + } + if task_runner.conn_id > 0 { + task_runner + .cm + .remove_connection(task_runner.conn_id, task_runner.close); + } + log::debug!("ipc task end"); + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main(flavor = "current_thread")] +pub async fn start_ipc(cm: ConnectionManager) { + #[cfg(windows)] + std::thread::spawn(move || { + log::info!("try create privacy mode window"); + if let Err(e) = crate::platform::windows::check_update_broker_process() { + log::warn!( + "Failed to check update broker process. Privacy mode may not work properly. {}", + e + ); + } + allow_err!(crate::win_privacy::start()); + }); + + match ipc::new_listener("_cm").await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + log::debug!("Got new connection"); + tokio::spawn(IpcTaskRunner::::ipc_task( + Connection::new(stream), + cm.clone(), + )); + } + Err(err) => { + log::error!("Couldn't get cm client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start cm ipc server: {}", err); + } + } + crate::platform::quit_gui(); +} + +#[cfg(target_os = "android")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_listen( + cm: ConnectionManager, + mut rx: mpsc::UnboundedReceiver, + tx: mpsc::UnboundedSender, +) { + let mut current_id = 0; + let mut write_jobs: Vec = Vec::new(); + loop { + match rx.recv().await { + Some(Data::Login { + id, + is_file_transfer, + port_forward, + peer_id, + name, + authorized, + keyboard, + clipboard, + audio, + file, + restart, + recording, + from_switch, + .. + }) => { + current_id = id; + cm.add_connection( + id, + is_file_transfer, + port_forward, + peer_id, + name, + authorized, + keyboard, + clipboard, + audio, + file, + restart, + recording, + from_switch, + tx.clone(), + ); + } + Some(Data::ChatMessage { text }) => { + cm.new_message(current_id, text); + } + Some(Data::FS(fs)) => { + handle_fs(fs, &mut write_jobs, &tx).await; + } + Some(Data::Close) => { + break; + } + None => { + break; + } + _ => {} + } + } + cm.remove_connection(current_id, true); +} + +async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &UnboundedSender) { + match fs { + ipc::FS::ReadDir { + dir, + include_hidden, + } => { + read_dir(&dir, include_hidden, tx).await; + } + ipc::FS::RemoveDir { + path, + id, + recursive, + } => { + remove_dir(path, id, recursive, tx).await; + } + ipc::FS::RemoveFile { path, id, file_num } => { + remove_file(path, id, file_num, tx).await; + } + ipc::FS::CreateDir { path, id } => { + create_dir(path, id, tx).await; + } + ipc::FS::NewWrite { + path, + id, + file_num, + mut files, + overwrite_detection, + } => { + // cm has no show_hidden context + // dummy remote, show_hidden, is_remote + write_jobs.push(fs::TransferJob::new_write( + id, + "".to_string(), + path, + file_num, + false, + false, + files + .drain(..) + .map(|f| FileEntry { + name: f.0, + modified_time: f.1, + ..Default::default() + }) + .collect(), + overwrite_detection, + )); + } + ipc::FS::CancelWrite { id } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.remove_download_file(); + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteDone { id, file_num } => { + if let Some(job) = fs::get_job(id, write_jobs) { + job.modify_time(); + send_raw(fs::new_done(id, file_num), tx); + fs::remove_job(id, write_jobs); + } + } + ipc::FS::WriteError { id, file_num, err } => { + if let Some(job) = fs::get_job(id, write_jobs) { + send_raw(fs::new_error(job.id(), err, file_num), tx); + fs::remove_job(job.id(), write_jobs); + } + } + ipc::FS::WriteBlock { + id, + file_num, + data, + compressed, + } => { + if let Some(job) = fs::get_job(id, write_jobs) { + if let Err(err) = job + .write(FileTransferBlock { + id, + file_num, + data, + compressed, + ..Default::default() + }) + .await + { + send_raw(fs::new_error(id, err, file_num), &tx); + } + } + } + ipc::FS::CheckDigest { + id, + file_num, + file_size, + last_modified, + is_upload, + } => { + if let Some(job) = fs::get_job(id, write_jobs) { + let mut req = FileTransferSendConfirmRequest { + id, + file_num, + union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), + ..Default::default() + }; + let digest = FileTransferDigest { + id, + file_num, + last_modified, + file_size, + ..Default::default() + }; + if let Some(file) = job.files().get(file_num as usize) { + let path = get_string(&job.join(&file.name)); + match is_write_need_confirmation(&path, &digest) { + Ok(digest_result) => { + match digest_result { + DigestCheckResult::IsSame => { + req.set_skip(true); + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } + DigestCheckResult::NeedConfirm(mut digest) => { + // upload to server, but server has the same file, request + digest.is_upload = is_upload; + let mut msg_out = Message::new(); + let mut fr = FileResponse::new(); + fr.set_digest(digest); + msg_out.set_file_response(fr); + send_raw(msg_out, &tx); + } + DigestCheckResult::NoSuchFile => { + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } + } + } + Err(err) => { + send_raw(fs::new_error(id, err, file_num), &tx); + } + } + } + } + } + _ => {} + } +} + +async fn read_dir(dir: &str, include_hidden: bool, tx: &UnboundedSender) { + let path = { + if dir.is_empty() { + Config::get_home() + } else { + fs::get_path(dir) + } + }; + if let Ok(Ok(fd)) = spawn_blocking(move || fs::read_dir(&path, include_hidden)).await { + let mut msg_out = Message::new(); + let mut file_response = FileResponse::new(); + file_response.set_dir(fd); + msg_out.set_file_response(file_response); + send_raw(msg_out, tx); + } +} + +async fn handle_result( + res: std::result::Result, S>, + id: i32, + file_num: i32, + tx: &UnboundedSender, +) { + match res { + Err(err) => { + send_raw(fs::new_error(id, err, file_num), tx); + } + Ok(Err(err)) => { + send_raw(fs::new_error(id, err, file_num), tx); + } + Ok(Ok(())) => { + send_raw(fs::new_done(id, file_num), tx); + } + } +} + +async fn remove_file(path: String, id: i32, file_num: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::remove_file(&path)).await, + id, + file_num, + tx, + ) + .await; +} + +async fn create_dir(path: String, id: i32, tx: &UnboundedSender) { + handle_result( + spawn_blocking(move || fs::create_dir(&path)).await, + id, + 0, + tx, + ) + .await; +} + +async fn remove_dir(path: String, id: i32, recursive: bool, tx: &UnboundedSender) { + let path = fs::get_path(&path); + handle_result( + spawn_blocking(move || { + if recursive { + fs::remove_all_empty_dir(&path) + } else { + std::fs::remove_dir(&path).map_err(|err| err.into()) + } + }) + .await, + id, + 0, + tx, + ) + .await; +} + +fn send_raw(msg: Message, tx: &UnboundedSender) { + match msg.write_to_bytes() { + Ok(bytes) => { + allow_err!(tx.send(Data::RawMessage(bytes))); + } + err => allow_err!(err), + } +} + +#[cfg(windows)] +fn cm_inner_send(id: i32, data: Data) { + let lock = CLIENTS.read().unwrap(); + if id != 0 { + if let Some(s) = lock.get(&id) { + allow_err!(s.tx.send(data)); + } + } else { + for s in lock.values() { + allow_err!(s.tx.send(data.clone())); + } + } +} + +pub fn can_elevate() -> bool { + #[cfg(windows)] + return !crate::platform::is_installed(); + #[cfg(not(windows))] + return false; +} + +pub fn elevate_portable(_id: i32) { + #[cfg(windows)] + { + let lock = CLIENTS.read().unwrap(); + if let Some(s) = lock.get(&_id) { + allow_err!(s.tx.send(ipc::Data::DataPortableService( + ipc::DataPortableService::RequestStart + ))); + } + } +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn handle_incoming_voice_call(id: i32, accept: bool) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::VoiceCallResponse(accept))); + }; +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn close_voice_call(id: i32) { + if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) { + allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); + }; +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs new file mode 100644 index 000000000..3b2ba0897 --- /dev/null +++ b/src/ui_interface.rs @@ -0,0 +1,972 @@ +use std::{ + collections::HashMap, + process::Child, + sync::{Arc, Mutex}, +}; + +#[cfg(any(target_os = "android", target_os = "ios"))] +use hbb_common::password_security; +use hbb_common::{ + allow_err, + config::{self, Config, LocalConfig, PeerConfig}, + directories_next, log, sleep, + tokio::{self, sync::mpsc, time}, +}; + +use hbb_common::{ + config::{RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, + futures::future::join_all, + protobuf::Message as _, + rendezvous_proto::*, +}; + +#[cfg(feature = "flutter")] +use crate::hbbs_http::account; +use crate::{common::SOFTWARE_UPDATE_URL, ipc}; + +type Message = RendezvousMessage; + +pub type Children = Arc)>>; +type Status = (i32, bool, i64, String); // (status_num, key_confirmed, mouse_time, id) + +lazy_static::lazy_static! { + static ref UI_STATUS : Arc> = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); + static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); + static ref ASYNC_JOB_STATUS : Arc> = Default::default(); + static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); + pub static ref OPTION_SYNCED : Arc> = Default::default(); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn get_id() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_id(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_id(); +} + +#[inline] +pub fn goto_install() { + allow_err!(crate::run_me(vec!["--install"])); +} + +#[inline] +pub fn install_me(_options: String, _path: String, _silent: bool, _debug: bool) { + #[cfg(windows)] + std::thread::spawn(move || { + allow_err!(crate::platform::windows::install_me( + &_options, _path, _silent, _debug + )); + std::process::exit(0); + }); +} + +#[inline] +pub fn update_me(_path: String) { + #[cfg(target_os = "linux")] + { + std::process::Command::new("pkexec") + .args(&["apt", "install", "-f", &_path]) + .spawn() + .ok(); + std::fs::remove_file(&_path).ok(); + crate::run_me(Vec::<&str>::new()).ok(); + } + #[cfg(windows)] + { + let mut path = _path; + if path.is_empty() { + if let Ok(tmp) = std::env::current_exe() { + path = tmp.to_string_lossy().to_string(); + } + } + std::process::Command::new(path) + .arg("--update") + .spawn() + .ok(); + std::process::exit(0); + } +} + +#[inline] +pub fn run_without_install() { + crate::run_me(vec!["--noinstall"]).ok(); + std::process::exit(0); +} + +#[inline] +pub fn show_run_without_install() -> bool { + let mut it = std::env::args(); + if let Some(tmp) = it.next() { + if crate::is_setup(&tmp) { + return it.next() == None; + } + } + false +} + +#[inline] +pub fn get_license() -> String { + #[cfg(windows)] + if let Some(lic) = crate::platform::windows::get_license() { + #[cfg(feature = "flutter")] + return format!("Key: {}\nHost: {}\nApi: {}", lic.key, lic.host, lic.api); + // default license format is html formed (sciter) + #[cfg(not(feature = "flutter"))] + return format!( + "
    Key: {}
    Host: {} Api: {}", + lic.key, lic.host, lic.api + ); + } + Default::default() +} + +#[inline] +#[cfg(target_os = "windows")] +pub fn get_option_opt(key: &str) -> Option { + OPTIONS.lock().unwrap().get(key).map(|x| x.clone()) +} + +#[inline] +pub fn get_option(key: String) -> String { + get_option_(&key) +} + +#[inline] +fn get_option_(key: &str) -> String { + let map = OPTIONS.lock().unwrap(); + if let Some(v) = map.get(key) { + v.to_owned() + } else { + "".to_owned() + } +} + +#[inline] +pub fn get_local_option(key: String) -> String { + LocalConfig::get_option(&key) +} + +#[inline] +pub fn set_local_option(key: String, value: String) { + LocalConfig::set_option(key, value); +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn get_local_flutter_config(key: String) -> String { + LocalConfig::get_flutter_config(&key) +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn set_local_flutter_config(key: String, value: String) { + LocalConfig::set_flutter_config(key, value); +} + +#[cfg(feature = "flutter")] +#[inline] +pub fn get_kb_layout_type() -> String { + LocalConfig::get_kb_layout_type() +} + +#[cfg(feature = "flutter")] +#[inline] +pub fn set_kb_layout_type(kb_layout_type: String) { + LocalConfig::set_kb_layout_type(kb_layout_type); +} + +#[inline] +pub fn peer_has_password(id: String) -> bool { + !PeerConfig::load(&id).password.is_empty() +} + +#[inline] +pub fn forget_password(id: String) { + let mut c = PeerConfig::load(&id); + c.password.clear(); + c.store(&id); +} + +#[inline] +pub fn get_peer_option(id: String, name: String) -> String { + let c = PeerConfig::load(&id); + c.options.get(&name).unwrap_or(&"".to_owned()).to_owned() +} + +#[inline] +pub fn set_peer_option(id: String, name: String, value: String) { + let mut c = PeerConfig::load(&id); + if value.is_empty() { + c.options.remove(&name); + } else { + c.options.insert(name, value); + } + c.store(&id); +} + +#[inline] +pub fn using_public_server() -> bool { + option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() + && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() +} + +#[inline] +pub fn get_options() -> String { + let options = OPTIONS.lock().unwrap(); + let mut m = serde_json::Map::new(); + for (k, v) in options.iter() { + m.insert(k.into(), v.to_owned().into()); + } + serde_json::to_string(&m).unwrap() +} + +#[inline] +pub fn test_if_valid_server(host: String) -> String { + hbb_common::socket_client::test_if_valid_server(&host) +} + +#[inline] +#[cfg(feature = "flutter")] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_sound_inputs() -> Vec { + let mut a = Vec::new(); + #[cfg(not(target_os = "linux"))] + { + fn get_sound_inputs_() -> Vec { + let mut out = Vec::new(); + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + if let Ok(devices) = host.devices() { + for device in devices { + if device.default_input_config().is_err() { + continue; + } + if let Ok(name) = device.name() { + out.push(name); + } + } + } + out + } + + let inputs = Arc::new(Mutex::new(Vec::new())); + let cloned = inputs.clone(); + // can not call below in UI thread, because conflict with sciter sound com initialization + std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs_()) + .join() + .ok(); + for name in inputs.lock().unwrap().drain(..) { + a.push(name); + } + } + #[cfg(target_os = "linux")] + { + let inputs: Vec = crate::platform::linux::get_pa_sources() + .drain(..) + .map(|x| x.1) + .collect(); + + for name in inputs { + a.push(name); + } + } + a +} + +#[inline] +pub fn set_options(m: HashMap) { + *OPTIONS.lock().unwrap() = m.clone(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::set_options(m).ok(); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_options(m); +} + +#[inline] +pub fn set_option(key: String, value: String) { + let mut options = OPTIONS.lock().unwrap(); + #[cfg(target_os = "macos")] + if &key == "stop-service" { + let is_stop = value == "Y"; + if is_stop && crate::platform::macos::uninstall(true) { + return; + } + } + if value.is_empty() { + options.remove(&key); + } else { + options.insert(key.clone(), value.clone()); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::set_options(options.clone()).ok(); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_option(key, value); +} + +#[inline] +pub fn install_path() -> String { + #[cfg(windows)] + return crate::platform::windows::get_install_info().1; + #[cfg(not(windows))] + return "".to_owned(); +} + +#[inline] +pub fn get_socks() -> Vec { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Vec::new(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let s = ipc::get_socks(); + match s { + None => Vec::new(), + Some(s) => { + let mut v = Vec::new(); + v.push(s.proxy); + v.push(s.username); + v.push(s.password); + v + } + } + } +} + +#[inline] +pub fn set_socks(proxy: String, username: String, password: String) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::set_socks(config::Socks5Server { + proxy, + username, + password, + }) + .ok(); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[inline] +pub fn is_installed() -> bool { + crate::platform::is_installed() +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +#[inline] +pub fn is_installed() -> bool { + false +} + +#[inline] +pub fn is_rdp_service_open() -> bool { + #[cfg(windows)] + return is_installed() && crate::platform::windows::is_rdp_service_open(); + #[cfg(not(windows))] + return false; +} + +#[inline] +pub fn is_share_rdp() -> bool { + #[cfg(windows)] + return crate::platform::windows::is_share_rdp(); + #[cfg(not(windows))] + return false; +} + +#[inline] +pub fn set_share_rdp(_enable: bool) { + #[cfg(windows)] + crate::platform::windows::set_share_rdp(_enable); +} + +#[inline] +pub fn is_installed_lower_version() -> bool { + #[cfg(not(windows))] + return false; + #[cfg(windows)] + { + let installed_version = crate::platform::windows::get_installed_version(); + let a = hbb_common::get_version_number(crate::VERSION); + let b = hbb_common::get_version_number(&installed_version); + return a > b; + } +} + +#[inline] +pub fn get_mouse_time() -> f64 { + let ui_status = UI_STATUS.lock().unwrap(); + let res = ui_status.2 as f64; + return res; +} + +#[inline] +pub fn check_mouse_time() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let sender = SENDER.lock().unwrap(); + allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); + } +} + +#[inline] +pub fn get_connect_status() -> Status { + let ui_statue = UI_STATUS.lock().unwrap(); + let res = ui_statue.clone(); + res +} + +#[inline] +pub fn temporary_password() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return password_security::temporary_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return TEMPORARY_PASSWD.lock().unwrap().clone(); +} + +#[inline] +pub fn update_temporary_password() { + #[cfg(any(target_os = "android", target_os = "ios"))] + password_security::update_temporary_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(ipc::update_temporary_password()); +} + +#[inline] +pub fn permanent_password() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_permanent_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_permanent_password(); +} + +#[inline] +pub fn set_permanent_password(password: String) { + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_permanent_password(&password); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(ipc::set_permanent_password(password)); +} + +#[inline] +pub fn get_peer(id: String) -> PeerConfig { + PeerConfig::load(&id) +} + +#[inline] +pub fn get_fav() -> Vec { + LocalConfig::get_fav() +} + +#[inline] +pub fn store_fav(fav: Vec) { + LocalConfig::set_fav(fav); +} + +#[inline] +pub fn is_process_trusted(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_process_trusted(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +#[inline] +pub fn is_can_screen_recording(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_can_screen_recording(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +#[inline] +pub fn is_installed_daemon(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_installed_daemon(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +#[inline] +#[cfg(feature = "flutter")] +pub fn is_can_input_monitoring(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_can_input_monitoring(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +#[inline] +pub fn get_error() -> String { + #[cfg(not(any(feature = "cli")))] + #[cfg(target_os = "linux")] + { + let dtype = crate::platform::linux::get_display_server(); + if "wayland" == dtype { + return crate::server::wayland::common_get_error(); + } + if dtype != "x11" { + return format!( + "{} {}, {}", + crate::client::translate("Unsupported display server ".to_owned()), + dtype, + crate::client::translate("x11 expected".to_owned()), + ); + } + } + return "".to_owned(); +} + +#[inline] +pub fn is_login_wayland() -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::is_login_wayland(); + #[cfg(not(target_os = "linux"))] + return false; +} + +#[inline] +pub fn current_is_wayland() -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::current_is_wayland(); + #[cfg(not(target_os = "linux"))] + return false; +} + +#[inline] +pub fn get_new_version() -> String { + hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) +} + +#[inline] +pub fn get_version() -> String { + crate::VERSION.to_owned() +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn get_app_name() -> String { + crate::get_app_name() +} + +#[cfg(windows)] +#[inline] +pub fn create_shortcut(_id: String) { + crate::platform::windows::create_shortcut(&_id).ok(); +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn discover() { + std::thread::spawn(move || { + allow_err!(crate::lan::discover()); + }); +} + +#[cfg(feature = "flutter")] +pub fn peer_to_map(id: String, p: PeerConfig) -> HashMap<&'static str, String> { + HashMap::<&str, String>::from_iter([ + ("id", id), + ("username", p.info.username.clone()), + ("hostname", p.info.hostname.clone()), + ("platform", p.info.platform.clone()), + ( + "alias", + p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), + ), + ]) +} + +#[inline] +pub fn get_lan_peers() -> Vec> { + config::LanPeers::load() + .peers + .iter() + .map(|peer| { + HashMap::<&str, String>::from_iter([ + ("id", peer.id.clone()), + ("username", peer.username.clone()), + ("hostname", peer.hostname.clone()), + ("platform", peer.platform.clone()), + ]) + }) + .collect() +} + +#[inline] +pub fn remove_discovered(id: String) { + let mut peers = config::LanPeers::load().peers; + peers.retain(|x| x.id != id); + config::LanPeers::store(&peers); +} + +#[inline] +pub fn get_uuid() -> String { + base64::encode(hbb_common::get_uuid()) +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn change_id(id: String) { + *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); + let old_id = get_id(); + std::thread::spawn(move || { + *ASYNC_JOB_STATUS.lock().unwrap() = change_id_shared(id, old_id).to_owned(); + }); +} + +#[inline] +pub fn post_request(url: String, body: String, header: String) { + *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); + std::thread::spawn(move || { + *ASYNC_JOB_STATUS.lock().unwrap() = match crate::post_request_sync(url, body, &header) { + Err(err) => err.to_string(), + Ok(text) => text, + }; + }); +} + +#[inline] +pub fn get_async_job_status() -> String { + ASYNC_JOB_STATUS.lock().unwrap().clone() +} + +#[inline] +pub fn get_langs() -> String { + crate::lang::LANGS.to_string() +} + +#[inline] +pub fn default_video_save_directory() -> String { + let appname = crate::get_app_name(); + + #[cfg(any(target_os = "android", target_os = "ios"))] + if let Ok(home) = config::APP_HOME_DIR.read() { + let mut path = home.to_owned(); + path.push_str("/RustDesk/ScreenRecord"); + return path; + } + + if let Some(user) = directories_next::UserDirs::new() { + if let Some(video_dir) = user.video_dir() { + return video_dir.join(appname).to_string_lossy().to_string(); + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(home) = crate::platform::get_active_user_home() { + let name = if cfg!(target_os = "macos") { + "Movies" + } else { + "Videos" + }; + return home.join(name).join(appname).to_string_lossy().to_string(); + } + + if let Ok(exe) = std::env::current_exe() { + if let Some(dir) = exe.parent() { + return dir.join("videos").to_string_lossy().to_string(); + } + } + "".to_owned() +} + +#[inline] +pub fn get_api_server() -> String { + crate::get_api_server( + get_option_("api-server"), + get_option_("custom-rendezvous-server"), + ) +} + +#[inline] +pub fn has_hwcodec() -> bool { + #[cfg(not(any(feature = "hwcodec", feature = "mediacodec")))] + return false; + #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] + return true; +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[inline] +pub fn is_root() -> bool { + crate::platform::is_root() +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +#[inline] +pub fn is_root() -> bool { + false +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[inline] +pub fn check_super_user_permission() -> bool { + #[cfg(feature = "flatpak")] + return true; + #[cfg(any(windows, target_os = "linux"))] + return crate::platform::check_super_user_permission().unwrap_or(false); + #[cfg(not(any(windows, target_os = "linux")))] + true +} + +#[allow(dead_code)] +pub fn check_zombie(children: Children) { + let mut deads = Vec::new(); + loop { + let mut lock = children.lock().unwrap(); + let mut n = 0; + for (id, c) in lock.1.iter_mut() { + if let Ok(Some(_)) = c.try_wait() { + deads.push(id.clone()); + n += 1; + } + } + for ref id in deads.drain(..) { + lock.1.remove(id); + } + if n > 0 { + lock.0 = true; + } + drop(lock); + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} + +pub fn start_option_status_sync() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let _sender = SENDER.lock().unwrap(); + } +} + +// not call directly +fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { + let (tx, rx) = mpsc::unbounded_channel::(); + std::thread::spawn(move || check_connect_status_(reconnect, rx)); + tx +} + +#[cfg(feature = "flutter")] +pub fn account_auth(op: String, id: String, uuid: String) { + account::OidcSession::account_auth(op, id, uuid); +} + +#[cfg(feature = "flutter")] +pub fn account_auth_cancel() { + account::OidcSession::auth_cancel(); +} + +#[cfg(feature = "flutter")] +pub fn account_auth_result() -> String { + serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() +} + +#[cfg(feature = "flutter")] +pub fn set_user_default_option(key: String, value: String) { + use hbb_common::config::UserDefaultConfig; + UserDefaultConfig::load().set(key, value); +} + +#[cfg(feature = "flutter")] +pub fn get_user_default_option(key: String) -> String { + use hbb_common::config::UserDefaultConfig; + UserDefaultConfig::load().get(&key) +} + +// notice: avoiding create ipc connection repeatedly, +// because windows named pipe has serious memory leak issue. +#[tokio::main(flavor = "current_thread")] +async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { + let mut key_confirmed = false; + let mut rx = rx; + let mut mouse_time = 0; + let mut id = "".to_owned(); + loop { + if let Ok(mut c) = ipc::connect(1000, "").await { + let mut timer = time::interval(time::Duration::from_secs(1)); + loop { + tokio::select! { + res = c.next() => { + match res { + Err(err) => { + log::error!("ipc connection closed: {}", err); + break; + } + Ok(Some(ipc::Data::MouseMoveTime(v))) => { + mouse_time = v; + UI_STATUS.lock().unwrap().2 = v; + } + Ok(Some(ipc::Data::Options(Some(v)))) => { + *OPTIONS.lock().unwrap() = v; + *OPTION_SYNCED.lock().unwrap() = true; + } + Ok(Some(ipc::Data::Config((name, Some(value))))) => { + if name == "id" { + id = value; + } else if name == "temporary-password" { + *TEMPORARY_PASSWD.lock().unwrap() = value; + } + } + Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { + if x > 0 { + x = 1 + } + key_confirmed = c; + *UI_STATUS.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone()); + } + _ => {} + } + } + Some(data) = rx.recv() => { + allow_err!(c.send(&data).await); + } + _ = timer.tick() => { + c.send(&ipc::Data::OnlineStatus(None)).await.ok(); + c.send(&ipc::Data::Options(None)).await.ok(); + c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok(); + c.send(&ipc::Data::Config(("temporary-password".to_owned(), None))).await.ok(); + } + } + } + } + if !reconnect { + OPTIONS + .lock() + .unwrap() + .insert("ipc-closed".to_owned(), "Y".to_owned()); + break; + } + *UI_STATUS.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone()); + sleep(1.).await; + } +} + +#[allow(dead_code)] +pub fn option_synced() -> bool { + OPTION_SYNCED.lock().unwrap().clone() +} + +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +#[tokio::main(flavor = "current_thread")] +pub(crate) async fn send_to_cm(data: &ipc::Data) { + if let Ok(mut c) = ipc::connect(1000, "_cm").await { + c.send(data).await.ok(); + } +} + +const INVALID_FORMAT: &'static str = "Invalid format"; +const UNKNOWN_ERROR: &'static str = "Unknown error"; + +#[tokio::main(flavor = "current_thread")] +pub async fn change_id_shared(id: String, old_id: String) -> &'static str { + if !hbb_common::is_valid_custom_id(&id) { + return INVALID_FORMAT; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let uuid = machine_uid::get().unwrap_or("".to_owned()); + #[cfg(any(target_os = "android", target_os = "ios"))] + let uuid = base64::encode(hbb_common::get_uuid()); + + if uuid.is_empty() { + log::error!("Failed to change id, uuid is_empty"); + return UNKNOWN_ERROR; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await; + #[cfg(any(target_os = "android", target_os = "ios"))] + let rendezvous_servers = Config::get_rendezvous_servers(); + + let mut futs = Vec::new(); + let err: Arc> = Default::default(); + for rendezvous_server in rendezvous_servers { + let err = err.clone(); + let id = id.to_owned(); + let uuid = uuid.clone(); + let old_id = old_id.clone(); + futs.push(tokio::spawn(async move { + let tmp = check_id(rendezvous_server, old_id, id, uuid).await; + if !tmp.is_empty() { + *err.lock().unwrap() = tmp; + } + })); + } + join_all(futs).await; + let err = *err.lock().unwrap(); + if err.is_empty() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::ipc::set_config_async("id", id.to_owned()).await.ok(); + #[cfg(any(target_os = "android", target_os = "ios"))] + { + Config::set_key_confirmed(false); + Config::set_id(&id); + } + } + err +} + +async fn check_id( + rendezvous_server: String, + old_id: String, + id: String, + uuid: String, +) -> &'static str { + if let Ok(mut socket) = hbb_common::socket_client::connect_tcp( + crate::check_port(rendezvous_server, RENDEZVOUS_PORT), + RENDEZVOUS_TIMEOUT, + ) + .await + { + let mut msg_out = Message::new(); + msg_out.set_register_pk(RegisterPk { + old_id, + id, + uuid: uuid.into(), + ..Default::default() + }); + let mut ok = false; + if socket.send(&msg_out).await.is_ok() { + if let Some(Ok(bytes)) = socket.next_timeout(3_000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => { + match rpr.result.enum_value_or_default() { + register_pk_response::Result::OK => { + ok = true; + } + register_pk_response::Result::ID_EXISTS => { + return "Not available"; + } + register_pk_response::Result::TOO_FREQUENT => { + return "Too frequent"; + } + register_pk_response::Result::NOT_SUPPORT => { + return "server_not_support"; + } + register_pk_response::Result::SERVER_ERROR => { + return "Server error"; + } + register_pk_response::Result::INVALID_ID_FORMAT => { + return INVALID_FORMAT; + } + _ => {} + } + } + _ => {} + } + } + } + } + if !ok { + return UNKNOWN_ERROR; + } + } else { + return "Failed to connect to rendezvous server"; + } + "" +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs new file mode 100644 index 000000000..f726ed526 --- /dev/null +++ b/src/ui_session_interface.rs @@ -0,0 +1,1100 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; +use std::str::FromStr; +use std::sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, Mutex, RwLock, +}; +use std::time::{Duration, SystemTime}; + +use async_trait::async_trait; +use bytes::Bytes; +use rdev::{Event, EventType::*}; +use uuid::Uuid; + +use hbb_common::config::{Config, LocalConfig, PeerConfig, RS_PUB_KEY}; +use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::{self, sync::mpsc}; +use hbb_common::{allow_err, message_proto::*}; +use hbb_common::{fs, get_version_number, log, Stream}; + +use crate::client::io_loop::Remote; +use crate::client::{ + check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay, + input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key, + LoginConfigHandler, QualityStatus, KEY_MAP, +}; +use crate::common::{self, GrabState}; +use crate::keyboard; +use crate::{client::Data, client::Interface}; + +pub static IS_IN: AtomicBool = AtomicBool::new(false); + +#[derive(Clone, Default)] +pub struct Session { + pub id: String, + pub password: String, + pub args: Vec, + pub lc: Arc>, + pub sender: Arc>>>, + pub thread: Arc>>>, + pub ui_handler: T, + pub server_keyboard_enabled: Arc>, + pub server_file_transfer_enabled: Arc>, + pub server_clipboard_enabled: Arc>, +} + +#[derive(Clone)] +pub struct SessionPermissionConfig { + pub lc: Arc>, + pub server_keyboard_enabled: Arc>, + pub server_file_transfer_enabled: Arc>, + pub server_clipboard_enabled: Arc>, +} + +impl SessionPermissionConfig { + pub fn is_text_clipboard_required(&self) -> bool { + *self.server_clipboard_enabled.read().unwrap() + && *self.server_keyboard_enabled.read().unwrap() + && !self.lc.read().unwrap().disable_clipboard.v + } +} + +impl Session { + pub fn get_permission_config(&self) -> SessionPermissionConfig { + SessionPermissionConfig { + lc: self.lc.clone(), + server_keyboard_enabled: self.server_keyboard_enabled.clone(), + server_file_transfer_enabled: self.server_file_transfer_enabled.clone(), + server_clipboard_enabled: self.server_clipboard_enabled.clone(), + } + } + + pub fn is_file_transfer(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::FILE_TRANSFER) + } + + pub fn is_port_forward(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::PORT_FORWARD) + } + + pub fn is_rdp(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) + } + + pub fn set_connection_info(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.direct = Some(direct); + lc.received = received; + } + + pub fn get_view_style(&self) -> String { + self.lc.read().unwrap().view_style.clone() + } + + pub fn get_scroll_style(&self) -> String { + self.lc.read().unwrap().scroll_style.clone() + } + + pub fn get_image_quality(&self) -> String { + self.lc.read().unwrap().image_quality.clone() + } + + pub fn get_custom_image_quality(&self) -> Vec { + self.lc.read().unwrap().custom_image_quality.clone() + } + + pub fn get_peer_version(&self) -> i64 { + self.lc.read().unwrap().version.clone() + } + + pub fn get_keyboard_mode(&self) -> String { + self.lc.read().unwrap().keyboard_mode.clone() + } + + pub fn save_keyboard_mode(&mut self, value: String) { + self.lc.write().unwrap().save_keyboard_mode(value); + } + + pub fn save_view_style(&mut self, value: String) { + self.lc.write().unwrap().save_view_style(value); + } + + pub fn save_scroll_style(&mut self, value: String) { + self.lc.write().unwrap().save_scroll_style(value); + } + + pub fn save_flutter_config(&mut self, k: String, v: String) { + self.lc.write().unwrap().save_ui_flutter(k, v); + } + + pub fn get_flutter_config(&self, k: String) -> String { + self.lc.write().unwrap().get_ui_flutter(&k) + } + + pub fn toggle_option(&mut self, name: String) { + let msg = self.lc.write().unwrap().toggle_option(name.clone()); + if name == "enable-file-transfer" { + self.send(Data::ToggleClipboardFile); + } + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + pub fn get_toggle_option(&self, name: String) -> bool { + self.lc.read().unwrap().get_toggle_option(&name) + } + + pub fn is_privacy_mode_supported(&self) -> bool { + self.lc.read().unwrap().is_privacy_mode_supported() + } + + pub fn is_text_clipboard_required(&self) -> bool { + *self.server_clipboard_enabled.read().unwrap() + && *self.server_keyboard_enabled.read().unwrap() + && !self.lc.read().unwrap().disable_clipboard.v + } + + pub fn refresh_video(&self) { + self.send(Data::Message(LoginConfigHandler::refresh())); + } + + pub fn record_screen(&self, start: bool, w: i32, h: i32) { + self.send(Data::RecordScreen(start, w, h, self.id.clone())); + } + + pub fn save_custom_image_quality(&mut self, custom_image_quality: i32) { + let msg = self + .lc + .write() + .unwrap() + .save_custom_image_quality(custom_image_quality); + self.send(Data::Message(msg)); + } + + pub fn save_image_quality(&mut self, value: String) { + let msg = self.lc.write().unwrap().save_image_quality(value); + if let Some(msg) = msg { + self.send(Data::Message(msg)); + } + } + + pub fn set_custom_fps(&mut self, custom_fps: i32) { + let msg = self.lc.write().unwrap().set_custom_fps(custom_fps); + self.send(Data::Message(msg)); + } + + pub fn get_remember(&self) -> bool { + self.lc.read().unwrap().remember + } + + pub fn set_write_override( + &mut self, + job_id: i32, + file_num: i32, + is_override: bool, + remember: bool, + is_upload: bool, + ) -> bool { + self.send(Data::SetConfirmOverrideFile(( + job_id, + file_num, + is_override, + remember, + is_upload, + ))); + true + } + + pub fn supported_hwcodec(&self) -> (bool, bool) { + #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] + { + let decoder = scrap::codec::Decoder::video_codec_state(&self.id); + let mut h264 = decoder.score_h264 > 0; + let mut h265 = decoder.score_h265 > 0; + let (encoding_264, encoding_265) = self + .lc + .read() + .unwrap() + .supported_encoding + .unwrap_or_default(); + h264 = h264 && encoding_264; + h265 = h265 && encoding_265; + return (h264, h265); + } + #[allow(unreachable_code)] + (false, false) + } + + pub fn change_prefer_codec(&self) { + let msg = self.lc.write().unwrap().change_prefer_codec(); + self.send(Data::Message(msg)); + } + + pub fn restart_remote_device(&self) { + let mut lc = self.lc.write().unwrap(); + lc.restarting_remote_device = true; + let msg = lc.restart_remote_device(); + self.send(Data::Message(msg)); + } + + pub fn get_audit_server(&self, typ: String) -> String { + if self.lc.read().unwrap().conn_id <= 0 + || LocalConfig::get_option("access_token").is_empty() + { + return "".to_owned(); + } + crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + typ, + ) + } + + pub fn send_note(&self, note: String) { + let url = self.get_audit_server("conn".to_string()); + let id = self.id.clone(); + let conn_id = self.lc.read().unwrap().conn_id; + std::thread::spawn(move || { + send_note(url, id, conn_id, note); + }); + } + + pub fn is_xfce(&self) -> bool { + crate::platform::is_xfce() + } + + pub fn get_supported_keyboard_modes(&self) -> Vec { + let version = self.get_peer_version(); + common::get_supported_keyboard_modes(version) + } + + pub fn remove_port_forward(&self, port: i32) { + let mut config = self.load_config(); + config.port_forwards = config + .port_forwards + .drain(..) + .filter(|x| x.0 != port) + .collect(); + self.save_config(config); + self.send(Data::RemovePortForward(port)); + } + + pub fn add_port_forward(&mut self, port: i32, remote_host: String, remote_port: i32) { + let mut config = self.load_config(); + if config + .port_forwards + .iter() + .filter(|x| x.0 == port) + .next() + .is_some() + { + return; + } + let pf = (port, remote_host, remote_port); + config.port_forwards.push(pf.clone()); + self.save_config(config); + self.send(Data::AddPortForward(pf)); + } + + pub fn get_id(&self) -> String { + self.id.clone() + } + + pub fn get_option(&self, k: String) -> String { + if k.eq("remote_dir") { + return self.lc.read().unwrap().get_remote_dir(); + } + self.lc.read().unwrap().get_option(&k) + } + + pub fn set_option(&self, k: String, mut v: String) { + let mut lc = self.lc.write().unwrap(); + if k.eq("remote_dir") { + v = lc.get_all_remote_dir(v); + } + lc.set_option(k, v); + } + + #[inline] + pub fn load_config(&self) -> PeerConfig { + load_config(&self.id) + } + + #[inline] + pub(super) fn save_config(&self, config: PeerConfig) { + self.lc.write().unwrap().save_config(config); + } + + pub fn is_restarting_remote_device(&self) -> bool { + self.lc.read().unwrap().restarting_remote_device + } + + #[inline] + pub fn peer_platform(&self) -> String { + self.lc.read().unwrap().info.platform.clone() + } + + pub fn get_platform(&self, is_remote: bool) -> String { + if is_remote { + self.peer_platform() + } else { + whoami::platform().to_string() + } + } + + pub fn get_path_sep(&self, is_remote: bool) -> &'static str { + let p = self.get_platform(is_remote); + if &p == "Windows" { + return "\\"; + } else { + return "/"; + } + } + + pub fn input_os_password(&self, pass: String, activate: bool) { + input_os_password(pass, activate, self.clone()); + } + + pub fn get_chatbox(&self) -> String { + #[cfg(feature = "inline")] + return crate::ui::inline::get_chatbox(); + #[cfg(not(feature = "inline"))] + return "".to_owned(); + } + + pub fn send_key_event(&self, evt: &KeyEvent) { + // mode: legacy(0), map(1), translate(2), auto(3) + let mut msg_out = Message::new(); + msg_out.set_key_event(evt.clone()); + self.send(Data::Message(msg_out)); + } + + pub fn send_chat(&self, text: String) { + let mut misc = Misc::new(); + misc.set_chat_message(ChatMessage { + text, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + pub fn switch_display(&self, display: i32) { + let mut misc = Misc::new(); + misc.set_switch_display(SwitchDisplay { + display, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + + pub fn enter(&self) { + #[cfg(target_os = "windows")] + { + match &self.lc.read().unwrap().keyboard_mode as _ { + "legacy" => rdev::set_get_key_unicode(true), + "translate" => rdev::set_get_key_unicode(true), + _ => {} + } + } + + IS_IN.store(true, Ordering::SeqCst); + keyboard::client::change_grab_status(GrabState::Run); + } + + pub fn leave(&self) { + #[cfg(target_os = "windows")] + { + rdev::set_get_key_unicode(false); + } + IS_IN.store(false, Ordering::SeqCst); + keyboard::client::change_grab_status(GrabState::Wait); + } + + // flutter only TODO new input + pub fn input_key( + &self, + name: &str, + down: bool, + press: bool, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let chars: Vec = name.chars().collect(); + if chars.len() == 1 { + let key = Key::_Raw(chars[0] as _); + self._input_key(key, down, press, alt, ctrl, shift, command); + } else { + if let Some(key) = KEY_MAP.get(name) { + self._input_key(key.clone(), down, press, alt, ctrl, shift, command); + } + } + } + + // flutter only TODO new input + pub fn input_string(&self, value: &str) { + let mut key_event = KeyEvent::new(); + key_event.set_seq(value.to_owned()); + let mut msg_out = Message::new(); + msg_out.set_key_event(key_event); + self.send(Data::Message(msg_out)); + } + + pub fn handle_flutter_key_event( + &self, + _name: &str, + keycode: i32, + scancode: i32, + lock_modes: i32, + down_or_up: bool, + ) { + if scancode < 0 || keycode < 0 { + return; + } + let keycode: u32 = keycode as u32; + let scancode: u32 = scancode as u32; + + #[cfg(not(target_os = "windows"))] + let key = rdev::key_from_code(keycode) as rdev::Key; + // Windows requires special handling + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(keycode, scancode); + + let event_type = if down_or_up { + KeyPress(key) + } else { + KeyRelease(key) + }; + let event = Event { + time: SystemTime::now(), + unicode: None, + code: keycode as _, + scan_code: scancode as _, + event_type: event_type, + }; + keyboard::client::process_event(&event, Some(lock_modes)); + } + + // flutter only TODO new input + fn _input_key( + &self, + key: Key, + down: bool, + press: bool, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + let v = if press { + 3 + } else if down { + 1 + } else { + 0 + }; + let mut key_event = KeyEvent::new(); + match key { + Key::Chr(chr) => { + key_event.set_chr(chr); + } + Key::ControlKey(key) => { + key_event.set_control_key(key.clone()); + } + Key::_Raw(raw) => { + key_event.set_chr(raw); + } + } + + if v == 1 { + key_event.down = true; + } else if v == 3 { + key_event.press = true; + } + keyboard::client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + key_event.mode = KeyboardMode::Legacy.into(); + + self.send_key_event(&key_event); + } + + pub fn send_mouse( + &self, + mask: i32, + x: i32, + y: i32, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + #[allow(unused_mut)] + let mut command = command; + #[cfg(windows)] + { + if !command && crate::platform::windows::get_win_key_state() { + command = true; + } + } + + // #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (alt, ctrl, shift, command) = + keyboard::client::get_modifiers_state(alt, ctrl, shift, command); + + send_mouse(mask, x, y, alt, ctrl, shift, command, self); + // on macos, ctrl + left button down = right button down, up won't emit, so we need to + // emit up myself if peer is not macos + // to-do: how about ctrl + left from win to macos + if cfg!(target_os = "macos") { + let buttons = mask >> 3; + let evt_type = mask & 0x7; + if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" { + self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command); + } + } + } + + pub fn reconnect(&self, force_relay: bool) { + self.send(Data::Close); + let cloned = self.clone(); + // override only if true + if true == force_relay { + cloned.lc.write().unwrap().force_relay = true; + } + let mut lock = self.thread.lock().unwrap(); + lock.take().map(|t| t.join()); + *lock = Some(std::thread::spawn(move || { + io_loop(cloned); + })); + } + + pub fn get_icon_path(&self, file_type: i32, ext: String) -> String { + let mut path = Config::icon_path(); + if file_type == FileType::DirLink as i32 { + let new_path = path.join("dir_link"); + if !std::fs::metadata(&new_path).is_ok() { + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::File as i32 { + if !ext.is_empty() { + path = path.join(format!("file.{}", ext)); + } else { + path = path.join("file"); + } + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + } else if file_type == FileType::FileLink as i32 { + let new_path = path.join("file_link"); + if !std::fs::metadata(&new_path).is_ok() { + path = path.join("file"); + if !std::fs::metadata(&path).is_ok() { + allow_err!(std::fs::File::create(&path)); + } + #[cfg(windows)] + allow_err!(std::os::windows::fs::symlink_file(&path, &new_path)); + #[cfg(not(windows))] + allow_err!(std::os::unix::fs::symlink(&path, &new_path)); + } + path = new_path; + } else if file_type == FileType::DirDrive as i32 { + if cfg!(windows) { + path = fs::get_path("C:"); + } else if cfg!(target_os = "macos") { + if let Ok(entries) = fs::get_path("/Volumes/").read_dir() { + for entry in entries { + if let Ok(entry) = entry { + path = entry.path(); + break; + } + } + } + } + } + fs::get_string(&path) + } + + pub fn login(&self, password: String, remember: bool) { + self.send(Data::Login((password, remember))); + } + + pub fn new_rdp(&self) { + self.send(Data::NewRDP); + } + + pub fn close(&self) { + self.send(Data::Close); + } + + pub fn load_last_jobs(&self) { + self.clear_all_jobs(); + let pc = self.load_config(); + if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { + // no last jobs + return; + } + // TODO: can add a confirm dialog + let mut cnt = 1; + for job_str in pc.transfer.read_jobs.iter() { + if !job_str.is_empty() { + self.load_last_job(cnt, job_str); + cnt += 1; + log::info!("restore read_job: {:?}", job_str); + } + } + for job_str in pc.transfer.write_jobs.iter() { + if !job_str.is_empty() { + self.load_last_job(cnt, job_str); + cnt += 1; + log::info!("restore write_job: {:?}", job_str); + } + } + self.update_transfer_list(); + } + + pub fn elevate_direct(&self) { + self.send(Data::ElevateDirect); + } + + pub fn elevate_with_logon(&self, username: String, password: String) { + self.send(Data::ElevateWithLogon(username, password)); + } + + #[tokio::main(flavor = "current_thread")] + pub async fn switch_sides(&self) { + match crate::ipc::connect(1000, "").await { + Ok(mut conn) => { + if conn + .send(&crate::ipc::Data::SwitchSidesRequest(self.id.to_string())) + .await + .is_ok() + { + if let Ok(Some(data)) = conn.next_timeout(1000).await { + match data { + crate::ipc::Data::SwitchSidesRequest(str_uuid) => { + if let Ok(uuid) = Uuid::from_str(&str_uuid) { + let mut misc = Misc::new(); + misc.set_switch_sides_request(SwitchSidesRequest { + uuid: Bytes::from(uuid.as_bytes().to_vec()), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + self.send(Data::Message(msg_out)); + } + } + _ => {} + } + } + } + } + Err(err) => { + log::info!("server not started (will try to start): {}", err); + } + } + } + + pub fn change_resolution(&self, width: i32, height: i32) { + let mut misc = Misc::new(); + misc.set_change_resolution(Resolution { + width, + height, + ..Default::default() + }); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(Data::Message(msg)); + } + + pub fn request_voice_call(&self) { + self.send(Data::NewVoiceCall); + } + + pub fn close_voice_call(&self) { + self.send(Data::CloseVoiceCall); + } + + pub fn show_relay_hint( + &mut self, + last_recv_time: tokio::time::Instant, + msgtype: &str, + title: &str, + text: &str, + ) -> bool { + let duration = Duration::from_secs(3); + let counter_interval = 3; + let lock = self.lc.read().unwrap(); + let success_time = lock.success_time; + let direct = lock.direct.unwrap_or(false); + let received = lock.received; + drop(lock); + if let Some(success_time) = success_time { + if direct && last_recv_time.duration_since(success_time) < duration { + let retry_for_relay = direct && !received; + let retry = check_if_retry(msgtype, title, text, retry_for_relay); + if retry && !retry_for_relay { + self.lc.write().unwrap().direct_error_counter += 1; + if self.lc.read().unwrap().direct_error_counter % counter_interval == 0 { + #[cfg(feature = "flutter")] + return true; + } + } + } else { + self.lc.write().unwrap().direct_error_counter = 0; + } + } + false + } +} + +pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { + fn set_cursor_data(&self, cd: CursorData); + fn set_cursor_id(&self, id: String); + fn set_cursor_position(&self, cp: CursorPosition); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool); + fn switch_display(&self, display: &SwitchDisplay); + fn set_peer_info(&self, peer_info: &PeerInfo); // flutter + fn set_displays(&self, displays: &Vec); + fn on_connected(&self, conn_type: ConnType); + fn update_privacy_mode(&self); + fn set_permission(&self, name: &str, value: bool); + fn close_success(&self); + fn update_quality_status(&self, qs: QualityStatus); + fn set_connection_type(&self, is_secured: bool, direct: bool); + fn job_error(&self, id: i32, err: String, file_num: i32); + fn job_done(&self, id: i32, file_num: i32); + fn clear_all_jobs(&self); + fn new_message(&self, msg: String); + fn update_transfer_list(&self); + fn load_last_job(&self, cnt: i32, job_json: &str); + fn update_folder_files( + &self, + id: i32, + entries: &Vec, + path: String, + is_local: bool, + only_count: bool, + ); + fn confirm_delete_files(&self, id: i32, i: i32, name: String); + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool); + fn update_block_input_state(&self, on: bool); + fn job_progress(&self, id: i32, file_num: i32, speed: f64, finished_size: f64); + fn adapt_size(&self); + fn on_rgba(&self, data: &mut Vec); + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool); + #[cfg(any(target_os = "android", target_os = "ios"))] + fn clipboard(&self, content: String); + fn cancel_msgbox(&self, tag: &str); + fn switch_back(&self, id: &str); + fn portable_service_running(&self, running: bool); + fn on_voice_call_started(&self); + fn on_voice_call_closed(&self, reason: &str); + fn on_voice_call_waiting(&self); + fn on_voice_call_incoming(&self); + fn get_rgba(&self) -> *const u8; + fn next_rgba(&self); +} + +impl Deref for Session { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.ui_handler + } +} + +impl DerefMut for Session { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.ui_handler + } +} + +impl FileManager for Session {} + +#[async_trait] +impl Interface for Session { + fn get_login_config_handler(&self) -> Arc> { + return self.lc.clone(); + } + + fn send(&self, data: Data) { + if let Some(sender) = self.sender.read().unwrap().as_ref() { + sender.send(data).ok(); + } + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { + let direct = self.lc.read().unwrap().direct.unwrap_or_default(); + let received = self.lc.read().unwrap().received; + let retry_for_relay = direct && !received; + let retry = check_if_retry(msgtype, title, text, retry_for_relay); + self.ui_handler.msgbox(msgtype, title, text, link, retry); + } + + fn handle_login_error(&mut self, err: &str) -> bool { + handle_login_error(self.lc.clone(), err, self) + } + + fn handle_peer_info(&mut self, mut pi: PeerInfo) { + log::debug!("handle_peer_info :{:?}", pi); + pi.username = self.lc.read().unwrap().get_username(&pi); + if pi.current_display as usize >= pi.displays.len() { + pi.current_display = 0; + } + if get_version_number(&pi.version) < get_version_number("1.1.10") { + self.set_permission("restart", false); + } + if self.is_file_transfer() { + if pi.username.is_empty() { + self.on_error("No active console user logged on, please connect and logon first."); + return; + } + } else if !self.is_port_forward() { + if pi.displays.is_empty() { + self.lc.write().unwrap().handle_peer_info(&pi); + self.update_privacy_mode(); + self.msgbox("error", "Remote Error", "No Display", ""); + return; + } + let p = self.lc.read().unwrap().should_auto_login(); + if !p.is_empty() { + input_os_password(p, true, self.clone()); + } + let current = &pi.displays[pi.current_display as usize]; + self.set_display( + current.x, + current.y, + current.width, + current.height, + current.cursor_embedded, + ); + } + self.update_privacy_mode(); + // Save recent peers, then push event to flutter. So flutter can refresh peer page. + self.lc.write().unwrap().handle_peer_info(&pi); + self.set_peer_info(&pi); + if self.is_file_transfer() { + self.close_success(); + } else if !self.is_port_forward() { + self.msgbox( + "success", + "Successful", + "Connected, waiting for image...", + "", + ); + self.lc.write().unwrap().success_time = Some(tokio::time::Instant::now()); + } + self.on_connected(self.lc.read().unwrap().conn_type); + #[cfg(windows)] + { + let mut path = std::env::temp_dir(); + path.push(&self.id); + let path = path.with_extension(crate::get_app_name().to_lowercase()); + std::fs::File::create(&path).ok(); + if let Some(path) = path.to_str() { + crate::platform::windows::add_recent_document(&path); + } + } + } + + async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { + handle_hash(self.lc.clone(), pass, hash, self, peer).await; + } + + async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { + handle_login_from_ui(self.lc.clone(), password, remember, peer).await; + } + + async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) { + if !t.from_client { + self.update_quality_status(QualityStatus { + delay: Some(t.last_delay as _), + target_bitrate: Some(t.target_bitrate as _), + ..Default::default() + }); + handle_test_delay(t, peer).await; + } + } +} + +impl Session { + pub fn lock_screen(&self) { + self.send_key_event(&crate::keyboard::client::event_lock_screen()); + } + pub fn ctrl_alt_del(&self) { + self.send_key_event(&crate::keyboard::client::event_ctrl_alt_del()); + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn io_loop(handler: Session) { + let (sender, mut receiver) = mpsc::unbounded_channel::(); + *handler.sender.write().unwrap() = Some(sender.clone()); + let mut options = crate::ipc::get_options_async().await; + let mut key = options.remove("key").unwrap_or("".to_owned()); + let token = LocalConfig::get_option("access_token"); + if key.is_empty() { + key = crate::platform::get_license_key(); + } + if key.is_empty() && !option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() { + key = RS_PUB_KEY.to_owned(); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if handler.is_port_forward() { + if handler.is_rdp() { + let port = handler + .get_option("rdp_port".to_owned()) + .parse::() + .unwrap_or(3389); + std::env::set_var( + "rdp_username", + handler.get_option("rdp_username".to_owned()), + ); + std::env::set_var( + "rdp_password", + handler.get_option("rdp_password".to_owned()), + ); + log::info!("Remote rdp port: {}", port); + start_one_port_forward(handler, 0, "".to_owned(), port, receiver, &key, &token).await; + } else if handler.args.len() == 0 { + let pfs = handler.lc.read().unwrap().port_forwards.clone(); + let mut queues = HashMap::>::new(); + for d in pfs { + sender.send(Data::AddPortForward(d)).ok(); + } + loop { + match receiver.recv().await { + Some(Data::AddPortForward((port, remote_host, remote_port))) => { + if port <= 0 || remote_port <= 0 { + continue; + } + let (sender, receiver) = mpsc::unbounded_channel::(); + queues.insert(port, sender); + let handler = handler.clone(); + let key = key.clone(); + let token = token.clone(); + tokio::spawn(async move { + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + &key, + &token, + ) + .await; + }); + } + Some(Data::RemovePortForward(port)) => { + if let Some(s) = queues.remove(&port) { + s.send(Data::Close).ok(); + } + } + Some(Data::Close) => { + break; + } + Some(d) => { + for (_, s) in queues.iter() { + s.send(d.clone()).ok(); + } + } + _ => {} + } + } + } else { + let port = handler.args[0].parse::().unwrap_or(0); + if handler.args.len() != 3 + || handler.args[2].parse::().unwrap_or(0) <= 0 + || port <= 0 + { + handler.on_error("Invalid arguments, usage:

    rustdesk --port-forward remote-id listen-port remote-host remote-port"); + } + let remote_host = handler.args[1].clone(); + let remote_port = handler.args[2].parse::().unwrap_or(0); + start_one_port_forward( + handler, + port, + remote_host, + remote_port, + receiver, + &key, + &token, + ) + .await; + } + return; + } + let frame_count = Arc::new(AtomicUsize::new(0)); + let frame_count_cl = frame_count.clone(); + let ui_handler = handler.ui_handler.clone(); + let (video_sender, audio_sender) = start_video_audio_threads(move |data: &mut Vec| { + frame_count_cl.fetch_add(1, Ordering::Relaxed); + ui_handler.on_rgba(data); + }); + + let mut remote = Remote::new( + handler, + video_sender, + audio_sender, + receiver, + sender, + frame_count, + ); + remote.io_loop(&key, &token).await; + remote.sync_jobs_status_to_local().await; +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +async fn start_one_port_forward( + handler: Session, + port: i32, + remote_host: String, + remote_port: i32, + receiver: mpsc::UnboundedReceiver, + key: &str, + token: &str, +) { + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + handler.password.clone(), + port, + handler.clone(), + receiver, + key, + token, + handler.lc.clone(), + remote_host, + remote_port, + ) + .await + { + handler.on_error(&format!("Failed to listen on {}: {}", port, err)); + } + log::info!("port forward (:{}) exit", port); +} + +#[tokio::main(flavor = "current_thread")] +async fn send_note(url: String, id: String, conn_id: i32, note: String) { + let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); + allow_err!(crate::post_request(url, body.to_string(), "").await); +} diff --git a/src/ui/win_privacy.rs b/src/win_privacy.rs similarity index 100% rename from src/ui/win_privacy.rs rename to src/win_privacy.rs