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..d8781bb0c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,55 @@ +name: 🐞 Bug report +description: Thanks for taking the time to fill out this bug report! Please fill the form in **English** +labels: ["bug"] +body: + - type: textarea + id: desc + attributes: + label: Bug Description + description: A clear and concise description of what the bug is (if it's a keyboard issue, provide the keyboard mode you're using. e.g. legacy, map, translate) + 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..ae2d0aa4c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,25 @@ +name: 🛠️ Feature request +description: Suggest an idea for RustDesk +labels: ["enhancement"] +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/bridge.yml b/.github/workflows/bridge.yml new file mode 100644 index 000000000..5d6ebbb57 --- /dev/null +++ b/.github/workflows/bridge.yml @@ -0,0 +1,73 @@ +# This yaml shares the build bridge steps with ci and nightly. +name: Build flutter-rust-bridge + +on: + workflow_call: + +jobs: + generate_bridge: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-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 \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e1702a60..bba114315 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,9 @@ name: CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -14,6 +17,8 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" jobs: # ensure_cargo_fmt: diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 7825286bd..ff3f29ee7 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -1,8 +1,11 @@ -name: Flutter CI +name: Full Flutter CI on: workflow_dispatch: pull_request: + paths-ignore: + - "docs/**" + - "README.md" push: branches: - master @@ -10,43 +13,111 @@ on: - '*' paths-ignore: - ".github/**" + - "docs/**" + - "README.md" + +env: + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.7.5" + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" + VERSION: "1.2.0" + NDK_VERSION: "r23" jobs: - build: + build-for-windows: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) runs-on: ${{ matrix.job.os }} strategy: - fail-fast: false + fail-fast: true matrix: job: - # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: i686-pc-windows-msvc , os: windows-2019 } - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - # - { target: x86_64-apple-darwin , os: macos-10.15 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - # - { target: x86_64-pc-windows-msvc , os: windows-2019 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: - name: Checkout source code uses: actions/checkout@v3 - - name: Install prerequisites - shell: bash - run: | - case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; - # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; - # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; - esac + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip + Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + cargo install flutter_rust_bridge_codegen + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + build-for-macOS: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-latest, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm yasm cmake gcc wget ninja pkg-config + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -56,14 +127,14 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v1 + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} - name: Install flutter rust bridge deps + shell: bash run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -71,29 +142,682 @@ jobs: uses: lukka/run-vcpkg@v7 with: setupOnly: true - vcpkgGitCommitId: '1d4128f08e30cec31b94500840c7eca8ebc579cb' + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - shell: bash - - name: Show version information (Rust, cargo, GCC) + - name: Show version information (Rust, cargo, Clang) shell: bash run: | - gcc --version || true + clang --version || true rustup -V rustup toolchain list rustup default cargo -V rustc -V - - name: Build rustdesk ffi lib - run: cargo build --features flutter --lib - - - name: Build Flutter + - name: Build rustdesk run: | - pushd flutter - flutter pub get - flutter build linux --debug -v + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + build-vcpkg-deps-linux: + uses: ./.github/workflows/vcpkg-deps-linux.yml + + generate-bridge-linux: + uses: ./.github/workflows/bridge.yml + + build-rustdesk-android: + needs: [generate-bridge-linux] + name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + arch: x86_64, + target: aarch64-linux-android, + os: ubuntu-20.04, + extra-build-features: "", + openssl-arch: android-arm64 + } + - { + arch: x86_64, + target: armv7-linux-androideabi, + os: ubuntu-18.04, + extra-build-features: "", + openssl-arch: android-arm + } + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: ${{ env.NDK_VERSION }} + add-to-path: true + + - name: Clone deps + shell: bash + run: | + pushd /opt + git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + esac + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/arm64-v8a/*.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/armeabi-v7a/*.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + esac popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . + + build-rustdesk-lib-linux-amd64: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + # use a high level qemu-user-static + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + # not ready yet + # distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + x86_64) + # no need mock on x86_64 + export VCPKG_ROOT=/opt/artifacts/vcpkg + cargo build --lib --features hwcodec,flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-lib-linux-arm: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + # use a high level qemu-user-static + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-20.04, + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { + # arch: armv7, + # target: arm-unknown-linux-gnueabihf, + # os: ubuntu-20.04, + # use-cross: true, + # extra-build-features: "", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install local registry + run: | + mkdir -p /opt/rust-registry + cargo install cargo-local-registry + + - name: Build local registry + uses: nick-fields/retry@v2 + id: build-local-registry + continue-on-error: true + with: + max_attempts: 3 + timeout_minutes: 15 + retry_on: error + command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Restore vcpkg files + uses: actions/download-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: /opt/artifacts/vcpkg/installed + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk library for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/rust-registry:/opt/rust-registry" + shell: /bin/bash + install: | + apt update -y + echo -e "installing deps" + apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ gcc libvpx-dev tree > /dev/null + # we have libopus compiled by us. + apt remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.64.0-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-1.64.0-${{ matrix.job.target }} && ./install.sh + rm -rf rust-1.64.0-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + replace-with = 'local-registry' + + [source.local-registry] + local-registry = '/opt/rust-registry/' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + # mock + case "${{ matrix.job.arch }}" in + aarch64) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/aarch64-linux-gnu/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + ls -l /opt/artifacts/vcpkg/installed/lib/ + mkdir -p /vcpkg/installed/arm64-linux + ln -s /usr/lib/aarch64-linux-gnu /vcpkg/installed/arm64-linux/lib + ln -s /usr/include /vcpkg/installed/arm64-linux/include + export VCPKG_ROOT=/vcpkg + # disable hwcodec for compilation + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + ;; + armv7) + cp -r /opt/artifacts/vcpkg/installed/lib/* /usr/lib/arm-linux-gnueabihf/ + cp -r /opt/artifacts/vcpkg/installed/include/* /usr/include/ + mkdir -p /vcpkg/installed/arm-linux + ln -s /usr/lib/arm-linux-gnueabihf /vcpkg/installed/arm-linux/lib + ln -s /usr/include /vcpkg/installed/arm-linux/include + export VCPKG_ROOT=/vcpkg + # disable hwcodec for compilation + cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }} --release + ;; + esac + + - name: Upload Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: target/release/liblibrustdesk.so + + build-rustdesk-linux-arm: + needs: [build-rustdesk-lib-linux-arm] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 # 20.04 has more performance on arm build + strategy: + fail-fast: true + matrix: + job: + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "", + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } + # - { + # arch: aarch64, + # target: aarch64-unknown-linux-gnu, + # os: ubuntu-18.04, # just for naming package, not running host + # use-cross: true, + # extra-build-features: "flatpak", + # } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "" } + # - { arch: armv7, target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } + # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - name: Download Flutter + shell: bash + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /opt + # clone repo and reset to flutter 3.7.0 + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + # reset to flutter 3.7.0 + git fetch + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 + popd + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04-rustdesk + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + --volume "/opt/flutter-elinux:/opt/flutter-elinux" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # we use flutter-elinux to build our rustdesk + export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. + flutter-elinux doctor -v + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + armv7) + sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py + sed -i "s/x64\/release/arm\/release/g" ./build.py + ;; + esac + python3 ./build.py --flutter --hwcodec --skip-cargo + + build-rustdesk-linux-amd64: + needs: [build-rustdesk-lib-linux-amd64] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 + strategy: + fail-fast: true + matrix: + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # Setup Flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + ls -l . + export PATH=/opt/flutter/bin:$PATH + flutter doctor -v + pushd /workspace + python3 ./build.py --flutter --hwcodec --skip-cargo diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 17e338edc..adef45cc1 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -7,14 +7,20 @@ on: workflow_dispatch: env: - LLVM_VERSION: "10.0" - # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first. - FLUTTER_VERSION: "3.0.5" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.7.0" TAG_NAME: "nightly" # vcpkg version: 2022.05.10 # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + NDK_VERSION: "r23" + #signing keys env variable checks + ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' + MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}' + # To make a custom build with your own servers set the below secret values + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' jobs: build-for-windows: @@ -47,17 +53,16 @@ jobs: run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.7.0-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-flutter-release.zip Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine - mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/ + mv -Force engine/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: - toolchain: "1.62" + toolchain: stable target: ${{ matrix.job.target }} override: true - components: rustfmt profile: minimal # minimal component installation (ie, no documentation) - uses: Swatinem/rust-cache@v2 @@ -66,12 +71,7 @@ jobs: - name: Install flutter rust bridge deps run: | - dart pub global activate ffigen --version 5.0.1 - $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe - Push-Location .. - git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 - Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location - Pop-Location + cargo install flutter_rust_bridge_codegen Push-Location flutter ; flutter pub get ; Pop-Location ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -87,7 +87,7 @@ jobs: shell: bash - name: Build rustdesk - run: python3 .\build.py --portable --hwcodec --flutter + run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver - name: Sign rustdesk files uses: GermanBluefox/code-sign-action@v7 @@ -142,23 +142,55 @@ jobs: job: - { target: x86_64-apple-darwin, - os: macos-10.15, + 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 + 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 }} - cache: true - name: Install Rust toolchain uses: actions-rs/toolchain@v1 @@ -175,10 +207,7 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | - dart pub global activate ffigen --version 5.0.1 - # flutter_rust_bridge - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart @@ -192,10 +221,6 @@ jobs: run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - - name: Install cargo bundle tools - run: | - cargo install cargo-bundle - - name: Show version information (Rust, cargo, Clang) shell: bash run: | @@ -211,10 +236,23 @@ jobs: # --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}-untested-${{ matrix.job.target }}.dmg" + mv "$name" "${name%%.dmg}-${{ matrix.job.target }}.dmg" done - name: Publish DMG package @@ -226,167 +264,12 @@ jobs: 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 + uses: ./.github/workflows/vcpkg-deps-linux.yml 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 + uses: ./.github/workflows/bridge.yml - - 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 ffigen - run: | - dart pub global activate ffigen --version 5.0.1 - - - name: Install flutter rust bridge deps - shell: bash - run: | - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd - 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 - ./flutter/lib/generated_bridge.dart - ./flutter/lib/generated_bridge.freezed.dart - - build-rustdesk-android-arm64: + build-rustdesk-android: needs: [generate-bridge-linux] name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] runs-on: ${{ matrix.job.os }} @@ -397,15 +280,17 @@ jobs: - { arch: x86_64, target: aarch64-linux-android, + os: ubuntu-20.04, + extra-build-features: "", + openssl-arch: android-arm64 + } + - { + arch: x86_64, + target: armv7-linux-androideabi, os: ubuntu-18.04, extra-build-features: "", + openssl-arch: android-arm } - # - { - # arch: x86_64, - # target: armv7-linux-androideabi, - # os: ubuntu-18.04, - # extra-build-features: "", - # } steps: - name: Install dependencies run: | @@ -421,15 +306,14 @@ jobs: - uses: nttld/setup-ndk@v1 id: setup-ndk with: - ndk-version: r22b + ndk-version: ${{ env.NDK_VERSION }} add-to-path: true - - name: Download deps + - name: Clone 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 + git clone https://github.com/Kingtous/rustdesk_thirdparty_lib.git --depth=1 - name: Restore bridge files uses: actions/download-artifact@master @@ -457,7 +341,7 @@ jobs: env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} - VCPKG_ROOT: /opt/vcpkg + VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg run: | rustup target add ${{ matrix.job.target }} cargo install cargo-ndk @@ -480,16 +364,12 @@ jobs: 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 /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/arm64-v8a/*.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so # build flutter pushd flutter @@ -498,6 +378,7 @@ jobs: ;; armv7-linux-androideabi) mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp /opt/rustdesk_thirdparty_lib/android/app/src/main/jniLibs/armeabi-v7a/*.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so # build flutter pushd flutter @@ -511,6 +392,7 @@ jobs: - uses: r0adkll/sign-android-release@v1 name: Sign app APK + if: env.ANDROID_SIGNING_KEY != null id: sign-rustdesk with: releaseDirectory: ./signed-apk @@ -523,12 +405,14 @@ jobs: BUILD_TOOLS_VERSION: "30.0.2" - name: Upload Artifacts + if: env.ANDROID_SIGNING_KEY != null uses: actions/upload-artifact@master with: name: rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release-signed.apk path: ${{steps.sign-rustdesk.outputs.signedReleaseFile}} - - name: Publish apk package + - name: Publish signed apk package + if: env.ANDROID_SIGNING_KEY != null uses: softprops/action-gh-release@v1 with: prerelease: true @@ -536,6 +420,15 @@ jobs: files: | ${{steps.sign-rustdesk.outputs.signedReleaseFile}} + - name: Publish unsigned apk package + if: env.ANDROID_SIGNING_KEY == null + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + build-rustdesk-lib-linux-amd64: needs: [generate-bridge-linux, build-vcpkg-deps-linux] name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] @@ -559,6 +452,12 @@ jobs: 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 @@ -595,14 +494,6 @@ jobs: key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} cache-directories: "/opt/rust-registry" - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - name: Install local registry run: | mkdir -p /opt/rust-registry @@ -715,6 +606,13 @@ jobs: use-cross: true, extra-build-features: "", } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } # - { arch: aarch64, target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true, extra-build-features: "flatpak" } # - { # arch: armv7, @@ -754,14 +652,6 @@ jobs: override: true profile: minimal # minimal component installation (ie, no documentation) - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) - - uses: Swatinem/rust-cache@v2 with: prefix-key: rustdesk-lib-cache @@ -893,6 +783,13 @@ jobs: use-cross: true, extra-build-features: "", } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + os: ubuntu-18.04, # just for naming package, not running host + use-cross: true, + extra-build-features: "appimage", + } # - { # arch: aarch64, # target: aarch64-unknown-linux-gnu, @@ -916,7 +813,7 @@ jobs: - name: Prepare env run: | sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools mkdir -p ./target/release/ - name: Restore the rustdesk lib file @@ -931,12 +828,12 @@ jobs: # disable git safe.directory git config --global --add safe.directory "*" pushd /opt - # clone repo and reset to flutter 3.0.5 + # clone repo and reset to flutter 3.7.0 git clone https://github.com/sony/flutter-elinux.git || true pushd flutter-elinux - # reset to flutter 3.0.5 + # reset to flutter 3.7.0 git fetch - git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + git reset --hard 51a1d685901f79fbac51665a967c3a1a789ecee5 popd - uses: Kingtous/run-on-arch-action@amd64-support @@ -961,10 +858,17 @@ jobs: git config --global --add safe.directory "*" pushd /workspace # we use flutter-elinux to build our rustdesk - sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py - # Setup flutter-elinux export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux. Run doctor to check if issues here. flutter-elinux doctor -v + # Patch arm64 engine for flutter 3.6.0+ + flutter-elinux precache --linux + pushd /tmp + curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.7.0-stable.tar.xz + tar -xvf flutter_linux_3.7.0-stable.tar.xz flutter/bin/cache/artifacts/engine/linux-x64/shader_lib + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib /opt/flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + popd # edit to corresponding arch case ${{ matrix.job.arch }} in aarch64) @@ -978,7 +882,7 @@ jobs: esac python3 ./build.py --flutter --hwcodec --skip-cargo # rpm package - echo -e "start packaging" + echo -e "start packaging fedora package" pushd /workspace case ${{ matrix.job.arch }} in armv7) @@ -995,12 +899,30 @@ jobs: for name in rustdesk*??.rpm; do mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" done + # rpm suse package + echo -e "start packaging suse package" + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec + sed -i "s/linux\/x64/linux\/arm/g" ./res/rpm-flutter-suse.spec + ;; + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" + done - name: Rename rustdesk shell: bash run: | for name in rustdesk*??.deb; do - mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + cp "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" done - name: Publish debian package @@ -1011,6 +933,29 @@ jobs: tag_name: ${{ env.TAG_NAME }} files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb + + - name: Build appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + shell: bash + run: | + # set-up appimage-builder + pushd /tmp + wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage + chmod +x appimage-builder-x86_64.AppImage + sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder + popd + # run appimage-builder + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml + + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage - name: Upload Artifact uses: actions/upload-artifact@master @@ -1108,6 +1053,12 @@ jobs: 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 @@ -1122,7 +1073,7 @@ jobs: - name: Prepare env run: | sudo apt update -y - sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools mkdir -p ./target/release/ - name: Restore the rustdesk lib file @@ -1172,15 +1123,30 @@ jobs: for name in rustdesk*??.rpm; do mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" done + # rpm suse package + pushd /workspace + case ${{ matrix.job.arch }} in + armv7) + sed -i "s/64bit/32bit/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + mkdir -p /opt/artifacts/rpm + for name in rustdesk*??.rpm; do + mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-suse.rpm" + done - name: Rename rustdesk shell: bash run: | for name in rustdesk*??.deb; do - mv "$name" "${name%%.deb}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb" + # 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 @@ -1244,6 +1210,29 @@ jobs: 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 @@ -1370,7 +1359,7 @@ jobs: 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 + flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.flatpak com.rustdesk.RustDesk - name: Publish flatpak package uses: softprops/action-gh-release@v1 diff --git a/.github/workflows/vcpkg-deps-linux.yml b/.github/workflows/vcpkg-deps-linux.yml new file mode 100644 index 000000000..89381114a --- /dev/null +++ b/.github/workflows/vcpkg-deps-linux.yml @@ -0,0 +1,94 @@ +name: Build vcpkg dependencies for linux clients + +on: + workflow_call: + +jobs: + build-vcpkg-deps-linux: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + # - { arch: armv7, os: ubuntu-20.04 } + - { arch: x86_64, os: ubuntu-20.04 } + - { arch: aarch64, os: ubuntu-20.04 } + steps: + - name: Create vcpkg artifacts folder + run: mkdir -p /opt/artifacts + + - name: Cache Vcpkg + id: cache-vcpkg + uses: actions/cache@v3 + with: + path: /opt/artifacts + key: vcpkg-${{ matrix.job.arch }} + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "/opt/artifacts" + dockerRunArgs: | + --volume "/opt/artifacts:/artifacts" + shell: /bin/bash + install: | + apt update -y + case "${{ matrix.job.arch }}" in + x86_64) + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev + ;; + 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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1ecea7af8..f17f8155b 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ cert.pfx sciter.dll **pdb src/bridge_generated.rs +src/bridge_generated.io.rs *deb rustdesk *.cache @@ -26,6 +27,7 @@ rustdesk appimage/AppDir appimage/*.AppImage appimage/appimage-build +appimage/*.xz # flutter flutter/linux/build/** flutter/linux/cmake-build-debug/** @@ -37,5 +39,12 @@ flatpak/.flatpak-builder/shared-modules/** flatpak/.flatpak-builder/shared-modules/*.tar.xz flatpak/.flatpak-builder/debian-binary flatpak/build/** +flatpak/repo/** +flatpak/*.flatpak # bridge file lib/generated_bridge.dart +# vscode devcontainer +.gitconfig +.vscode-server/ +.ssh +.devcontainer/.* diff --git a/Cargo.lock b/Cargo.lock index cdccce4cb..e632114af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" dependencies = [ "gimli", ] @@ -17,12 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "ahash" version = "0.7.6" @@ -45,11 +39,13 @@ dependencies = [ [[package]] name = "allo-isolate" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb993621e6bf1b67591005b0adad126159a0ab31af379743906158aed5330d0" +checksum = "8ed55848be9f41d44c79df6045b680a74a78bc579e0813f7f196cd7928e22fb1" dependencies = [ + "anyhow", "atomic", + "chrono", ] [[package]] @@ -109,12 +105,12 @@ dependencies = [ [[package]] name = "android_logger" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e9dd62f37dea550caf48c77591dc50bd1a378ce08855be1a0c42a97b7550fb" +checksum = "8619b80c242aa7bd638b5c7ddd952addeecb71f69c75e33f1d47b2804f8f883a" dependencies = [ "android_log-sys", - "env_logger 0.9.3", + "env_logger 0.10.0", "log", "once_cell", ] @@ -139,15 +135,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" [[package]] name = "arboard" -version = "2.1.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb" +checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" dependencies = [ "clipboard-win", "core-graphics 0.22.3", @@ -156,6 +152,7 @@ dependencies = [ "objc", "objc-foundation", "objc_id", + "once_cell", "parking_lot 0.12.1", "thiserror", "winapi 0.3.9", @@ -164,13 +161,12 @@ dependencies = [ [[package]] name = "async-broadcast" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" dependencies = [ "event-listener", "futures-core", - "parking_lot 0.12.1", ] [[package]] @@ -198,6 +194,18 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock", + "autocfg 1.1.0", + "blocking", + "futures-lite", +] + [[package]] name = "async-io" version = "1.12.0" @@ -213,19 +221,18 @@ dependencies = [ "parking", "polling", "slab", - "socket2 0.4.7", + "socket2 0.4.9", "waker-fn", "windows-sys 0.42.0", ] [[package]] name = "async-lock" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" dependencies = [ "event-listener", - "futures-lite", ] [[package]] @@ -248,13 +255,13 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +checksum = "3b015a331cc64ebd1774ba119538573603427eaace0a1950c423ab971f903796" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -265,35 +272,35 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.59" +version = "0.1.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" +checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[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.7", "libc", ] [[package]] name = "atk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +checksum = "11ad703eb64dc058024f0e57ccfa069e15a413b98dbd50a1a950e743b7f11148" dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -309,9 +316,9 @@ dependencies = [ [[package]] name = "atomic-waker" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" +checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599" [[package]] name = "atty" @@ -319,7 +326,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ - "hermit-abi", + "hermit-abi 0.1.19", "libc", "winapi 0.3.9", ] @@ -341,24 +348,30 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ "addr2line", "cc", "cfg-if 1.0.0", "libc", - "miniz_oxide 0.5.4", + "miniz_oxide", "object", "rustc-demangle", ] [[package]] -name = "base64" -version = "0.13.1" +name = "base-x" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" [[package]] name = "bindgen" @@ -375,12 +388,12 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "regex", "rustc-hash", "shlex", - "which 4.3.0", + "which", ] [[package]] @@ -395,14 +408,42 @@ dependencies = [ "lazy_static", "lazycell", "peeking_take_while", - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "regex", "rustc-hash", "shlex", - "syn", + "syn 1.0.109", ] +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2 1.0.51", + "quote 1.0.23", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", + "which", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -429,9 +470,9 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -463,25 +504,31 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.3.2" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] [[package]] -name = "bumpalo" -version = "3.11.1" +name = "build-target" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytemuck" -version = "1.12.3" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" [[package]] name = "byteorder" @@ -491,33 +538,34 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" dependencies = [ - "serde 1.0.149", + "serde 1.0.154", ] [[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.7", "libc", + "once_cell", "thiserror", ] [[package]] name = "cairo-sys-rs" -version = "0.15.1" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +checksum = "7c48f4af05fabdcfa9658178e1326efa061853f040ce7d72e33af6885196f421" dependencies = [ - "glib-sys 0.15.10", + "glib-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -534,11 +582,11 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" +checksum = "6031a462f977dd38968b6f23378356512feeace69cef817e1a4475108093cec3" dependencies = [ - "serde 1.0.149", + "serde 1.0.154", ] [[package]] @@ -547,7 +595,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.149", + "serde 1.0.154", ] [[package]] @@ -558,35 +606,35 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver 1.0.14", - "serde 1.0.149", - "serde_json 1.0.89", + "semver 1.0.16", + "serde 1.0.154", + "serde_json 1.0.94", ] [[package]] name = "cbindgen" -version = "0.23.0" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6d248e3ca02f3fbfabcb9284464c596baec223a26d91bbf44a5a62ddb0d900" +checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" dependencies = [ "clap 3.2.23", - "heck 0.4.0", + "heck 0.4.1", "indexmap", "log", - "proc-macro2", - "quote", - "serde 1.0.149", - "serde_json 1.0.89", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "serde 1.0.154", + "serde_json 1.0.94", + "syn 1.0.109", "tempfile", - "toml", + "toml 0.5.11", ] [[package]] name = "cc" -version = "1.0.77" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" dependencies = [ "jobserver", ] @@ -644,9 +692,9 @@ dependencies = [ [[package]] name = "cidr-utils" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355d5b5df67e58b523953d0c1a8d3d2c05f5af51f1332b0199b9c92263614ed0" +checksum = "fdfa36f04861d39453affe1cf084ce2d6554021a84eb6f31ebdeafb6fb92a01c" dependencies = [ "debug-helper", "num-bigint", @@ -657,9 +705,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +checksum = "77ed9a53e5d4d9c573ae844bfac6872b159cb1d1585a83b29e7a64b7eef7332a" dependencies = [ "glob", "libc", @@ -690,7 +738,7 @@ dependencies = [ "atty", "bitflags", "clap_derive", - "clap_lex", + "clap_lex 0.2.4", "indexmap", "once_cell", "strsim 0.10.0", @@ -698,17 +746,30 @@ dependencies = [ "textwrap 0.16.0", ] +[[package]] +name = "clap" +version = "4.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" +dependencies = [ + "bitflags", + "clap_lex 0.3.2", + "is-terminal", + "strsim 0.10.0", + "termcolor", +] + [[package]] name = "clap_derive" version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ - "heck 0.4.0", + "heck 0.4.1", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -720,6 +781,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clipboard" version = "0.1.0" @@ -727,16 +797,16 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.149", + "serde 1.0.154", "serde_derive", "thiserror", ] [[package]] name = "clipboard-win" -version = "4.4.2" +version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" dependencies = [ "error-code", "str-buf", @@ -808,6 +878,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colored" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" +dependencies = [ + "atty", + "lazy_static", + "winapi 0.3.9", +] + [[package]] name = "combine" version = "4.6.6" @@ -820,9 +901,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" dependencies = [ "crossbeam-utils", ] @@ -833,9 +914,19 @@ version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.149", + "serde 1.0.154", "thiserror", - "toml", + "toml 0.5.11", +] + +[[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]] @@ -947,27 +1038,26 @@ dependencies = [ [[package]] name = "cpal" -version = "0.13.5" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74117836a5124f3629e4b474eed03e479abaf98988b4bb317e29f08cfe0e4116" +checksum = "f342c1b63e185e9953584ff2199726bf53850d96610a310e3aca09e9405a2d0b" 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.2", + "ndk 0.7.0", + "ndk-context", "oboe", - "parking_lot 0.11.2", + "once_cell", + "parking_lot 0.12.1", "stdweb", "thiserror", "web-sys", - "winapi 0.3.9", + "windows 0.37.0", ] [[package]] @@ -990,9 +1080,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -1000,9 +1090,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", @@ -1011,14 +1101,14 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", "crossbeam-utils", - "memoffset 0.7.1", + "memoffset 0.8.0", "scopeguard", ] @@ -1034,13 +1124,19 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1053,12 +1149,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.4" +version = "3.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1631ca6e3c59112501a9d87fd86f21591ff77acd31331e8a73f8d80a65bbdd71" +checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" dependencies = [ - "nix 0.26.1", - "windows-sys 0.42.0", + "nix 0.26.2", + "windows-sys 0.45.0", ] [[package]] @@ -1069,9 +1165,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "cxx" -version = "1.0.83" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" +checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72" dependencies = [ "cc", "cxxbridge-flags", @@ -1081,45 +1177,45 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.83" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" +checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613" dependencies = [ "cc", "codespan-reporting", "once_cell", - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "scratch", - "syn", + "syn 1.0.109", ] [[package]] name = "cxxbridge-flags" -version = "1.0.83" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" +checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97" [[package]] name = "cxxbridge-macro" -version = "1.0.83" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" +checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] name = "dark-light" -version = "0.2.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413487ef345ab5cdfbf23e66070741217a701bce70f2f397a54221b4f2b6056a" +checksum = "a62007a65515b3cd88c733dd3464431f05d2ad066999a824259d8edc3cf6f645" dependencies = [ "dconf_rs", "detect-desktop-environment", - "dirs", + "dirs 4.0.0", "objc", "rust-ini", "web-sys", @@ -1146,10 +1242,10 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "strsim 0.10.0", - "syn", + "syn 1.0.109", ] [[package]] @@ -1159,8 +1255,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.109", +] + +[[package]] +name = "dart-sys" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8b5680b5c2cc52f50acb2457d9b3a3b58adcca785db13a0e3655626f601de6" +dependencies = [ + "cc", ] [[package]] @@ -1284,9 +1389,9 @@ dependencies = [ [[package]] name = "dbus" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" dependencies = [ "libc", "libdbus-sys", @@ -1295,9 +1400,9 @@ dependencies = [ [[package]] name = "dbus-crossroads" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "554114296d012b33fdaf362a733db6dc5f73c4c9348b8b620ddd42e61b406e30" +checksum = "3a4c83437187544ba5142427746835061b330446ca8902eabd70e4afb8f76de0" dependencies = [ "dbus", ] @@ -1316,8 +1421,9 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" [[package]] name = "default-net" -version = "0.11.0" -source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e349ed1e06fb344a7dd8b5a676375cf671b31e8900075dd2be816efc063a63" dependencies = [ "libc", "memalloc", @@ -1326,13 +1432,14 @@ dependencies = [ ] [[package]] -name = "deflate" -version = "0.8.6" +name = "delegate" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" +checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd" dependencies = [ - "adler32", - "byteorder", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -1341,9 +1448,9 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -1372,6 +1479,16 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if 0.1.10", + "dirs-sys", +] + [[package]] name = "dirs" version = "4.0.0" @@ -1413,6 +1530,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "discard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0" + [[package]] name = "dispatch" version = "0.2.0" @@ -1428,6 +1551,29 @@ dependencies = [ "libloading", ] +[[package]] +name = "dlopen" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" +dependencies = [ + "dlopen_derive", + "lazy_static", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "dlopen_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" +dependencies = [ + "libc", + "quote 0.6.13", + "syn 0.15.44", +] + [[package]] name = "dlv-list" version = "0.3.0" @@ -1442,7 +1588,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.149", + "serde 1.0.154", "strsim 0.10.0", ] @@ -1465,25 +1611,25 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.149", + "serde 1.0.154", "serde_derive", "thiserror", ] [[package]] name = "ed25519" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ "signature", ] [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "embed-resource" @@ -1493,16 +1639,16 @@ checksum = "e62abb876c07e4754fae5c14cafa77937841f01740637e17d78dc04352f32a5e" dependencies = [ "cc", "rustc_version 0.4.0", - "toml", + "toml 0.5.11", "vswhom", "winreg 0.10.1", ] [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ "cfg-if 1.0.0", ] @@ -1513,12 +1659,11 @@ version = "0.0.14" dependencies = [ "core-graphics 0.22.3", "hbb_common", - "libc", "log", "objc", "pkg-config", "rdev", - "serde 1.0.149", + "serde 1.0.154", "serde_derive", "tfc", "unicode-segmentation", @@ -1527,34 +1672,34 @@ dependencies = [ [[package]] name = "enum-map" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a56d54c8dd9b3ad34752ed197a4eb2a6601bc010808eb097a04a58ae4c43e1" +checksum = "50c25992259941eb7e57b936157961b217a4fc8597829ddef0596d6c3cd86e1a" dependencies = [ "enum-map-derive", ] [[package]] name = "enum-map-derive" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27" +checksum = "2a4da76b3b6116d758c7ba93f7ec6a35d2e2cf24feda76c6e38a375f4d5c59f2" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] name = "enum_dispatch" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb" +checksum = "11f36e95862220b211a6e2aa5eca09b4fa391b13cd52ceb8035a24bf65a79de2" dependencies = [ "once_cell", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -1564,7 +1709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" dependencies = [ "enumflags2_derive", - "serde 1.0.149", + "serde 1.0.154", ] [[package]] @@ -1573,9 +1718,9 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -1601,6 +1746,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "epoll" version = "4.3.1" @@ -1618,10 +1776,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e" dependencies = [ "proc-macro-error", - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "rustversion", - "syn", + "syn 1.0.109", "synstructure", ] @@ -1636,6 +1794,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "errno" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "errno-dragonfly" version = "0.1.2" @@ -1673,43 +1842,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] -name = "failure" -version = "0.1.8" +name = "exr" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +checksum = "e8af5ef47e2ed89d23d0ecbc1b681b30390069de70260937877514377fc24feb" dependencies = [ - "backtrace", + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "smallvec", + "threadpool", + "zune-inflate", ] [[package]] name = "fastrand" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] [[package]] -name = "field-offset" -version = "0.3.4" +name = "fern" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92" +checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a" dependencies = [ - "memoffset 0.6.5", - "rustc_version 0.3.3", + "chrono", + "colored", + "log", +] + +[[package]] +name = "field-offset" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cf3a800ff6e860c863ca6d4b16fd999db8b752819c1606884047b73e468535" +dependencies = [ + "memoffset 0.8.0", + "rustc_version 0.4.0", ] [[package]] name = "filetime" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -1719,71 +1906,102 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "miniz_oxide 0.6.2", + "miniz_oxide", ] [[package]] name = "flexi_logger" -version = "0.22.6" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c76a80dd14a27fc3d8bc696502132cb52b3f227256fd8601166c3a35e45f409" +checksum = "6eae57842a8221ef13f1f207632d786a175dd13bd8fbdc8be9d852f7c9cf1046" dependencies = [ - "ansi_term", - "atty", "chrono", "crossbeam-channel", "crossbeam-queue", "glob", + "is-terminal", "lazy_static", "log", + "nu-ansi-term", "regex", - "rustversion", "thiserror", - "time 0.3.9", +] + +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.5", ] [[package]] name = "flutter_rust_bridge" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d71ff30fc2ae7c18508b517488a89051d81e3bfc4d48d4a6cf54b5dab789" dependencies = [ "allo-isolate", "anyhow", + "build-target", + "bytemuck", + "cc", + "chrono", + "console_error_panic_hook", + "dart-sys", "flutter_rust_bridge_macros", + "js-sys", "lazy_static", + "libc", + "log", "parking_lot 0.12.1", "threadpool", + "wasm-bindgen", + "web-sys", ] [[package]] name = "flutter_rust_bridge_codegen" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a75a72411f0c5b480e4671417f52780172053128cf87d5614a9757d7680a0" dependencies = [ "anyhow", + "atty", "cargo_metadata", "cbindgen", + "chrono", + "clap 3.2.23", "convert_case", + "delegate", "enum_dispatch", - "env_logger 0.9.3", + "fern", + "itertools 0.10.5", "lazy_static", "log", "pathdiff", - "quote", + "quote 1.0.23", "regex", - "serde 1.0.149", + "serde 1.0.154", "serde_yaml", - "structopt", - "syn", + "strum_macros 0.24.3", + "syn 1.0.109", "tempfile", "thiserror", - "toml", + "toml 0.5.11", + "topological-sort", ] [[package]] name = "flutter_rust_bridge_macros" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.68.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6187d1635afede47c23a9979f85c3dc57e3391eb9c690b7fe95715bf945da72" [[package]] name = "fnv" @@ -1815,6 +2033,19 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fruitbasket" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898289b8e0528c84fb9b88f15ac9d5109bcaf23e0e49bb6f64deee0d86b6a351" +dependencies = [ + "dirs 2.0.2", + "objc", + "objc-foundation", + "objc_id", + "time 0.1.45", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -1845,9 +2076,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" dependencies = [ "futures-channel", "futures-core", @@ -1860,9 +2091,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" dependencies = [ "futures-core", "futures-sink", @@ -1870,15 +2101,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" [[package]] name = "futures-executor" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" dependencies = [ "futures-core", "futures-task", @@ -1887,9 +2118,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" [[package]] name = "futures-lite" @@ -1908,32 +2139,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] name = "futures-sink" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" [[package]] name = "futures-task" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" [[package]] name = "futures-util" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" dependencies = [ "futures-channel", "futures-core", @@ -1947,74 +2178,92 @@ dependencies = [ "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[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.7", "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.7", "libc", ] [[package]] name = "gdk-pixbuf-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +checksum = "3092cf797a5f1210479ea38070d9ae8a5b8e9f8f1be9f32f4643c529c7d70016" dependencies = [ - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] [[package]] name = "gdk-sys" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +checksum = "d76354f97a913e55b984759a997b693aa7dc71068c9e98bcce51aa167a0a5c5a" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", "pkg-config", "system-deps 6.0.3", ] +[[package]] +name = "gdkwayland-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4511710212ed3020b61a8622a37aa6f0dd2a84516575da92e9b96928dcbe83ba" +dependencies = [ + "gdk-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "pkg-config", + "system-deps 6.0.3", +] + +[[package]] +name = "gdkx11-sys" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa2bf8b5b8c414bc5d05e48b271896d0fd3ddb57464a3108438082da61de6af" +dependencies = [ + "gdk-sys", + "glib-sys 0.16.3", + "libc", + "system-deps 6.0.3", + "x11 2.21.0", +] + [[package]] name = "generic-array" version = "0.14.6" @@ -2042,46 +2291,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", ] [[package]] name = "gimli" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" [[package]] name = "gio" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +checksum = "2a1c84b4534a290a29160ef5c6eff2a9c95833111472e824fc5cb78b513dd092" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-io", - "gio-sys 0.15.10", - "glib 0.15.12", + "futures-util", + "gio-sys", + "glib 0.16.7", "libc", "once_cell", + "pin-project-lite", + "smallvec", "thiserror", ] -[[package]] -name = "gio-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" -dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", - "libc", - "system-deps 6.0.3", - "winapi 0.3.9", -] - [[package]] name = "gio-sys" version = "0.16.3" @@ -2116,29 +2367,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.15.12" +version = "0.16.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "glib-macros 0.15.11", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", - "libc", - "once_cell", - "smallvec", - "thiserror", -] - -[[package]] -name = "glib" -version = "0.16.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cd04d150a2c63e6779f43aec7e04f5374252479b7bed5f45146d9c0e821f161" +checksum = "ddd4df61a866ed7259d6189b8bcb1464989a77f1d85d25d002279bbe9dd38b2f" dependencies = [ "bitflags", "futures-channel", @@ -2146,7 +2377,7 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys 0.16.3", + "gio-sys", "glib-macros 0.16.3", "glib-sys 0.16.3", "gobject-sys 0.16.3", @@ -2164,27 +2395,12 @@ checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" dependencies = [ "anyhow", "heck 0.3.3", - "itertools", + "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "glib-macros" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" -dependencies = [ - "anyhow", - "heck 0.4.0", - "proc-macro-crate 1.2.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -2194,12 +2410,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" dependencies = [ "anyhow", - "heck 0.4.0", - "proc-macro-crate 1.2.1", + "heck 0.4.1", + "proc-macro-crate 1.3.1", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -2212,16 +2428,6 @@ dependencies = [ "system-deps 1.3.2", ] -[[package]] -name = "glib-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" -dependencies = [ - "libc", - "system-deps 6.0.3", -] - [[package]] name = "glib-sys" version = "0.16.3" @@ -2234,9 +2440,9 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" @@ -2249,17 +2455,6 @@ dependencies = [ "system-deps 1.3.2", ] -[[package]] -name = "gobject-sys" -version = "0.15.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" -dependencies = [ - "glib-sys 0.15.10", - "libc", - "system-deps 6.0.3", -] - [[package]] name = "gobject-sys" version = "0.16.3" @@ -2288,7 +2483,7 @@ dependencies = [ "gstreamer-sys", "libc", "muldiv", - "num-rational", + "num-rational 0.3.2", "once_cell", "paste", "pretty-hex", @@ -2406,9 +2601,9 @@ dependencies = [ [[package]] name = "gtk" -version = "0.15.5" +version = "0.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +checksum = "e4d3507d43908c866c805f74c9dd593c0ce7ba5c38e576e41846639cdcd4bee6" dependencies = [ "atk", "bitflags", @@ -2418,7 +2613,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gio", - "glib 0.15.12", + "glib 0.16.7", "gtk-sys", "gtk3-macros", "libc", @@ -2429,17 +2624,17 @@ dependencies = [ [[package]] name = "gtk-sys" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +checksum = "89b5f8946685d5fe44497007786600c2f368ff6b1e61a16251c89f72a97520a3" dependencies = [ "atk-sys", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys 0.15.10", - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "gio-sys", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "pango-sys", "system-deps 6.0.3", @@ -2447,23 +2642,23 @@ dependencies = [ [[package]] name = "gtk3-macros" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" +checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff" dependencies = [ "anyhow", - "proc-macro-crate 1.2.1", + "proc-macro-crate 1.3.1", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] name = "h2" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" +checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" dependencies = [ "bytes", "fnv", @@ -2478,6 +2673,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2492,33 +2696,38 @@ name = "hbb_common" version = "0.1.0" dependencies = [ "anyhow", + "backtrace", "bytes", "chrono", "confy", "directories-next", "dirs-next", - "env_logger 0.9.3", + "env_logger 0.10.0", "filetime", + "flexi_logger", "futures", "futures-util", "lazy_static", + "libc", "log", "mac_address", "machine-uid", + "osascript", "protobuf", "protobuf-codegen", "quinn", "rand 0.8.5", "regex", - "serde 1.0.149", + "serde 1.0.154", "serde_derive", - "serde_json 1.0.89", + "serde_json 1.0.94", "socket2 0.3.19", "sodiumoxide", + "sysinfo", "tokio", "tokio-socks", "tokio-util", - "toml", + "toml 0.7.2", "winapi 0.3.9", "zstd", ] @@ -2534,9 +2743,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" @@ -2547,6 +2756,21 @@ dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -2561,13 +2785,13 @@ checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", - "itoa 1.0.4", + "itoa 1.0.6", ] [[package]] @@ -2602,21 +2826,21 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#e819484c4c010199f2a0977bdf306b4edbeafbae" +source = "git+https://github.com/21pages/hwcodec#d55f7761ef692fae738259d8c14506d901eb824c" dependencies = [ "bindgen 0.59.2", "cc", "log", - "serde 1.0.149", + "serde 1.0.154", "serde_derive", - "serde_json 1.0.89", + "serde_json 1.0.94", ] [[package]] name = "hyper" -version = "0.14.23" +version = "0.14.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" +checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c" dependencies = [ "bytes", "futures-channel", @@ -2627,9 +2851,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.4", + "itoa 1.0.6", "pin-project-lite", - "socket2 0.4.7", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -2691,17 +2915,20 @@ dependencies = [ [[package]] name = "image" -version = "0.23.14" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" dependencies = [ "bytemuck", "byteorder", "color_quant", - "num-iter", - "num-rational", + "exr", + "gif", + "jpeg-decoder", + "num-rational 0.4.1", "num-traits 0.2.15", "png", + "scoped_threadpool", "tiff", ] @@ -2728,8 +2955,8 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", ] [[package]] @@ -2774,6 +3001,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "io-lifetimes" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" +dependencies = [ + "libc", + "windows-sys 0.45.0", +] + [[package]] name = "iovec" version = "0.1.4" @@ -2785,9 +3022,21 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" +checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" + +[[package]] +name = "is-terminal" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] [[package]] name = "itertools" @@ -2798,6 +3047,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" @@ -2806,9 +3064,9 @@ checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" [[package]] name = "itoa" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "jni" @@ -2824,6 +3082,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" @@ -2832,24 +3104,27 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" dependencies = [ "libc", ] [[package]] name = "jpeg-decoder" -version = "0.1.22" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" dependencies = [ "wasm-bindgen", ] @@ -2864,6 +3139,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.154", + "unicode-segmentation", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -2877,12 +3163,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.7", "gtk", "gtk-sys", "libappindicator-sys", @@ -2891,9 +3183,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", @@ -2902,15 +3194,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.138" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libdbus-sys" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b" +checksum = "9f8d7ae751e1cb825c840ae5e682f59b098cdfd213c350ac268b61449a5f58a0" dependencies = [ "pkg-config", ] @@ -2927,9 +3219,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.26.0" +version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17be42160017e0ae993c03bfdab4ecb6f82ce3f8d515bd8da8fdf18d10703663" +checksum = "1745b20bfc194ac12ef828f144f0ec2d4a7fe993281fa3567a0bd4969aee6890" dependencies = [ "bitflags", "libc", @@ -2941,9 +3233,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.25.0" +version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cbf1a1dfd69a48cb60906399fa1d17f1b75029ef51c0789597be792dfd0bcd5" +checksum = "5ced94199e6e44133431374e4043f34e1f0697ebfb7b7d6c244a65bfaedf0e31" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -2952,9 +3244,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.19.2" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c73f96f9ca34809692c4760cfe421225860aa000de50edab68a16221fd27cc1" +checksum = "84e423d9c619c908ce9b4916080e65ab586ca55b8c4939379f15e6e72fb43842" dependencies = [ "libpulse-sys", "pkg-config", @@ -2962,9 +3254,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.19.3" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991e6bd0efe2a36e6534e136e7996925e4c1a8e35b7807fe533f2beffff27c30" +checksum = "2191e6880818d1df4cf72eac8e91dce7a5a52ba0da4b2a5cdafabc22b937eadb" dependencies = [ "libc", "num-derive", @@ -2995,10 +3287,38 @@ dependencies = [ ] [[package]] -name = "link-cplusplus" -version = "1.0.7" +name = "libxdo" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +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.21.0", +] + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" dependencies = [ "cc", ] @@ -3009,6 +3329,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.4.9" @@ -3119,6 +3445,15 @@ dependencies = [ "autocfg 1.1.0", ] +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg 1.1.0", +] + [[package]] name = "mime" version = "0.3.16" @@ -3131,34 +3466,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - -[[package]] -name = "miniz_oxide" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" -dependencies = [ - "adler", - "autocfg 1.1.0", -] - -[[package]] -name = "miniz_oxide" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.6.2" @@ -3189,14 +3496,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -3249,12 +3556,41 @@ dependencies = [ "glob", ] +[[package]] +name = "muda" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee091608fe840d80c6b25e8f838964b264ee8e02e82ae0a38b2d900464d1582" +dependencies = [ + "cocoa", + "crossbeam-channel", + "gdk", + "gdk-pixbuf", + "gtk", + "keyboard-types", + "libxdo", + "objc", + "once_cell", + "png", + "thiserror", + "windows-sys 0.45.0", +] + [[package]] name = "muldiv" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + [[package]] name = "ndk" version = "0.5.0" @@ -3291,7 +3627,7 @@ dependencies = [ "jni-sys", "ndk-sys 0.4.1+23.1.7779620", "num_enum", - "raw-window-handle 0.5.0", + "raw-window-handle 0.5.1", "thiserror", ] @@ -3316,21 +3652,6 @@ dependencies = [ "ndk-sys 0.2.2", ] -[[package]] -name = "ndk-glue" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.6.0", - "ndk-context", - "ndk-macro", - "ndk-sys 0.3.0", -] - [[package]] name = "ndk-macro" version = "0.3.0" @@ -3338,10 +3659,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", - "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -3419,35 +3740,23 @@ dependencies = [ [[package]] name = "nix" -version = "0.25.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ - "autocfg 1.1.0", "bitflags", "cfg-if 1.0.0", "libc", - "memoffset 0.6.5", + "memoffset 0.7.1", "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]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", @@ -3455,13 +3764,23 @@ dependencies = [ [[package]] name = "ntapi" -version = "0.3.7" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi 0.3.9", +] + [[package]] name = "num-bigint" version = "0.4.3" @@ -3475,9 +3794,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d" dependencies = [ "num-traits 0.2.15", ] @@ -3488,9 +3807,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.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -3504,10 +3823,10 @@ dependencies = [ ] [[package]] -name = "num-iter" -version = "0.1.43" +name = "num-rational" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" dependencies = [ "autocfg 1.1.0", "num-integer", @@ -3516,9 +3835,9 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg 1.1.0", "num-integer", @@ -3545,42 +3864,33 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] [[package]] name = "num_enum" -version = "0.5.7" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.7" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ - "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -3590,6 +3900,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -3603,6 +3914,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" @@ -3614,9 +3934,9 @@ dependencies = [ [[package]] name = "object" -version = "0.29.0" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "memchr", ] @@ -3627,7 +3947,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" dependencies = [ - "jni", + "jni 0.19.0", "ndk 0.6.0", "ndk-context", "num-derive", @@ -3646,9 +3966,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.16.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl-probe" @@ -3668,14 +3988,26 @@ dependencies = [ [[package]] name = "ordered-stream" -version = "0.1.2" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ca8c99d73c6e92ac1358f9f692c22c0bfd9c4701fa086f5d365c0d4ea818ea" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] +[[package]] +name = "os-version" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8a1fed76ac765e39058ca106b6229a93c5a60292a1bd4b602ce2be11e1c020" +dependencies = [ + "anyhow", + "plist", + "uname", + "winapi 0.3.9", +] + [[package]] name = "os_str_bytes" version = "6.4.1" @@ -3683,19 +4015,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] -name = "padlock" -version = "0.2.0" +name = "osascript" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10569378a1dacd9f30dbe7ae49e054d2c45dc2f8ee49899903e09c3924e8b6f" +checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" +dependencies = [ + "serde 1.0.154", + "serde_derive", + "serde_json 1.0.94", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" [[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.7", "libc", "once_cell", "pango-sys", @@ -3703,12 +4047,12 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.15.10" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +checksum = "9e134909a9a293e04d2cc31928aa95679c5e4df954d0b85483159bd20d8f047f" dependencies = [ - "glib-sys 0.15.10", - "gobject-sys 0.15.10", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", "libc", "system-deps 6.0.3", ] @@ -3742,7 +4086,7 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core 0.8.5", + "parking_lot_core 0.8.6", ] [[package]] @@ -3752,14 +4096,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.5", + "parking_lot_core 0.9.7", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ "cfg-if 1.0.0", "instant", @@ -3771,22 +4115,22 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] name = "paste" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" [[package]] name = "pathdiff" @@ -3806,16 +4150,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" -[[package]] -name = "pest" -version = "2.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" -dependencies = [ - "thiserror", - "ucd-trie", -] - [[package]] name = "phf" version = "0.7.24" @@ -3869,9 +4203,9 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -3893,29 +4227,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] -name = "png" -version = "0.16.8" +name = "plist" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +checksum = "ffac6a51110e97610dd3ac73e34a65b27e56a1e305df41bad1f616d8e1cb22f4" +dependencies = [ + "base64", + "indexmap", + "line-wrap", + "quick-xml", + "serde 1.0.154", + "time 0.3.20", +] + +[[package]] +name = "png" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" dependencies = [ "bitflags", "crc32fast", - "deflate", - "miniz_oxide 0.3.7", + "flate2", + "miniz_oxide", ] [[package]] name = "polling" -version = "2.5.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" +checksum = "7e1f879b2998099c2d69ab9605d145d5b661195627eccc680002c4918a7fb6fa" dependencies = [ "autocfg 1.1.0", + "bitflags", "cfg-if 1.0.0", + "concurrent-queue", "libc", "log", - "wepoll-ffi", - "windows-sys 0.42.0", + "pin-project-lite", + "windows-sys 0.45.0", ] [[package]] @@ -3945,18 +4295,17 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "thiserror", - "toml", + "toml_edit", ] [[package]] @@ -3966,9 +4315,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.51", + "quote 1.0.23", + "syn 1.0.109", "version_check", ] @@ -3978,16 +4327,25 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "version_check", ] [[package]] name = "proc-macro2" -version = "1.0.47" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" dependencies = [ "unicode-ident", ] @@ -4032,7 +4390,7 @@ dependencies = [ "protobuf-support", "tempfile", "thiserror", - "which 4.3.0", + "which", ] [[package]] @@ -4058,17 +4416,25 @@ dependencies = [ ] [[package]] -name = "quinn" -version = "0.8.5" +name = "quick-xml" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b435e71d9bfa0d8889927231970c51fb89c58fa63bffcab117c9c7a41e5ef8f" +checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445cbfe2382fa023c4f2f3c7e1c95c03dcc1df2bf23cebcb2b13e1402c4394d1" dependencies = [ "bytes", - "futures-channel", - "futures-util", - "fxhash", + "pin-project-lite", "quinn-proto", "quinn-udp", + "rustc-hash", "rustls", "thiserror", "tokio", @@ -4078,17 +4444,16 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.8.4" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fce546b9688f767a57530652488420d419a8b1f44a478b451c3d1ab6d992a55" +checksum = "72ef4ced82a24bb281af338b9e8f94429b6eca01b4e66d899f40031f074e74c9" dependencies = [ "bytes", - "fxhash", "rand 0.8.5", "ring", + "rustc-hash", "rustls", "rustls-native-certs", - "rustls-pemfile 0.2.1", "slab", "thiserror", "tinyvec", @@ -4098,25 +4463,33 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.1.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b07946277141531aea269befd949ed16b2c85a780ba1043244eda0969e538e54" +checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4" dependencies = [ - "futures-util", "libc", "quinn-proto", - "socket2 0.4.7", - "tokio", + "socket2 0.4.9", "tracing", + "windows-sys 0.42.0", ] [[package]] name = "quote" -version = "1.0.21" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" dependencies = [ - "proc-macro2", + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2 1.0.51", ] [[package]] @@ -4272,18 +4645,15 @@ dependencies = [ [[package]] name = "raw-window-handle" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" -dependencies = [ - "cty", -] +checksum = "4f851a03551ceefd30132e447f07f96cb7011d6b658374f3aed847333adb5559" [[package]] name = "rayon" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" dependencies = [ "either", "rayon-core", @@ -4291,9 +4661,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -4304,24 +4674,25 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/fufesou/rdev#196b589573f90703a601e6b105dd7c917fc388f9" +source = "git+https://github.com/fufesou/rdev#eaa35ff9af22891b4aae3a0a5e83472c16177cd8" dependencies = [ "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", + "mio 0.8.6", "strum 0.24.1", "strum_macros 0.24.3", "widestring 1.0.2", "winapi 0.3.9", - "x11 2.20.1", + "x11 2.21.0", ] [[package]] @@ -4364,9 +4735,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ "aho-corasick", "memchr", @@ -4379,15 +4750,6 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi 0.3.9", -] - [[package]] name = "repng" version = "0.2.2" @@ -4400,9 +4762,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" +checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9" dependencies = [ "base64", "bytes", @@ -4422,9 +4784,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile 1.0.1", - "serde 1.0.149", - "serde_json 1.0.89", + "rustls-pemfile", + "serde 1.0.154", + "serde_json 1.0.94", "serde_urlencoded", "tokio", "tokio-rustls", @@ -4446,7 +4808,7 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", + "spin 0.5.2", "untrusted", "web-sys", "winapi 0.3.9", @@ -4498,12 +4860,12 @@ dependencies = [ [[package]] name = "runas" -version = "0.2.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a620b0994a180cdfa25c0439e6d58c0628272571501880d626ffff58e96a0799" +checksum = "ed87390fefd18965ff20baae5aeb9913bcf82d2b59dc04c0f6d8f17f7be56ff2" dependencies = [ "cc", - "which 3.1.1", + "which", ] [[package]] @@ -4538,11 +4900,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc_version" -version = "0.3.3" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" dependencies = [ - "semver 0.11.0", + "semver 0.9.0", ] [[package]] @@ -4551,14 +4913,14 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.14", + "semver 1.0.16", ] [[package]] name = "rustdesk" version = "1.2.0" dependencies = [ - "android_logger 0.11.1", + "android_logger 0.11.3", "arboard", "async-process", "async-trait", @@ -4568,7 +4930,7 @@ dependencies = [ "cfg-if 1.0.0", "chrono", "cidr-utils", - "clap 3.2.23", + "clap 4.1.8", "clipboard", "cocoa", "core-foundation 0.9.3", @@ -4581,22 +4943,21 @@ dependencies = [ "dbus-crossroads", "default-net", "dispatch", + "dlopen", "enigo", - "errno", + "errno 0.3.0", "evdev", - "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", - "glib 0.16.5", - "gtk", + "fruitbasket", "hbb_common", + "hex", "hound", + "image", "impersonate_system", "include_dir", - "jni", + "jni 0.19.0", "lazy_static", - "libappindicator", - "libc", "libpulse-binding", "libpulse-simple-binding", "mac_address", @@ -4605,6 +4966,8 @@ dependencies = [ "mouce", "num_cpus", "objc", + "objc_id", + "os-version", "parity-tokio-ipc", "rdev", "repng", @@ -4616,17 +4979,17 @@ dependencies = [ "samplerate", "sciter-rs", "scrap", - "serde 1.0.149", + "serde 1.0.154", "serde_derive", - "serde_json 1.0.89", + "serde_json 1.0.94", "sha2", "shared_memory", "shutdown_hooks", "simple_rc", "sys-locale", - "sysinfo", "system_shutdown", - "tray-item", + "tao", + "tray-icon", "trayicon", "url", "uuid", @@ -4645,7 +5008,7 @@ name = "rustdesk-portable-packer" version = "0.1.0" dependencies = [ "brotli", - "dirs", + "dirs 4.0.0", "embed-resource", "md5", ] @@ -4666,10 +5029,24 @@ dependencies = [ ] [[package]] -name = "rustls" -version = "0.20.7" +name = "rustix" +version = "0.36.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" +checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" +dependencies = [ + "bitflags", + "errno 0.2.8", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "log", "ring", @@ -4684,40 +5061,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" dependencies = [ "openssl-probe", - "rustls-pemfile 1.0.1", + "rustls-pemfile", "schannel", "security-framework", ] [[package]] name = "rustls-pemfile" -version = "0.2.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" -dependencies = [ - "base64", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ "base64", ] [[package]] name = "rustversion" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" [[package]] name = "same-file" @@ -4739,12 +5113,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] @@ -4764,6 +5137,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.1.0" @@ -4775,7 +5154,7 @@ name = "scrap" version = "0.5.0" dependencies = [ "android_logger 0.10.1", - "bindgen 0.59.2", + "bindgen 0.64.0", "block", "cfg-if 1.0.0", "dbus", @@ -4785,16 +5164,15 @@ 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.149", - "serde_json 1.0.89", + "serde 1.0.154", + "serde_json 1.0.94", "target_build_utils", "tracing", "webm", @@ -4803,9 +5181,9 @@ dependencies = [ [[package]] name = "scratch" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" [[package]] name = "sct" @@ -4819,9 +5197,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ "bitflags", "core-foundation 0.9.3", @@ -4832,9 +5210,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ "core-foundation-sys 0.8.3", "libc", @@ -4842,30 +5220,27 @@ dependencies = [ [[package]] name = "semver" -version = "0.11.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ "semver-parser", ] [[package]] name = "semver" -version = "1.0.14" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" dependencies = [ - "serde 1.0.149", + "serde 1.0.154", ] [[package]] name = "semver-parser" -version = "0.10.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" @@ -4875,22 +5250,22 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.149" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" +checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.149" +version = "1.0.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" +checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -4907,24 +5282,33 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" dependencies = [ - "itoa 1.0.4", + "itoa 1.0.6", "ryu", - "serde 1.0.149", + "serde 1.0.154", ] [[package]] name = "serde_repr" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" +checksum = "395627de918015623b32e7669714206363a7fc00382bf477e72c1f7533e8eafc" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + +[[package]] +name = "serde_spanned" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +dependencies = [ + "serde 1.0.154", ] [[package]] @@ -4934,9 +5318,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.4", + "itoa 1.0.6", "ryu", - "serde 1.0.149", + "serde 1.0.154", ] [[package]] @@ -4947,10 +5331,19 @@ checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", - "serde 1.0.149", + "serde 1.0.154", "yaml-rust", ] +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + [[package]] name = "sha1" version = "0.10.5" @@ -4962,6 +5355,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + [[package]] name = "sha2" version = "0.10.6" @@ -5000,9 +5399,9 @@ checksum = "6057adedbec913419c92996f395ba69931acbd50b7d56955394cd3f7bedbfa45" [[package]] name = "signal-hook" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9" dependencies = [ "libc", "signal-hook-registry", @@ -5010,9 +5409,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] @@ -5023,13 +5422,19 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +[[package]] +name = "simd-adler32" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18" + [[package]] name = "simple_rc" version = "0.1.0" dependencies = [ "confy", "hbb_common", - "serde 1.0.149", + "serde 1.0.154", "serde_derive", "walkdir", ] @@ -5042,9 +5447,9 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg 1.1.0", ] @@ -5087,9 +5492,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi 0.3.9", @@ -5104,7 +5509,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.149", + "serde 1.0.154", ] [[package]] @@ -5113,6 +5518,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dccf47db1b41fa1573ed27ccf5e08e3ca771cb994f776668c5ebda893b248fc" +dependencies = [ + "lock_api", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -5121,9 +5535,52 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stdweb" -version = "0.1.3" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" +checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5" +dependencies = [ + "discard", + "rustc_version 0.2.3", + "stdweb-derive", + "stdweb-internal-macros", + "stdweb-internal-runtime", + "wasm-bindgen", +] + +[[package]] +name = "stdweb-derive" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef" +dependencies = [ + "proc-macro2 1.0.51", + "quote 1.0.23", + "serde 1.0.154", + "serde_derive", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11" +dependencies = [ + "base-x", + "proc-macro2 1.0.51", + "quote 1.0.23", + "serde 1.0.154", + "serde_derive", + "serde_json 1.0.94", + "sha1 0.6.1", + "syn 1.0.109", +] + +[[package]] +name = "stdweb-internal-runtime" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0" [[package]] name = "str-buf" @@ -5149,30 +5606,6 @@ 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" @@ -5192,9 +5625,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ "heck 0.3.3", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -5203,21 +5636,32 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ - "heck 0.4.0", - "proc-macro2", - "quote", + "heck 0.4.1", + "proc-macro2 1.0.51", + "quote 1.0.23", "rustversion", - "syn", + "syn 1.0.109", ] [[package]] name = "syn" -version = "1.0.105" +version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" +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.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2 1.0.51", + "quote 1.0.23", "unicode-ident", ] @@ -5227,30 +5671,30 @@ 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.51", + "quote 1.0.23", + "syn 1.0.109", + "unicode-xid 0.2.4", ] [[package]] name = "sys-locale" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3358acbb4acd4146138b9bda219e904a6bb5aaaa237f8eed06f4d6bc1580ecee" +checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee" dependencies = [ "js-sys", "libc", "wasm-bindgen", "web-sys", - "winapi 0.3.9", + "windows-sys 0.45.0", ] [[package]] name = "sysinfo" -version = "0.24.7" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb4ebf3d49308b99e6e9dc95e989e2fdbdc210e4f67c39db0bb89ba927001c" +checksum = "d3e847e2de7a137c8c2cede5095872dbb00f4f9bf34d061347e36b43322acd56" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", @@ -5293,7 +5737,7 @@ dependencies = [ "strum 0.18.0", "strum_macros 0.18.0", "thiserror", - "toml", + "toml 0.5.11", "version-compare 0.0.10", ] @@ -5304,19 +5748,75 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff" dependencies = [ "cfg-expr", - "heck 0.4.0", + "heck 0.4.1", "pkg-config", - "toml", + "toml 0.5.11", "version-compare 0.1.1", ] [[package]] name = "system_shutdown" -version = "3.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "035e081d603551d8d78db27d2232913269c749ea67648c369100049820406a14" +checksum = "7567f71160af5e9abfb4f5a21532cf2174cefe91ac5c336419295685a695cc66" dependencies = [ - "winapi 0.3.9", + "windows 0.44.0", + "zbus", +] + +[[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.7", + "glib-sys 0.16.3", + "gtk", + "image", + "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", + "raw-window-handle 0.5.1", + "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.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -5338,23 +5838,22 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" dependencies = [ "cfg-if 1.0.0", "fastrand", - "libc", "redox_syscall", - "remove_dir_all", - "winapi 0.3.9", + "rustix", + "windows-sys 0.42.0", ] [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -5386,8 +5885,9 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/fufesou/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a" +source = "git+https://github.com/fufesou/The-Fat-Controller#1dba9a39c089ac9a7853b9dd5399c1d4aa3157d3" dependencies = [ + "anyhow", "core-graphics 0.22.3", "unicode-segmentation", "winapi 0.3.9", @@ -5396,22 +5896,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -5425,12 +5925,12 @@ dependencies = [ [[package]] name = "tiff" -version = "0.6.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" dependencies = [ + "flate2", "jpeg-decoder", - "miniz_oxide 0.4.4", "weezl", ] @@ -5447,21 +5947,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.9" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ - "itoa 1.0.4", - "libc", - "num_threads", + "itoa 1.0.6", + "serde 1.0.154", + "time-core", "time-macros", ] [[package]] -name = "time-macros" -version = "0.2.4" +name = "time-core" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] [[package]] name = "tinyvec" @@ -5474,28 +5983,28 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.23.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" +checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" dependencies = [ "autocfg 1.1.0", "bytes", "libc", "memchr", - "mio 0.8.5", + "mio 0.8.6", "num_cpus", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2 0.4.7", + "socket2 0.4.9", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -5504,9 +6013,9 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -5538,9 +6047,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" dependencies = [ "bytes", "futures-core", @@ -5556,13 +6065,53 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ - "serde 1.0.149", + "serde 1.0.154", ] +[[package]] +name = "toml" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" +dependencies = [ + "serde 1.0.154", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde 1.0.154", +] + +[[package]] +name = "toml_edit" +version = "0.19.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825" +dependencies = [ + "indexmap", + "serde 1.0.154", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[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" @@ -5587,9 +6136,9 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -5612,21 +6161,22 @@ dependencies = [ ] [[package]] -name = "tray-item" -version = "0.7.1" +name = "tray-icon" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0914b62e00e8f51241806cb9f9c4ea6b10c75d94cae02c89278de6f4b98c7d0f" +checksum = "f87445e3a107818c17d87e8369db30a6fc25539bface8351efe2132b22e47dbc" dependencies = [ "cocoa", "core-graphics 0.22.3", - "gtk", + "crossbeam-channel", + "dirs-next", "libappindicator", - "libc", + "muda", "objc", - "objc-foundation", - "objc_id", - "padlock", - "winapi 0.3.9", + "once_cell", + "png", + "thiserror", + "windows-sys 0.45.0", ] [[package]] @@ -5640,9 +6190,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "typenum" @@ -5650,12 +6200,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "ucd-trie" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" - [[package]] name = "uds_windows" version = "1.0.2" @@ -5667,16 +6211,25 @@ dependencies = [ ] [[package]] -name = "unicode-bidi" -version = "0.3.8" +name = "uname" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c" [[package]] name = "unicode-ident" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-normalization" @@ -5689,9 +6242,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" [[package]] name = "unicode-width" @@ -5699,6 +6252,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + [[package]] name = "unicode-xid" version = "0.2.4" @@ -5720,14 +6279,14 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde 1.0.149", + "serde 1.0.154", ] [[package]] name = "uuid" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ "getrandom", ] @@ -5826,9 +6385,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -5836,24 +6395,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" +checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -5863,32 +6422,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ - "quote", + "quote 1.0.23", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "wayland-client" @@ -5947,8 +6506,8 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "xml-rs", ] @@ -5965,9 +6524,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" dependencies = [ "js-sys", "wasm-bindgen", @@ -6016,30 +6575,11 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - [[package]] name = "which" -version = "3.1.1" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" -dependencies = [ - "failure", - "libc", -] - -[[package]] -name = "which" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", "libc", @@ -6048,11 +6588,10 @@ dependencies = [ [[package]] name = "whoami" -version = "1.2.3" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6631b6a2fd59b1841b622e8f1a7ad241ef0a46f2d580464ce8140ac94cbd571" +checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3" dependencies = [ - "bumpalo", "wasm-bindgen", "web-sys", ] @@ -6156,6 +6695,52 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.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.51", + "quote 1.0.23", + "syn 1.0.109", +] + +[[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.51", + "quote 1.0.23", + "syn 1.0.109", +] + [[package]] name = "windows-service" version = "0.4.0" @@ -6181,19 +6766,6 @@ dependencies = [ "windows_x86_64_msvc 0.28.0", ] -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", -] - [[package]] name = "windows-sys" version = "0.42.0" @@ -6201,19 +6773,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_x86_64_msvc 0.42.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.1", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" @@ -6235,15 +6831,15 @@ checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" @@ -6265,15 +6861,15 @@ checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" @@ -6295,15 +6891,15 @@ checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" @@ -6325,21 +6921,21 @@ checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" [[package]] name = "windows_x86_64_gnu" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" @@ -6361,15 +6957,15 @@ checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" [[package]] name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "winit" @@ -6387,9 +6983,9 @@ dependencies = [ "lazy_static", "libc", "log", - "mio 0.8.5", + "mio 0.8.6", "ndk 0.5.0", - "ndk-glue 0.5.2", + "ndk-glue", "ndk-sys 0.2.2", "objc", "parking_lot 0.11.2", @@ -6404,6 +7000,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "winnow" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee7b2c67f962bf5042bfd8b6a916178df33a26eec343ae064cb8e069f638fa6f" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.6.2" @@ -6428,17 +7033,14 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c" dependencies = [ - "toml", + "toml 0.5.11", ] [[package]] name = "wol-rs" -version = "0.9.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f97e69b28b256ccfb02472c25057132e234aa8368fea3bb0268def564ce1f2" -dependencies = [ - "clap 3.2.23", -] +checksum = "48dc5e486e34a31515518d370cdd8bf59ec696323fe8f92b858e43942e84a765" [[package]] name = "ws2_32-sys" @@ -6470,9 +7072,9 @@ dependencies = [ [[package]] name = "x11" -version = "2.20.1" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2638d5b9c17ac40575fb54bb461a4b1d2a8d1b4ffcc4ff237d254ec59ddeb82" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" dependencies = [ "libc", "pkg-config", @@ -6480,25 +7082,35 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.1" +version = "2.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" dependencies = [ - "lazy_static", "libc", + "once_cell", "pkg-config", ] [[package]] name = "x11rb" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e99be55648b3ae2a52342f9a870c0e138709a3493261ce9b469afe6e4df6d8a" +checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" dependencies = [ "gethostname", - "nix 0.22.3", + "nix 0.24.3", "winapi 0.3.9", "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +dependencies = [ + "nix 0.24.3", ] [[package]] @@ -6527,13 +7139,13 @@ dependencies = [ [[package]] name = "zbus" -version = "3.6.2" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938ea6da98c75c2c37a86007bd17fd8e208cbec24e086108c87ece98e9edec0d" +checksum = "20aae5dd5b051971cd2f49f9f3b860e57b2b495ba5ba254eaec42d34ede57e97" dependencies = [ "async-broadcast", - "async-channel", "async-executor", + "async-fs", "async-io", "async-lock", "async-recursion", @@ -6541,20 +7153,20 @@ dependencies = [ "async-trait", "byteorder", "derivative", - "dirs", + "dirs 4.0.0", "enumflags2", "event-listener", "futures-core", "futures-sink", "futures-util", "hex", - "nix 0.25.1", + "nix 0.26.2", "once_cell", "ordered-stream", "rand 0.8.5", - "serde 1.0.149", + "serde 1.0.154", "serde_repr", - "sha1", + "sha1 0.10.5", "static_assertions", "tracing", "uds_windows", @@ -6566,24 +7178,25 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.6.2" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" +checksum = "9264b3a1bcf5503d4e0348b6e7efe1da58d4f92a913c15ed9e63b52de85faaa1" dependencies = [ - "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.51", + "quote 1.0.23", "regex", - "syn", + "syn 1.0.109", + "zvariant_utils", ] [[package]] name = "zbus_names" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c737644108627748a660d038974160e0cbb62605536091bdfa28fd7f64d43c8" +checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" dependencies = [ - "serde 1.0.149", + "serde 1.0.154", "static_assertions", "zvariant", ] @@ -6618,27 +7231,48 @@ dependencies = [ ] [[package]] -name = "zvariant" -version = "3.9.0" +name = "zune-inflate" +version = "0.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8c89c183461e11867ded456db252eae90874bc6769b7adbea464caa777e51" +checksum = "a01728b79fb9b7e28a8c11f715e1cd8dc2cda7416a007d66cac55cebb3a8ac6b" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zvariant" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe4914a985446d6fd287019b5fceccce38303d71407d9e6e711d44954a05d8" dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.149", + "serde 1.0.154", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" -version = "3.9.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" +checksum = "34c20260af4b28b3275d6676c7e2a6be0d4332e8e0aba4616d34007fd84e462a" dependencies = [ - "proc-macro-crate 1.2.1", - "proc-macro2", - "quote", - "syn", + "proc-macro-crate 1.3.1", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b22993dbc4d128a17a3b6c92f1c63872dd67198537ee728d8b5d7c40640a8b" +dependencies = [ + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] diff --git a/Cargo.toml b/Cargo.toml index 82c35de79..bf81664e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ inline = [] hbbs = [] cli = [] with_rc = ["simple_rc"] +flutter_texture_render = [] appimage = [] flatpak = [] use_samplerate = ["samplerate"] @@ -43,35 +44,34 @@ 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" +runas = "1.0" 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 } async-trait = "0.1" uuid = { version = "1.0", features = ["v4"] } -clap = "3.0" +clap = "4.1" rpassword = "7.0" -base64 = "0.13" -sysinfo = "0.24" +base64 = "0.21" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } -default-net = { git = "https://github.com/Kingtous/default-net" } -wol-rs = "0.9.1" -flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } -errno = "0.2.8" +default-net = "0.12.0" +wol-rs = "1.0" +flutter_rust_bridge = { version = "1.61.1", optional = true } +errno = "0.3" rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } +dlopen = "0.1" +hex = "0.4.3" 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" +cpal = "0.14" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] machine-uid = "0.2" @@ -81,15 +81,14 @@ sys-locale = "0.2" enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } ctrlc = "3.2" -arboard = "2.0" +arboard = "3.2" #minreq = { version = "2.4", features = ["punycode", "https-native"] } -system_shutdown = "3.0.0" +system_shutdown = "4.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"] } +winapi = { version = "0.3", features = ["winuser", "wincrypt"] } winreg = "0.10" windows-service = "0.4" virtual_display = { path = "libs/virtual_display" } @@ -104,8 +103,14 @@ dispatch = "0.2" core-foundation = "0.9" core-graphics = "0.22" include_dir = "0.7.2" -tray-item = "0.7" # looks better than trayicon -dark-light = "0.2" +dark-light = "1.0" +fruitbasket = "0.10.0" +objc_id = "0.1.1" + +[target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies] +tray-icon = "0.4" +tao = { git = "https://github.com/tauri-apps/tao", branch = "muda" } +image = "0.24" [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.25" } @@ -116,19 +121,17 @@ mouce = { git="https://github.com/fufesou/mouce.git" } evdev = { git="https://github.com/fufesou/evdev" } dbus = "0.9" dbus-crossroads = "0.5" -gtk = "0.15" -libappindicator = "0.7" -glib = "0.16.5" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.11" jni = "0.19" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } +flutter_rust_bridge = "1.61.1" [workspace] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/simple_rc", "libs/portable"] +exclude = ["vdi/host"] [package.metadata.winres] LegalCopyright = "Copyright © 2022 Purslane, Inc." @@ -143,7 +146,8 @@ winapi = { version = "0.3", features = [ "winnt" ] } cc = "1.0" hbb_common = { path = "libs/hbb_common" } simple_rc = { path = "libs/simple_rc", optional = true } -flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } +flutter_rust_bridge_codegen = "1.61.1" +os-version = "0.2" [dev-dependencies] hound = "3.5" @@ -154,7 +158,6 @@ identifier = "com.carriez.rustdesk" icon = ["res/32x32.png", "res/128x128.png", "res/128x128@2x.png"] deb_depends = ["libgtk-3-0", "libxcb-randr0", "libxdo3", "libxfixes3", "libxcb-shape0", "libxcb-xfixes0", "libasound2", "libsystemd0", "curl", "libvdpau1", "libva2"] osx_minimum_system_version = "10.14" -resources = ["res/mac-tray-light.png","res/mac-tray-dark.png"] #https://github.com/johnthagen/min-sized-rust [profile.release] diff --git a/README.md b/README.md index 79255e455..4e3b309c5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@

- RustDesk - Dit fjernskrivebord
+ 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] | [Dansk]
+ [Українська] | [č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

@@ -19,7 +19,7 @@ Yet another remote desktop software, written in Rust. Works out of the box, no c RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for help getting started. -[**How does RustDesk work?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) [**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) @@ -31,15 +31,23 @@ RustDesk welcomes contribution from everyone. See [`docs/CONTRIBUTING.md`](docs/ ## Free Public Servers -Below are the servers you are using for free, it may change along the time. If you are not close to one of these, your network may be slow. +Below are the servers you are using for free, they may change over time. If you are not close to one of these, your network may be slow. | Location | Vendor | Specification | | --------- | ------------- | ------------------ | | Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | | 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 | +| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | + +## 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 @@ -198,7 +206,7 @@ Please ensure that you are running these commands from the root of the RustDesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client ## Snapshot diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml new file mode 100644 index 000000000..87c854d5d --- /dev/null +++ b/appimage/AppImageBuilder-aarch64.yml @@ -0,0 +1,91 @@ +# 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' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-security main restricted + universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + include: + - libc6 + - libgtk-3-0 + - libxcb-randr0 + - libxdo3 + - libxfixes3 + - libxcb-shape0 + - libxcb-xfixes0 + - libasound2 + - libsystemd0 + - curl + - libva-drm2 + - libva-x11-2 + - libvdpau1 + - libgstreamer-plugins-base1.0-0 + - libwayland-cursor0 + - libwayland-egl1 + - libpulse0 + 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..8f72afd08 --- /dev/null +++ b/appimage/AppImageBuilder-x86_64.yml @@ -0,0 +1,93 @@ +# 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://archive.ubuntu.com/ubuntu/ bionic-security 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 + - libwayland-cursor0 + - libwayland-egl1 + - libpulse0 + 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/build.py b/build.py index ca91b3581..4a39f596d 100755 --- a/build.py +++ b/build.py @@ -18,14 +18,11 @@ exe_path = 'target/release/' + hbb_name flutter_win_target_dir = 'flutter/build/windows/runner/Release/' skip_cargo = False -def custom_os_system(cmd): - err = os._system(cmd) +def system2(cmd): + err = os.system(cmd) if err != 0: print(f"Error occurred when executing: {cmd}. Exiting.") sys.exit(-1) -# replace prebuilt os.system -os._system = os.system -os.system = custom_os_system def get_version(): with open("Cargo.toml", encoding="utf-8") as fh: @@ -40,7 +37,7 @@ def parse_rc_features(feature): 'IddDriver': { 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64.zip', 'checksum_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/checksum_md5', - 'exclude': ['README.md'], + 'exclude': ['README.md', 'certmgr.exe', 'install_cert_runas_admin.bat'], }, 'PrivacyMode': { 'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1' @@ -99,6 +96,11 @@ def make_parser(): 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', @@ -139,8 +141,8 @@ def generate_build_script_for_docker(): # build rustdesk ./build.py --flutter --hwcodec ''') - os.system("chmod +x /tmp/build.sh") - os.system("bash /tmp/build.sh") + system2("chmod +x /tmp/build.sh") + system2("bash /tmp/build.sh") def download_extract_features(features, res_dir): @@ -234,15 +236,18 @@ def get_features(args): features.append('hwcodec') if args.flutter: features.append('flutter') + features.append('flutter_texture_render') if args.flatpak: features.append('flatpak') + if args.appimage: + 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) + system2('/bin/rm -rf %s' % control_file_path) content = """Package: rustdesk Version: %s @@ -260,91 +265,89 @@ Description: A remote control software. def ffi_bindgen_function_refactor(): # workaround ffigen - os.system( + system2( 'sed -i "s/ffi.NativeFunction> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit") - os.system('mkdir -p tmpdeb/DEBIAN') + system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) - os.system('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') + system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - os.system('dpkg-deb -b tmpdeb rustdesk.deb;') + system2('dpkg-deb -b tmpdeb rustdesk.deb;') - os.system('/bin/rm -rf tmpdeb/') - os.system('/bin/rm -rf ../res/DEBIAN/control') + system2('/bin/rm -rf tmpdeb/') + system2('/bin/rm -rf ../res/DEBIAN/control') os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) os.chdir("..") def build_flutter_dmg(version, features): if not skip_cargo: - os.system(f'cargo build --features {features} --lib --release') + # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project + system2(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') # copy dylib - os.system( + system2( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") - # ffi_bindgen_function_refactor() - # limitations from flutter rust bridge - os.system('sed -i "" "s/char \*\*rustdesk_core_main(int \*args_len);//" flutter/macos/Runner/bridge_generated.h') os.chdir('flutter') - os.system('flutter build macos --release') - os.system( - "create-dmg rustdesk.dmg ./build/macos/Build/Products/Release/rustdesk.app") + system2('flutter build macos --release') + system2( + "create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg") os.chdir("..") def build_flutter_arch_manjaro(version, features): if not skip_cargo: - os.system(f'cargo build --features {features} --lib --release') + system2(f'cargo build --features {features} --lib --release') ffi_bindgen_function_refactor() os.chdir('flutter') - os.system('flutter build linux --release') - os.system('strip build/linux/x64/release/bundle/lib/librustdesk.so') + system2('flutter build linux --release') + system2('strip build/linux/x64/release/bundle/lib/librustdesk.so') os.chdir('../res') - os.system('HBB=`pwd`/.. FLUTTER=1 makepkg -f') + system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f') def build_flutter_windows(version, features): if not skip_cargo: - os.system(f'cargo build --features {features} --lib --release') + system2(f'cargo build --features {features} --lib --release') if not os.path.exists("target/release/librustdesk.dll"): print("cargo build failed, please check rust source code.") exit(-1) os.chdir('flutter') - os.system('flutter build windows --release') + system2('flutter build windows --release') os.chdir('..') shutil.copy2('target/release/deps/dylib_virtual_display.dll', flutter_win_target_dir) os.chdir('libs/portable') - os.system('pip3 install -r requirements.txt') - os.system( + system2('pip3 install -r requirements.txt') + system2( f'python3 ./generate.py -f ../../{flutter_win_target_dir} -o . -e ../../{flutter_win_target_dir}/rustdesk.exe') os.chdir('../..') if os.path.exists('./rustdesk_portable.exe'): @@ -365,22 +368,15 @@ def main(): parser = make_parser() args = parser.parse_args() - shutil.copy2('Cargo.toml', 'Cargo.toml.bk') - shutil.copy2('src/main.rs', 'src/main.rs.bk') - if windows: - txt = open('src/main.rs', encoding='utf8').read() - with open('src/main.rs', 'wt', encoding='utf8') as fh: - fh.write(txt.replace( - '//#![windows_subsystem', '#![windows_subsystem')) if os.path.exists(exe_path): os.unlink(exe_path) if os.path.isfile('/usr/bin/pacman'): - os.system('git checkout src/ui/common.tis') + system2('git checkout src/ui/common.tis') version = get_version() features = ','.join(get_features(args)) flutter = args.flutter if not flutter: - os.system('python3 res/inline-sciter.py') + system2('python3 res/inline-sciter.py') print(args.skip_cargo) if args.skip_cargo: skip_cargo = True @@ -388,55 +384,55 @@ def main(): if windows: # build virtual display dynamic library os.chdir('libs/virtual_display/dylib') - os.system('cargo build --release') + system2('cargo build --release') os.chdir('../../..') if flutter: build_flutter_windows(version, features) return - os.system('cargo build --release --features ' + features) - # os.system('upx.exe target/release/rustdesk.exe') - os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe') + system2('cargo build --release --features ' + features) + # system2('upx.exe target/release/rustdesk.exe') + system2('mv target/release/rustdesk.exe target/release/RustDesk.exe') pa = os.environ.get('P') if pa: - os.system( + system2( f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com ' 'target\\release\\rustdesk.exe') else: print('Not signed') - os.system( + system2( f'cp -rf target/release/RustDesk.exe rustdesk-{version}-win7-install.exe') elif os.path.isfile('/usr/bin/pacman'): # pacman -S -needed base-devel - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) + system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version) if flutter: build_flutter_arch_manjaro(version, features) else: - os.system('cargo build --release --features ' + features) - os.system('git checkout src/ui/common.tis') - os.system('strip target/release/rustdesk') - os.system('ln -s res/pacman_install && ln -s res/PKGBUILD') - os.system('HBB=`pwd` makepkg -f') - os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % ( + system2('cargo build --release --features ' + features) + system2('git checkout src/ui/common.tis') + system2('strip target/release/rustdesk') + system2('ln -s res/pacman_install && ln -s res/PKGBUILD') + system2('HBB=`pwd` makepkg -f') + system2('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % ( version, version)) # pacman -U ./rustdesk.pkg.tar.zst elif os.path.isfile('/usr/bin/yum'): - os.system('cargo build --release --features ' + features) - os.system('strip target/release/rustdesk') - os.system( + system2('cargo build --release --features ' + features) + system2('strip target/release/rustdesk') + system2( "sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba res/rpm.spec') - os.system( + system2('HBB=`pwd` rpmbuild -ba res/rpm.spec') + system2( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % ( version, version)) # yum localinstall rustdesk.rpm elif os.path.isfile('/usr/bin/zypper'): - os.system('cargo build --release --features ' + features) - os.system('strip target/release/rustdesk') - os.system( + system2('cargo build --release --features ' + features) + system2('strip target/release/rustdesk') + system2( "sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version) - os.system('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') - os.system( + system2('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec') + system2( 'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % ( version, version)) # yum localinstall rustdesk.rpm @@ -446,18 +442,18 @@ def main(): build_flutter_dmg(version, features) pass else: - # os.system( + # system2( # 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') build_flutter_deb(version, features) else: - os.system('cargo bundle --release --features ' + features) + system2('cargo bundle --release --features ' + features) if osx: - os.system( + system2( 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') - os.system( + system2( 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') # https://github.com/sindresorhus/create-dmg - os.system('/bin/rm -rf *.dmg') + system2('/bin/rm -rf *.dmg') plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" txt = open(plist).read() with open(plist, "wt") as fh: @@ -467,8 +463,9 @@ def main(): """)) pa = os.environ.get('P') if pa: - os.system(''' + system2(''' # buggy: rcodesign sign ... path/*, have to sign one by one + # install rcodesign via cargo install apple-codesign #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk #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 @@ -476,14 +473,20 @@ def main(): codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app '''.format(pa)) - os.system('create-dmg target/release/bundle/osx/RustDesk.app') + system2('create-dmg target/release/bundle/osx/RustDesk.app') os.rename('RustDesk %s.dmg' % version, 'rustdesk-%s.dmg' % version) if pa: - os.system(''' + system2(''' + # https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html + # https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html + # https://developer.apple.com/developer-id/ + # 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://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html + # 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'))) @@ -491,34 +494,32 @@ def main(): print('Not signed') else: # buid deb package - os.system( + system2( 'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') - os.system('dpkg-deb -R rustdesk.deb tmpdeb') - os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( + system2('dpkg-deb -R rustdesk.deb tmpdeb') + system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + system2( 'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( + system2( 'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') - os.system( + system2( 'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') - os.system( + system2( 'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop') - os.system('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') - os.system('strip tmpdeb/usr/bin/rustdesk') - os.system('mkdir -p tmpdeb/usr/lib/rustdesk') - os.system('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') - os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') + system2('cp -a res/DEBIAN/* tmpdeb/DEBIAN/') + system2('strip tmpdeb/usr/bin/rustdesk') + system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') + system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') md5_file('usr/lib/rustdesk/libsciter-gtk.so') - os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') + system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) - os.system("mv Cargo.toml.bk Cargo.toml") - os.system("mv src/main.rs.bk src/main.rs") def md5_file(fn): md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest() - os.system('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) + system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) if __name__ == "__main__": diff --git a/build.rs b/build.rs index 67e40752c..bf141e539 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,23 @@ #[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"; + let mut b = cc::Build::new(); + if let Ok(os_version::OsVersion::MacOS(v)) = os_version::detect() { + let v = v.version; + if v.contains("10.14") { + b.flag("-DNO_InputMonitoringAuthStatus=1"); + } + } + b.file(file).compile("macos"); + println!("cargo:rerun-if-changed={}", file); } #[cfg(all(windows, feature = "inline"))] @@ -78,26 +92,35 @@ fn install_oboe() { #[cfg(feature = "flutter")] fn gen_flutter_rust_bridge() { + use lib_flutter_rust_bridge_codegen::{ + config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts, + }; let llvm_path = match std::env::var("LLVM_HOME") { Ok(path) => Some(vec![path]), Err(_) => None, }; // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); - // settings for fbr_codegen - let opts = lib_flutter_rust_bridge_codegen::Opts { + // Options for frb_codegen + let raw_opts = RawOpts { // Path of input Rust code - rust_input: "src/flutter_ffi.rs".to_string(), + rust_input: vec!["src/flutter_ffi.rs".to_string()], // Path of output generated Dart code - dart_output: "flutter/lib/generated_bridge.dart".to_string(), + dart_output: vec!["flutter/lib/generated_bridge.dart".to_string()], // Path of output generated C header c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]), - // for other options lets use default + /// Path to the installed LLVM llvm_path, + // for other options use defaults ..Default::default() }; - // run fbr_codegen - lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap(); + // get opts from raw opts + let configs = config_parse(raw_opts); + // generation of rust api for ffi + let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap(); + for config in configs.iter() { + frb_codegen(config, &all_symbols).unwrap(); + } } fn main() { @@ -117,5 +140,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/docs/CODE_OF_CONDUCT-NL.md b/docs/CODE_OF_CONDUCT-NL.md new file mode 100644 index 000000000..49923a2d6 --- /dev/null +++ b/docs/CODE_OF_CONDUCT-NL.md @@ -0,0 +1,136 @@ + +# Gedragscode Overeenkomst Medewerkers + +## Onze Belofte + +Wij als leden, medewerkers en leiders beloven deelname aan onze +gemeenschap een pesterij-vrije ervaring te maken voor iedereen, ongeacht leeftijd, lichaamsgrootte, +zichtbare of onzichtbare handicap, etniciteit, geslachtskenmerken, gender +identiteit en expressie, ervaringsniveau, opleiding, sociaal-economische status, +nationaliteit, persoonlijk voorkomen, ras, religie of seksuele identiteit +en geaardheid. + +Wij beloven te handelen en met elkaar om te gaan op manieren die bijdragen aan een open, gastvrije, +diverse, inclusieve en gezonde gemeenschap. + +## Onze Normen + +Voorbeelden van gedrag dat bijdraagt tot een positieve omgeving voor onze +gemeenschap omvatten: + +* Medeleven en vriendelijkheid tonen tegenover andere mensen +* Respect hebben voor verschillende meningen, standpunten en ervaringen +* Constructieve feedback geven en met dank aanvaarden +* Verantwoordelijkheid accepteren en excuses aanbieden aan degenen die door onze fouten zijn getroffen, + en leren van de ervaring +* Focussen op wat het beste is, niet alleen voor ons als individu, maar voor de + totale gemeenschap + +Voorbeelden van onaanvaardbaar gedrag zijn: + +* Het gebruik van seksueel getinte taal of beelden, en seksuele aandacht of + alle soorten avances +* Treiteren, beledigende of denigrerende opmerkingen en persoonlijke of politieke aanvallen. +* Openbare of persoonlijke intimidatie +* Publiceren van andermans persoonlijke informatie, zoals een fysiek adres of e-mail, + zonder hun uitdrukkelijke toestemming +* Ander gedrag dat normaal als ongepast kan worden beschouwd in een + professionele omgeving + +## Verantwoordelijkheden inzake Handhaving + +De leiders van de Gemeenschap zijn verantwoordelijk voor het verduidelijken +en handhaven van onze normen voor aanvaardbaar gedrag en zullen passende +en billijke corrigerende maatregelen nemen als reactie op gedrag dat zij ongepast, +bedreigend, beledigend of schadelijk achten. + +Leiders van de Gemeenschap hebben het recht en de verantwoordelijkheid om +commentaar, bijdragen, code, wikibewerkingen, issues en andere bijdragen die +niet in overeenstemming zijn met deze Gedragscode te verwijderen, te bewerken of +af te wijzen, en zullen de redenen voor moderatiebeslissingen zo nodig meedelen. + +## Toepassingsgebied + +Deze Gedragscode geldt binnen alle gemeenschapsruimtes en is ook van toepassing +wanneer iemand de gemeenschap officieel vertegenwoordigt in openbare ruimtes. +Voorbeelden van het vertegenwoordigen van onze gemeenschap zijn het gebruik van +een officieel e-mailadres, het posten via een officieel sociaal media-account of het +optreden als aangewezen vertegenwoordiger bij een online of offline evenement. + +## Handhaving + +Gevallen van beledigend, intimiderend of anderszins onaanvaardbaar gedrag kunnen +worden gemeld aan de gemeenschapsleiders die verantwoordelijk zijn voor de +handhaving op [info@rustdesk.com](mailto:info@rustdesk.com). +Alle klachten zullen snel en eerlijk worden onderzocht. + +Alle leiders van de gemeenschap zijn verplicht de privacy en de veiligheid van +de melder van een incident te respecteren. + +## Handhaving Richtlijnen + +De leiders van de Gemeenschap volgen deze Communautaire Impact Richtlijnen bij +het bepalen van de consequenties voor elke actie die zij in strijd achten +met deze Gedragscode: + +### 1. Rechtzetting + +**Gevolgen Gemeenschap**: Gebruik van ongepast taalgebruik of ander gedrag +dat onprofessioneel of ongewenst wordt geacht in de gemeenschap. + +**Gevolgen**: Een persoonlijke, schriftelijke waarschuwing van de leiders van +de gemeenschap, met duidelijkheid over de aard van de overtreding en een +uitleg waarom het gedrag ongepast was. +Een publieke verontschuldiging kan worden gevraagd. + +### 2. Waarschuwing + +**Gevolgen Gemeenschap**: Een overtreding door een enkel incident of +een reeks handelingen. + +**Gevolgen**: Geen interactie met de betrokken personen, inclusief +ongevraagde interactie met degenen die de Gedragscode handhaven, +gedurende een bepaalde periode. Dit omvat het vermijden van interacties +in gemeenschapsruimtes en externe kanalen zoals sociale media. +Overtreding van deze voorwaarden kan leiden tot een tijdelijke +of permanente uitsluiting. + +### 3. Tijdelijke Uitsluiting + +**Gevolgen Gemeenschap**: Een ernstige schending van de +gemeenschapsnormen, waaronder aanhoudend ongepast gedrag. + +**Gevolgen**: Een tijdelijk verbod op elke vorm van interactie +of openbare communicatie met de gemeenschap voor een bepaalde + periode. Geen openbare of private interactie met de betrokkenen, + inclusief ongevraagde interactie met degenen die de gedragscode + handhaven, is gedurende deze periode toegestaan. + Overtreding van deze voorwaarden kan leiden tot een permanente uitsluiting. + +### 4. Permanente Uitsluiting + +**Gevolgen Gemeenschap**: Aantonen van een patroon van schending van +de gemeenschapsnormen, waaronder aanhoudend ongepast gedrag, intimidatie +van een individu, of agressie tegen of vernedering van klassen van individuen. + +**Gevolgen**: Een permanente uitsluiting van elke vorm van publieke interactie +binnen de gemeenschap. + +## Naamsvermelding + +Deze gedragscode is overgenomen uit de [Bijdrager Overeenkomst][homepagina], +versie 2.0, beschikbaar op +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +De Invloed op Richtlijnen voor Gemeenschap zijn gebaseerd op +[Mozilla's gedragscode handhavingslijst][Mozilla CoC]. + +Voor antwoorden op veelgestelde vragen over deze gedragscode, zie de FAQ op +[https://www.contributor-covenant.org/faq][FAQ]. Vertalingen zijn beschikbaar +op [https://www.contributor-covenant.org/translations][translations]. + +[homepagina]: 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 +[vertalingen]: https://www.contributor-covenant.org/translations diff --git a/docs/CONTRIBUTING-DE.md b/docs/CONTRIBUTING-DE.md new file mode 100644 index 000000000..6258a9a7a --- /dev/null +++ b/docs/CONTRIBUTING-DE.md @@ -0,0 +1,50 @@ +# Beitrge zu RustDesk + +RustDesk begrt Beitrge von jedem. Hier sind die Richtlinien, wenn Sie uns +helfen mchten: + +## Beitrge + +Beitrge zu RustDesk oder seinen Abhngigkeiten sollten in Form von Pull +Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur +(jemand mit der Erlaubnis, Korrekturen einzubringen) geprft und entweder in den +Hauptbaum eingefgt oder Feedback fr notwendige nderungen gegeben. Alle +Beitrge sollten diesem Format folgen, auch die von Hauptakteuren. + +Wenn Sie an einem Problem arbeiten mchten, melden Sie es bitte zuerst an, indem +Sie auf GitHub erklren, dass Sie daran arbeiten mchten. Damit soll verhindert +werden, dass Beitrge zum gleichen Thema doppelt bearbeitet werden. + +## Checkliste fr Pull Requests + +- Verzweigen Sie sich vom Master-Branch und, falls ntig, wechseln Sie zum + aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das + Zusammenfhren mit dem Master nicht reibungslos funktioniert, werden Sie + mglicherweise aufgefordert, Ihre nderungen zu berarbeiten. + +- Commits sollten so klein wie mglich sein und gleichzeitig sicherstellen, dass + jeder Commit unabhngig voneinander korrekt ist (d. h., jeder Commit sollte + sich bersetzen lassen und Tests bestehen). + +- Commits sollten von einem "Herkunftszertifikat fr Entwickler" + (https://developercertificate.org) begleitet werden, das besagt, dass Sie (und + ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE) + einverstanden sind. In Git ist dies die Option `-s` fr `git commit`. + +- Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur + Begutachtung bentigen, knnen Sie einem Gutachter mit @ antworten und um eine + Begutachtung des Pull Requests oder einen Kommentar bitten. Sie knnen auch + per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten. + +- Fgen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue + Funktion beziehen. + +Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Verhalten + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Kommunikation + +RustDesk-Mitarbeiter arbeiten hufig im [Discord](https://discord.gg/nDceKgxnkV). diff --git a/docs/CONTRIBUTING-NL.md b/docs/CONTRIBUTING-NL.md new file mode 100644 index 000000000..a39e8cef1 --- /dev/null +++ b/docs/CONTRIBUTING-NL.md @@ -0,0 +1,50 @@ +# Bijdragen aan RustDesk + +RustDesk verwelkomt bijdragen van iedereen. Hier zijn de richtlijnen als u denkt +ons te willen helpen: + +## Bijdragen + +Bijdragen aan RustDesk of haar afhankelijkheden moeten worden gedaan in de +vorm van GitHub pull verzoeken. Elk pull verzoek zal worden beoordeeld door +een core bijdrager (iemand met toestemming om patches te plaatsen) en ofwel +worden geplaatst in de hoofd structuur of feedback krijgen voor veranderingen +die nodig zouden zijn. Alle bijdragen zouden dit formaat moeten volgen, +zelfs die van kernmedewerkers. + +Als je aan een onderwerp wilt werken, eis het dan eerst op door commentaar +te geven op het GitHub onderwerp dat je eraan wilt werken. Dit is om dubbele +inspanningen van medewerkers aan hetzelfde issue te voorkomen. + +## Checklist Pull Aanvragen + +- Maak een vertakking vanaf de master tak en, indien nodig, veranker naar de + huidige master tak voordat je je pull verzoek indient. Als je het niet netjes + samenvoegt met master kan je gevraagd worden om je wijzigingen + opnieuw op te bouwen. + +- Toezeggingen moeten zo klein mogelijk zijn, terwijl er voor gezorgd moet + worden dat elke toezegging onafhankelijk correct is (dat wil zeggen, elke + toezegging moet compileren en testen doorstaan). + +- Toezeggingen moeten vergezeld gaan van een Certificaat van Oorsprong + van de Ontwikkelaar (http://developercertificate.org) ondertekening, die aangeeft + dat u (en uw werkgever indien van toepassing) akkoord gaat met de + voorwaarden van het [project licentie](../LICENCE). + In git is dit de `-s` optie van `git commit` + +- Als je patch niet beoordeeld wordt of je hebt een specifiek persoon nodig om hem + te beoordelen kunt u @-reply een reviewer vragen in het pull verzoek of een + commentaar, of je kunt om een review vragen via [email](mailto:info@rustdesk.com). + +- Tests toevoegen die relevant zijn voor de gerepareerde bug of de nieuwe functie. + +Voor specifieke git instructies, zie [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Gedrag + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Communicatie + +RustDesk medewerkers bezoeken frequent [Discord](https://discord.gg/nDceKgxnkV). diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f3165a684..31fd632e6 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -35,7 +35,7 @@ efforts from contributors on the same issue. - Add tests relevant to the fixed bug or new feature. -For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow). +For specific git instructions, see [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). ## Conduct diff --git a/docs/DEVCONTAINER-DE.md b/docs/DEVCONTAINER-DE.md new file mode 100644 index 000000000..2a0d73f17 --- /dev/null +++ b/docs/DEVCONTAINER-DE.md @@ -0,0 +1,14 @@ + +Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Binrprogramm im Debug-Modus erstellt. + +Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an. + +Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgefhrt werden mssen, um bestimmte Builds zu erstellen. + +Kommando|Build-Typ|Modus +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|debug +`.devcontainer/build.sh --release linux`|Linux|release +`.devcontainer/build.sh --debug android`|android-arm64|debug +`.devcontainer/build.sh --release android`|android-arm64|release + diff --git a/docs/DEVCONTAINER-NL.md b/docs/DEVCONTAINER-NL.md new file mode 100644 index 000000000..cd6ae456d --- /dev/null +++ b/docs/DEVCONTAINER-NL.md @@ -0,0 +1,15 @@ + +Na de start van devcontainer in docker container wordt een linux binaire in foutmodus aangemaakt. + +Momenteel biedt devcontainer linux en android builds in zowel foutopsporing- als uitgave modus. + +Hieronder staat de tabel met commando's die vanuit de root van het project moeten worden +uitgevoerd om specifieke builds te maken. + +Commando|Build Type|Modus +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|debug +`.devcontainer/build.sh --release linux`|Linux|release +`.devcontainer/build.sh --debug android`|android-arm64|debug +`.devcontainer/build.sh --release android`|android-arm64|debug + diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md new file mode 100644 index 000000000..067e0ecf9 --- /dev/null +++ b/docs/DEVCONTAINER.md @@ -0,0 +1,14 @@ + +After the start of devcontainer in docker container, a linux binary in debug mode is created. + +Currently devcontainer offers linux and android builds in both debug and release mode. + +Below is the table on commands to run from root of the project for creating specific builds. + +Command|Build Type|Mode +-|-|-| +`.devcontainer/build.sh --debug linux`|Linux|debug +`.devcontainer/build.sh --release linux`|Linux|release +`.devcontainer/build.sh --debug android`|android-arm64|debug +`.devcontainer/build.sh --release android`|android-arm64|debug + diff --git a/docs/README-AR.md b/docs/README-AR.md index ad7303806..4f5769839 100644 --- a/docs/README-AR.md +++ b/docs/README-AR.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
- [English] | [Українська] | [č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 نحن بحاجة إلى مساعدتك لترجمة هذا

diff --git a/docs/README-CS.md b/docs/README-CS.md index d56464eff..74c6fcb19 100644 --- a/docs/README-CS.md +++ b/docs/README-CS.md @@ -5,7 +5,7 @@ DockerStrukturaUkázky
- [English] | [Українська] | [中文] | [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

diff --git a/docs/README-DA.md b/docs/README-DA.md index dde5c7a0d..d7283d8ab 100644 --- a/docs/README-DA.md +++ b/docs/README-DA.md @@ -5,7 +5,7 @@ DockerFilstrukturSkærmbilleder
- [English] | [Українська] | [č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] | [Ελληνικά]
Vi har brug for din hjælp til at oversætte denne README, RustDesk UI og Dokument til dit modersmål

diff --git a/docs/README-DE.md b/docs/README-DE.md index 0b51d8fdd..2c159bd07 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -1,63 +1,92 @@

RustDesk - Your remote desktop
- Server • - Kompilieren • + Server • + KompilierenDockerDateistrukturScreenshots
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
- Wir brauchen deine Hilfe um diese README Datei zu verbessern und aktualisieren + [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά]
+ Wir brauchen Ihre Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in Ihre Muttersprache zu übersetzen.

-Rede mit uns: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Reden Sie 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) -Das hier ist ein Programm was, man nutzen kann, um einen Computer fernzusteuern, es wurde in Rust geschrieben. Es funktioniert ohne Konfiguration oder ähnliches, man kann es einfach direkt nutzen. Du hast volle Kontrolle über deine Daten und brauchst dir daher auch keine Sorgen um die Sicherheit dieser Daten zu machen. Du kannst unseren Rendezvous/Relay Server nutzen, [einen eigenen Server eröffnen](https://rustdesk.com/server) oder [einen neuen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). +RustDesk ist eine in Rust geschriebene Remote-Desktop-Software, die out of the box ohne besondere Konfiguration funktioniert. Sie haben die volle Kontrolle über Ihre Daten und müssen sich keine Sorgen um die Sicherheit machen. Sie können unseren Rendezvous/Relay-Server nutzen, [einen eigenen Server aufsetzen](https://rustdesk.com/server) oder [einen eigenen Server programmieren](https://github.com/rustdesk/rustdesk-server-demo). -RustDesk heißt jegliche Mitarbeit willkommen. Schau dir [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) an, wenn du Hilfe brauchst für den Start. +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -[**PROGRAMM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) +RustDesk heißt jegliche Mitarbeit willkommen. Schauen Sie sich [CONTRIBUTING-DE.md](CONTRIBUTING-DE.md) an, wenn Sie Unterstützung beim Start brauchen. -## Kostenlose öffentliche Server +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) -Hier sind die Server, die du kostenlos nutzen kannst, es kann sein das sich diese Liste immer mal wieder ändert. Falls du nicht in der Nähe einer dieser Server bist, kann es sein, dass deine Verbindung langsam sein wird. +[**Programm herunterladen**](https://github.com/rustdesk/rustdesk/releases) -| Standort | Serverart | Spezifikationen | Kommentare | -| --------- | ------------- | ------------------ | ---------- | -| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | | -| Germany | Codext | 2 vCPU / 4GB RAM | -| Germany | Hetzner | 4 vCPU / 8GB RAM | -| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | -| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +[**Nächtliche Erstellung**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) + +## Freie öffentliche Server + +Nachfolgend sind die Server gelistet, die Sie kostenlos nutzen können. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls Sie nicht in der Nähe einer dieser Server sind, kann es sein, dass Ihre Verbindung langsam sein wird. +| Standort | Anbieter | Spezifikation | +| --------- | ------------- | ------------------ | +| Südkorea (Seoul) | [AWS lightsail](https://aws.amazon.com/de/) | 1 vCPU / 0,5 GB RAM | +| Deutschland | [Hetzner](https://www.hetzner.com/de/) | 2 vCPU / 4 GB RAM | +| Deutschland | [Codext](https://codext.de/) | 4 vCPU / 8 GB RAM | +| Finnland (Helsinki) | [Netlock](https://netlockendpoint.com/de/index.html) | 4 vCPU / 8 GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com/de/index.html) | 4 vCPU / 8 GB RAM | +| Ukraine (Kiew) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM | + +## Dev-Container + +[![In Dev-Containern öffnen](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) + +Wenn Sie VS Code und Docker bereits installiert haben, können Sie auf das Abzeichen oben klicken, um loszulegen. Wenn Sie darauf klicken, wird VS Code automatisch die Dev-Container-Erweiterung installieren, den Quellcode in ein Container-Volume klonen und einen Dev-Container für die Verwendung aufsetzen. + +Weitere Informationen finden Sie in [DEVCONTAINER-DE.md](DEVCONTAINER-DE.md). ## Abhängigkeiten -Die Desktop-Versionen nutzen [Sciter](https://sciter.com/) für die Oberfläche, bitte lade die dynamische Sciter Bibliothek selbst herunter. +Desktop-Versionen verwenden [Sciter](https://sciter.com/) oder Flutter für die GUI, dieses Tutorial ist nur für Sciter. + +Bitte laden Sie die dynamische Bibliothek Sciter selbst herunter. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | -[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -## Die groben Schritte zum Kompilieren +## Grobe Schritte zum Kompilieren -- Bereite deine Rust Entwicklungsumgebung und C++ Entwicklungsumgebung vor +- Bereiten Sie Ihre Rust-Entwicklungsumgebung und C++-Build-Umgebung vor -- Installiere [vcpkg](https://github.com/microsoft/vcpkg) und füge die `VCPKG_ROOT` Systemumgebungsvariable hinzu +- Installieren Sie [vcpkg](https://github.com/microsoft/vcpkg) und fügen Sie die Systemumgebungsvariable `VCPKG_ROOT` hinzu - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` - - Linux/MacOS: `vcpkg install libvpx libyuv opus` + - Linux/macOS: `vcpkg install libvpx libyuv opus` -- Nutze `cargo run` +- Nutzen Sie `cargo run` + +## [Erstellen](https://rustdesk.com/docs/de/dev/build/) ## Kompilieren auf Linux ### Ubuntu 18 (Debian 10) ```sh -sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ``` +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel +``` ### Fedora 28 (CentOS 8) ```sh @@ -82,7 +111,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus ``` -### libvpx reparieren (Für Fedora) +### libvpx reparieren (für Fedora) ```sh cd vcpkg/buildtrees/libvpx/src @@ -105,16 +134,40 @@ cd rustdesk mkdir -p target/debug wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so mv libsciter-gtk.so target/debug -cargo run +VCPKG_ROOT=$HOME/vcpkg cargo run ``` -### Ändere Wayland zu X11 (Xorg) +### Wayland zu X11 (Xorg) ändern -RustDesk unterstützt "Wayland" nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) um Xorg als Standard GNOME Session zu nutzen. +RustDesk unterstützt Wayland nicht. Siehe [hier](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/), um Xorg als Standard-GNOME-Sitzung zu nutzen. -## Auf Docker Kompilieren +## Wayland-Unterstützung -Beginne damit das Repository zu klonen und den Docker Container zu bauen: +Wayland scheint keine API für das Senden von Tastatureingaben an andere Fenster zu bieten. Daher verwendet RustDesk eine API von einer niedrigeren Ebene, nämlich dem Gerät `/dev/uinput` (Linux-Kernelebene). + +Wenn Wayland die kontrollierte Seite ist, müssen Sie wie folgt vorgehen: +```bash +# Dienst uinput starten +$ sudo rustdesk --service +$ rustdesk +``` +**Hinweis**: Die Wayland-Bildschirmaufnahme verwendet verschiedene Schnittstellen. RustDesk unterstützt derzeit nur org.freedesktop.portal.ScreenCast. +```bash +$ dbus-send --session --print-reply \ + --dest=org.freedesktop.portal.Desktop \ + /org/freedesktop/portal/desktop \ + org.freedesktop.DBus.Properties.Get \ + string:org.freedesktop.portal.ScreenCast string:version +# Keine Unterstützung +Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast” +# Unterstützung +method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2 + variant uint32 4 +``` + +## Auf Docker kompilieren + +Beginnen Sie damit, das Repository zu klonen und den Docker-Container zu bauen: ```sh git clone https://github.com/rustdesk/rustdesk @@ -122,36 +175,38 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -Jedes Mal, wenn du das Programm Kompilieren musst, nutze diesen Befehl: +Führen Sie jedes Mal, wenn Sie das Programm kompilieren müssen, folgenden Befehl aus: ```sh docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder ``` -Bedenke, dass das erste Mal Kompilieren länger dauern kann, da die Abhängigkeiten erst kompiliert werden müssen bevor sie zwischengespeichert werden können. Darauf folgende Kompiliervorgänge werden schneller sein. Falls du zusätzliche oder andere Argumente für den Kompilierbefehl angeben musst, kannst du diese am Ende des Befehls an der `` Position machen. Wenn du zum Beispiel eine optimierte Releaseversion kompilieren willst, kannst du das tun, indem du `--release` am Ende des Befehls anhängst. Das daraus entstehende Programm kannst du im “target” Ordner auf deinem System finden. Du kannst es mit folgenden Befehlen ausführen: +Bedenken Sie, dass das erste Kompilieren länger dauern kann, bis die Abhängigkeiten zwischengespeichert sind. Nachfolgende Kompiliervorgänge sind schneller. Wenn Sie verschiedene Argumente für den Kompilierbefehl angeben müssen, können Sie dies am Ende des Befehls an der Position `` tun. Wenn Sie zum Beispiel eine optimierte Releaseversion kompilieren wollen, können Sie `--release` am Ende des Befehls anhängen. Das daraus entstehende Programm finden Sie im Zielordner auf Ihrem System. Sie können es mit folgendem Befehl ausführen: ```sh target/debug/rustdesk ``` -Oder, wenn du eine Releaseversion benutzt: +Oder, wenn Sie eine Releaseversion benutzen: ```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. +Bitte stellen Sie sicher, dass Sie diese Befehle im Stammverzeichnis des RustDesk-Repositorys nutzen. Ansonsten kann es passieren, dass das Programm die Ressourcen nicht finden kann. Bitte bedenken Sie 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 Ihrem eigentlichen System. ## Dateistruktur -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video Codec, Konfiguration, TCP/UDP Wrapper, Protokoll Puffer, fs Funktionen für Dateitransfer, und ein paar andere nützliche Funktionen +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: Video-Codec, Konfiguration, TCP/UDP-Wrapper, Protokoll-Puffer, fs-Funktionen für Dateitransfer und ein paar andere nützliche Funktionen - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: Bildschirmaufnahme -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus und Tastatur Steuerung +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Plattformspezifische Maus- und Tastatursteuerung - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerk Verbindungen +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: Audio/Zwischenablage/Eingabe/Videodienste und Netzwerkverbindungen - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: Starten einer Peer-Verbindung -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, für Verbindung von außen warten, direkt (TCP hole punching) oder weitergeleitet +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Mit [rustdesk-server](https://github.com/rustdesk/rustdesk-server) kommunizieren, warten auf direkte (TCP hole punching) oder weitergeleitete Verbindung - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Plattformspezifischer Code +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter-Code für Handys +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript für Flutter-Webclient ## Screenshots diff --git a/docs/README-EO.md b/docs/README-EO.md index 7471636eb..4bca4a793 100644 --- a/docs/README-EO.md +++ b/docs/README-EO.md @@ -5,7 +5,7 @@ DockerStrukturoEkrankopio
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [한국어] | [العربي] | [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

@@ -27,8 +27,9 @@ Malsupre estas la serviloj, kiuj vi uzas senpage, ĝi povas ŝanĝi laŭlonge de | 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 | +| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | ## Dependantaĵoj diff --git a/docs/README-ES.md b/docs/README-ES.md index 16f65adcc..66fc609fb 100644 --- a/docs/README-ES.md +++ b/docs/README-ES.md @@ -5,7 +5,7 @@ DockerEstructuraCapturas de pantalla
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt]
+ [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

@@ -34,8 +34,9 @@ A continuación se muestran los servidores gratuitos, pueden cambiar a medida qu | 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 | +| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | ## Dependencias diff --git a/docs/README-FA.md b/docs/README-FA.md index d86c82836..177e3c122 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -1,60 +1,60 @@ -

+

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]
- ‫برای ترجمه این RustDesk UI ،README و Doc به زبان مادری شما به کمکتون نیاز داریم + تصاویر محیط نرم‌افزار • + ساختار • + داکر • + ساخت • + سرور

+

[English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]

+

برای ترجمه این سند (README)، رابط کاربری RustDesk، و مستندات آن به زبان مادری شما به کمکتان نیازمندیم.

-با ما گپ بزنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) +با ما گفتگو کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -یک نرم افزار دیگر کنترل دسکتاپ از راه دور، که با Rust نوشته شده است. راه اندازی سریع وبدون نیاز به تنظیمات. شما کنترل کاملی بر داده های خود دارید، بدون هیچ گونه نگرانی امنیتی. +راست‌دسک (RustDesk) نرم‌افزاری برای کارکردن با رایانه‌ی رومیزی از راه دور است و با زبان برنامه‌نویسی Rust نوشته شده است. نیاز به تنظیمات چندانی ندارد و شما را قادر می سازد تا بدون نگرانی از امنیت اطلاعات خود بر آن‌ها کنترل کامل داشته باشید. + می‌توانید از سرور rendezvous/relay ما استفاده کنید، [سرور خودتان را راه‌اندازی کنید](https://rustdesk.com/server) یا [ سرورrendezvous/relay خود را بنویسید](https://github.com/rustdesk/rustdesk). -‫راست دسک (RustDesk) از مشارکت همه استقبال می کند. برای راهنمایی جهت مشارکت به [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. +ما از مشارکت همه استقبال می کنیم. برای راهنمایی جهت مشارکت به[`docs/CONTRIBUTING.md`](CONTRIBUTING.md) مراجعه کنید. -[راست دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[راست‌دسک چطور کار می کند؟](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) -[دانلود باینری](https://github.com/rustdesk/rustdesk/releases) +[دریافت نرم‌افزار](https://github.com/rustdesk/rustdesk/releases) ## سرورهای عمومی رایگان -سرورهایی زیر را به صورت رایگان میتوانید استفاده می کنید. این لیست ممکن است در طول زمان تغییر کند. اگر به این سرورها نزدیک نیستید، ممکن است سرویس شما کند شود. +شما مي‌توانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر می‌کند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد. | موقعیت | سرویس دهنده | مشخصات | | --------- | ------------- | ------------------ | -| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM | -| Germany | Hetzner | 2 vCPU / 4GB RAM | -| Germany | Codext | 4 vCPU / 8GB RAM | -| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | -| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| کره‌ی جنوبی، سئول | AWS lightsail | 1 vCPU / 0.5GB RAM | +| آلمان | Hetzner | 2 vCPU / 4GB RAM | +| آلمان | Codext | 4 vCPU / 8GB RAM | +| فنلاند، هلسینکی | 0x101 Cyber Security | 4 vCPU / 8GB RAM | +| ایالات متحده، اَشبرن | 0x101 Cyber Security | 4 vCPU / 8GB RAM | ## وابستگی ها -نسخه‌های دسکتاپ از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند، لطفا کتابخانه پویا sciter را خودتان دانلود کنید. +نسخه‌های رومیزی از [sciter](https://sciter.com/) برای رابط کاربری گرافیکی استفاده می‌کنند. خواهشمندیم کتابخانه‌ی پویای sciter را خودتان دانلود کنید از این منابع دریافت کنید. -[ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | -[لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | -[مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) +- [ویندوز](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) +- [لینوکس](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) +- [مک](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -نسخه های موبایل از Flutter استفاده می کنند. بعداً نسخه دسکتاپ را از Sciter به Flutter منتقل خواهیم کرد. +نسخه های همراه از Flutter استفاده می کنند. نسخه‌ی رومیزی را هم از Sciter به Flutter منتقل خواهیم کرد. -## مراحل بنیادین برای ساخت +## نیازمندی‌های ساخت -‫- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید +- محیط توسعه نرم افزار Rust و محیط ساخت ++C خود را آماده کنید -‫- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید: - - - Windows: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` - - Linux/MacOS: `vcpkg install libvpx libyuv opus` - -- run `cargo run` +- نرم افزار [vcpkg](https://github.com/microsoft/vcpkg) را نصب کنید و متغیر `VCPKG_ROOT` را به درستی تنظیم کنید. +- بسته‌های vcpkg مورد نیاز را نصب کنید: + - ویندوز: `vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static` + - مک و لینوکس: `vcpkg install libvpx libyuv opus` +- این دستور را اجرا کنید: `cargo run` ## [ساخت](https://rustdesk.com/docs/en/dev/build/) @@ -118,11 +118,11 @@ VCPKG_ROOT=$HOME/vcpkg cargo run ### تغییر Wayland به (X11 (Xorg -راست دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید. +راست‌دسک از Wayland پشتیبانی نمی کند. برای جایگزنی Xorg به عنوان پیش‌فرض GNOM، [اینجا](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) را کلیک کنید. ## نحوه ساخت با داکر -این مخزن گیت را کلون کنید و کانتینر را به روش زیر بسازید +این مخزن Git را دریافت کنید و کانتینر را به روش زیر بسازید ```sh git clone https://github.com/rustdesk/rustdesk @@ -130,13 +130,13 @@ cd rustdesk docker build -t "rustdesk-builder" . ``` -سپس، هر بار که نیاز به ساخت اپلیکیشن داشتید، دستور زیر را اجرا کنید: +سپس، هر بار که نیاز به ساخت نرم‌افزار داشتید، دستور زیر را اجرا کنید: ```sh docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder ``` -توجه داشته باشید که ساخت اول ممکن است قبل از کش شدن وابستگی ها بیشتر طول بکشد، دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور: +توجه داشته باشید که نخستین ساخت ممکن است به دلیل محلی نبودن وابستگی‌ها بیشتر طول بکشد. اما دفعات بعدی سریعتر خواهند بود. علاوه بر این، اگر نیاز به تعیین آرگومان های مختلف برای دستور ساخت دارید، می توانید این کار را در انتهای دستور ساخت و از طریق `` انجام دهید. به عنوان مثال، اگر می خواهید یک نسخه نهایی بهینه سازی شده ایجاد کنید، دستور بالا را تایپ کنید و در انتها `release--` را اضافه کنید. فایل اجرایی به دست آمده در پوشه مقصد در سیستم شما در دسترس خواهد بود و می تواند با دستور: ```sh target/debug/rustdesk @@ -163,7 +163,7 @@ target/release/rustdesk - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client -## اسکرین شات ها +## تصاویر محیط نرم‌افزار ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-FI.md b/docs/README-FI.md index f7a087087..8674bc1b3 100644 --- a/docs/README-FI.md +++ b/docs/README-FI.md @@ -5,7 +5,7 @@ DockerRakenneTilannevedos
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [മലയാളം] | [日本語] | [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

@@ -27,8 +27,9 @@ Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan | 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 | +| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | ## Riippuvuudet diff --git a/docs/README-FR.md b/docs/README-FR.md index fdb253bd0..c11edc211 100644 --- a/docs/README-FR.md +++ b/docs/README-FR.md @@ -5,7 +5,7 @@ Docker - Structure - Images
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [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.

diff --git a/docs/README-GR.md b/docs/README-GR.md new file mode 100644 index 000000000..8ec98030d --- /dev/null +++ b/docs/README-GR.md @@ -0,0 +1,219 @@ +

+ RustDesk - Your remote desktop
+ Διακομιστές • + Build • + Docker • + Δομή • + Στιγμιότυπα
+ [English] | [Українська] | [česky] | [中文] | | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk]
+ Χρειαζόμαστε τη βοήθειά σας για να μεταφράσουμε αυτό το αρχείο README, το RustDesk UI και το Doc στη μητρική σας γλώσσα +

+ +Επικοινωνήστε μαζί μας μέσω: [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. Δεν χρειάζεται κάποια παραμετροποίηση, λειτουργεί αμέσως μετά την εγκατάσταση. Έχετε τον πλήρη έλεγχο των δεδομένων σας, χωρίς να ανησυχείτε για την ασφάλειά τους. Μπορείτε να χρησιμοποιήσετε τους προκαθορισμένους διακομιστές rendezvous/αναμετάδοσης, [να εγκαταστήσετε τον δικό σας διακομιστή](https://rustdesk.com/server), ή [να αναπτύξετε ένα δικό σας διακομιστή rendezvous/αναμετάδοσης](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`](docs/CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε. + +[**Συχνές ερωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**Κατεβάστε τα αρχεία**](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) + +## Δωρεάν δημόσιοι διακομιστές + +Παρακάτω είναι οι διακομιστές που χρησιμοποιούνται δωρεάν, ενδέχεται να αλλάξουν με την πάροδο του χρόνου. Εάν δεν είστε κοντά σε ένα από αυτούς, το δίκτυό σας ίσως να είναι αργό. +| Περιοχή | Πάροχος | Προδιαγραφές | +| --------- | ------------- | ------------------ | +| Σεούλ | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Γερμανία | Hetzner | 2 vCPU / 4GB RAM | +| Γερμανία | Codext | 4 vCPU / 8GB RAM | +| Φινλανδία (Ελσίνκι) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| ΗΠΑ (Άσμπερν) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ουκρανία (Κίεβο) | [dc.volia](https://dc.volia.com) | 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) + +Αν έχετε εγκατεστημένα το VS Code και το Docker, μπορείτε να ξεκινήσετε κάνοντας κλικ στην παραπάνω εικόνα. Αυτό θα έχει ως αποτέλεσμα, το VS Code να εγκαταστήσει αυτόματα την επέκταση Dev Containers, εάν χρειάζεται, θα κλωνοποιήσει τον πηγαίο κώδικα σε έναν νέο container και θα εκκινήσει ένα Dev Container για χρήση προγραμματισμού. + +Για περισσότερες πληροφορίες μεταβείτε στο [DEVCONTAINER.md](docs/DEVCONTAINER.md). + +## Προαπαιτούμενα για build + +Στις παραθυρικές εκδόσεις χρησιμοποιείται είτε το [sciter](https://sciter.com/) είτε το Flutter, τα παρακάτω βήματα είναι μόνο για το Sciter. + +Παρακαλώ κατεβάστε μόνοι σας την δυναμική βιβλιοθήκη 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) + +## Γενικά βήματα ώστε να κάνετε build + +- Προετοιμάστε τα περιβάλλοντα προγραμματισμού 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` + +## [Build](https://rustdesk.com/docs/en/dev/build/) + +## Πως να το κάνετε build στο 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 + +```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 +``` + +### Build + +```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 περιβάλλον. + +## Υποστήριξη Wayland + +Το Wayland προς το παρόν δεν διαθέτει κάποιο API το οποίο να στέλνει τα πατήματα πλήκτρων στα υπόλοιπα παράθυρα. Για τον λόγο αυτό, το Rustdesk χρησιμοποιεί ένα API από κατώτερο επίπεδο, όπως το `/dev/uinput` (Linux kernel level). + +Σε περίπτωση που το Wayland είναι η ελεγχόμενη πλευρά, θα πρέπει να ξεκινήσετε με τον παρακάτω τρόπο: +```bash +# Start uinput service +$ sudo rustdesk --service +$ rustdesk +``` +**Σημείωση**: Η εγγραφή οθόνης του Wayland χρησιμοποιεί διαφορετικές διεπαφές. Το RustDesk προς το παρόν υποστηρίζει μόνο 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 +``` + +## Πως να κάνετε build στο Docker + +Ξεκινήστε κλωνοποιώντας το αποθετήριο και κάνοντας build το docker container: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Στη συνέχεια, κάθε φορά που επιθυμείτε να κάνετε build την εφαρμογή, εκτελέστε την ακόλουθη εντολή: + +```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 +``` + +Σημειώστε ότι το πρώτο build μπορεί να διαρκέσει περισσότερο, ώστε να αποθηκευτούν στην προσωρινή μνήμη οι εξαρτήσεις, τα επόμενα build θα είναι ταχύτερα. Επιπλέον, εάν πρέπει να καθορίσετε διαφορετικές παραμέτρους στην εντολή build, μπορείτε να το κάνετε στο τέλος της εντολής με την χρήση ``. Για παράδειγμα, εάν επιθυμείτε να δημιουργήσετε μια βελτιστοποιημένη έκδοση της εφαρμογής, θα εκτελέσετε την παραπάνω εντολή ακολουθούμενη από το `--release`. Το εκτελέσιμο αρχείο θα είναι διαθέσιμο στον προκαθορισμένο φάκελο στο σύστημά σας και μπορεί να εκτελεστεί με: + +```sh +target/debug/rustdesk +``` + +Ή στην περίπτωση μιας βελτιστοποιημένης έκδοσης της εφαρμογής εκτελέστε: + +```sh +target/release/rustdesk +``` + +Βεβαιωθείτε ότι εκτελείτε αυτές τις εντολές από την αρχική διαδρομή του αποθετηρίου του Rustdesk, διαφορετικά η εφαρμογή ενδέχεται να μην είναι σε θέση να βρεί τους απαιτούμενους πόρους. Σημειώστε επίσης ότι άλλες υποεντολές, όπως το `install` ή το `run` δεν υποστηρίζονται επί του παρόντος μέσω αυτής της μεθόδου καθώς θα εγκαταστήσουν ή θα εκτελέσουν το πρόγραμμα εντός του container αντί του κεντρικού υπολογιστή. + +## Δομή φακέλων + +- **[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/docs/README-HU.md b/docs/README-HU.md index 6c22a3b7c..9582cf1c6 100644 --- a/docs/README-HU.md +++ b/docs/README-HU.md @@ -5,7 +5,7 @@ DockerStruktúraKépernyőképek
- [English] | [Українська] | [česky] | [中文] | [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

@@ -35,8 +35,9 @@ Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingy | 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 | +| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | ## Dependencies diff --git a/docs/README-ID.md b/docs/README-ID.md index 9616cd31d..702966566 100644 --- a/docs/README-ID.md +++ b/docs/README-ID.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [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

@@ -27,8 +27,9 @@ Di bawah ini adalah server yang bisa Anda gunakan secara gratis, dapat berubah s | 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 | +| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | ## Dependencies diff --git a/docs/README-IT.md b/docs/README-IT.md index f074510c9..2dec27e40 100644 --- a/docs/README-IT.md +++ b/docs/README-IT.md @@ -5,7 +5,7 @@ DockerStrutturaScreenshots
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Русский] | [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

@@ -27,8 +27,9 @@ Qui sotto trovate i server che possono essere usati gratuitamente, la lista potr | 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 | +| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | ## Dipendenze diff --git a/docs/README-JP.md b/docs/README-JP.md index 6d3b6d380..fafc5ef8c 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
- [English] | [Українська] | [č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,7 +14,7 @@ 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) @@ -58,7 +58,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON -## [Build](https://rustdesk.com/docs/en/dev/build/) +## [ビルド](https://rustdesk.com/docs/en/dev/build/) ## Linuxでのビルド手順 @@ -105,7 +105,7 @@ cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ cd ``` -### Build +### ビルド ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh @@ -154,7 +154,7 @@ target/release/rustdesk これらのコマンドをRustDeskリポジトリのルートから実行していることを確認してください。そうしないと、アプリケーションが必要なリソースを見つけられない可能性があります。また、 `install` や `run` などの他の cargo サブコマンドは、ホストではなくコンテナ内にプログラムをインストールまたは実行するため、現在この方法ではサポートされていないことに注意してください。 -## File Structure +## ファイル構造 - **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: ビデオコーデック、コンフィグ、tcp/udpラッパー、protobuf、ファイル転送用のfs関数、その他のユーティリティ関数 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: スクリーンキャプチャ @@ -165,7 +165,7 @@ target/release/rustdesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server), と通信し、リモートダイレクト (TCP hole punching) または中継接続を待つ。 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: プラットフォーム固有のコード -## Snapshot +## スナップショット ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-KR.md b/docs/README-KR.md index 8cefbbcee..6f9ba2221 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
- [English] | [Українська] | [č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를 모국어로 번역하기 위한 당신의 도움의 필요합니다.

diff --git a/docs/README-ML.md b/docs/README-ML.md index 288a78db8..5b4c3782a 100644 --- a/docs/README-ML.md +++ b/docs/README-ML.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
- [English] | [Українська] | [č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 നിങ്ങളുടെ മാതൃഭാഷയിലേക്ക് വിവർത്തനം ചെയ്യാൻ ഞങ്ങൾക്ക് നിങ്ങളുടെ സഹായം ആവശ്യമാണ്

diff --git a/docs/README-NL.md b/docs/README-NL.md index 1aca2b893..4a5372e67 100644 --- a/docs/README-NL.md +++ b/docs/README-NL.md @@ -5,7 +5,7 @@ DockerStructuurSnapshot
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [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

@@ -27,8 +27,9 @@ Onderstaande servers zijn de servers die je gratis kunt gebruiken, ze kunnen op | 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 | +| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | ## Afhankelijkheden diff --git a/docs/README-PL.md b/docs/README-PL.md index 85c5f4a61..df8254f3d 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -5,7 +5,7 @@ DockerStrukturaSnapshot
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [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

@@ -27,8 +27,9 @@ Poniżej znajdują się serwery, z których można korzystać za darmo, może si | 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 | +| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM | ## Zależności diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md index f9d5e0fc3..491d53154 100644 --- a/docs/README-PTBR.md +++ b/docs/README-PTBR.md @@ -5,7 +5,7 @@ DockerEstruturaScreenshots
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [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

diff --git a/docs/README-RU.md b/docs/README-RU.md index 242341a6b..b050d40ac 100644 --- a/docs/README-RU.md +++ b/docs/README-RU.md @@ -5,7 +5,7 @@ DockerStructureSnapshot
- [English] | [Українська] | [č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 на ваш родной язык

diff --git a/docs/README-UA.md b/docs/README-UA.md index 3615b9064..222da34d2 100644 --- a/docs/README-UA.md +++ b/docs/README-UA.md @@ -1,11 +1,11 @@

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

@@ -19,24 +19,37 @@ RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) для допомоги на початку роботи. +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + [**Як працює RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) -[**ЗАВАНТАЖИТИ ДОДАТОК**](https://github.com/rustdesk/rustdesk/releases) +[**ЗАВАНТАЖИТИ ЗАСТОСУНОК**](https://github.com/rustdesk/rustdesk/releases) -[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) ## Безкоштовні загальнодоступні сервери Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно. | Місцезнаходження | Постачальник | Технічні характеристики | | --------- | ------------- | ------------------ | -| Сеул | AWS lightsail | 1 vCPU / 0.5GB RAM | +| Південна Корея (Сеул) | 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 | +| США (Даллас) | Vultr | 1 vCPU / 1GB RAM +| Німеччина | Hetzner | 2 VCPU / 4GB RAM | +| Німеччина | Codext | 4 vCPU / 8GB RAM | +| Фінляндія (Гельсінкі) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| США (Ешберн) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM | +| Україна (Київ) | [dc.volia](https://dc.volia.com) | 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) + +Якщо у вас уже встановлено VS Code і Docker, ви можете натиснути значок вище, щоб почати. Клацання призведе до того, що VS Code автоматично встановить розширення Dev Containers, якщо це необхідно, клонує виcхідний код у том контейнера та розгорне контейнер dev для використання. + +Дивіться [DEVCONTAINER.md](docs/DEVCONTAINER.md) для додаткової інфо. ## Залежності @@ -64,9 +77,16 @@ RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIB ### 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 @@ -91,30 +111,6 @@ 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 @@ -183,8 +179,10 @@ target/release/rustdesk - **[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/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)**: специфічний для платформи код +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter для мобільних пристроїв +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript для Flutter веб клієнту ## Знімки diff --git a/docs/README-VN.md b/docs/README-VN.md index 295f54c6b..2f66d011d 100644 --- a/docs/README-VN.md +++ b/docs/README-VN.md @@ -5,7 +5,7 @@ DockerCấu trúc tệp tinSnapshot
- [English] | [Українська] | [česky] | [中文] | [Magyar] | [Español] | [فارسی] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Русский] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي]
+ [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

diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 7ec87ec50..27c35ff57 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -5,7 +5,7 @@ Docker结构截图
- [English] | [Українська] | [č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) diff --git a/docs/SECURITY-NL.md b/docs/SECURITY-NL.md new file mode 100644 index 000000000..463b3228e --- /dev/null +++ b/docs/SECURITY-NL.md @@ -0,0 +1,11 @@ +# Veiligheidsbeleid + +## Een Kwetsbaarheid Melden + +Wij hechten veel waarde aan de veiligheid van het project. We moedigen alle gebruikers aan om kwetsbaarheden die ze ontdekken +aan ons te melden. Als u een beveiligingslek in het RustDesk project vindt, meld dit dan op verantwoorde wijze door +een e-mail te sturen naar info@rustdesk.com. + +Op dit moment hebben we geen bug premie programma. We zijn een klein team dat een groot probleem probeert op te lossen. +We verzoeken u dringend om alle kwetsbaarheden op verantwoorde wijze te melden, zodat we verder kunnen bouwen aan +een veilige applicatie voor de hele gemeenschap. diff --git a/docs/SECURITY.md b/docs/SECURITY.md index f1114f913..c595885f2 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,13 +1,9 @@ # Security Policy -## Supported Versions - -| Version | Supported | -| --------- | ------------------ | -| 1.1.x | :white_check_mark: | -| 1.x | :white_check_mark: | -| Below 1.0 | :x: | - ## Reporting a Vulnerability -Here we should write what to do in case of a security vulnerability +We value security for the project very highly. We encourage all users to report any vulnerabilities they discover to us. +If you find a security vulnerability in the RustDesk project, please report it responsibly by sending an email to info@rustdesk.com. + +At this juncture, we don't have a bug bounty program. We are a small team trying to solve a big problem. We urge you to report any vulnerabilities responsibly +so that we can continue building a secure application for the entire community. diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 1f35ef92d..f78b3a20b 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -2,7 +2,7 @@ An open-source remote desktop application, the open source TeamViewer alternativ Source code: https://github.com/rustdesk/rustdesk Doc: https://rustdesk.com/docs/en/manual/mobile/ -In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Addroid remote control. +In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Android remote control. In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. diff --git a/fastlane/metadata/android/nl-NL/full_description.txt b/fastlane/metadata/android/nl-NL/full_description.txt new file mode 100644 index 000000000..b60d52ceb --- /dev/null +++ b/fastlane/metadata/android/nl-NL/full_description.txt @@ -0,0 +1,11 @@ +Een open-source toepassing voor bureaublad op afstand, het open-source alternatief voor TeamViewer. +Bron code: https://github.com/rustdesk/rustdesk +Doc: https://rustdesk.com/docs/en/manual/mobile/ + +Om ervoor te zorgen dat een extern apparaat uw Android-apparaat via muis of aanraking kan besturen, moet u RustDesk toestaan de "Toegankelijkheid" service te gebruiken. RustDesk gebruikt AccessibilityService API om Android afstandsbediening te kunnen implementeren. + +Naast bediening op afstand kunt u met RustDesk ook eenvoudig bestanden overzetten tussen Android-apparaten en pc's. + +U hebt volledige controle over uw gegevens, en u hoeft zich geen zorgen te maken over de veiligheid. U kunt onze rendez-vous/relay server gebruiken, of zelf hosten, of uw eigen rendez-vous/relay server schrijven. Self-hosting server is gratis en open source: https://github.com/rustdesk/rustdesk-server + +Download en installeer de desktop versie vanaf: https://rustdesk.com, dan kunt u uw desktop benaderen en bedienen vanaf uw mobiel, of uw mobiel bedienen vanaf uw desktop. diff --git a/fastlane/metadata/android/nl-NL/short_description.txt b/fastlane/metadata/android/nl-NL/short_description.txt new file mode 100644 index 000000000..18a46000b --- /dev/null +++ b/fastlane/metadata/android/nl-NL/short_description.txt @@ -0,0 +1 @@ +Een open-source toepassing voor bureaublad op afstand, het open-source alternatief voor TeamViewer. diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index d7f6e316e..3cfca4d30 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -1,9 +1,10 @@ { - "app-id": "org.rustdesk.rustdesk", + "id": "com.rustdesk.RustDesk", "runtime": "org.freedesktop.Platform", "runtime-version": "21.08", "sdk": "org.freedesktop.Sdk", "command": "rustdesk", + "icon": "share/rustdesk/files/rustdesk.png", "modules": [ "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", "xdotool.json", @@ -13,13 +14,22 @@ "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" + "cp -r ./usr/* /app/", + "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk", + "mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop", + "sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop", + "sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop", + "for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png logo.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done" ], + "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"], "sources": [ { "type": "file", "path": "../rustdesk-1.2.0.deb" + }, + { + "type": "file", + "path": "../res/logo.svg" } ] } @@ -35,4 +45,4 @@ "--socket=pulseaudio", "--talk-name=org.freedesktop.Flatpak" ] -} \ No newline at end of file +} diff --git a/flutter/.gitignore b/flutter/.gitignore index 3cbfc0f54..9c7e52c12 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -54,3 +54,4 @@ lib/generated_bridge.freezed.dart flutter_export_environment.sh Flutter-Generated.xcconfig key.jks +macos/rustdesk.xcodeproj/project.xcworkspace/ diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 04b2ccc9a..b3c655917 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -11,19 +11,25 @@ - + + + android:requestLegacyExternalStorage="true" + android:roundIcon="@mipmap/ic_launcher" + android:supportsRtl="true"> + android:enabled="true" + android:exported="true"> + + + @@ -50,8 +56,6 @@ android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:windowSoftInputMode="adjustResize"> - - @@ -59,6 +63,11 @@ + + - + \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt index 328701567..71bbba754 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt @@ -1,21 +1,45 @@ package com.carriez.flutter_hbb +import android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +import android.Manifest.permission.SYSTEM_ALERT_WINDOW import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build +import android.util.Log import android.widget.Toast +import com.hjq.permissions.XXPermissions +import io.flutter.embedding.android.FlutterActivity + +const val DEBUG_BOOT_COMPLETED = "com.carriez.flutter_hbb.DEBUG_BOOT_COMPLETED" class BootReceiver : BroadcastReceiver() { + private val logTag = "tagBootReceiver" + override fun onReceive(context: Context, intent: Intent) { - if ("android.intent.action.BOOT_COMPLETED" == intent.action){ - val it = Intent(context,MainService::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + Log.d(logTag, "onReceive ${intent.action}") + + if (Intent.ACTION_BOOT_COMPLETED == intent.action || DEBUG_BOOT_COMPLETED == intent.action) { + // check SharedPreferences config + val prefs = context.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) + if (!prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) { + Log.d(logTag, "KEY_START_ON_BOOT_OPT is false") + return } - Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show(); + // check pre-permission + if (!XXPermissions.isGranted(context, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, SYSTEM_ALERT_WINDOW)){ + Log.d(logTag, "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS or SYSTEM_ALERT_WINDOW is not granted") + return + } + + val it = Intent(context, MainService::class.java).apply { + action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE + putExtra(EXT_INIT_FROM_BOOT, true) + } + Toast.makeText(context, "RustDesk is Open", Toast.LENGTH_LONG).show() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(it) - }else{ + } else { context.startService(it) } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index fd340f7ed..52a5ff75e 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -7,35 +7,29 @@ package com.carriez.flutter_hbb * Inspired by [droidVNC-NG] https://github.com/bk138/droidVNC-NG */ -import android.app.Activity import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection -import android.media.projection.MediaProjectionManager import android.os.Build import android.os.IBinder -import android.provider.Settings import android.util.Log import android.view.WindowManager -import androidx.annotation.RequiresApi +import com.hjq.permissions.XXPermissions import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel -const val MEDIA_REQUEST_CODE = 42 class MainActivity : FlutterActivity() { companion object { - lateinit var flutterMethodChannel: MethodChannel + var flutterMethodChannel: MethodChannel? = null } private val channelTag = "mChannel" private val logTag = "mMainActivity" - private var mediaProjectionResultIntent: Intent? = null private var mainService: MainService? = null - @RequiresApi(Build.VERSION_CODES.M) override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) if (MainService.isReady) { @@ -46,169 +40,32 @@ class MainActivity : FlutterActivity() { flutterMethodChannel = MethodChannel( flutterEngine.dartExecutor.binaryMessenger, channelTag - ).apply { - // make sure result is set, otherwise flutter will await forever - setMethodCallHandler { call, result -> - when (call.method) { - "init_service" -> { - Intent(activity, MainService::class.java).also { - bindService(it, serviceConnection, Context.BIND_AUTO_CREATE) - } - if (MainService.isReady) { - result.success(false) - return@setMethodCallHandler - } - getMediaProjection() - result.success(true) - } - "start_capture" -> { - mainService?.let { - result.success(it.startCapture()) - } ?: let { - result.success(false) - } - } - "stop_service" -> { - Log.d(logTag, "Stop service") - mainService?.let { - it.destroy() - result.success(true) - } ?: let { - result.success(false) - } - } - "check_permission" -> { - if (call.arguments is String) { - result.success(checkPermission(context, call.arguments as String)) - } else { - result.success(false) - } - } - "request_permission" -> { - if (call.arguments is String) { - requestPermission(context, call.arguments as String) - result.success(true) - } else { - result.success(false) - } - } - "check_video_permission" -> { - mainService?.let { - result.success(it.checkMediaPermission()) - } ?: let { - result.success(false) - } - } - "check_service" -> { - flutterMethodChannel.invokeMethod( - "on_state_changed", - mapOf("name" to "input", "value" to InputService.isOpen.toString()) - ) - flutterMethodChannel.invokeMethod( - "on_state_changed", - mapOf("name" to "media", "value" to MainService.isReady.toString()) - ) - result.success(true) - } - "init_input" -> { - initInput() - result.success(true) - } - "stop_input" -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - InputService.ctx?.disableSelf() - } - InputService.ctx = null - flutterMethodChannel.invokeMethod( - "on_state_changed", - mapOf("name" to "input", "value" to InputService.isOpen.toString()) - ) - result.success(true) - } - "cancel_notification" -> { - try { - val id = call.arguments as Int - mainService?.cancelNotification(id) - } finally { - result.success(true) - } - } - "enable_soft_keyboard" -> { - // https://blog.csdn.net/hanye2020/article/details/105553780 - try { - if (call.arguments as Boolean) { - window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - } else { - window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) - } - } finally { - result.success(true) - } - } - else -> { - result.error("-1", "No such method", null) - } - } - } - } - } - - private fun getMediaProjection() { - val mMediaProjectionManager = - getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager - val mIntent = mMediaProjectionManager.createScreenCaptureIntent() - startActivityForResult(mIntent, MEDIA_REQUEST_CODE) - } - - private fun initService() { - if (mediaProjectionResultIntent == null) { - Log.w(logTag, "initService fail,mediaProjectionResultIntent is null") - return - } - Log.d(logTag, "Init service") - val serviceIntent = Intent(this, MainService::class.java) - serviceIntent.action = INIT_SERVICE - serviceIntent.putExtra(EXTRA_MP_DATA, mediaProjectionResultIntent) - - launchMainService(serviceIntent) - } - - private fun launchMainService(intent: Intent) { - // TEST api < O - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } - } - - private fun initInput() { - val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent) - } + ) + initFlutterChannel(flutterMethodChannel!!) } override fun onResume() { super.onResume() val inputPer = InputService.isOpen activity.runOnUiThread { - flutterMethodChannel.invokeMethod( + flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "input", "value" to inputPer.toString()) ) } } + private fun requestMediaProjection() { + val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply { + action = ACT_REQUEST_MEDIA_PROJECTION + } + startActivityForResult(intent, REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION) + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == MEDIA_REQUEST_CODE) { - if (resultCode == Activity.RESULT_OK && data != null) { - mediaProjectionResultIntent = data - initService() - } else { - flutterMethodChannel.invokeMethod("on_media_projection_canceled", null) - } + if (requestCode == REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION && resultCode == RES_FAILED) { + flutterMethodChannel?.invokeMethod("on_media_projection_canceled", null) } } @@ -232,4 +89,138 @@ class MainActivity : FlutterActivity() { mainService = null } } + + private fun initFlutterChannel(flutterMethodChannel: MethodChannel) { + flutterMethodChannel.setMethodCallHandler { call, result -> + // make sure result will be invoked, otherwise flutter will await forever + when (call.method) { + "init_service" -> { + Intent(activity, MainService::class.java).also { + bindService(it, serviceConnection, Context.BIND_AUTO_CREATE) + } + if (MainService.isReady) { + result.success(false) + return@setMethodCallHandler + } + requestMediaProjection() + result.success(true) + } + "start_capture" -> { + mainService?.let { + result.success(it.startCapture()) + } ?: let { + result.success(false) + } + } + "stop_service" -> { + Log.d(logTag, "Stop service") + mainService?.let { + it.destroy() + result.success(true) + } ?: let { + result.success(false) + } + } + "check_permission" -> { + if (call.arguments is String) { + result.success(XXPermissions.isGranted(context, call.arguments as String)) + } else { + result.success(false) + } + } + "request_permission" -> { + if (call.arguments is String) { + requestPermission(context, call.arguments as String) + result.success(true) + } else { + result.success(false) + } + } + START_ACTION -> { + if (call.arguments is String) { + startAction(context, call.arguments as String) + result.success(true) + } else { + result.success(false) + } + } + "check_video_permission" -> { + mainService?.let { + result.success(it.checkMediaPermission()) + } ?: let { + result.success(false) + } + } + "check_service" -> { + Companion.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to InputService.isOpen.toString()) + ) + Companion.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "media", "value" to MainService.isReady.toString()) + ) + result.success(true) + } + "stop_input" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + InputService.ctx?.disableSelf() + } + InputService.ctx = null + Companion.flutterMethodChannel?.invokeMethod( + "on_state_changed", + mapOf("name" to "input", "value" to InputService.isOpen.toString()) + ) + result.success(true) + } + "cancel_notification" -> { + if (call.arguments is Int) { + val id = call.arguments as Int + mainService?.cancelNotification(id) + } else { + result.success(true) + } + } + "enable_soft_keyboard" -> { + // https://blog.csdn.net/hanye2020/article/details/105553780 + if (call.arguments as Boolean) { + window.clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) + } + result.success(true) + + } + GET_START_ON_BOOT_OPT -> { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + result.success(prefs.getBoolean(KEY_START_ON_BOOT_OPT, false)) + } + SET_START_ON_BOOT_OPT -> { + if (call.arguments is Boolean) { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + val edit = prefs.edit() + edit.putBoolean(KEY_START_ON_BOOT_OPT, call.arguments as Boolean) + edit.apply() + result.success(true) + } else { + result.success(false) + } + } + SYNC_APP_DIR_CONFIG_PATH -> { + if (call.arguments is String) { + val prefs = getSharedPreferences(KEY_SHARED_PREFERENCES, MODE_PRIVATE) + val edit = prefs.edit() + edit.putString(KEY_APP_DIR_CONFIG_PATH, call.arguments as String) + edit.apply() + result.success(true) + } else { + result.success(false) + } + } + else -> { + result.error("-1", "No such method", null) + } + } + } + } } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index ac736ffdc..1c3fbce6c 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -35,6 +35,7 @@ import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat +import io.flutter.embedding.android.FlutterActivity import java.util.concurrent.Executors import kotlin.concurrent.thread import org.json.JSONException @@ -43,10 +44,6 @@ import java.nio.ByteBuffer import kotlin.math.max import kotlin.math.min -const val EXTRA_MP_DATA = "mp_intent" -const val INIT_SERVICE = "init_service" -const val ACTION_LOGIN_REQ_NOTIFY = "ACTION_LOGIN_REQ_NOTIFY" -const val EXTRA_LOGIN_REQ_NOTIFY = "EXTRA_LOGIN_REQ_NOTIFY" const val DEFAULT_NOTIFY_TITLE = "RustDesk" const val DEFAULT_NOTIFY_TEXT = "Service is running" @@ -147,7 +144,11 @@ class MainService : Service() { // jvm call rust private external fun init(ctx: Context) - private external fun startServer() + + /// When app start on boot, app_dir will not be passed from flutter + /// so pass a app_dir here to rust server + private external fun startServer(app_dir: String) + private external fun startService() private external fun onVideoFrameUpdate(buf: ByteBuffer) private external fun onAudioFrameUpdate(buf: ByteBuffer) private external fun translateLocale(localeName: String, input: String): String @@ -195,6 +196,7 @@ class MainService : Service() { override fun onCreate() { super.onCreate() + Log.d(logTag,"MainService onCreate") HandlerThread("Service", Process.THREAD_PRIORITY_BACKGROUND).apply { start() serviceLooper = looper @@ -202,7 +204,13 @@ class MainService : Service() { } updateScreenInfo(resources.configuration.orientation) initNotification() - startServer() + + // keep the config dir same with flutter + val prefs = applicationContext.getSharedPreferences(KEY_SHARED_PREFERENCES, FlutterActivity.MODE_PRIVATE) + val configPath = prefs.getString(KEY_APP_DIR_CONFIG_PATH, "") ?: "" + startServer(configPath) + + createForegroundNotification() } override fun onDestroy() { @@ -277,22 +285,30 @@ class MainService : Service() { } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.d("whichService", "this service:${Thread.currentThread()}") + Log.d("whichService", "this service: ${Thread.currentThread()}") super.onStartCommand(intent, flags, startId) - if (intent?.action == INIT_SERVICE) { - Log.d(logTag, "service starting:${startId}:${Thread.currentThread()}") + if (intent?.action == ACT_INIT_MEDIA_PROJECTION_AND_SERVICE) { createForegroundNotification() - val mMediaProjectionManager = + + if (intent.getBooleanExtra(EXT_INIT_FROM_BOOT, false)) { + startService() + } + Log.d(logTag, "service starting: ${startId}:${Thread.currentThread()}") + val mediaProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager - intent.getParcelableExtra(EXTRA_MP_DATA)?.let { + + intent.getParcelableExtra(EXT_MEDIA_PROJECTION_RES_INTENT)?.let { mediaProjection = - mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it) + mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, it) checkMediaPermission() init(this) _isReady = true + } ?: let { + Log.d(logTag, "getParcelableExtra intent null, invoke requestMediaProjection") + requestMediaProjection() } } - return START_NOT_STICKY // don't use sticky (auto restart),the new service (from auto restart) will lose control + return START_NOT_STICKY // don't use sticky (auto restart), the new service (from auto restart) will lose control } override fun onConfigurationChanged(newConfig: Configuration) { @@ -300,6 +316,14 @@ class MainService : Service() { updateScreenInfo(newConfig.orientation) } + private fun requestMediaProjection() { + val intent = Intent(this, PermissionRequestTransparentActivity::class.java).apply { + action = ACT_REQUEST_MEDIA_PROJECTION + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + } + @SuppressLint("WrongConstant") private fun createSurface(): Surface? { return if (useVP9) { @@ -400,13 +424,13 @@ class MainService : Service() { fun checkMediaPermission(): Boolean { Handler(Looper.getMainLooper()).post { - MainActivity.flutterMethodChannel.invokeMethod( + MainActivity.flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "media", "value" to isReady.toString()) ) } Handler(Looper.getMainLooper()).post { - MainActivity.flutterMethodChannel.invokeMethod( + MainActivity.flutterMethodChannel?.invokeMethod( "on_state_changed", mapOf("name" to "input", "value" to InputService.isOpen.toString()) ) @@ -594,12 +618,12 @@ 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) .setContentTitle(DEFAULT_NOTIFY_TITLE) - .setContentText(translate(DEFAULT_NOTIFY_TEXT) + '!') + .setContentText(translate(DEFAULT_NOTIFY_TEXT)) .setOnlyAlertOnce(true) .setContentIntent(pendingIntent) .setColor(ContextCompat.getColor(this, R.color.primary)) @@ -653,8 +677,8 @@ class MainService : Service() { @SuppressLint("UnspecifiedImmutableFlag") private fun genLoginRequestPendingIntent(res: Boolean): PendingIntent { val intent = Intent(this, MainService::class.java).apply { - action = ACTION_LOGIN_REQ_NOTIFY - putExtra(EXTRA_LOGIN_REQ_NOTIFY, res) + action = ACT_LOGIN_REQ_NOTIFY + putExtra(EXT_LOGIN_REQ_NOTIFY, res) } return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.getService(this, 111, intent, FLAG_IMMUTABLE) @@ -665,7 +689,7 @@ class MainService : Service() { private fun setTextNotification(_title: String?, _text: String?) { val title = _title ?: DEFAULT_NOTIFY_TITLE - val text = _text ?: translate(DEFAULT_NOTIFY_TEXT) + '!' + val text = _text ?: translate(DEFAULT_NOTIFY_TEXT) val notification = notificationBuilder .clearActions() .setStyle(null) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt new file mode 100644 index 000000000..3beb7ec6b --- /dev/null +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/PermissionRequestTransparentActivity.kt @@ -0,0 +1,54 @@ +package com.carriez.flutter_hbb + +import android.app.Activity +import android.content.Intent +import android.media.projection.MediaProjectionManager +import android.os.Build +import android.os.Bundle +import android.util.Log + +class PermissionRequestTransparentActivity: Activity() { + private val logTag = "permissionRequest" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Log.d(logTag, "onCreate PermissionRequestTransparentActivity: intent.action: ${intent.action}") + + when (intent.action) { + ACT_REQUEST_MEDIA_PROJECTION -> { + val mediaProjectionManager = + getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + val intent = mediaProjectionManager.createScreenCaptureIntent() + startActivityForResult(intent, REQ_REQUEST_MEDIA_PROJECTION) + } + else -> finish() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQ_REQUEST_MEDIA_PROJECTION) { + if (resultCode == RESULT_OK && data != null) { + launchService(data) + } else { + setResult(RES_FAILED) + } + } + + finish() + } + + private fun launchService(mediaProjectionResultIntent: Intent) { + Log.d(logTag, "Launch MainService") + val serviceIntent = Intent(this, MainService::class.java) + serviceIntent.action = ACT_INIT_MEDIA_PROJECTION_AND_SERVICE + serviceIntent.putExtra(EXT_MEDIA_PROJECTION_RES_INTENT, mediaProjectionResultIntent) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(serviceIntent) + } else { + startService(serviceIntent) + } + } + +} \ No newline at end of file diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt index 4bf244a06..f8ef07fd1 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/common.kt @@ -1,5 +1,6 @@ package com.carriez.flutter_hbb +import android.Manifest.permission.* import android.annotation.SuppressLint import android.content.Context import android.content.Intent @@ -12,8 +13,8 @@ import android.os.Build import android.os.Handler import android.os.Looper import android.os.PowerManager -import android.provider.Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS -import android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS +import android.provider.Settings +import android.provider.Settings.* import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat.getSystemService import com.hjq.permissions.Permission @@ -22,6 +23,31 @@ import java.nio.ByteBuffer import java.util.* +// intent action, extra +const val ACT_REQUEST_MEDIA_PROJECTION = "REQUEST_MEDIA_PROJECTION" +const val ACT_INIT_MEDIA_PROJECTION_AND_SERVICE = "INIT_MEDIA_PROJECTION_AND_SERVICE" +const val ACT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" +const val EXT_INIT_FROM_BOOT = "EXT_INIT_FROM_BOOT" +const val EXT_MEDIA_PROJECTION_RES_INTENT = "MEDIA_PROJECTION_RES_INTENT" +const val EXT_LOGIN_REQ_NOTIFY = "LOGIN_REQ_NOTIFY" + +// Activity requestCode +const val REQ_INVOKE_PERMISSION_ACTIVITY_MEDIA_PROJECTION = 101 +const val REQ_REQUEST_MEDIA_PROJECTION = 201 + +// Activity responseCode +const val RES_FAILED = -100 + +// Flutter channel +const val START_ACTION = "start_action" +const val GET_START_ON_BOOT_OPT = "get_start_on_boot_opt" +const val SET_START_ON_BOOT_OPT = "set_start_on_boot_opt" +const val SYNC_APP_DIR_CONFIG_PATH = "sync_app_dir" + +const val KEY_SHARED_PREFERENCES = "KEY_SHARED_PREFERENCES" +const val KEY_START_ON_BOOT_OPT = "KEY_START_ON_BOOT_OPT" +const val KEY_APP_DIR_CONFIG_PATH = "KEY_APP_DIR_CONFIG_PATH" + @SuppressLint("ConstantLocale") val LOCAL_NAME = Locale.getDefault().toString() val SCREEN_INFO = Info(0, 0, 1, 200) @@ -30,61 +56,13 @@ data class Info( var width: Int, var height: Int, var scale: Int, var dpi: Int ) -@RequiresApi(Build.VERSION_CODES.LOLLIPOP) -fun testVP9Support(): Boolean { - return true - val res = MediaCodecList(MediaCodecList.ALL_CODECS) - .findEncoderForFormat( - MediaFormat.createVideoFormat( - MediaFormat.MIMETYPE_VIDEO_VP9, - SCREEN_INFO.width, - SCREEN_INFO.width - ) - ) - return res != null -} - -@RequiresApi(Build.VERSION_CODES.M) fun requestPermission(context: Context, type: String) { - val permission = when (type) { - "ignore_battery_optimizations" -> { - try { - context.startActivity(Intent(ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { - data = Uri.parse("package:" + context.packageName) - }) - } catch (e:Exception) { - e.printStackTrace() - } - return - } - "application_details_settings" -> { - try { - context.startActivity(Intent().apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - action = "android.settings.APPLICATION_DETAILS_SETTINGS" - data = Uri.parse("package:" + context.packageName) - }) - } catch (e:Exception) { - e.printStackTrace() - } - return - } - "audio" -> { - Permission.RECORD_AUDIO - } - "file" -> { - Permission.MANAGE_EXTERNAL_STORAGE - } - else -> { - return - } - } XXPermissions.with(context) - .permission(permission) + .permission(type) .request { _, all -> if (all) { Handler(Looper.getMainLooper()).post { - MainActivity.flutterMethodChannel.invokeMethod( + MainActivity.flutterMethodChannel?.invokeMethod( "on_android_permission_result", mapOf("type" to type, "result" to all) ) @@ -93,24 +71,18 @@ fun requestPermission(context: Context, type: String) { } } -@RequiresApi(Build.VERSION_CODES.M) -fun checkPermission(context: Context, type: String): Boolean { - val permission = when (type) { - "ignore_battery_optimizations" -> { - val pw = context.getSystemService(Context.POWER_SERVICE) as PowerManager - return pw.isIgnoringBatteryOptimizations(context.packageName) - } - "audio" -> { - Permission.RECORD_AUDIO - } - "file" -> { - Permission.MANAGE_EXTERNAL_STORAGE - } - else -> { - return false - } +fun startAction(context: Context, action: String) { + try { + context.startActivity(Intent(action).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + // don't pass package name when launch ACTION_ACCESSIBILITY_SETTINGS + if (ACTION_ACCESSIBILITY_SETTINGS != action) { + data = Uri.parse("package:" + context.packageName) + } + }) + } catch (e: Exception) { + e.printStackTrace() } - return XXPermissions.isGranted(context, permission) } class AudioReader(val bufSize: Int, private val maxFrames: Int) { diff --git a/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..65291b96e --- /dev/null +++ b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..65291b96e --- /dev/null +++ b/flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index d5d2c49c8..d05404d3a 100644 Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..3742f241f Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..964c5faa0 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png new file mode 100644 index 000000000..79a814f59 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png differ diff --git a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png new file mode 100644 index 000000000..814ba4549 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index e30cc5019..f16b3d61d 100644 Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..de17ccbda Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..2136a2f3c Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png new file mode 100644 index 000000000..c179bf053 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 41ccba607..d9bd8fdfe 100644 Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..f8ced45f1 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..415eca622 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png new file mode 100644 index 000000000..d82d1a81b Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index c10349d71..eba179347 100644 Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..0f46fafaf Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..87889c953 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png new file mode 100644 index 000000000..2cbe6eaf1 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 52fde7830..a8d80d2a2 100644 Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 000000000..88eafe8dd Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..00709a815 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png new file mode 100644 index 000000000..209c5f977 Binary files /dev/null and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png differ diff --git a/flutter/android/app/src/main/res/values/ic_launcher_background.xml b/flutter/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 000000000..ab9832824 --- /dev/null +++ b/flutter/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #ffffff + \ No newline at end of file diff --git a/flutter/android/app/src/main/res/values/styles.xml b/flutter/android/app/src/main/res/values/styles.xml index d74aa35c2..146267c91 100644 --- a/flutter/android/app/src/main/res/values/styles.xml +++ b/flutter/android/app/src/main/res/values/styles.xml @@ -15,4 +15,12 @@ + diff --git a/flutter/assets/GitHub.svg b/flutter/assets/GitHub.svg new file mode 100644 index 000000000..ef0bb12a7 --- /dev/null +++ b/flutter/assets/GitHub.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/Github.svg b/flutter/assets/Github.svg deleted file mode 100644 index a5bd1de81..000000000 --- a/flutter/assets/Github.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg index b7bb2f42f..df394a84f 100644 --- a/flutter/assets/Google.svg +++ b/flutter/assets/Google.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/Okta.svg b/flutter/assets/Okta.svg index 0fa45b93d..931e72844 100644 --- a/flutter/assets/Okta.svg +++ b/flutter/assets/Okta.svg @@ -1,30 +1 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/flutter/assets/actions.svg b/flutter/assets/actions.svg new file mode 100644 index 000000000..0a3c4bc42 --- /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..32d8dc815 --- /dev/null +++ b/flutter/assets/actions_mobile.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/android.svg b/flutter/assets/android.svg index e46dab11e..6fd89c9ab 100644 --- a/flutter/assets/android.svg +++ b/flutter/assets/android.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/arrow.svg b/flutter/assets/arrow.svg new file mode 100644 index 000000000..050145388 --- /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..fb7c9d292 --- /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..299e3d0cf --- /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..3a8bae7e3 --- /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..0dd66b666 --- /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..eb8cd8cf6 --- /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..c32e48397 --- /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..b787cf0b1 --- /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..22e7cdd63 --- /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..69eeb4a9c --- /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..992d21d42 --- /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..ab93bba60 --- /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..c98e485ad --- /dev/null +++ b/flutter/assets/home.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/insecure.svg b/flutter/assets/insecure.svg index 37bb196e3..5a344dd04 100644 --- a/flutter/assets/insecure.svg +++ b/flutter/assets/insecure.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/insecure_relay.svg b/flutter/assets/insecure_relay.svg index f08bee6a6..17b474e6e 100644 --- a/flutter/assets/insecure_relay.svg +++ b/flutter/assets/insecure_relay.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg index 69f0c96cb..163e045e1 100644 --- a/flutter/assets/kb_layout_iso.svg +++ b/flutter/assets/kb_layout_iso.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg index 09a055be3..cfbb046ca 100644 --- a/flutter/assets/kb_layout_not_iso.svg +++ b/flutter/assets/kb_layout_not_iso.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/flutter/assets/keyboard.svg b/flutter/assets/keyboard.svg new file mode 100644 index 000000000..0e94a5a62 --- /dev/null +++ b/flutter/assets/keyboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/linux.svg b/flutter/assets/linux.svg index 74248b5f0..2c3697be9 100644 --- a/flutter/assets/linux.svg +++ b/flutter/assets/linux.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/flutter/assets/logo.ico b/flutter/assets/logo.ico deleted file mode 100644 index d5080c1f7..000000000 Binary files a/flutter/assets/logo.ico and /dev/null differ diff --git a/flutter/assets/logo.png b/flutter/assets/logo.png deleted file mode 100644 index ede0e00c4..000000000 Binary files a/flutter/assets/logo.png and /dev/null differ diff --git a/flutter/assets/logo.svg b/flutter/assets/logo.svg index 0001d0762..4d43f8bcd 100644 --- a/flutter/assets/logo.svg +++ b/flutter/assets/logo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/mac.svg b/flutter/assets/mac.svg index 8092b3af3..ccf9c7aab 100644 --- a/flutter/assets/mac.svg +++ b/flutter/assets/mac.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/pinned.svg b/flutter/assets/pinned.svg new file mode 100644 index 000000000..872f38a17 --- /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..70f106139 --- /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..4ca2c96b8 --- /dev/null +++ b/flutter/assets/refresh.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/screen.svg b/flutter/assets/screen.svg new file mode 100644 index 000000000..dab5f87a4 --- /dev/null +++ b/flutter/assets/screen.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..adc061f6d --- /dev/null +++ b/flutter/assets/search.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/secure.svg b/flutter/assets/secure.svg index 29e1d3c4f..fcd99f2f5 100644 --- a/flutter/assets/secure.svg +++ b/flutter/assets/secure.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/flutter/assets/secure_relay.svg b/flutter/assets/secure_relay.svg index 8ecbdb47b..af54808a8 100644 --- a/flutter/assets/secure_relay.svg +++ b/flutter/assets/secure_relay.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/assets/transfer.svg b/flutter/assets/transfer.svg new file mode 100644 index 000000000..ad0072bb6 --- /dev/null +++ b/flutter/assets/transfer.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/assets/trash.svg b/flutter/assets/trash.svg new file mode 100644 index 000000000..c5436511b --- /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..e69da2ae4 --- /dev/null +++ b/flutter/assets/unpinned.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/voice_call.svg b/flutter/assets/voice_call.svg new file mode 100644 index 000000000..bf90ec958 --- /dev/null +++ b/flutter/assets/voice_call.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/voice_call_waiting.svg b/flutter/assets/voice_call_waiting.svg new file mode 100644 index 000000000..f1771c3fd --- /dev/null +++ b/flutter/assets/voice_call_waiting.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/win.svg b/flutter/assets/win.svg index 326f7829d..a0f7e3def 100644 --- a/flutter/assets/win.svg +++ b/flutter/assets/win.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/flutter/build_android.sh b/flutter/build_android.sh index 01ff23488..c6b639f87 100755 --- a/flutter/build_android.sh +++ b/flutter/build_android.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash -$ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* -flutter build apk --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build apk ---split-per-abi --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info -flutter build appbundle --target-platform android-arm64,android-arm --release --obfuscate --split-debug-info ./split-debug-info + +MODE=${MODE:=release} +$ANDROID_NDK_HOME/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* +flutter build apk --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build apk --split-per-abi --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info +flutter build appbundle --target-platform android-arm64,android-arm --${MODE} --obfuscate --split-debug-info ./split-debug-info # build in linux # $ANDROID_NDK/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android-strip android/app/src/main/jniLibs/arm64-v8a/* diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index c35862a8c..16cef3177 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 900bd13fa..298f4d9af 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 5fc34ce9a..fd3b01b6d 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index ab315a4c6..18ebaab69 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 6d69c01e1..a8ee14a31 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index b6c8034cd..a83f88b05 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index cf6c7c775..331e72531 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 5fc34ce9a..fd3b01b6d 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 6928a4e6d..aee7e4321 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a13129e15..2d0da17b1 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index a13129e15..2d0da17b1 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 319e70f91..7ee56922e 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 229bdf563..76abd423b 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index caffb26a3..e08138333 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 751104548..46de51af6 100644 Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 8d047dd0d..1b6afd584 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -3,13 +3,11 @@ import 'dart:convert'; import 'dart:ffi' hide Size; import 'dart:io'; import 'dart:math'; -import 'dart:typed_data'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; -import 'package:win32/win32.dart' as win32; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,14 +16,18 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:texture_rgba_renderer/texture_rgba_renderer.dart'; import 'package:uni_links/uni_links.dart'; import 'package:uni_links_desktop/uni_links_desktop.dart'; -import 'package:window_manager/window_manager.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:window_size/window_size.dart' as window_size; import 'package:url_launcher/url_launcher.dart'; +import 'package:win32/win32.dart' as win32; +import 'package:window_manager/window_manager.dart'; +import 'package:window_size/window_size.dart' as window_size; +import '../consts.dart'; import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/remote_page.dart'; @@ -33,8 +35,6 @@ import 'models/input_model.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; -import '../consts.dart'; - final globalKey = GlobalKey(); final navigationBarKey = GlobalKey(); @@ -46,10 +46,20 @@ var isWebDesktop = false; var version = ""; int androidVersion = 0; +/// Incriment count for textureId. +int _textureId = 0; +int get newTextureId => _textureId++; +final textureRenderer = TextureRgbaRenderer(); + /// only available for Windows target int windowsBuildNumber = 0; DesktopType? desktopType; +/// Check if the app is running with single view mode. +bool isSingleViewApp() { + return desktopType == DesktopType.cm; +} + /// * debug or test only, DO NOT enable in release build bool isTest = false; @@ -99,28 +109,48 @@ class IconFont { class ColorThemeExtension extends ThemeExtension { const ColorThemeExtension({ required this.border, + required this.border2, required this.highlight, + required this.drag_indicator, + required this.shadow, }); final Color? border; + final Color? border2; final Color? highlight; + final Color? drag_indicator; + final Color? shadow; - static const light = ColorThemeExtension( + static final light = ColorThemeExtension( border: Color(0xFFCCCCCC), + border2: Color(0xFFBBBBBB), highlight: Color(0xFFE5E5E5), + drag_indicator: Colors.grey[800], + shadow: Colors.black, ); - static const dark = ColorThemeExtension( + static final dark = ColorThemeExtension( border: Color(0xFF555555), + border2: Color(0xFFE5E5E5), highlight: Color(0xFF3F3F3F), + drag_indicator: Colors.grey, + shadow: Colors.grey, ); @override - ThemeExtension copyWith( - {Color? border, Color? highlight}) { + ThemeExtension copyWith({ + Color? border, + Color? border2, + Color? highlight, + Color? drag_indicator, + Color? shadow, + }) { return ColorThemeExtension( border: border ?? this.border, + border2: border2 ?? this.border2, highlight: highlight ?? this.highlight, + drag_indicator: drag_indicator ?? this.drag_indicator, + shadow: shadow ?? this.shadow, ); } @@ -132,7 +162,10 @@ class ColorThemeExtension extends ThemeExtension { } return ColorThemeExtension( border: Color.lerp(border, other.border, t), + border2: Color.lerp(border2, other.border2, t), highlight: Color.lerp(highlight, other.highlight, t), + drag_indicator: Color.lerp(drag_indicator, other.drag_indicator, t), + shadow: Color.lerp(shadow, other.shadow, t), ); } } @@ -140,15 +173,14 @@ class ColorThemeExtension extends ThemeExtension { class MyTheme { MyTheme._(); - static const Color grayBg = Color(0xFFEEEEEE); - static const Color white = Color(0xFFFFFFFF); + static const Color grayBg = Color(0xFFEFEFF2); static const Color accent = Color(0xFF0071FF); static const Color accent50 = Color(0x770071FF); static const Color accent80 = Color(0xAA0071FF); static const Color canvasColor = Color(0xFF212121); static const Color border = Color(0xFFCCCCCC); static const Color idColor = Color(0xFF00B6F0); - static const Color darkGray = Color(0xFFB9BABC); + static const Color darkGray = Color.fromARGB(255, 148, 148, 148); static const Color cmIdColor = Color(0xFF21790B); static const Color dark = Colors.black87; static const Color button = Color(0xFF2C8CFF); @@ -156,8 +188,29 @@ class MyTheme { static ThemeData lightTheme = ThemeData( brightness: Brightness.light, - backgroundColor: Color(0xFFFFFFFF), - scaffoldBackgroundColor: Color(0xFFEEEEEE), + hoverColor: Color.fromARGB(255, 224, 224, 224), + scaffoldBackgroundColor: Colors.white, + dialogBackgroundColor: Colors.white, + dialogTheme: DialogTheme( + elevation: 15, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + side: BorderSide( + width: 1, + color: grayBg, + ), + ), + ), + inputDecorationTheme: isDesktop + ? InputDecorationTheme( + fillColor: grayBg, + filled: true, + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ) + : null, textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19, color: Colors.black87), titleSmall: TextStyle(fontSize: 14, color: Colors.black87), @@ -165,8 +218,8 @@ class MyTheme { bodyMedium: TextStyle(fontSize: 14, color: Colors.black87, height: 1.25), labelLarge: TextStyle(fontSize: 16.0, color: MyTheme.accent80)), + cardColor: grayBg, hintColor: Color(0xFFAAAAAA), - primarySwatch: Colors.blue, visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( labelColor: Colors.black87, @@ -176,9 +229,51 @@ class MyTheme { splashFactory: isDesktop ? NoSplash.splashFactory : null, textButtonTheme: isDesktop ? TextButtonThemeData( - style: ButtonStyle(splashFactory: NoSplash.splashFactory), + style: TextButton.styleFrom( + splashFactory: NoSplash.splashFactory, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), ) : null, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: MyTheme.accent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + backgroundColor: grayBg, + foregroundColor: Colors.black87, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + checkboxTheme: const CheckboxThemeData( + splashRadius: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ), + listTileTheme: ListTileThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ), + menuBarTheme: MenuBarThemeData( + style: + MenuStyle(backgroundColor: MaterialStatePropertyAll(Colors.white))), + colorScheme: ColorScheme.light( + primary: Colors.blue, secondary: accent, background: grayBg), ).copyWith( extensions: >[ ColorThemeExtension.light, @@ -187,8 +282,29 @@ class MyTheme { ); static ThemeData darkTheme = ThemeData( brightness: Brightness.dark, - backgroundColor: Color(0xFF252525), - scaffoldBackgroundColor: Color(0xFF141414), + hoverColor: Color.fromARGB(255, 45, 46, 53), + scaffoldBackgroundColor: Color(0xFF18191E), + dialogBackgroundColor: Color(0xFF18191E), + dialogTheme: DialogTheme( + elevation: 15, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + side: BorderSide( + width: 1, + color: Color(0xFF24252B), + ), + ), + ), + inputDecorationTheme: isDesktop + ? InputDecorationTheme( + fillColor: Color(0xFF24252B), + filled: true, + isDense: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ) + : null, textTheme: const TextTheme( titleLarge: TextStyle(fontSize: 19), titleSmall: TextStyle(fontSize: 14), @@ -196,20 +312,74 @@ class MyTheme { bodyMedium: TextStyle(fontSize: 14, height: 1.25), labelLarge: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, color: accent80)), - cardColor: Color(0xFF252525), - primarySwatch: Colors.blue, + cardColor: Color(0xFF24252B), visualDensity: VisualDensity.adaptivePlatformDensity, tabBarTheme: const TabBarTheme( labelColor: Colors.white70, ), + scrollbarTheme: ScrollbarThemeData( + thumbColor: MaterialStateProperty.all(Colors.grey[500]), + ), splashColor: Colors.transparent, highlightColor: Colors.transparent, splashFactory: isDesktop ? NoSplash.splashFactory : null, textButtonTheme: isDesktop ? TextButtonThemeData( - style: ButtonStyle(splashFactory: NoSplash.splashFactory), + style: TextButton.styleFrom( + splashFactory: NoSplash.splashFactory, + disabledForegroundColor: Colors.white70, + foregroundColor: Colors.white70, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18.0), + ), + ), ) : null, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: MyTheme.accent, + foregroundColor: Colors.white, + disabledForegroundColor: Colors.white70, + disabledBackgroundColor: Colors.white10, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + backgroundColor: Color(0xFF24252B), + side: BorderSide(color: Colors.white12, width: 0.5), + disabledForegroundColor: Colors.white70, + foregroundColor: Colors.white70, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + ), + ), + ), + checkboxTheme: const CheckboxThemeData( + splashRadius: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ), + listTileTheme: ListTileThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(5), + ), + ), + ), + menuBarTheme: MenuBarThemeData( + style: MenuStyle( + backgroundColor: MaterialStatePropertyAll(Color(0xFF121212)))), + colorScheme: ColorScheme.dark( + primary: Colors.blue, + secondary: accent, + background: Color(0xFF24252B), + ), ).copyWith( extensions: >[ ColorThemeExtension.dark, @@ -221,16 +391,18 @@ class MyTheme { return themeModeFromString(bind.mainGetLocalOption(key: kCommConfKeyTheme)); } - static void changeDarkMode(ThemeMode mode) { + static void changeDarkMode(ThemeMode mode) async { Get.changeThemeMode(mode); - if (desktopType == DesktopType.main) { + if (desktopType == DesktopType.main || isAndroid || isIOS) { if (mode == ThemeMode.system) { - bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); + await bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); } else { - bind.mainSetLocalOption( + await bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - bind.mainChangeTheme(dark: mode.toShortString()); + await bind.mainChangeTheme(dark: mode.toShortString()); + // Synchronize the window theme of the system. + updateSystemWindowTheme(); } } @@ -283,7 +455,7 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom( ); List supportedLocales = const [ - // specify CN/TW to fix CJK issue in flutter + Locale('en', 'US'), Locale('zh', 'CN'), Locale('zh', 'TW'), Locale('zh', 'SG'), @@ -305,7 +477,7 @@ List supportedLocales = const [ Locale('vi'), Locale('pl'), Locale('kz'), - Locale('en', 'US'), + Locale('es'), ]; String formatDurationToTime(Duration duration) { @@ -327,6 +499,9 @@ closeConnection({String? id}) { } void window_on_top(int? id) { + if (!isDesktop) { + return; + } if (id == null) { // main window windowManager.restore(); @@ -363,20 +538,25 @@ class Dialog { } } +class OverlayKeyState { + final _overlayKey = GlobalKey(); + + /// use global overlay by default + OverlayState? get state => + _overlayKey.currentState ?? globalKey.currentState?.overlay; + + GlobalKey? get key => _overlayKey; +} + class OverlayDialogManager { - OverlayState? _overlayState; final Map _dialogs = {}; + var _overlayKeyState = OverlayKeyState(); int _tagCount = 0; OverlayEntry? _mobileActionsOverlayEntry; - /// By default OverlayDialogManager use global overlay - OverlayDialogManager() { - _overlayState = globalKey.currentState?.overlay; - } - - void setOverlayState(OverlayState? overlayState) { - _overlayState = overlayState; + void setOverlayState(OverlayKeyState overlayKeyState) { + _overlayKeyState = overlayKeyState; } void dismissAll() { @@ -400,7 +580,7 @@ class OverlayDialogManager { bool useAnimation = true, bool forceGlobal = false}) { final overlayState = - forceGlobal ? globalKey.currentState?.overlay : _overlayState; + forceGlobal ? globalKey.currentState?.overlay : _overlayKeyState.state; if (overlayState == null) { return Future.error( @@ -424,7 +604,7 @@ class OverlayDialogManager { BackButtonInterceptor.removeByName(dialogTag); } - dialog.entry = OverlayEntry(builder: (_) { + dialog.entry = OverlayEntry(builder: (context) { bool innerClicked = false; return Listener( onPointerUp: (_) { @@ -434,7 +614,9 @@ class OverlayDialogManager { innerClicked = false; }, child: Container( - color: Colors.black12, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black12 + : Colors.black45, child: StatefulBuilder(builder: (context, setState) { return Listener( onPointerUp: (_) => innerClicked = true, @@ -483,12 +665,14 @@ class OverlayDialogManager { Offstage( offstage: !showCancel, child: Center( - child: TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate('Cancel'), - style: - const TextStyle(color: MyTheme.accent))))) + child: isDesktop + ? dialogButton('Cancel', onPressed: cancel) + : TextButton( + style: flatButtonStyle, + onPressed: cancel, + child: Text(translate('Cancel'), + style: const TextStyle( + color: MyTheme.accent))))) ])), onCancel: showCancel ? cancel : null, ); @@ -504,7 +688,8 @@ class OverlayDialogManager { void showMobileActionsOverlay({FFI? ffi}) { if (_mobileActionsOverlayEntry != null) return; - if (_overlayState == null) return; + final overlayState = _overlayKeyState.state; + if (overlayState == null) return; // compute overlay position final screenW = MediaQuery.of(globalKey.currentContext!).size.width; @@ -530,7 +715,7 @@ class OverlayDialogManager { onHidePressed: () => hideMobileActionsOverlay(), ); }); - _overlayState!.insert(overlay); + overlayState.insert(overlay); _mobileActionsOverlayEntry = overlay; } @@ -549,6 +734,10 @@ class OverlayDialogManager { hideMobileActionsOverlay(); } } + + bool existing(String tag) { + return _dialogs.keys.contains(tag); + } } void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { @@ -604,13 +793,15 @@ class CustomAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { - FocusNode focusNode = FocusNode(); - // request focus if there is no focused FocusNode in the dialog + // request focus + FocusScopeNode scopeNode = FocusScopeNode(); Future.delayed(Duration.zero, () { - if (!focusNode.hasFocus) focusNode.requestFocus(); + if (!scopeNode.hasFocus) scopeNode.requestFocus(); }); - return Focus( - focusNode: focusNode, + const double padding = 30; + bool tabTapped = false; + return FocusScope( + node: scopeNode, autofocus: true, onKey: (node, key) { if (key.logicalKey == LogicalKeyboardKey.escape) { @@ -618,21 +809,36 @@ class CustomAlertDialog extends StatelessWidget { onCancel?.call(); } return KeyEventResult.handled; // avoid TextField exception on escape - } else if (onSubmit != null && + } else if (!tabTapped && + onSubmit != null && key.logicalKey == LogicalKeyboardKey.enter) { if (key is RawKeyDownEvent) onSubmit?.call(); return KeyEventResult.handled; + } else if (key.logicalKey == LogicalKeyboardKey.tab) { + if (key is RawKeyDownEvent) { + scopeNode.nextFocus(); + tabTapped = true; + } + return KeyEventResult.handled; } return KeyEventResult.ignored; }, child: AlertDialog( scrollable: true, title: title, - contentPadding: EdgeInsets.symmetric( - horizontal: contentPadding ?? 25, vertical: 10), - content: - ConstrainedBox(constraints: contentBoxConstraints, child: content), + titlePadding: EdgeInsets.fromLTRB(padding, 24, padding, 0), + contentPadding: EdgeInsets.fromLTRB( + contentPadding ?? padding, + 25, + contentPadding ?? padding, + actions is List ? 10 : padding, + ), + content: ConstrainedBox( + constraints: contentBoxConstraints, + child: content, + ), actions: actions, + actionsPadding: EdgeInsets.fromLTRB(padding, 0, padding, padding), ), ); } @@ -640,7 +846,7 @@ class CustomAlertDialog extends StatelessWidget { void msgBox(String id, String type, String title, String text, String link, OverlayDialogManager dialogManager, - {bool? hasCancel}) { + {bool? hasCancel, ReconnectHandle? reconnect}) { dialogManager.dismissAll(); List buttons = []; bool hasOk = false; @@ -664,30 +870,36 @@ void msgBox(String id, String type, String title, String text, String link, if (type != "connecting" && type != "success" && !type.contains("nook")) { hasOk = true; - buttons.insert(0, msgBoxButton(translate('OK'), submit)); + buttons.insert(0, dialogButton('OK', onPressed: submit)); } hasCancel ??= !type.contains("error") && !type.contains("nocancel") && type != "restarting"; if (hasCancel) { - buttons.insert(0, msgBoxButton(translate('Cancel'), cancel)); + buttons.insert( + 0, dialogButton('Cancel', onPressed: cancel, isOutline: true)); } - // TODO: test this button if (type.contains("hasclose")) { buttons.insert( 0, - msgBoxButton(translate('Close'), () { + dialogButton('Close', onPressed: () { dialogManager.dismissAll(); })); } + if (reconnect != null && title == "Connection Error") { + buttons.insert( + 0, + dialogButton('Reconnect', isOutline: true, onPressed: () { + reconnect(dialogManager, id, false); + })); + } if (link.isNotEmpty) { - buttons.insert(0, msgBoxButton(translate('JumpLink'), jumplink)); + buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); } dialogManager.show( (setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), - content: - SelectableText(translate(text), style: const TextStyle(fontSize: 15)), + title: null, + content: SelectionArea(child: msgboxContent(type, title, text)), actions: buttons, onSubmit: hasOk ? submit : null, onCancel: hasCancel == true ? cancel : null, @@ -696,30 +908,73 @@ void msgBox(String id, String type, String title, String text, String link, ); } -Widget msgBoxButton(String text, void Function() onPressed) { - return ButtonTheme( - padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - //limits the touch area to the button area - minWidth: 0, - //wraps child's width - height: 0, - child: TextButton( - style: flatButtonStyle, - onPressed: onPressed, - child: - Text(translate(text), style: TextStyle(color: MyTheme.accent)))); +Color? _msgboxColor(String type) { + if (type == "input-password" || type == "custom-os-password") { + return Color(0xFFAD448E); + } + if (type.contains("success")) { + return Color(0xFF32bea6); + } + if (type.contains("error") || type == "re-input-password") { + return Color(0xFFE04F5F); + } + return Color(0xFF2C8CFF); } -Widget _msgBoxTitle(String title) => - Text(translate(title), style: TextStyle(fontSize: 21)); +Widget msgboxIcon(String type) { + IconData? iconData; + if (type.contains("error") || type == "re-input-password") { + iconData = Icons.cancel; + } + if (type.contains("success")) { + iconData = Icons.check_circle; + } + if (type == "wait-uac" || type == "wait-remote-accept-nook") { + iconData = Icons.hourglass_top; + } + if (type == 'on-uac' || type == 'on-foreground-elevated') { + iconData = Icons.admin_panel_settings; + } + if (type == "info") { + iconData = Icons.info; + } + if (iconData != null) { + return Icon(iconData, size: 50, color: _msgboxColor(type)) + .marginOnly(right: 16); + } + + return Offstage(); +} + +// title should be null +Widget msgboxContent(String type, String title, String text) { + return Row( + children: [ + msgboxIcon(type), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate(title), + style: TextStyle(fontSize: 21), + ).marginOnly(bottom: 10), + Text(translate(text), style: const TextStyle(fontSize: 15)), + ], + ), + ), + ], + ).marginOnly(bottom: 12); +} void msgBoxCommon(OverlayDialogManager dialogManager, String title, Widget content, List buttons, {bool hasCancel = true}) { - dialogManager.dismissAll(); dialogManager.show((setState, close) => CustomAlertDialog( - title: _msgBoxTitle(title), + title: Text( + translate(title), + style: TextStyle(fontSize: 21), + ), content: content, actions: buttons, onCancel: hasCancel ? close : null, @@ -797,21 +1052,14 @@ class AccessibilityListener extends StatelessWidget { } } -class PermissionManager { +class AndroidPermissionManager { static Completer? _completer; static Timer? _timer; static var _current = ""; - static final permissions = [ - "audio", - "file", - "ignore_battery_optimizations", - "application_details_settings" - ]; - static bool isWaitingFile() { if (_completer != null) { - return !_completer!.isCompleted && _current == "file"; + return !_completer!.isCompleted && _current == kManageExternalStorage; } return false; } @@ -820,31 +1068,33 @@ class PermissionManager { if (isDesktop) { return Future.value(true); } - if (!permissions.contains(type)) { - return Future.error("Wrong permission!$type"); - } return gFFI.invokeMethod("check_permission", type); } + // startActivity goto Android Setting's page to request permission manually by user + static void startAction(String action) { + gFFI.invokeMethod(AndroidChannel.kStartAction, action); + } + + /// We use XXPermissions to request permissions, + /// for supported types, see https://github.com/getActivity/XXPermissions/blob/e46caea32a64ad7819df62d448fb1c825481cd28/library/src/main/java/com/hjq/permissions/Permission.java static Future request(String type) { if (isDesktop) { return Future.value(true); } - if (!permissions.contains(type)) { - return Future.error("Wrong permission!$type"); - } gFFI.invokeMethod("request_permission", type); - if (type == "ignore_battery_optimizations") { - return Future.value(false); + + // clear last task + if (_completer?.isCompleted == false) { + _completer?.complete(false); } + _timer?.cancel(); + _current = type; _completer = Completer(); - gFFI.invokeMethod("request_permission", type); - // timeout - _timer?.cancel(); - _timer = Timer(Duration(seconds: 60), () { + _timer = Timer(Duration(seconds: 120), () { if (_completer == null) return; if (!_completer!.isCompleted) { _completer!.complete(false); @@ -977,11 +1227,13 @@ Future matchPeer(String searchText, Peer peer) async { /// Get the image for the current [platform]. Widget getPlatformImage(String platform, {double size = 50}) { - platform = platform.toLowerCase(); - if (platform == 'mac os') { + if (platform == kPeerPlatformMacOS) { platform = 'mac'; - } else if (platform != 'linux' && platform != 'android') { + } else if (platform != kPeerPlatformLinux && + platform != kPeerPlatformAndroid) { platform = 'win'; + } else { + platform = platform.toLowerCase(); } return SvgPicture.asset('assets/$platform.svg', height: size, width: size); } @@ -1204,10 +1456,12 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { /// [Availability] /// initUniLinks should only be used on macos/windows. /// we use dbus for linux currently. -Future initUniLinks() async { - if (!Platform.isWindows && !Platform.isMacOS) { - return; +Future initUniLinks() async { + if (Platform.isLinux) { + return false; } + // Register uni links for Windows. The required info of url scheme is already + // declared in `Info.plist` for macOS. if (Platform.isWindows) { registerProtocol('rustdesk'); } @@ -1215,22 +1469,33 @@ Future initUniLinks() async { try { final initialLink = await getInitialLink(); if (initialLink == null) { - return; + return false; } - parseRustdeskUri(initialLink); + return parseRustdeskUri(initialLink); } catch (err) { debugPrintStack(label: "$err"); + return false; } } -StreamSubscription? listenUniLinks() { - if (!(Platform.isWindows || Platform.isMacOS)) { +/// Listen for uni links. +/// +/// * handleByFlutter: Should uni links be handled by Flutter. +/// +/// Returns a [StreamSubscription] which can listen the uni links. +StreamSubscription? listenUniLinks({handleByFlutter = true}) { + if (Platform.isLinux) { return null; } final sub = uriLinkStream.listen((Uri? uri) { + debugPrint("A uri was received: $uri."); if (uri != null) { - callUniLinksUriHandler(uri); + if (handleByFlutter) { + callUniLinksUriHandler(uri); + } else { + bind.sendUrlScheme(url: uri.toString()); + } } else { print("uni listen error: uri is empty."); } @@ -1240,25 +1505,44 @@ StreamSubscription? listenUniLinks() { return sub; } -/// Returns true if we successfully handle the startup arguments. +/// Handle command line arguments +/// +/// * Returns true if we successfully handle the startup arguments. bool checkArguments() { + if (kBootArgs.isNotEmpty) { + final ret = parseRustdeskUri(kBootArgs.first); + if (ret) { + return true; + } + } + // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args - final connectIndex = bootArgs.indexOf("--connect"); + var connectIndex = kBootArgs.indexOf("--connect"); if (connectIndex == -1) { return false; } - String? arg = - bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1]; - if (arg != null) { - if (arg.startsWith(kUniLinksPrefix)) { - return parseRustdeskUri(arg); + String? id = + kBootArgs.length <= connectIndex + 1 ? null : kBootArgs[connectIndex + 1]; + String? password = + kBootArgs.length <= connectIndex + 2 ? null : kBootArgs[connectIndex + 2]; + if (password != null && password.startsWith("--")) { + password = null; + } + final switchUuidIndex = kBootArgs.indexOf("--switch_uuid"); + String? switchUuid = kBootArgs.length <= switchUuidIndex + 1 + ? null + : kBootArgs[switchUuidIndex + 1]; + if (id != null) { + if (id.startsWith(kUniLinksPrefix)) { + return parseRustdeskUri(id); } else { // remove "--connect xxx" in the `bootArgs` array - bootArgs.removeAt(connectIndex); - bootArgs.removeAt(connectIndex); + kBootArgs.removeAt(connectIndex); + kBootArgs.removeAt(connectIndex); // fallback to peer id Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(arg); + rustDeskWinManager.newRemoteDesktop(id, + password: password, switch_uuid: switchUuid); }); return true; } @@ -1274,7 +1558,7 @@ bool checkArguments() { bool parseRustdeskUri(String uriPath) { final uri = Uri.tryParse(uriPath); if (uri == null) { - print("uri is not valid: $uriPath"); + debugPrint("uri is not valid: $uriPath"); return false; } return callUniLinksUriHandler(uri); @@ -1288,8 +1572,10 @@ bool callUniLinksUriHandler(Uri uri) { // new connection if (uri.authority == "connection" && uri.path.startsWith("/new/")) { final peerId = uri.path.substring("/new/".length); + var param = uri.queryParameters; + String? switch_uuid = param["switch_uuid"]; Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(peerId); + rustDeskWinManager.newRemoteDesktop(peerId, switch_uuid: switch_uuid); }); return true; } @@ -1299,13 +1585,14 @@ bool callUniLinksUriHandler(Uri uri) { connectMainDesktop(String id, {required bool isFileTransfer, required bool isTcpTunneling, - required bool isRDP}) async { + required bool isRDP, + bool? forceRelay}) async { if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); + await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); + await rustDeskWinManager.newPortForward(id, isRDP, forceRelay: forceRelay); } else { - await rustDeskWinManager.newRemoteDesktop(id); + await rustDeskWinManager.newRemoteDesktop(id, forceRelay: forceRelay); } } @@ -1319,29 +1606,32 @@ connect(BuildContext context, String id, bool isRDP = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); + final oldId = id; + id = await bind.mainHandleRelayId(id: id); + final forceRelay = id != oldId; assert(!(isFileTransfer && isTcpTunneling && isRDP), "more than one connect type"); if (isDesktop) { if (desktopType == DesktopType.main) { - await connectMainDesktop( - id, - isFileTransfer: isFileTransfer, - isTcpTunneling: isTcpTunneling, - isRDP: isRDP, - ); + await connectMainDesktop(id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + forceRelay: forceRelay); } else { await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { 'id': id, 'isFileTransfer': isFileTransfer, 'isTcpTunneling': isTcpTunneling, 'isRDP': isRDP, + "forceRelay": forceRelay, }); } } else { if (isFileTransfer) { - if (!await PermissionManager.check("file")) { - if (!await PermissionManager.request("file")) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { + if (!await AndroidPermissionManager.request(kManageExternalStorage)) { return; } } @@ -1367,7 +1657,7 @@ connect(BuildContext context, String id, } } -Future> getHttpHeaders() async { +Map getHttpHeaders() { return { 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}' }; @@ -1418,7 +1708,7 @@ bool isRunningInPortableMode() { } /// Window status callback -void onActiveWindowChanged() async { +Future onActiveWindowChanged() async { print( "[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}"); if (rustDeskWinManager.getActiveWindows().isEmpty) { @@ -1431,8 +1721,12 @@ void onActiveWindowChanged() async { } catch (err) { debugPrintStack(label: "$err"); } finally { + debugPrint("Start closing RustDesk..."); await windowManager.setPreventClose(false); await windowManager.close(); + if (Platform.isMacOS) { + RdPlatformChannel.instance.terminate(); + } } } } @@ -1529,10 +1823,15 @@ class ServerConfig { /// 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)); - + var json = {}; + try { + // back compatible + json = jsonDecode(msg); + } catch (err) { + final input = msg.split('').reversed.join(''); + final bytes = base64Decode(base64.normalize(input)); + json = jsonDecode(utf8.decode(bytes)); + } idServer = json['host'] ?? ''; relayServer = json['relay'] ?? ''; apiServer = json['api'] ?? ''; @@ -1560,3 +1859,151 @@ class ServerConfig { apiServer = options['api-server'] ?? "", key = options['key'] ?? ""; } + +Widget dialogButton(String text, + {required VoidCallback? onPressed, + bool isOutline = false, + Widget? icon, + TextStyle? style, + ButtonStyle? buttonStyle}) { + if (isDesktop) { + if (isOutline) { + return icon == null + ? OutlinedButton( + onPressed: onPressed, + child: Text(translate(text), style: style), + ) + : OutlinedButton.icon( + icon: icon, + onPressed: onPressed, + label: Text(translate(text), style: style), + ); + } else { + return icon == null + ? ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), + onPressed: onPressed, + child: Text(translate(text), style: style), + ) + : ElevatedButton.icon( + icon: icon, + style: ElevatedButton.styleFrom(elevation: 0).merge(buttonStyle), + onPressed: onPressed, + label: Text(translate(text), style: style), + ); + } + } else { + return TextButton( + onPressed: onPressed, + child: Text( + translate(text), + style: style, + ), + ); + } +} + +int version_cmp(String v1, String v2) { + return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2); +} + +String getWindowName({WindowType? overrideType}) { + switch (overrideType ?? kWindowType) { + case WindowType.Main: + return "RustDesk"; + case WindowType.FileTransfer: + return "File Transfer - RustDesk"; + case WindowType.PortForward: + return "Port Forward - RustDesk"; + case WindowType.RemoteDesktop: + return "Remote Desktop - RustDesk"; + default: + break; + } + return "RustDesk"; +} + +String getWindowNameWithId(String id, {WindowType? overrideType}) { + return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}"; +} + +Future updateSystemWindowTheme() async { + // Set system window theme for macOS. + final userPreference = MyTheme.getThemeModePreference(); + if (userPreference != ThemeMode.system) { + if (Platform.isMacOS) { + await RdPlatformChannel.instance.changeSystemWindowTheme( + userPreference == ThemeMode.light + ? SystemWindowTheme.light + : SystemWindowTheme.dark); + } + } +} + +/// macOS only +/// +/// Note: not found a general solution for rust based AVFoundation bingding. +/// [AVFoundation] crate has compile error. +const kMacOSPermChannel = MethodChannel("org.rustdesk.rustdesk/macos"); + +enum PermissionAuthorizeType { + undetermined, + authorized, + denied, // and restricted +} + +Future osxCanRecordAudio() async { + int res = await kMacOSPermChannel.invokeMethod("canRecordAudio"); + print(res); + if (res > 0) { + return PermissionAuthorizeType.authorized; + } else if (res == 0) { + return PermissionAuthorizeType.undetermined; + } else { + return PermissionAuthorizeType.denied; + } +} + +Future osxRequestAudio() async { + return await kMacOSPermChannel.invokeMethod("requestRecordAudio"); +} + +class DraggableNeverScrollableScrollPhysics extends ScrollPhysics { + /// Creates scroll physics that does not let the user scroll. + const DraggableNeverScrollableScrollPhysics({super.parent}); + + @override + DraggableNeverScrollableScrollPhysics applyTo(ScrollPhysics? ancestor) { + return DraggableNeverScrollableScrollPhysics(parent: buildParent(ancestor)); + } + + @override + bool shouldAcceptUserOffset(ScrollMetrics position) { + // TODO: find a better solution to check if the offset change is caused by the scrollbar. + // Workaround: when dragging with the scrollbar, it always triggers an [IdleScrollActivity]. + if (position is ScrollPositionWithSingleContext) { + // ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member + return position.activity is IdleScrollActivity; + } + return false; + } + + @override + bool get allowImplicitScrolling => false; +} + +Widget futureBuilder( + {required Future? future, required Widget Function(dynamic data) hasData}) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return hasData(snapshot.data!); + } else { + if (snapshot.hasError) { + debugPrint(snapshot.error.toString()); + } + return Container(); + } + }); +} diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index 856f88d20..4717143fd 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -1,12 +1,26 @@ +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 is_admin = false; + bool isAdmin = false; UserPayload.fromJson(Map json) : name = json['name'] ?? '', @@ -14,7 +28,7 @@ class UserPayload { note = json['note'] ?? '', status = json['status'], grp = json['grp'] ?? '', - is_admin = json['is_admin'] == true; + isAdmin = json['is_admin'] == true; } class PeerPayload { @@ -37,3 +51,71 @@ class PeerPayload { 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 index ebac18dac..bc1a562b9 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -19,6 +19,8 @@ class PrivacyModeState { final key = tag(id); if (Get.isRegistered(tag: key)) { Get.delete(tag: key); + } else { + Get.find(tag: key).value = false; } } @@ -33,6 +35,8 @@ class BlockInputState { if (!Get.isRegistered(tag: key)) { final RxBool state = false.obs; Get.put(state, tag: key); + } else { + Get.find(tag: key).value = false; } } @@ -54,6 +58,8 @@ class CurrentDisplayState { if (!Get.isRegistered(tag: key)) { final RxInt state = RxInt(0); Get.put(state, tag: key); + } else { + Get.find(tag: key).value = 0; } } @@ -123,6 +129,8 @@ class ShowRemoteCursorState { if (!Get.isRegistered(tag: key)) { final RxBool state = false.obs; Get.put(state, tag: key); + } else { + Get.find(tag: key).value = false; } } @@ -145,6 +153,8 @@ class KeyboardEnabledState { // Server side, default true final RxBool state = true.obs; Get.put(state, tag: key); + } else { + Get.find(tag: key).value = true; } } @@ -164,9 +174,10 @@ class RemoteCursorMovedState { 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); + } else { + Get.find(tag: key).value = false; } } @@ -186,9 +197,10 @@ class RemoteCountState { 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); + } else { + Get.find(tag: key).value = 1; } } @@ -210,6 +222,8 @@ class PeerBoolOption { if (!Get.isRegistered(tag: key)) { final RxBool value = RxBool(init_getter()); Get.put(value, tag: key); + } else { + Get.find(tag: key).value = init_getter(); } } @@ -232,6 +246,8 @@ class PeerStringOption { if (!Get.isRegistered(tag: key)) { final RxString value = RxString(init_getter()); Get.put(value, tag: key); + } else { + Get.find(tag: key).value = init_getter(); } } diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index fbeca25b2..85c77d8ed 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -3,14 +3,12 @@ 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 'package:flutter_hbb/desktop/widgets/login.dart'; import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; import 'package:get/get.dart'; import '../../common.dart'; -import '../../desktop/pages/desktop_home_page.dart'; -import '../../mobile/pages/settings_page.dart'; +import 'login.dart'; class AddressBook extends StatefulWidget { final EdgeInsets? menuPadding; @@ -41,27 +39,12 @@ class _AddressBookState extends State { } }); - handleLogin() { - // TODO refactor login dialog for desktop and mobile - if (isDesktop) { - loginDialog(); - } else { - showLogin(gFFI.dialogManager); - } - } - Future buildBody(BuildContext context) async { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( - child: InkWell( - onTap: handleLogin, - child: Text( - translate("Login"), - style: const TextStyle(decoration: TextDecoration.underline), - ), - ), - ); + child: ElevatedButton( + onPressed: loginDialog, child: Text(translate("Login")))); } else { if (gFFI.abModel.abLoading.value) { return const Center( @@ -167,13 +150,13 @@ class _AddressBookState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(translate('Tags')), - GestureDetector( - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; + Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; menuPos = RelativeRect.fromLTRB(x, y, x, y); }, - onTap: () => _showMenu(menuPos), + onPointerUp: (_) => _showMenu(menuPos), child: ActionMore()), ], ); @@ -291,11 +274,7 @@ class _AddressBookState extends State { TextField( controller: idController, inputFormatters: [IDTextInputFormatter()], - decoration: InputDecoration( - isDense: true, - border: OutlineInputBorder(), - errorText: errorMsg), - style: style, + decoration: InputDecoration(errorText: errorMsg), ), Align( alignment: Alignment.centerLeft, @@ -306,11 +285,6 @@ class _AddressBookState extends State { ).marginOnly(top: 8, bottom: 2), TextField( controller: aliasController, - decoration: InputDecoration( - border: OutlineInputBorder(), - isDense: true, - ), - style: style, ), Align( alignment: Alignment.centerLeft, @@ -346,8 +320,8 @@ class _AddressBookState extends State { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -396,11 +370,10 @@ class _AddressBookState extends State { child: TextField( maxLines: null, decoration: InputDecoration( - border: const OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg), ), controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], @@ -413,8 +386,8 @@ class _AddressBookState extends State { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index 510ce1f73..9460f4f41 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -51,7 +51,7 @@ class ChatPage extends StatelessWidget implements PageShape { return Stack( children: [ LayoutBuilder(builder: (context, constraints) { - return DashChat( + final chat = DashChat( onSend: (chatMsg) { chatModel.send(chatMsg); chatModel.inputNode.requestFocus(); @@ -73,9 +73,10 @@ class ChatPage extends StatelessWidget implements PageShape { ? InputDecoration( isDense: true, hintText: - "${translate('Write a message')}...", + "${translate('Write a message')}", filled: true, - fillColor: Theme.of(context).backgroundColor, + fillColor: + Theme.of(context).colorScheme.background, contentPadding: EdgeInsets.all(10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), @@ -87,18 +88,40 @@ class ChatPage extends StatelessWidget implements PageShape { ) : defaultInputDecoration( hintText: - "${translate('Write a message')}...", - fillColor: Theme.of(context).backgroundColor), + "${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, - showTime: true, - currentUserTextColor: Colors.white, textColor: Colors.white, maxWidth: constraints.maxWidth * 0.7, + messageTextBuilder: (message, _, __) { + final isOwnMessage = + message.user.id == currentUser.id; + return Column( + crossAxisAlignment: isOwnMessage + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + Text(message.text, + style: TextStyle(color: Colors.white)), + Padding( + padding: const EdgeInsets.only(top: 5), + child: Text( + "${message.createdAt.hour}:${message.createdAt.minute}", + style: TextStyle( + color: Colors.white, + fontSize: 10, + ), + ), + ), + ], + ); + }, messageDecorationBuilder: (_, __, ___) => defaultMessageDecoration( color: MyTheme.accent80, @@ -108,6 +131,7 @@ class ChatPage extends StatelessWidget implements PageShape { borderBottomLeft: 8, )), ); + return SelectionArea(child: chat); }), desktopType == DesktopType.cm || chatModel.currentID == ChatModel.clientModeID diff --git a/flutter/lib/common/widgets/custom_password.dart b/flutter/lib/common/widgets/custom_password.dart new file mode 100644 index 000000000..99ece2434 --- /dev/null +++ b/flutter/lib/common/widgets/custom_password.dart @@ -0,0 +1,121 @@ +// https://github.com/rodrigobastosv/fancy_password_field +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:get/get.dart'; +import 'package:password_strength/password_strength.dart'; + +abstract class ValidationRule { + String get name; + bool validate(String value); +} + +class UppercaseValidationRule extends ValidationRule { + @override + String get name => translate('uppercase'); + @override + bool validate(String value) { + return value.contains(RegExp(r'[A-Z]')); + } +} + +class LowercaseValidationRule extends ValidationRule { + @override + String get name => translate('lowercase'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[a-z]')); + } +} + +class DigitValidationRule extends ValidationRule { + @override + String get name => translate('digit'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[0-9]')); + } +} + +class SpecialCharacterValidationRule extends ValidationRule { + @override + String get name => translate('special character'); + + @override + bool validate(String value) { + return value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]')); + } +} + +class MinCharactersValidationRule extends ValidationRule { + final int _numberOfCharacters; + MinCharactersValidationRule(this._numberOfCharacters); + + @override + String get name => translate('length>=$_numberOfCharacters'); + + @override + bool validate(String value) { + return value.length >= _numberOfCharacters; + } +} + +class PasswordStrengthIndicator extends StatelessWidget { + final RxString password; + final double weakMedium = 0.33; + final double mediumStrong = 0.67; + const PasswordStrengthIndicator({Key? key, required this.password}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + var strength = estimatePasswordStrength(password.value); + return Row( + children: [ + Expanded( + child: _indicator( + password.isEmpty ? Colors.grey : _getColor(strength))), + Expanded( + child: _indicator(password.isEmpty || strength < weakMedium + ? Colors.grey + : _getColor(strength))), + Expanded( + child: _indicator(password.isEmpty || strength < mediumStrong + ? Colors.grey + : _getColor(strength))), + Text(password.isEmpty ? '' : translate(_getLabel(strength))) + .marginOnly(left: password.isEmpty ? 0 : 8), + ], + ); + }); + } + + Widget _indicator(Color color) { + return Container( + height: 8, + color: color, + ); + } + + String _getLabel(double strength) { + if (strength < weakMedium) { + return 'Weak'; + } else if (strength < mediumStrong) { + return 'Medium'; + } else { + return 'Strong'; + } + } + + Color _getColor(double strength) { + if (strength < weakMedium) { + return Colors.yellow; + } else if (strength < mediumStrong) { + return Colors.blue; + } else { + return Colors.green; + } + } +} diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index a6de0384f..af75a557b 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -1,18 +1,83 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; import '../../common.dart'; +import '../../models/model.dart'; import '../../models/platform_model.dart'; +void clientClose(String id, OverlayDialogManager dialogManager) { + msgBox(id, 'info', 'Close', 'Are you sure to close the connection?', '', + dialogManager); +} + +abstract class ValidationRule { + String get name; + bool validate(String value); +} + +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 = isDesktop + ? '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}' + : violations.map((r) => r.name).join(', '); + }); + return; + } + setState(() { msg = ""; isInProgress = true; @@ -31,7 +96,9 @@ void changeIdDialog() { } setState(() { isInProgress = false; - msg = translate(status); + msg = isDesktop + ? '${translate('Prompt')}: ${translate(status)}' + : translate(status); }); } @@ -46,26 +113,53 @@ void changeIdDialog() { ), TextField( decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg)), + labelText: translate('Your new ID'), + errorText: msg.isEmpty ? null : translate(msg), + suffixText: '${rxId.value.length}/16', + suffixStyle: const TextStyle(fontSize: 12, color: Colors.grey)), inputFormatters: [ LengthLimitingTextInputFormatter(16), // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) ], - maxLength: 16, controller: controller, - focusNode: FocusNode()..requestFocus(), + autofocus: true, + onChanged: (value) { + setState(() { + rxId.value = value.trim(); + msg = ''; + }); + }, ), const SizedBox( - height: 4.0, + height: 8.0, ), + isDesktop + ? 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(), + )).marginOnly(bottom: 8) + : SizedBox.shrink(), Offstage( offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -95,11 +189,10 @@ void changeWhiteList({Function()? callback}) async { child: TextField( maxLines: null, decoration: InputDecoration( - border: const OutlineInputBorder(), errorText: msg.isEmpty ? null : translate(msg), ), controller: controller, - focusNode: FocusNode()..requestFocus()), + autofocus: true), ), ], ), @@ -111,48 +204,46 @@ void changeWhiteList({Function()? callback}) async { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - await bind.mainSetOption(key: 'whitelist', value: ''); - callback?.call(); - close(); - }, - child: Text(translate("Clear"))), - TextButton( - onPressed: () async { - setState(() { - msg = ""; - isInProgress = true; - }); - newWhiteListField = controller.text.trim(); - var newWhiteList = ""; - if (newWhiteListField.isEmpty) { - // pass - } else { - final ips = - newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); - // test ip - final ipMatch = RegExp( - r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$"); - final ipv6Match = RegExp( - r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$"); - for (final ip in ips) { - if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) { - msg = "${translate("Invalid IP")} $ip"; - setState(() { - isInProgress = false; - }); - return; - } + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Clear", onPressed: () async { + await bind.mainSetOption(key: 'whitelist', value: ''); + callback?.call(); + close(); + }, isOutline: true), + dialogButton( + "OK", + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + newWhiteListField = controller.text.trim(); + var newWhiteList = ""; + if (newWhiteListField.isEmpty) { + // pass + } else { + final ips = newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + // test ip + final ipMatch = RegExp( + r"^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]?|0)(\/([1-9]|[1-2][0-9]|3[0-2])){0,1}$"); + final ipv6Match = RegExp( + r"^(((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*::((?:[0-9A-Fa-f]{1,4}))*((?::[0-9A-Fa-f]{1,4}))*|((?:[0-9A-Fa-f]{1,4}))((?::[0-9A-Fa-f]{1,4})){7})(\/([1-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])){0,1}$"); + for (final ip in ips) { + if (!ipMatch.hasMatch(ip) && !ipv6Match.hasMatch(ip)) { + msg = "${translate("Invalid IP")} $ip"; + setState(() { + isInProgress = false; + }); + return; } - newWhiteList = ips.join(','); } - await bind.mainSetOption(key: 'whitelist', value: newWhiteList); - callback?.call(); - close(); - }, - child: Text(translate("OK"))), + newWhiteList = ips.join(','); + } + await bind.mainSetOption(key: 'whitelist', value: newWhiteList); + callback?.call(); + close(); + }, + ), ], onCancel: close, ); @@ -188,24 +279,566 @@ Future changeDirectAccessPort( r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), ], controller: controller, - focusNode: FocusNode()..requestFocus()), + autofocus: true), ), ], ), ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - await bind.mainSetOption( - key: 'direct-access-port', value: controller.text); - close(); - }, - child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: () async { + await bind.mainSetOption( + key: 'direct-access-port', value: controller.text); + close(); + }), ], onCancel: close, ); }); return controller.text; } + +class DialogTextField extends StatelessWidget { + final String title; + final String? hintText; + final bool obscureText; + final String? errorText; + final String? helperText; + final Widget? prefixIcon; + final Widget? suffixIcon; + final TextEditingController controller; + final FocusNode? focusNode; + + static const kUsernameTitle = 'Username'; + static const kUsernameIcon = Icon(Icons.account_circle_outlined); + static const kPasswordTitle = 'Password'; + static const kPasswordIcon = Icon(Icons.lock_outline); + + DialogTextField( + {Key? key, + this.focusNode, + this.obscureText = false, + this.errorText, + this.helperText, + this.prefixIcon, + this.suffixIcon, + this.hintText, + required this.title, + required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: title, + hintText: hintText, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + helperText: helperText, + helperMaxLines: 8, + errorText: errorText, + ), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, + ), + ), + ], + ).paddingSymmetric(vertical: 4.0); + } +} + +class PasswordWidget extends StatefulWidget { + PasswordWidget({ + Key? key, + required this.controller, + this.autoFocus = true, + this.hintText, + this.errorText, + }) : super(key: key); + + final TextEditingController controller; + final bool autoFocus; + final String? hintText; + final String? errorText; + + @override + State createState() => _PasswordWidgetState(); +} + +class _PasswordWidgetState extends State { + bool _passwordVisible = false; + final _focusNode = FocusNode(); + Timer? _timer; + + @override + void initState() { + super.initState(); + if (widget.autoFocus) { + _timer = + Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); + } + } + + @override + void dispose() { + _timer?.cancel(); + _focusNode.unfocus(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DialogTextField( + title: translate(DialogTextField.kPasswordTitle), + hintText: translate(widget.hintText ?? 'Enter your password'), + controller: widget.controller, + prefixIcon: DialogTextField.kPasswordIcon, + suffixIcon: IconButton( + icon: Icon( + // Based on passwordVisible state choose the icon + _passwordVisible ? Icons.visibility : Icons.visibility_off, + color: MyTheme.lightTheme.primaryColor), + onPressed: () { + // Update the state i.e. toggle the state of passwordVisible variable + setState(() { + _passwordVisible = !_passwordVisible; + }); + }, + ), + obscureText: !_passwordVisible, + errorText: widget.errorText, + focusNode: _focusNode, + ); + } +} + +void wrongPasswordDialog( + String id, OverlayDialogManager dialogManager, type, title, text) { + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + cancel() { + close(); + closeConnection(); + } + + submit() { + enterPasswordDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + onSubmit: submit, + onCancel: cancel, + actions: [ + dialogButton( + 'Cancel', + onPressed: cancel, + isOutline: true, + ), + dialogButton( + 'Retry', + onPressed: submit, + ), + ]); + }); +} + +void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { + await _connectDialog( + id, + dialogManager, + peerPasswordController: TextEditingController(), + ); +} + +void enterUserLoginDialog(String id, OverlayDialogManager dialogManager) async { + await _connectDialog( + id, + dialogManager, + usernameController: TextEditingController(), + passwordController: TextEditingController(), + ); +} + +void enterUserLoginAndPasswordDialog( + String id, OverlayDialogManager dialogManager) async { + await _connectDialog( + id, + dialogManager, + usernameController: TextEditingController(), + passwordController: TextEditingController(), + peerPasswordController: TextEditingController(), + ); +} + +_connectDialog( + String id, + OverlayDialogManager dialogManager, { + TextEditingController? usernameController, + TextEditingController? passwordController, + TextEditingController? peerPasswordController, +}) async { + var remember = await bind.sessionGetRemember(id: id) ?? false; + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + cancel() { + close(); + closeConnection(); + } + + submit() { + final username = usernameController?.text.trim() ?? ''; + final password = passwordController?.text.trim() ?? ''; + final peerPassword = peerPasswordController?.text.trim() ?? ''; + if (peerPasswordController != null && peerPassword.isEmpty) return; + gFFI.login( + // username, + // password, + id, + peerPassword, + remember, + ); + close(); + dialogManager.showLoading(translate('Logging in...'), + onCancel: closeConnection); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + (usernameController == null + ? Text(translate('Password Required')) + : Tooltip( + message: translate('login_linux_tooltip_tip'), + child: Text(translate('login_linux_tip')), + )) + .paddingOnly(left: 10), + ], + ), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + usernameController == null + ? Offstage() + : DialogTextField( + title: translate(DialogTextField.kUsernameTitle), + controller: usernameController, + prefixIcon: DialogTextField.kUsernameIcon, + errorText: null, + ), + passwordController == null + ? Offstage() + : PasswordWidget( + controller: passwordController, + autoFocus: false, + ), + usernameController == null || peerPasswordController == null + ? Offstage() + : const Divider(), + peerPasswordController == null + ? Offstage() + : PasswordWidget( + controller: peerPasswordController, + autoFocus: usernameController == null, + hintText: 'enter_rustdesk_passwd_tip', + ), + peerPasswordController == null + ? Offstage() + : CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('remember_rustdesk_passwd_tip'), + ), + value: remember, + onChanged: (v) { + if (v != null) { + setState(() => remember = v); + } + }, + ), + ]), + actions: [ + dialogButton( + 'Cancel', + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + 'OK', + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); +} + +void showWaitUacDialog( + String id, OverlayDialogManager dialogManager, String type) { + dialogManager.dismissAll(); + dialogManager.show( + tag: '$id-wait-uac', + (setState, close) => CustomAlertDialog( + title: null, + content: msgboxContent(type, 'Wait', 'wait_accept_uac_tip'), + )); +} + +// Another username && password dialog? +void showRequestElevationDialog(String id, OverlayDialogManager dialogManager) { + RxString groupValue = ''.obs; + RxString errUser = ''.obs; + RxString errPwd = ''.obs; + TextEditingController userController = TextEditingController(); + TextEditingController pwdController = TextEditingController(); + + void onRadioChanged(String? value) { + if (value != null) { + groupValue.value = value; + } + } + + const minTextStyle = TextStyle(fontSize: 14); + + var content = Obx(() => Column(children: [ + Row( + children: [ + Radio( + value: '', + groupValue: groupValue.value, + onChanged: onRadioChanged), + Expanded( + child: + Text(translate('Ask the remote user for authentication'))), + ], + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate( + 'Choose this if the remote account is administrator'), + style: TextStyle(fontSize: 13)) + .marginOnly(left: 40), + ).marginOnly(bottom: 15), + Row( + children: [ + Radio( + value: 'logon', + groupValue: groupValue.value, + onChanged: onRadioChanged), + Expanded( + child: Text(translate( + 'Transmit the username and password of administrator')), + ) + ], + ), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + '${translate('Username')}:', + style: minTextStyle, + ).marginOnly(right: 10)), + Expanded( + flex: 3, + child: TextField( + controller: userController, + style: minTextStyle, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), + hintText: translate('eg: admin'), + errorText: errUser.isEmpty ? null : errUser.value), + onChanged: (s) { + if (s.isNotEmpty) { + errUser.value = ''; + } + }, + ), + ) + ], + ).marginOnly(left: 40), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + '${translate('Password')}:', + style: minTextStyle, + ).marginOnly(right: 10)), + Expanded( + flex: 3, + child: TextField( + controller: pwdController, + obscureText: true, + style: minTextStyle, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), + errorText: errPwd.isEmpty ? null : errPwd.value), + onChanged: (s) { + if (s.isNotEmpty) { + errPwd.value = ''; + } + }, + ), + ), + ], + ).marginOnly(left: 40), + Align( + alignment: Alignment.centerLeft, + child: Text(translate('still_click_uac_tip'), + style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)) + .marginOnly(top: 20)), + ])); + + dialogManager.dismissAll(); + dialogManager.show(tag: '$id-request-elevation', (setState, close) { + void submit() { + if (groupValue.value == 'logon') { + if (userController.text.isEmpty) { + errUser.value = translate('Empty Username'); + return; + } + if (pwdController.text.isEmpty) { + errPwd.value = translate('Empty Password'); + return; + } + bind.sessionElevateWithLogon( + id: id, + username: userController.text, + password: pwdController.text); + } else { + bind.sessionElevateDirect(id: id); + } + } + + return CustomAlertDialog( + title: Text(translate('Request Elevation')), + content: content, + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showOnBlockDialog( + String id, + String type, + String title, + String text, + OverlayDialogManager dialogManager, +) { + if (dialogManager.existing('$id-wait-uac') || + dialogManager.existing('$id-request-elevation')) { + return; + } + dialogManager.show(tag: '$id-$type', (setState, close) { + void submit() { + close(); + showRequestElevationDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, + "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}"), + actions: [ + dialogButton('Wait', onPressed: close, isOutline: true), + dialogButton('Request Elevation', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showElevationError(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.show(tag: '$id-$type', (setState, close) { + void submit() { + close(); + showRequestElevationDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + actions: [ + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + dialogButton('Retry', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +void showWaitAcceptDialog(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.dismissAll(); + dialogManager.show((setState, close) { + onCancel() { + closeConnection(); + } + + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, text), + actions: [ + dialogButton('Cancel', onPressed: onCancel, isOutline: true), + ], + onCancel: onCancel, + ); + }); +} + +void showRestartRemoteDevice( + PeerInfo pi, String id, OverlayDialogManager dialogManager) async { + final res = + await dialogManager.show((setState, close) => CustomAlertDialog( + title: Row(children: [ + Icon(Icons.warning_rounded, color: Colors.redAccent, size: 28), + Text(translate("Restart Remote Device")).paddingOnly(left: 10), + ]), + content: Text( + "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: () => close(true), + ), + ], + onCancel: close, + onSubmit: () => close(true), + )); + if (res == true) bind.sessionRestartRemoteDevice(id: id); +} diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart new file mode 100644 index 000000000..3959c301c --- /dev/null +++ b/flutter/lib/common/widgets/login.dart @@ -0,0 +1,633 @@ +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'; +import './dialog.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(DialogTextField.kUsernameTitle), + controller: username, + focusNode: userFocusNode, + prefixIcon: DialogTextField.kUsernameIcon, + errorText: usernameMsg), + PasswordWidget( + controller: pass, + autoFocus: false, + 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, + )), + ), + ])), + ], + )); + } +} + +/// 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)), + 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/overlay.dart b/flutter/lib/common/widgets/overlay.dart index 81797962e..c67f0f7fb 100644 --- a/flutter/lib/common/widgets/overlay.dart +++ b/flutter/lib/common/widgets/overlay.dart @@ -1,5 +1,7 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import '../../consts.dart'; @@ -95,12 +97,14 @@ class DraggableChatWindow extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 8), - child: Row(children: [ - Icon(Icons.chat_bubble_outline, - size: 20, color: Theme.of(context).colorScheme.primary), - SizedBox(width: 6), - Text(translate("Chat")) - ])), + child: Obx(() => Opacity( + opacity: chatModel.isWindowFocus.value ? 1.0 : 0.4, + child: Row(children: [ + Icon(Icons.chat_bubble_outline, + size: 20, color: Theme.of(context).colorScheme.primary), + SizedBox(width: 6), + Text(translate("Chat")) + ])))), Padding( padding: EdgeInsets.all(2), child: ActionIcon( @@ -303,59 +307,119 @@ class _DraggableState extends State { if (widget.checkKeyboard) { checkKeyboard(); } - if (widget.checkKeyboard) { + if (widget.checkScreenSize) { checkScreenSize(); } - return Positioned( - top: _position.dy, - left: _position.dx, - width: widget.width, - height: widget.height, - child: widget.builder(context, onPanUpdate)); + return Stack(children: [ + Positioned( + top: _position.dy, + left: _position.dx, + width: widget.width, + height: widget.height, + child: widget.builder(context, onPanUpdate)) + ]); } } class QualityMonitor extends StatelessWidget { - static const textStyle = TextStyle(color: MyTheme.grayBg); final QualityMonitorModel qualityMonitorModel; QualityMonitor(this.qualityMonitorModel); + Widget _row(String info, String? value, {Color? rightColor}) { + return Row( + children: [ + Expanded( + flex: 8, + child: AutoSizeText(info, + style: TextStyle(color: Color.fromARGB(255, 210, 210, 210)), + textAlign: TextAlign.right, + maxLines: 1)), + Spacer(flex: 1), + Expanded( + flex: 8, + child: AutoSizeText(value ?? '', + style: TextStyle(color: rightColor ?? Colors.white), + maxLines: 1)), + ], + ); + } + @override Widget build(BuildContext context) => ChangeNotifierProvider.value( value: qualityMonitorModel, child: Consumer( - builder: (context, qualityMonitorModel, child) => Positioned( - top: 10, - right: 10, - child: qualityMonitorModel.show - ? Container( - padding: const EdgeInsets.all(8), - color: MyTheme.canvasColor.withAlpha(120), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Speed: ${qualityMonitorModel.data.speed ?? ''}", - style: textStyle, - ), - Text( - "FPS: ${qualityMonitorModel.data.fps ?? ''}", - style: textStyle, - ), - Text( - "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", - style: textStyle, - ), - Text( - "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", - style: textStyle, - ), - Text( - "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", - style: textStyle, - ), - ], - ), - ) - : const SizedBox.shrink()))); + builder: (context, qualityMonitorModel, child) => qualityMonitorModel + .show + ? Container( + constraints: BoxConstraints(maxWidth: 200), + padding: const EdgeInsets.all(8), + color: MyTheme.canvasColor.withAlpha(150), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _row("Speed", qualityMonitorModel.data.speed ?? '-'), + _row("FPS", qualityMonitorModel.data.fps ?? '-'), + _row( + "Delay", "${qualityMonitorModel.data.delay ?? '-'}ms", + rightColor: Colors.green), + _row("Target Bitrate", + "${qualityMonitorModel.data.targetBitrate ?? '-'}kb"), + _row( + "Codec", qualityMonitorModel.data.codecFormat ?? '-'), + ], + ), + ) + : const SizedBox.shrink())); +} + +class BlockableOverlayState extends OverlayKeyState { + final _middleBlocked = false.obs; + + VoidCallback? onMiddleBlockedClick; // to-do use listener + + RxBool get middleBlocked => _middleBlocked; + + void addMiddleBlockedListener(void Function(bool) cb) { + _middleBlocked.listen(cb); + } + + void setMiddleBlocked(bool blocked) { + if (blocked != _middleBlocked.value) { + _middleBlocked.value = blocked; + } + } +} + +class BlockableOverlay extends StatelessWidget { + final Widget underlying; + final List? upperLayer; + + final BlockableOverlayState state; + + BlockableOverlay( + {required this.underlying, required this.state, this.upperLayer}); + + @override + Widget build(BuildContext context) { + final initialEntries = [ + OverlayEntry(builder: (_) => underlying), + + /// middle layer + OverlayEntry( + builder: (context) => Obx(() => Listener( + onPointerDown: (_) { + state.onMiddleBlockedClick?.call(); + }, + child: Container( + color: + state.middleBlocked.value ? Colors.transparent : null)))), + ]; + + if (upperLayer != null) { + initialEntries.addAll(upperLayer!); + } + + /// set key + return Overlay(key: state.key, initialEntries: initialEntries); + } } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index a98739606..90946ca74 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -42,6 +42,7 @@ class _PeerCardState extends State<_PeerCard> with AutomaticKeepAliveClientMixin { var _menuPos = RelativeRect.fill; final double _cardRadius = 16; + final double _tileRadius = 5; final double _borderWidth = 2; @override @@ -116,27 +117,32 @@ class _PeerCardState extends State<_PeerCard> Widget _buildDesktop() { final peer = super.widget.peer; - var deco = Rx(BoxDecoration( + var deco = Rx( + BoxDecoration( border: Border.all(color: Colors.transparent, width: _borderWidth), - borderRadius: peerCardUiType.value == PeerUiType.grid - ? BorderRadius.circular(_cardRadius) - : null)); + borderRadius: BorderRadius.circular( + peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius, + ), + ), + ); 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); + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: _borderWidth), + borderRadius: BorderRadius.circular( + peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius, + ), + ); }, onExit: (evt) { deco.value = BoxDecoration( - border: Border.all(color: Colors.transparent, width: _borderWidth), - borderRadius: peerCardUiType.value == PeerUiType.grid - ? BorderRadius.circular(_cardRadius) - : null); + border: Border.all(color: Colors.transparent, width: _borderWidth), + borderRadius: BorderRadius.circular( + peerCardUiType.value == PeerUiType.grid ? _cardRadius : _tileRadius, + ), + ); }, child: GestureDetector( onDoubleTap: () => widget.connect(context, peer.id), @@ -163,6 +169,10 @@ class _PeerCardState extends State<_PeerCard> Container( decoration: BoxDecoration( color: str2color('${peer.id}${peer.platform}', 0x7f), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(_tileRadius), + bottomLeft: Radius.circular(_tileRadius), + ), ), alignment: Alignment.center, width: 42, @@ -170,8 +180,13 @@ class _PeerCardState extends State<_PeerCard> ), Expanded( child: Container( - decoration: - BoxDecoration(color: Theme.of(context).backgroundColor), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.only( + topRight: Radius.circular(_tileRadius), + bottomRight: Radius.circular(_tileRadius), + ), + ), child: Row( children: [ Expanded( @@ -266,7 +281,7 @@ class _PeerCardState extends State<_PeerCard> ), ), Container( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -316,21 +331,11 @@ class _PeerCardState extends State<_PeerCard> bool get wantKeepAlive => true; } -enum CardType { - recent, - fav, - lan, - ab, - grp, -} - abstract class BasePeerCard extends StatelessWidget { final Peer peer; final EdgeInsets? menuPadding; - final CardType cardType; - BasePeerCard( - {required this.peer, required this.cardType, this.menuPadding, Key? key}) + BasePeerCard({required this.peer, this.menuPadding, Key? key}) : super(key: key); @override @@ -435,7 +440,7 @@ abstract class BasePeerCard extends StatelessWidget { if (Navigator.canPop(context)) { Navigator.pop(context); } - _rdpDialog(id, cardType); + _rdpDialog(id); }, )), )) @@ -480,6 +485,12 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + Future _isForceAlwaysRelay(String id) async { + return (await bind.mainGetPeerOption(id: id, key: 'force-always-relay')) + .isNotEmpty; + } + @protected Future> _forceAlwaysRelayAction(String id) async { const option = 'force-always-relay'; @@ -487,16 +498,12 @@ abstract class BasePeerCard extends StatelessWidget { switchType: SwitchType.scheckbox, text: translate('Always connect via relay'), getter: () async { - if (cardType == CardType.ab) { - return gFFI.abModel.find(id)?.forceAlwaysRelay ?? false; - } else { - return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; - } + return await _isForceAlwaysRelay(id); }, setter: (bool v) async { gFFI.abModel.setPeerForceAlwaysRelay(id, v); await bind.mainSetPeerOption( - id: id, key: option, value: bool2option('force-always-relay', v)); + id: id, key: option, value: bool2option(option, v)); }, padding: menuPadding, dismissOnClicked: true, @@ -523,20 +530,24 @@ abstract class BasePeerCard extends StatelessWidget { String id, Future Function() reloadFunc, {bool isLan = false}) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Remove'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Delete'), + style: style?.copyWith(color: Colors.red), + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.delete_forever, color: Colors.red), + ), + ).marginOnly(right: 4)), + ], ), proc: () { - () async { - if (isLan) { - // TODO - } else { - await bind.mainRemovePeer(id: id); - } - removePreference(id); - await reloadFunc(); - }(); + _delete(id, isLan, reloadFunc); }, padding: menuPadding, dismissOnClicked: true, @@ -561,9 +572,21 @@ abstract class BasePeerCard extends StatelessWidget { @protected MenuEntryBase _addFavAction(String id) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Add to Favorites'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Add to Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star_outline), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { @@ -583,9 +606,21 @@ abstract class BasePeerCard extends StatelessWidget { MenuEntryBase _rmFavAction( String id, Future Function() reloadFunc) { return MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Remove from Favorites'), - style: style, + childBuilder: (TextStyle? style) => Row( + children: [ + Text( + translate('Remove from Favorites'), + style: style, + ), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: 0.8, + child: Icon(Icons.star), + ), + ).marginOnly(right: 4)), + ], ), proc: () { () async { @@ -621,14 +656,13 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + Future _getAlias(String id) async => + await bind.mainGetPeerOption(id: id, key: 'alias'); + void _rename(String id) async { RxBool isInProgress = false.obs; - String name; - if (cardType == CardType.ab) { - name = gFFI.abModel.find(id)?.alias ?? ""; - } else { - name = await bind.mainGetPeerOption(id: id, key: 'alias'); - } + String name = await _getAlias(id); var controller = TextEditingController(text: name); gFFI.dialogManager.show((setState, close) { submit() async { @@ -636,13 +670,19 @@ abstract class BasePeerCard extends StatelessWidget { String name = controller.text.trim(); await bind.mainSetPeerAlias(id: id, alias: name); gFFI.abModel.setPeerAlias(id, name); - update(); + _update(); close(); isInProgress.value = false; } return CustomAlertDialog( - title: Text(translate('Rename')), + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.edit_rounded, color: MyTheme.accent), + Text(translate('Rename')).paddingOnly(left: 10), + ], + ), content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -650,9 +690,8 @@ abstract class BasePeerCard extends StatelessWidget { child: Form( child: TextFormField( controller: controller, - focusNode: FocusNode()..requestFocus(), - decoration: - const InputDecoration(border: OutlineInputBorder()), + autofocus: true, + decoration: InputDecoration(labelText: translate('Name')), ), ), ), @@ -662,8 +701,17 @@ abstract class BasePeerCard extends StatelessWidget { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), ], onSubmit: submit, onCancel: close, @@ -671,34 +719,65 @@ abstract class BasePeerCard extends StatelessWidget { }); } - void update() { - switch (cardType) { - case CardType.recent: - bind.mainLoadRecentPeers(); - break; - case CardType.fav: - bind.mainLoadFavPeers(); - break; - case CardType.lan: - bind.mainLoadLanPeers(); - break; - case CardType.ab: - gFFI.abModel.pullAb(); - break; - case CardType.grp: - gFFI.groupModel.pull(); - break; - } + @protected + void _update(); + + void _delete(String id, bool isLan, Function reloadFunc) async { + gFFI.dialogManager.show( + (setState, close) { + submit() async { + if (isLan) { + bind.mainRemoveDiscovered(id: id); + } else { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + await bind.mainStoreFav(favs: favs); + } + await bind.mainRemovePeer(id: id); + } + removePreference(id); + await reloadFunc(); + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.delete_rounded, + color: Colors.red, + ), + Text(translate('Delete')).paddingOnly( + left: 10, + ), + ], + ), + content: SizedBox.shrink(), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }, + ); } } class RecentPeerCard extends BasePeerCard { RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.recent, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -707,6 +786,9 @@ class RecentPeerCard extends BasePeerCard { _connectAction(context, peer), _transferFileAction(context, peer.id), ]; + + final List favs = (await bind.mainGetFav()).toList(); + if (isDesktop && peer.platform != 'Android') { menuItems.add(_tcpTunnelingAction(context, peer.id)); } @@ -720,27 +802,37 @@ class RecentPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadRecentPeers(); - })); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } - menuItems.add(_addFavAction(peer.id)); - if (!gFFI.abModel.idContainBy(peer.id)) { - menuItems.add(_addToAb(peer)); + + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); } + + if (gFFI.userModel.userName.isNotEmpty) { + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } + } + + menuItems.add(MenuEntryDivider()); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadRecentPeers(); + })); return menuItems; } + + @protected + @override + void _update() => bind.mainLoadRecentPeers(); } class FavoritePeerCard extends BasePeerCard { FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.fav, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -762,29 +854,34 @@ class FavoritePeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async { - await bind.mainLoadFavPeers(); - })); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } menuItems.add(_rmFavAction(peer.id, () async { await bind.mainLoadFavPeers(); })); - if (!gFFI.abModel.idContainBy(peer.id)) { - menuItems.add(_addToAb(peer)); + + if (gFFI.userModel.userName.isNotEmpty) { + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } } + + menuItems.add(MenuEntryDivider()); + menuItems.add(_removeAction(peer.id, () async { + await bind.mainLoadFavPeers(); + })); return menuItems; } + + @protected + @override + void _update() => bind.mainLoadFavPeers(); } class DiscoveredPeerCard extends BasePeerCard { DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.lan, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -793,6 +890,9 @@ class DiscoveredPeerCard extends BasePeerCard { _connectAction(context, peer), _transferFileAction(context, peer.id), ]; + + final List favs = (await bind.mainGetFav()).toList(); + if (isDesktop && peer.platform != 'Android') { menuItems.add(_tcpTunnelingAction(context, peer.id)); } @@ -804,22 +904,36 @@ class DiscoveredPeerCard extends BasePeerCard { if (Platform.isWindows) { menuItems.add(_createShortCutAction(peer.id)); } - menuItems.add(MenuEntryDivider()); - menuItems.add(_removeAction(peer.id, () async {})); - if (!gFFI.abModel.idContainBy(peer.id)) { - menuItems.add(_addToAb(peer)); + + if (!favs.contains(peer.id)) { + menuItems.add(_addFavAction(peer.id)); + } else { + menuItems.add(_rmFavAction(peer.id, () async {})); } + + if (gFFI.userModel.userName.isNotEmpty) { + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } + } + + menuItems.add(MenuEntryDivider()); + menuItems.add( + _removeAction(peer.id, () async { + await bind.mainLoadLanPeers(); + }, isLan: true), + ); return menuItems; } + + @protected + @override + void _update() => bind.mainLoadLanPeers(); } class AddressBookPeerCard extends BasePeerCard { AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.ab, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -841,16 +955,32 @@ class AddressBookPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_renameAction(peer.id)); - menuItems.add(_removeAction(peer.id, () async {})); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); } if (gFFI.abModel.tags.isNotEmpty) { menuItems.add(_editTagAction(peer.id)); } + + menuItems.add(MenuEntryDivider()); + menuItems.add(_removeAction(peer.id, () async {})); return menuItems; } + @protected + @override + Future _isForceAlwaysRelay(String id) async => + gFFI.abModel.find(id)?.forceAlwaysRelay ?? false; + + @protected + @override + Future _getAlias(String id) async => + gFFI.abModel.find(id)?.alias ?? ''; + + @protected + @override + void _update() => gFFI.abModel.pullAb(); + @protected @override MenuEntryBase _removeAction( @@ -931,8 +1061,8 @@ class AddressBookPeerCard extends BasePeerCard { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -943,11 +1073,7 @@ class AddressBookPeerCard extends BasePeerCard { class MyGroupPeerCard extends BasePeerCard { MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) - : super( - peer: peer, - cardType: CardType.grp, - menuPadding: menuPadding, - key: key); + : super(peer: peer, menuPadding: menuPadding, key: key); @override Future>> _buildMenuItems( @@ -974,18 +1100,15 @@ class MyGroupPeerCard extends BasePeerCard { } return menuItems; } + + @protected + @override + void _update() => gFFI.groupModel.pull(); } -void _rdpDialog(String id, CardType card) async { - String port, username; - if (card == CardType.ab) { - port = gFFI.abModel.find(id)?.rdpPort ?? ''; - username = gFFI.abModel.find(id)?.rdpUsername ?? ''; - } else { - port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); - username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); - } - +void _rdpDialog(String id) async { + final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); + final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); final portController = TextEditingController(text: port); final userController = TextEditingController(text: username); final passwordController = TextEditingController( @@ -1007,7 +1130,7 @@ void _rdpDialog(String id, CardType card) async { } return CustomAlertDialog( - title: Text('RDP ${translate('Settings')}'), + title: Text(translate('RDP Settings')), content: ConstrainedBox( constraints: const BoxConstraints(minWidth: 500), child: Column( @@ -1018,69 +1141,63 @@ void _rdpDialog(String id, CardType card) async { ), Row( children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - "${translate('Port')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + isDesktop + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Port')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), 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'), + decoration: InputDecoration( + labelText: isDesktop ? null : translate('Port'), + hintText: '3389'), controller: portController, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: isDesktop ? 8 : 0), Row( children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - "${translate('Username')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + isDesktop + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), Expanded( child: TextField( - decoration: - const InputDecoration(border: OutlineInputBorder()), + decoration: InputDecoration( + labelText: isDesktop ? null : translate('Username')), controller: userController, ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: isDesktop ? 8 : 0), Row( children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('Password')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + isDesktop + ? ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.right, + ).marginOnly(right: 10)) + : SizedBox.shrink(), Expanded( child: Obx(() => TextField( obscureText: secure.value, decoration: InputDecoration( - border: const OutlineInputBorder(), + labelText: isDesktop ? null : translate('Password'), suffixIcon: IconButton( onPressed: () => secure.value = !secure.value, icon: Icon(secure.value @@ -1090,13 +1207,13 @@ void _rdpDialog(String id, CardType card) async { )), ), ], - ), + ).marginOnly(bottom: isDesktop ? 8 : 0), ], ), ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -1115,21 +1232,21 @@ Widget getOnline(double rightPadding, bool online) { } class ActionMore extends StatelessWidget { - final RxBool _iconMoreHover = false.obs; + final RxBool _hover = false.obs; @override Widget build(BuildContext context) { - return MouseRegion( - onEnter: (_) => _iconMoreHover.value = true, - onExit: (_) => _iconMoreHover.value = false, + return InkWell( + onTap: () {}, + onHover: (value) => _hover.value = value, child: Obx(() => CircleAvatar( radius: 14, - backgroundColor: _iconMoreHover.value + backgroundColor: _hover.value ? Theme.of(context).scaffoldBackgroundColor - : Theme.of(context).backgroundColor, + : Theme.of(context).colorScheme.background, child: Icon(Icons.more_vert, size: 18, - color: _iconMoreHover.value + color: _hover.value ? Theme.of(context).textTheme.titleLarge?.color : Theme.of(context) .textTheme diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 0c24fe7ea..2d36d9150 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,6 +1,8 @@ +import 'dart:math'; import 'dart:ui' as ui; import 'package:bot_toast/bot_toast.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/address_book.dart'; import 'package:flutter_hbb/common/widgets/my_group.dart'; @@ -11,106 +13,16 @@ 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 'package:dropdown_button2/dropdown_button2.dart'; import '../../common.dart'; import '../../models/platform_model.dart'; -const int groupTabIndex = 4; -const String defaultGroupTabname = 'Group'; - -class StatePeerTab { - final RxInt currentTab = 0.obs; - final RxInt tabHiddenFlag = 0.obs; - final RxList tabNames = [ - 'Recent Sessions', - 'Favorites', - 'Discovered', - 'Address Book', - defaultGroupTabname, - ].obs; - - StatePeerTab._() { - tabHiddenFlag.value = (int.tryParse( - bind.getLocalFlutterConfig(k: 'hidden-peer-card'), - radix: 2) ?? - 0); - var tabs = _notHiddenTabs(); - currentTab.value = - int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; - if (!tabs.contains(currentTab.value)) { - currentTab.value = 0; - } - } - static final StatePeerTab instance = StatePeerTab._(); - - check() { - var tabs = _notHiddenTabs(); - if (filterGroupCard()) { - if (currentTab.value == groupTabIndex) { - currentTab.value = - tabs.firstWhereOrNull((e) => e != groupTabIndex) ?? 0; - bind.setLocalFlutterConfig( - k: 'peer-tab-index', v: currentTab.value.toString()); - } - } else { - if (gFFI.userModel.isAdmin.isFalse && - gFFI.userModel.groupName.isNotEmpty) { - tabNames[groupTabIndex] = gFFI.userModel.groupName.value; - } else { - tabNames[groupTabIndex] = defaultGroupTabname; - } - if (tabs.contains(groupTabIndex) && - int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == - groupTabIndex) { - currentTab.value = groupTabIndex; - } - } - } - - List currentTabs() { - var v = List.empty(growable: true); - for (int i = 0; i < tabNames.length; i++) { - if (!_isTabHidden(i) && !_isTabFilter(i)) { - v.add(i); - } - } - return v; - } - - bool filterGroupCard() { - if (gFFI.groupModel.users.isEmpty || - (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { - return true; - } else { - return false; - } - } - - bool _isTabHidden(int tabindex) { - return tabHiddenFlag & (1 << tabindex) != 0; - } - - bool _isTabFilter(int tabIndex) { - if (tabIndex == groupTabIndex) { - return filterGroupCard(); - } - return false; - } - - List _notHiddenTabs() { - var v = List.empty(growable: true); - for (int i = 0; i < tabNames.length; i++) { - if (!_isTabHidden(i)) { - v.add(i); - } - } - return v; - } -} - -final statePeerTab = StatePeerTab.instance; - class PeerTabPage extends StatefulWidget { const PeerTabPage({Key? key}) : super(key: key); @override @@ -129,6 +41,8 @@ EdgeInsets? _menuPadding() { class _PeerTabPageState extends State with SingleTickerProviderStateMixin { + bool _hideSort = bind.getLocalFlutterConfig(k: 'peer-tab-index') == '0'; + final List<_TabEntry> entries = [ _TabEntry( RecentPeersView( @@ -156,11 +70,10 @@ class _PeerTabPageState extends State ), () => {}), ]; + final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50)); @override void initState() { - adjustTab(); - final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); if (uiType != '') { peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index @@ -172,16 +85,12 @@ class _PeerTabPageState extends State Future handleTabSelection(int tabIndex) async { if (tabIndex < entries.length) { - statePeerTab.currentTab.value = tabIndex; + gFFI.peerTabModel.setCurrentTab(tabIndex); entries[tabIndex].load(); + _hideSort = tabIndex == 0; } } - @override - void dispose() { - super.dispose(); - } - @override Widget build(BuildContext context) { return Column( @@ -191,21 +100,27 @@ class _PeerTabPageState extends State 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))), - const PeerSearchBar(), - Offstage( - offstage: !isDesktop, - child: _createPeerViewTypeSwitch(context) - .marginOnly(left: 13)), - ], - )), + 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)), + Offstage( + offstage: _hideSort, + child: PeerSortDropdown().marginOnly(left: 8), + ), + ], + ), + ), ), _createPeersView(), ], @@ -213,119 +128,146 @@ class _PeerTabPageState extends State } Widget _createSwitchBar(BuildContext context) { - final textColor = Theme.of(context).textTheme.titleLarge?.color; - return Obx(() { - var tabs = statePeerTab.currentTabs(); - return ListView( - scrollDirection: Axis.horizontal, - physics: NeverScrollableScrollPhysics(), - controller: ScrollController(), - children: tabs.map((t) { - return InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: statePeerTab.currentTab.value == t - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), - ), - child: Align( - alignment: Alignment.center, - child: Text( - translatedTabname(t), - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: statePeerTab.currentTab.value == t - ? textColor - : textColor - ?..withOpacity(0.5)), - ), - )), - onTap: () async { - await handleTabSelection(t); - await bind.setLocalFlutterConfig( - k: 'peer-tab-index', v: t.toString()); + final model = Provider.of(context); + int indexCounter = -1; + return ReorderableListView( + buildDefaultDragHandles: false, + onReorder: (oldIndex, newIndex) { + model.onReorder(oldIndex, newIndex); + }, + scrollDirection: Axis.horizontal, + physics: NeverScrollableScrollPhysics(), + scrollController: model.sc, + children: model.visibleOrderedTabs.map((t) { + indexCounter++; + return ReorderableDragStartListener( + key: ValueKey(t), + index: indexCounter, + child: VisibilityDetector( + key: ValueKey(t), + onVisibilityChanged: (info) { + final id = (info.key as ValueKey).value; + model.setTabFullyVisible(id, info.visibleFraction > 0.99); }, - ); - }).toList()); - }); + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + if (!model.sc.canScroll) return; + _scrollDebounce.call(() { + model.sc.animateTo(model.sc.offset + e.scrollDelta.dy, + duration: Duration(milliseconds: 200), + curve: Curves.ease); + }); + } + }, + child: InkWell( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: model.currentTab == t + ? Theme.of(context).colorScheme.background + : null, + borderRadius: BorderRadius.circular(6), + ), + child: Align( + alignment: Alignment.center, + child: Text( + model.translatedTabname(t), + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: model.currentTab == t + ? MyTheme.tabbar(context).selectedTextColor + : MyTheme.tabbar(context).unSelectedTextColor + ?..withOpacity(0.5)), + ), + )), + onTap: () async { + await handleTabSelection(t); + await bind.setLocalFlutterConfig( + k: 'peer-tab-index', v: t.toString()); + }, + ), + ), + ), + ); + }).toList()); } - translatedTabname(int index) { - if (index < statePeerTab.tabNames.length) { - final name = statePeerTab.tabNames[index]; - if (index == groupTabIndex) { - if (name == defaultGroupTabname) { - return translate(name); - } else { - return name; - } - } else { - return translate(name); - } - } - assert(false); - return index.toString(); + Widget buildScrollJumper() { + final model = Provider.of(context); + return Offstage( + offstage: !model.showScrollBtn, + child: Row( + children: [ + GestureDetector( + child: Icon(Icons.arrow_left, + size: 22, + color: model.leftFullyVisible + ? Theme.of(context).disabledColor + : null), + onTap: model.sc.backward), + GestureDetector( + child: Icon(Icons.arrow_right, + size: 22, + color: model.rightFullyVisible + ? Theme.of(context).disabledColor + : null), + onTap: model.sc.forward) + ], + )); } Widget _createPeersView() { - final verticalMargin = isDesktop ? 12.0 : 6.0; - return Expanded( - child: Obx(() { - var tabs = statePeerTab.currentTabs(); - if (tabs.isEmpty) { - return visibleContextMenuListener(Center( - child: Text(translate('Right click to select tabs')), - )); + final model = Provider.of(context); + Widget child; + if (model.visibleOrderedTabs.isEmpty) { + child = visibleContextMenuListener(Center( + child: Text(translate('Right click to select tabs')), + )); + } else { + if (model.visibleOrderedTabs.contains(model.currentTab)) { + child = entries[model.currentTab].widget; } else { - if (tabs.contains(statePeerTab.currentTab.value)) { - return entries[statePeerTab.currentTab.value].widget; - } else { - statePeerTab.currentTab.value = tabs[0]; - return entries[statePeerTab.currentTab.value].widget; - } + model.setCurrentTab(model.visibleOrderedTabs[0]); + child = entries[0].widget; } - }).marginSymmetric(vertical: verticalMargin)); + } + return Expanded( + child: child.marginSymmetric(vertical: isDesktop ? 12.0 : 6.0)); } Widget _createPeerViewTypeSwitch(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; - final activeDeco = BoxDecoration(color: Theme.of(context).backgroundColor); - 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(), + final deco = BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(5), ); - } + final types = [PeerUiType.grid, PeerUiType.list]; - adjustTab() { - var tabs = statePeerTab.currentTabs(); - if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) { - statePeerTab.currentTab.value = tabs[0]; - } + return Obx( + () => Container( + padding: EdgeInsets.all(4.0), + decoration: deco, + child: InkWell( + onTap: () async { + final type = types.elementAt( + peerCardUiType.value == types.elementAt(0) ? 1 : 0); + await bind.setLocalFlutterConfig( + k: 'peer-card-ui-type', v: type.index.toString()); + peerCardUiType.value = type; + }, + child: Icon( + peerCardUiType.value == PeerUiType.grid + ? Icons.list_rounded + : Icons.grid_view_rounded, + size: 18, + color: textColor, + )), + ), + ); } Widget visibleContextMenuListener(Widget child) { @@ -347,44 +289,36 @@ class _PeerTabPageState extends State } Widget visibleContextMenu(CancelFunc cancelFunc) { - return Obx(() { - final List menu = List.empty(growable: true); - for (int i = 0; i < statePeerTab.tabNames.length; i++) { - if (i == groupTabIndex && statePeerTab.filterGroupCard()) { - continue; - } - int bitMask = 1 << i; - menu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translatedTabname(i), - getter: () async { - return statePeerTab.tabHiddenFlag & bitMask == 0; - }, - setter: (show) async { - if (show) { - statePeerTab.tabHiddenFlag.value &= ~bitMask; - } else { - statePeerTab.tabHiddenFlag.value |= bitMask; - } - await bind.setLocalFlutterConfig( - k: 'hidden-peer-card', - v: statePeerTab.tabHiddenFlag.value.toRadixString(2)); - cancelFunc(); - adjustTab(); - })); - } - return mod_menu.PopupMenu( - items: menu - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: MyTheme.accent, - height: 20.0, - dividerHeight: 12.0, - ))) - .expand((i) => i) - .toList()); - }); + final model = Provider.of(context); + final List menu = List.empty(growable: true); + final List menuIndex = List.empty(growable: true); + var list = model.orderedNotFilteredTabs(); + for (int i = 0; i < list.length; i++) { + int tabIndex = list[i]; + int bitMask = 1 << tabIndex; + menuIndex.add(tabIndex); + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: model.translatedTabname(tabIndex), + getter: () async { + return model.tabHiddenFlag & bitMask == 0; + }, + setter: (show) async { + model.onHideShow(tabIndex, show); + cancelFunc(); + })); + } + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: MyTheme.accent, + height: 20.0, + dividerHeight: 12.0, + ))) + .expand((i) => i) + .toList()); } } @@ -419,11 +353,16 @@ class _PeerSearchBarState extends State { Widget _buildSearchBar() { RxBool focused = false.obs; FocusNode focusNode = FocusNode(); - focusNode.addListener(() => focused.value = focusNode.hasFocus); + focusNode.addListener(() { + focused.value = focusNode.hasFocus; + peerSearchTextController.selection = TextSelection( + baseOffset: 0, + extentOffset: peerSearchTextController.value.text.length); + }); return Container( width: 120, decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(6), ), child: Obx(() => Row( @@ -488,3 +427,98 @@ class _PeerSearchBarState extends State { ); } } + +class PeerSortDropdown extends StatefulWidget { + const PeerSortDropdown({super.key}); + + @override + State createState() => _PeerSortDropdownState(); +} + +class _PeerSortDropdownState extends State { + @override + void initState() { + if (!PeerSortType.values.contains(peerSort.value)) { + peerSort.value = PeerSortType.remoteId; + bind.setLocalFlutterConfig( + k: "peer-sorting", + v: peerSort.value, + ); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + final deco = BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(5), + ); + + final translated_text = + PeerSortType.values.map((e) => translate(e)).toList(); + + final double max_width = + 50 + translated_text.map((e) => e.length).reduce(max) * 10; + + return Container( + padding: EdgeInsets.all(4.0), + decoration: deco, + child: DropdownButtonHideUnderline( + child: DropdownButton2( + onChanged: (v) async { + if (v != null) { + setState(() => peerSort.value = v); + await bind.setLocalFlutterConfig( + k: "peer-sorting", + v: peerSort.value, + ); + } + }, + customButton: Icon( + Icons.sort, + size: 18, + ), + isExpanded: true, + dropdownStyleData: DropdownStyleData( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.circular(10), + ), + width: max_width, + ), + items: [ + DropdownMenuItem( + alignment: Alignment.center, + child: Text( + translate("Sort by"), + style: TextStyle(fontWeight: FontWeight.bold), + ), + enabled: false, + ), + ...translated_text + .map>( + (String value) => DropdownMenuItem( + value: value, + child: Row( + children: [ + Icon( + value == peerSort.value + ? Icons.radio_button_checked_rounded + : Icons.radio_button_off_rounded, + size: 18, + ).paddingOnly(right: 12), + Text( + value, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ) + .toList(), + ]), + ), + ); + } +} diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 9c98f24b8..16a6f98ef 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'dart:collection'; 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'; @@ -16,8 +16,36 @@ import 'peer_card.dart'; typedef PeerFilter = bool Function(Peer peer); typedef PeerCardBuilder = Widget Function(Peer peer); +class PeerSortType { + static const String remoteId = 'Remote ID'; + static const String remoteHost = 'Remote Host'; + static const String username = 'Username'; + // static const String status = 'Status'; + + static List values = [ + PeerSortType.remoteId, + PeerSortType.remoteHost, + PeerSortType.username, + // PeerSortType.status + ]; +} + +class LoadEvent { + static const String recent = 'load_recent_peers'; + static const String favorite = 'load_fav_peers'; + static const String lan = 'load_lan_peers'; + static const String addressBook = 'load_address_book_peers'; +} + /// for peer search text, global obs value final peerSearchText = "".obs; + +/// for peer sort, global obs value +final peerSort = bind.getLocalFlutterConfig(k: 'peer-sorting').obs; + +// list for listener +final obslist = [peerSearchText, peerSort].obs; + final peerSearchTextController = TextEditingController(text: peerSearchText.value); @@ -40,12 +68,19 @@ class _PeersView extends StatefulWidget { /// State for the peer widget. class _PeersViewState extends State<_PeersView> with WindowListener { static const int _maxQueryCount = 3; + final HashMap _emptyMessages = HashMap.from({ + LoadEvent.recent: 'empty_recent_tip', + LoadEvent.favorite: 'empty_favorite_tip', + LoadEvent.lan: 'empty_lan_tip', + LoadEvent.addressBook: 'empty_address_book_tip', + }); 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 _queryCount = 0; + var _loaded = false; var _exit = false; late final mobileWidth = () { @@ -78,12 +113,12 @@ class _PeersViewState extends State<_PeersView> with WindowListener { @override void onWindowFocus() { - _queryCoun = 0; + _queryCount = 0; } @override void onWindowMinimize() { - _queryCoun = _maxQueryCount; + _queryCount = _maxQueryCount; } @override @@ -91,17 +126,49 @@ class _PeersViewState extends State<_PeersView> with WindowListener { 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)), + builder: (context, peers, child) => peers.peers.isEmpty && _loaded + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.sentiment_very_dissatisfied_rounded, + color: Theme.of(context).tabBarTheme.labelColor, + size: 40, + ).paddingOnly(bottom: 10), + Text( + translate( + _emptyMessages[widget.peers.loadEvent] ?? 'Empty', + ), + textAlign: TextAlign.center, + style: TextStyle( + color: Theme.of(context).tabBarTheme.labelColor, + ), + ), + ], + ), + ) + : _buildPeersView(peers), + ), ); } + onVisibilityChanged(VisibilityInfo info) { + final peerId = _peerId((info.key as ValueKey).value); + if (info.visibleFraction > 0.00001) { + _curPeers.add(peerId); + } else { + _curPeers.remove(peerId); + } + _lastChangeTime = DateTime.now(); + } + + String _cardId(String id) => widget.peers.name + id; + String _peerId(String cardId) => cardId.replaceAll(widget.peers.name, ''); + Widget _buildPeersView(Peers peers) { - final body = ObxValue((searchText) { + _loaded = true; + final body = ObxValue((filters) { return FutureBuilder>( builder: (context, snapshot) { if (snapshot.hasData) { @@ -109,16 +176,8 @@ class _PeersViewState extends State<_PeersView> with WindowListener { 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(); - }, + key: ValueKey(_cardId(peer.id)), + onVisibilityChanged: onVisibilityChanged, child: widget.peerCardBuilder(peer), ); cards.add(isDesktop @@ -139,9 +198,9 @@ class _PeersViewState extends State<_PeersView> with WindowListener { ); } }, - future: matchPeers(searchText.value, peers.peers), + future: matchPeers(filters[0].value, filters[1].value, peers.peers), ); - }, peerSearchText); + }, obslist); return body; } @@ -149,6 +208,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener { // ignore: todo // TODO: variables walk through async tasks? void _startCheckOnlines() { + final queryInterval = const Duration(seconds: 20); () async { while (!_exit) { final now = DateTime.now(); @@ -158,18 +218,18 @@ class _PeersViewState extends State<_PeersView> with WindowListener { platformFFI.ffiBind .queryOnlines(ids: _curPeers.toList(growable: false)); _lastQueryPeers = {..._curPeers}; - _lastQueryTime = DateTime.now(); - _queryCoun = 0; + _lastQueryTime = DateTime.now().subtract(queryInterval); + _queryCount = 0; } } } else { - if (_queryCoun < _maxQueryCount) { - if (now.difference(_lastQueryTime) > const Duration(seconds: 20)) { + if (_queryCount < _maxQueryCount) { + if (now.difference(_lastQueryTime) >= queryInterval) { if (_curPeers.isNotEmpty) { platformFFI.ffiBind .queryOnlines(ids: _curPeers.toList(growable: false)); _lastQueryTime = DateTime.now(); - _queryCoun += 1; + _queryCount += 1; } } } @@ -179,11 +239,40 @@ class _PeersViewState extends State<_PeersView> with WindowListener { }(); } - Future>? matchPeers(String searchText, List peers) async { + Future>? matchPeers( + String searchText, String sortedBy, List peers) async { if (widget.peerFilter != null) { peers = peers.where((peer) => widget.peerFilter!(peer)).toList(); } + // fallback to id sorting + if (!PeerSortType.values.contains(sortedBy)) { + sortedBy = PeerSortType.remoteId; + bind.setLocalFlutterConfig( + k: "peer-sorting", + v: sortedBy, + ); + } + + if (widget.peers.loadEvent != LoadEvent.recent) { + switch (sortedBy) { + case PeerSortType.remoteId: + peers.sort((p1, p2) => p1.getId().compareTo(p2.getId())); + break; + case PeerSortType.remoteHost: + peers.sort((p1, p2) => + p1.hostname.toLowerCase().compareTo(p2.hostname.toLowerCase())); + break; + case PeerSortType.username: + peers.sort((p1, p2) => + p1.username.toLowerCase().compareTo(p2.username.toLowerCase())); + break; + // case PeerSortType.status: + // peers.sort((p1, p2) => p1.online ? -1 : 1); + // break; + } + } + searchText = searchText.trim(); if (searchText.isEmpty) { return peers; @@ -197,6 +286,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener { filteredList.add(peers[i]); } } + return filteredList; } } @@ -232,7 +322,7 @@ class RecentPeersView extends BasePeersView { : super( key: key, name: 'recent peer', - loadEvent: 'load_recent_peers', + loadEvent: LoadEvent.recent, peerCardBuilder: (Peer peer) => RecentPeerCard( peer: peer, menuPadding: menuPadding, @@ -254,7 +344,7 @@ class FavoritePeersView extends BasePeersView { : super( key: key, name: 'favorite peer', - loadEvent: 'load_fav_peers', + loadEvent: LoadEvent.favorite, peerCardBuilder: (Peer peer) => FavoritePeerCard( peer: peer, menuPadding: menuPadding, @@ -276,7 +366,7 @@ class DiscoveredPeersView extends BasePeersView { : super( key: key, name: 'discovered peer', - loadEvent: 'load_lan_peers', + loadEvent: LoadEvent.lan, peerCardBuilder: (Peer peer) => DiscoveredPeerCard( peer: peer, menuPadding: menuPadding, @@ -301,7 +391,7 @@ class AddressBookPeersView extends BasePeersView { : super( key: key, name: 'address book peer', - loadEvent: 'load_address_book_peers', + loadEvent: LoadEvent.addressBook, peerFilter: (Peer peer) => _hitTag(gFFI.abModel.selectedTags, peer.tags), peerCardBuilder: (Peer peer) => AddressBookPeerCard( diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 017850cf5..dd39cbdfd 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -62,11 +62,9 @@ class RawPointerMouseRegion extends StatelessWidget { }, onPointerMove: inputModel.onPointMoveImage, onPointerSignal: inputModel.onPointerSignalImage, - /* onPointerPanZoomStart: inputModel.onPointerPanZoomStart, onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate, onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd, - */ child: MouseRegion( cursor: cursor ?? MouseCursor.defer, onEnter: onEnter, diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 50e7f594b..e2a3c6f0b 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,25 +1,36 @@ -import 'package:flutter/material.dart'; import 'dart:io'; +import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/state_model.dart'; const double kDesktopRemoteTabBarHeight = 28.0; +const int kMainWindowId = 0; -/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" +const String kPeerPlatformWindows = "Windows"; +const String kPeerPlatformLinux = "Linux"; +const String kPeerPlatformMacOS = "Mac OS"; +const String kPeerPlatformAndroid = "Android"; + +/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)', "Install Page" const String kAppTypeMain = "main"; + +/// [kAppTypeConnectionManager] only for 'Desktop CM Page' +const String kAppTypeConnectionManager = "cm"; + const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; const String kAppTypeDesktopPortForward = "port forward"; const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowGetWindowInfo = "get_window_info"; +const String kWindowDisableGrabKeyboard = "disable_grab_keyboard"; const String kWindowActionRebuild = "rebuild"; const String kWindowEventHide = "hide"; const String kWindowEventShow = "show"; const String kWindowConnect = "connect"; const String kUniLinksPrefix = "rustdesk://"; -const String kActionNewConnection = "connection/new/"; const String kTabLabelHomePage = "Home"; const String kTabLabelSettingPage = "Settings"; @@ -44,6 +55,26 @@ const int kMobileMaxDisplayHeight = 1280; const int kDesktopMaxDisplayWidth = 1920; const int kDesktopMaxDisplayHeight = 1080; +const double kDesktopFileTransferNameColWidth = 200; +const double kDesktopFileTransferModifiedColWidth = 120; +const double kDesktopFileTransferMinimumWidth = 100; +const double kDesktopFileTransferMaximumWidth = 300; +const double kDesktopFileTransferRowHeight = 30.0; +const double kDesktopFileTransferHeaderHeight = 25.0; + +EdgeInsets get kDragToResizeAreaPadding => + !kUseCompatibleUiMode && Platform.isLinux + ? stateGlobal.fullscreen || stateGlobal.maximize + ? EdgeInsets.zero + : EdgeInsets.all(5.0) + : EdgeInsets.zero; +// https://en.wikipedia.org/wiki/Non-breaking_space +const int $nbsp = 0x00A0; + +extension StringExtension on String { + String get nonBreaking => replaceAll(' ', String.fromCharCode($nbsp)); +} + const Size kConnectionManagerWindowSize = Size(300, 400); // Tabbar transition duration, now we remove the duration const Duration kTabTransitionDuration = Duration.zero; @@ -58,6 +89,7 @@ const kDefaultScrollAmountMultiplier = 5.0; const kDefaultScrollDuration = Duration(milliseconds: 50); const kDefaultMouseWheelThrottleDuration = Duration(milliseconds: 50); const kFullScreenEdgeSize = 0.0; +const kMaximizeEdgeSize = 0.0; var kWindowEdgeSize = Platform.isWindows ? 1.0 : 5.0; const kWindowBorderWidth = 1.0; const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); @@ -100,6 +132,33 @@ const kRemoteImageQualityLow = 'low'; /// [kRemoteImageQualityCustom] Custom image quality. const kRemoteImageQualityCustom = 'custom'; +/// [kRemoteAudioGuestToHost] Guest to host audio mode(default). +const kRemoteAudioGuestToHost = 'guest-to-host'; + +/// [kRemoteAudioDualWay] dual-way audio mode(default). +const kRemoteAudioDualWay = 'dual-way'; + +const kIgnoreDpi = true; + +/// Android constants +const kActionApplicationDetailsSettings = + "android.settings.APPLICATION_DETAILS_SETTINGS"; +const kActionAccessibilitySettings = "android.settings.ACCESSIBILITY_SETTINGS"; + +const kRecordAudio = "android.permission.RECORD_AUDIO"; +const kManageExternalStorage = "android.permission.MANAGE_EXTERNAL_STORAGE"; +const kRequestIgnoreBatteryOptimizations = + "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; +const kSystemAlertWindow = "android.permission.SYSTEM_ALERT_WINDOW"; + +/// Android channel invoke type key +class AndroidChannel { + static final kStartAction = "start_action"; + static final kGetStartOnBootOpt = "get_start_on_boot_opt"; + static final kSetStartOnBootOpt = "set_start_on_boot_opt"; + static final kSyncAppDirConfigPath = "sync_app_dir"; +} + /// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels /// see [LogicalKeyboardKey.keyLabel] const Map logicalKeyMap = { diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index 7500fe99e..a593810fd 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -6,9 +6,9 @@ import 'dart:io'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/common/widgets/address_book.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'; @@ -16,10 +16,11 @@ import 'package:window_manager/window_manager.dart'; import '../../common.dart'; import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peers_view.dart'; import '../../models/platform_model.dart'; import '../widgets/button.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; + /// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget { const ConnectionPage({Key? key}) : super(key: key); @@ -46,7 +47,7 @@ class _ConnectionPageState extends State var svcStatusCode = 0.obs; var svcIsUsingPublicServer = true.obs; - bool isWindowMinisized = false; + bool isWindowMinimized = false; @override void initState() { @@ -66,6 +67,9 @@ class _ConnectionPageState extends State }); _idFocusNode.addListener(() { _idInputFocused.value = _idFocusNode.hasFocus; + // select all to faciliate removing text, just following the behavior of address input of chrome + _idController.selection = TextSelection( + baseOffset: 0, extentOffset: _idController.value.text.length); }); windowManager.addListener(this); } @@ -82,16 +86,28 @@ class _ConnectionPageState extends State void onWindowEvent(String eventName) { super.onWindowEvent(eventName); if (eventName == 'minimize') { - isWindowMinisized = true; + isWindowMinimized = true; } else if (eventName == 'maximize' || eventName == 'restore') { - if (isWindowMinisized && Platform.isWindows) { - // windows can't update when minisized. + if (isWindowMinimized && Platform.isWindows) { + // windows can't update when minimized. Get.forceAppUpdate(); } - isWindowMinisized = false; + isWindowMinimized = false; } } + @override + void onWindowEnterFullScreen() { + // Remove edge border by setting the value to zero. + stateGlobal.resizeEdgeSize.value = 0; + } + + @override + void onWindowLeaveFullScreen() { + // Restore edge border to default edge size. + stateGlobal.resizeEdgeSize.value = kWindowEdgeSize; + } + @override void onWindowClose() { super.onWindowClose(); @@ -107,7 +123,7 @@ class _ConnectionPageState extends State scrollController: _scrollController, child: CustomScrollView( controller: _scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), slivers: [ SliverList( delegate: SliverChildListDelegate([ @@ -136,7 +152,7 @@ class _ConnectionPageState extends State /// Callback for the connect button. /// Connects to the selected peer. void onConnect({bool isFileTransfer = false}) { - final id = _idController.id; + var id = _idController.id; connect(context, id, isFileTransfer: isFileTransfer); } @@ -147,9 +163,8 @@ class _ConnectionPageState extends State width: 320 + 20 * 2, padding: const EdgeInsets.fromLTRB(20, 24, 20, 22), decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(13)), - ), + borderRadius: const BorderRadius.all(Radius.circular(13)), + border: Border.all(color: Theme.of(context).colorScheme.background)), child: Ink( child: Column( children: [ @@ -172,6 +187,7 @@ class _ConnectionPageState extends State Expanded( child: Obx( () => TextField( + maxLength: 90, autocorrect: false, enableSuggestions: false, keyboardType: TextInputType.visiblePassword, @@ -179,31 +195,19 @@ class _ConnectionPageState extends State style: const TextStyle( fontFamily: 'WorkSans', fontSize: 22, - height: 1, + height: 1.4, ), maxLines: 1, cursorColor: Theme.of(context).textTheme.titleLarge?.color, decoration: InputDecoration( + filled: false, + 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)), + horizontal: 15, vertical: 13)), controller: _idController, inputFormatters: [IDTextInputFormatter()], onSubmitted: (s) { diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 1e8512b2e..e66e3f284 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -3,9 +3,10 @@ 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/material.dart'; 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'; @@ -42,6 +43,8 @@ class _DesktopHomePageState extends State var svcStopped = false.obs; var watchIsCanScreenRecording = false; var watchIsProcessTrust = false; + var watchIsInputMonitoring = false; + var watchIsCanRecordAudio = false; Timer? _updateTimer; @override @@ -51,10 +54,7 @@ class _DesktopHomePageState extends State crossAxisAlignment: CrossAxisAlignment.start, children: [ buildLeftPane(context), - const VerticalDivider( - width: 1, - thickness: 1, - ), + const VerticalDivider(width: 1), Expanded( child: buildRightPane(context), ), @@ -67,17 +67,27 @@ class _DesktopHomePageState extends State value: gFFI.serverModel, child: Container( width: 200, - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, child: DesktopScrollWrapper( scrollController: _leftPaneScrollController, child: SingleChildScrollView( controller: _leftPaneScrollController, + physics: DraggableNeverScrollableScrollPhysics(), child: Column( children: [ buildTip(context), buildIDBoard(context), buildPasswordBoard(context), - buildHelpCards(), + FutureBuilder( + future: buildHelpCards(), + builder: (_, data) { + if (data.hasData) { + return data.data!; + } else { + return const Offstage(); + } + }, + ), ], ), ), @@ -144,7 +154,7 @@ class _DesktopHomePageState extends State readOnly: true, decoration: InputDecoration( border: InputBorder.none, - contentPadding: EdgeInsets.only(bottom: 20), + contentPadding: EdgeInsets.only(top: 10, bottom: 10), ), style: TextStyle( fontSize: 22, @@ -171,7 +181,7 @@ class _DesktopHomePageState extends State radius: 15, backgroundColor: hover.value ? Theme.of(context).scaffoldBackgroundColor - : Theme.of(context).backgroundColor, + : Theme.of(context).colorScheme.background, child: Icon( Icons.more_vert_outlined, size: 20, @@ -228,7 +238,8 @@ class _DesktopHomePageState extends State readOnly: true, decoration: InputDecoration( border: InputBorder.none, - contentPadding: EdgeInsets.only(bottom: 2), + contentPadding: + EdgeInsets.only(top: 14, bottom: 10), ), style: TextStyle(fontSize: 15), ), @@ -240,9 +251,9 @@ class _DesktopHomePageState extends State Icons.refresh, color: refreshHover.value ? textColor - : Color(0xFFDDDDDD), // TODO + : Color(0xFFDDDDDD), size: 22, - ).marginOnly(right: 8, bottom: 2), + ).marginOnly(right: 8, top: 4), ), onTap: () => bind.mainUpdateTemporaryPassword(), onHover: (value) => refreshHover.value = value, @@ -251,11 +262,10 @@ class _DesktopHomePageState extends State child: Obx( () => Icon( Icons.edit, - color: editHover.value - ? textColor - : Color(0xFFDDDDDD), // TODO + color: + editHover.value ? textColor : Color(0xFFDDDDDD), size: 22, - ).marginOnly(right: 8, bottom: 2), + ).marginOnly(right: 8, top: 4), ), onTap: () => DesktopSettingPage.switch2page(1), onHover: (value) => editHover.value = value, @@ -300,7 +310,7 @@ class _DesktopHomePageState extends State ); } - Widget buildHelpCards() { + Future buildHelpCards() async { if (updateUrl.isNotEmpty) { return buildInstallCard( "Status", @@ -334,6 +344,12 @@ class _DesktopHomePageState extends State 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)) { @@ -341,6 +357,15 @@ class _DesktopHomePageState extends State bind.mainIsInstalledDaemon(prompt: true); }); } + //// Disable microphone configuration for macOS. We will request the permission when needed. + // else if ((await osxCanRecordAudio() != + // PermissionAuthorizeType.authorized)) { + // return buildInstallCard("Permissions", "config_microphone", "Configure", + // () async { + // osxRequestAudio(); + // watchIsCanRecordAudio = true; + // }); + // } } else if (Platform.isLinux) { if (bind.mainCurrentIsWayland()) { return buildInstallCard( @@ -467,6 +492,30 @@ class _DesktopHomePageState extends State 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); @@ -500,15 +549,16 @@ class _DesktopHomePageState extends State } else if (call.method == kWindowActionRebuild) { reloadCurrentWindow(); } else if (call.method == kWindowEventShow) { - rustDeskWinManager.registerActiveWindow(call.arguments["id"]); + await rustDeskWinManager.registerActiveWindow(call.arguments["id"]); } else if (call.method == kWindowEventHide) { - rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]); + 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'], ); } }); @@ -530,6 +580,14 @@ void setPasswordDialog() async { final p1 = TextEditingController(text: pw); var errMsg0 = ""; var errMsg1 = ""; + final RxString rxPass = pw.trim().obs; + final rules = [ + DigitValidationRule(), + UppercaseValidationRule(), + LowercaseValidationRule(), + // SpecialCharacterValidationRule(), + MinCharactersValidationRule(8), + ]; gFFI.dialogManager.show((setState, close) { submit() { @@ -538,15 +596,20 @@ void setPasswordDialog() async { errMsg1 = ""; }); final pass = p0.text.trim(); - if (pass.length < 6 && pass.isNotEmpty) { - setState(() { - errMsg0 = translate("Too short, at least 6 characters."); - }); - return; + if (pass.isNotEmpty) { + final Iterable violations = rules.where((r) => !r.validate(pass)); + if (violations.isNotEmpty) { + setState(() { + errMsg0 = + '${translate('Prompt')}: ${violations.map((r) => r.name).join(', ')}'; + }); + return; + } } if (p1.text.trim() != pass) { setState(() { - errMsg1 = translate("The confirmation is not identical."); + errMsg1 = + '${translate('Prompt')}: ${translate("The confirmation is not identical.")}'; }); return; } @@ -566,23 +629,46 @@ void setPasswordDialog() async { ), Row( children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - "${translate('Password')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), Expanded( child: TextField( obscureText: true, decoration: InputDecoration( - border: const OutlineInputBorder(), + labelText: translate('Password'), errorText: errMsg0.isNotEmpty ? errMsg0 : null), controller: p0, - focusNode: FocusNode()..requestFocus(), + 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( + labelText: translate('Confirmation'), + errorText: errMsg1.isNotEmpty ? errMsg1 : null), + controller: p1, + onChanged: (value) { + setState(() { + errMsg1 = ''; + }); + }, ), ), ], @@ -590,32 +676,30 @@ void setPasswordDialog() async { const SizedBox( height: 8.0, ), - Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text("${translate('Confirmation')}:") - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: errMsg1.isNotEmpty ? errMsg1 : null), - controller: p1, - ), - ), - ], - ), + Obx(() => Wrap( + runSpacing: 8, + spacing: 4, + children: rules.map((e) { + var checked = e.validate(rxPass.value.trim()); + return Chip( + label: Text( + e.name, + style: TextStyle( + color: checked + ? const Color(0xFF0A9471) + : Color.fromARGB(255, 198, 86, 157)), + ), + backgroundColor: checked + ? const Color(0xFFD0F7ED) + : Color.fromARGB(255, 247, 205, 232)); + }).toList(), + )) ], ), ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 15f78daeb..66ef83d31 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -8,7 +8,6 @@ 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/desktop/widgets/login.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:get/get.dart'; @@ -18,8 +17,9 @@ 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 _kTabWidth = 200; const double _kTabHeight = 42; const double _kCardFixedWidth = 540; const double _kCardLeftMargin = 15; @@ -33,6 +33,7 @@ const double _kContentFontSize = 15; const Color _accentColor = MyTheme.accent; const String _kSettingPageControllerTag = 'settingPageController'; const String _kSettingPageIndexTag = 'settingPageIndex'; +const int _kPageCount = 6; class _TabInfo { late final String label; @@ -51,7 +52,7 @@ class DesktopSettingPage extends StatefulWidget { State createState() => _DesktopSettingPageState(); static void switch2page(int page) { - if (page >= 5) return; + if (page >= _kPageCount) return; try { if (Get.isRegistered(tag: _kSettingPageControllerTag)) { DesktopTabPage.onAddSetting(initialPage: page); @@ -75,6 +76,7 @@ class _DesktopSettingPageState extends State _TabInfo('Security', Icons.enhanced_encryption_outlined, Icons.enhanced_encryption), _TabInfo('Network', Icons.link_outlined, Icons.link), + _TabInfo('Display', Icons.desktop_windows_outlined, Icons.desktop_windows), _TabInfo('Account', Icons.person_outline, Icons.person), _TabInfo('About', Icons.info_outline, Icons.info) ]; @@ -88,7 +90,8 @@ class _DesktopSettingPageState extends State @override void initState() { super.initState(); - selectedIndex = (widget.initialPage < 5 ? widget.initialPage : 0).obs; + selectedIndex = + (widget.initialPage < _kPageCount ? widget.initialPage : 0).obs; Get.put(selectedIndex, tag: _kSettingPageIndexTag); controller = PageController(initialPage: widget.initialPage); Get.put(controller, tag: _kSettingPageControllerTag); @@ -105,7 +108,7 @@ class _DesktopSettingPageState extends State Widget build(BuildContext context) { super.build(context); return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: Row( children: [ SizedBox( @@ -117,7 +120,7 @@ class _DesktopSettingPageState extends State ], ), ), - const VerticalDivider(thickness: 1, width: 1), + const VerticalDivider(width: 1), Expanded( child: Container( color: Theme.of(context).scaffoldBackgroundColor, @@ -125,11 +128,12 @@ class _DesktopSettingPageState extends State scrollController: controller, child: PageView( controller: controller, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: const [ _General(), _Safety(), _Network(), + _Display(), _Account(), _About(), ], @@ -166,7 +170,7 @@ class _DesktopSettingPageState extends State return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: tabs .asMap() @@ -230,7 +234,7 @@ class _GeneralState extends State<_General> { return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ theme(), @@ -274,6 +278,15 @@ class _GeneralState extends State<_General> { _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', + ), + ) ]); } @@ -306,7 +319,7 @@ class _GeneralState extends State<_General> { bind.mainSetOption(key: 'audio-input', value: device); } - return _futureBuilder(future: () async { + return futureBuilder(future: () async { List devices = (await bind.mainGetSoundInputs()).toList(); if (Platform.isWindows) { devices.insert(0, 'System Sound'); @@ -333,7 +346,7 @@ class _GeneralState extends State<_General> { } Widget record(BuildContext context) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { String customDirectory = await bind.mainGetOption(key: 'video-save-directory'); String defaultDirectory = await bind.mainDefaultVideoSaveDirectory(); @@ -368,8 +381,13 @@ class _GeneralState extends State<_General> { ), ElevatedButton( onPressed: () async { + String? initialDirectory; + if (await Directory.fromUri(Uri.directory(dir)) + .exists()) { + initialDirectory = dir; + } String? selectedDirectory = await FilePicker.platform - .getDirectoryPath(initialDirectory: dir); + .getDirectoryPath(initialDirectory: initialDirectory); if (selectedDirectory != null) { await bind.mainSetOption( key: 'video-save-directory', @@ -386,7 +404,7 @@ class _GeneralState extends State<_General> { } Widget language() { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { String langs = await bind.mainGetLangs(); String lang = bind.mainGetLocalOption(key: kCommConfKeyLang); return {'langs': langs, 'lang': lang}; @@ -443,7 +461,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return DesktopScrollWrapper( scrollController: scrollController, child: SingleChildScrollView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, child: Column( children: [ @@ -474,7 +492,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget _permissions(context, bool stopService) { bool enabled = !locked; - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOption(key: 'access-mode'); }(), hasData: (data) { String accessMode = data! as String; @@ -525,6 +543,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { translate('Screen Share'), translate('Deny remote access'), ], + enabled: enabled, initialKey: initialKey, onChanged: (mode) async { String modeValue; @@ -637,7 +656,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { context, onChanged != null)), ), ], - ).paddingSymmetric(horizontal: 10), + ).paddingOnly(right: 10), onTap: () => onChanged?.call(value), )) .toList(); @@ -654,6 +673,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return _Card(title: 'Password', children: [ _ComboBox( + enabled: !locked, keys: modeKeys, values: modeValues, initialKey: modeInitialKey, @@ -662,6 +682,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { if (usePassword) radios[0], if (usePassword) _SubLabeledWidget( + context, 'One-time password length', Row( children: [ @@ -688,6 +709,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { child: _OptionCheckBox(context, 'Enable RDP', 'enable-rdp', enabled: enabled), ), + shareRdp(context, enabled), _OptionCheckBox(context, 'Deny LAN Discovery', 'enable-lan-discovery', reverse: true, enabled: enabled), ...directIp(context), @@ -695,6 +717,33 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ]); } + shareRdp(BuildContext context, bool enabled) { + onChanged(bool b) async { + await bind.mainSetShareRdp(enable: b); + setState(() {}); + } + + bool value = bind.mainIsShareRdp(); + return Offstage( + offstage: !(Platform.isWindows && bind.mainIsRdpServiceOpen()), + child: GestureDetector( + child: Row( + children: [ + Checkbox( + value: value, + onChanged: enabled ? (_) => onChanged(!value) : null) + .marginOnly(right: 5), + Expanded( + child: Text(translate('Enable RDP session sharing'), + style: + TextStyle(color: _disabledTextColor(context, enabled))), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: enabled ? () => onChanged(!value) : null), + ); + } + List directIp(BuildContext context) { TextEditingController controller = TextEditingController(); update() => setState(() {}); @@ -702,7 +751,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { return [ _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', update: update, enabled: !locked), - _futureBuilder( + futureBuilder( future: () async { String enabled = await bind.mainGetOption(key: 'direct-server'); String port = await bind.mainGetOption(key: 'direct-access-port'); @@ -715,11 +764,12 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { controller.text = data['port'].toString(); return Offstage( offstage: !enabled, - child: Row(children: [ - _SubLabeledWidget( - 'Port', + child: _SubLabeledWidget( + context, + 'Port', + Row(children: [ SizedBox( - width: 80, + width: 95, child: TextField( controller: controller, enabled: enabled && !locked, @@ -728,31 +778,29 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { 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: InputBorder.none, - contentPadding: EdgeInsets.only(right: 5), - isCollapsed: true, + contentPadding: + EdgeInsets.symmetric(vertical: 12, horizontal: 12), ), - ), + ).marginOnly(right: 15), ), - enabled: enabled && !locked, - ).marginOnly(left: 5), - Obx(() => ElevatedButton( - onPressed: applyEnabled.value && enabled && !locked - ? () async { - applyEnabled.value = false; - await bind.mainSetOption( - key: 'direct-access-port', - value: controller.text); - } - : null, - child: Text( - translate('Apply'), - ), - ).marginOnly(left: 20)) - ]), + Obx(() => ElevatedButton( + onPressed: applyEnabled.value && enabled && !locked + ? () async { + applyEnabled.value = false; + await bind.mainSetOption( + key: 'direct-access-port', + value: controller.text); + } + : null, + child: Text( + translate('Apply'), + ), + )) + ]), + enabled: enabled && !locked, + ), ); }, ), @@ -761,7 +809,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { Widget whitelist() { bool enabled = !locked; - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOption(key: 'whitelist'); }(), hasData: (data) { RxBool hasWhitelist = (data as String).isNotEmpty.obs; @@ -867,7 +915,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { scrollController: scrollController, child: ListView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), children: [ _lock(locked, 'Unlock Network Settings', () { locked = false; @@ -887,7 +935,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } server(bool enabled) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { return await bind.mainGetOptions(); }(), hasData: (data) { // Setting page is not modal, oldOptions should only be used when getting options, never when setting. @@ -1030,7 +1078,7 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { Row( mainAxisAlignment: MainAxisAlignment.end, children: [_Button('Apply', submit, enabled: enabled)], - ).marginOnly(top: 15), + ).marginOnly(top: 10), ], ) ]); @@ -1038,6 +1086,249 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } } +class _Display extends StatefulWidget { + const _Display({Key? key}) : super(key: key); + + @override + State<_Display> createState() => _DisplayState(); +} + +class _DisplayState extends State<_Display> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return DesktopScrollWrapper( + scrollController: scrollController, + child: ListView( + controller: scrollController, + physics: DraggableNeverScrollableScrollPhysics(), + children: [ + viewStyle(context), + scrollStyle(context), + imageQuality(context), + codec(context), + other(context), + ]).marginOnly(bottom: _kListViewBottomMargin)); + } + + Widget viewStyle(BuildContext context) { + final key = 'view_style'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + return _Card(title: 'Default View Style', children: [ + _Radio(context, + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + label: 'Scale original', + onChanged: onChanged), + _Radio(context, + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + label: 'Scale adaptive', + onChanged: onChanged), + ]); + } + + Widget scrollStyle(BuildContext context) { + final key = 'scroll_style'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + return _Card(title: 'Default Scroll Style', children: [ + _Radio(context, + value: kRemoteScrollStyleAuto, + groupValue: groupValue, + label: 'ScrollAuto', + onChanged: onChanged), + _Radio(context, + value: kRemoteScrollStyleBar, + groupValue: groupValue, + label: 'Scrollbar', + onChanged: onChanged), + ]); + } + + Widget imageQuality(BuildContext context) { + final key = 'image_quality'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + final qualityKey = 'custom_image_quality'; + final qualityValue = + (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? + 50.0) + .obs; + final fpsKey = 'custom-fps'; + final fpsValue = + (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0) + .obs; + return _Card(title: 'Default Image Quality', children: [ + _Radio(context, + value: kRemoteImageQualityBest, + groupValue: groupValue, + label: 'Good image quality', + onChanged: onChanged), + _Radio(context, + value: kRemoteImageQualityBalanced, + groupValue: groupValue, + label: 'Balanced', + onChanged: onChanged), + _Radio(context, + value: kRemoteImageQualityLow, + groupValue: groupValue, + label: 'Optimize reaction time', + onChanged: onChanged), + _Radio(context, + value: kRemoteImageQualityCustom, + groupValue: groupValue, + label: 'Custom', + onChanged: onChanged), + Offstage( + offstage: groupValue != kRemoteImageQualityCustom, + child: Column( + children: [ + Obx(() => Row( + children: [ + Slider( + value: qualityValue.value, + min: 10.0, + max: 100.0, + divisions: 18, + onChanged: (double value) async { + qualityValue.value = value; + await bind.mainSetUserDefaultOption( + key: qualityKey, value: value.toString()); + }, + ), + SizedBox( + width: 40, + child: Text( + '${qualityValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )) + ], + )), + Obx(() => Row( + children: [ + Slider( + value: fpsValue.value, + min: 10.0, + max: 120.0, + divisions: 22, + onChanged: (double value) async { + fpsValue.value = value; + await bind.mainSetUserDefaultOption( + key: fpsKey, value: value.toString()); + }, + ), + SizedBox( + width: 40, + child: Text( + '${fpsValue.value.round()}', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + )), + ], + ), + ) + ]); + } + + Widget codec(BuildContext context) { + if (!bind.mainHasHwcodec()) { + return Offstage(); + } + final key = 'codec-preference'; + onChanged(String value) async { + await bind.mainSetUserDefaultOption(key: key, value: value); + setState(() {}); + } + + final groupValue = bind.mainGetUserDefaultOption(key: key); + + return _Card(title: 'Default Codec', children: [ + _Radio(context, + value: 'auto', + groupValue: groupValue, + label: 'Auto', + onChanged: onChanged), + _Radio(context, + value: 'vp9', + groupValue: groupValue, + label: 'VP9', + onChanged: onChanged), + _Radio(context, + value: 'h264', + groupValue: groupValue, + label: 'H264', + onChanged: onChanged), + _Radio(context, + value: 'h265', + groupValue: groupValue, + label: 'H265', + onChanged: onChanged), + ]); + } + + Widget otherRow(String label, String key) { + final value = bind.mainGetUserDefaultOption(key: key) == 'Y'; + onChanged(bool b) async { + await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : ''); + setState(() {}); + } + + return GestureDetector( + child: Row( + children: [ + Checkbox(value: value, onChanged: (_) => onChanged(!value)) + .marginOnly(right: 5), + Expanded( + child: Text(translate(label)), + ) + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: () => onChanged(!value)); + } + + Widget other(BuildContext context) { + return _Card(title: 'Other Default Options', children: [ + otherRow('View Mode', 'view_only'), + otherRow('show_monitors_tip', 'show_monitors_toolbar'), + 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); @@ -1052,7 +1343,7 @@ class _AccountState extends State<_Account> { return DesktopScrollWrapper( scrollController: scrollController, child: ListView( - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), controller: scrollController, children: [ _Card(title: 'Account', children: [accountAction()]), @@ -1081,7 +1372,7 @@ class _About extends StatefulWidget { class _AboutState extends State<_About> { @override Widget build(BuildContext context) { - return _futureBuilder(future: () async { + return futureBuilder(future: () async { final license = await bind.mainGetLicense(); final version = await bind.mainGetVersion(); final buildDate = await bind.mainGetBuildDate(); @@ -1096,7 +1387,7 @@ class _AboutState extends State<_About> { scrollController: scrollController, child: SingleChildScrollView( controller: scrollController, - physics: NeverScrollableScrollPhysics(), + physics: DraggableNeverScrollableScrollPhysics(), child: _Card(title: '${translate('About')} RustDesk', children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1104,10 +1395,12 @@ class _AboutState extends State<_About> { const SizedBox( height: 8.0, ), - Text('${translate('Version')}: $version') - .marginSymmetric(vertical: 4.0), - Text('${translate('Build Date')}: $buildDate') - .marginSymmetric(vertical: 4.0), + SelectionArea( + child: Text('${translate('Version')}: $version') + .marginSymmetric(vertical: 4.0)), + SelectionArea( + child: Text('${translate('Build Date')}: $buildDate') + .marginSymmetric(vertical: 4.0)), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); @@ -1128,7 +1421,8 @@ class _AboutState extends State<_About> { decoration: const BoxDecoration(color: Color(0xFF2c8cff)), padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 8), - child: Row( + child: SelectionArea( + child: Row( children: [ Expanded( child: Column( @@ -1148,7 +1442,7 @@ class _AboutState extends State<_About> { ), ), ], - ), + )), ).marginSymmetric(vertical: 4.0) ], ).marginOnly(left: _kContentHMargin) @@ -1212,7 +1506,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, bool enabled = true, Icon? checkedIcon, bool? fakeValue}) { - return _futureBuilder( + return futureBuilder( future: bind.mainGetOption(key: key), hasData: (data) { bool value = option2bool(key, data.toString()); @@ -1223,7 +1517,7 @@ Widget _OptionCheckBox(BuildContext context, String label, String key, ref.value = option; if (reverse) option = !option; String value = bool2option(key, option); - bind.mainSetOption(key: key, value: value); + await bind.mainSetOption(key: key, value: value); update?.call(); } } @@ -1329,63 +1623,22 @@ Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { } // ignore: non_constant_identifier_names -Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { - RxBool hover = false.obs; +Widget _SubLabeledWidget(BuildContext context, String label, Widget child, + {bool enabled = true}) { return Row( children: [ - MouseRegion( - onEnter: (_) => hover.value = true, - onExit: (_) => hover.value = false, - child: Obx( - () { - return Container( - height: 32, - decoration: BoxDecoration( - border: Border.all( - color: hover.value && enabled - ? const Color(0xFFD7D7D7) - : const Color(0xFFCBCBCB), - width: hover.value && enabled ? 2 : 1)), - child: Row( - children: [ - Container( - height: 28, - color: (hover.value && enabled) - ? const Color(0xFFD7D7D7) - : const Color(0xFFCBCBCB), - alignment: Alignment.center, - padding: const EdgeInsets.symmetric( - horizontal: 5, vertical: 2), - child: Text( - '${translate(label)}: ', - style: const TextStyle(fontWeight: FontWeight.w300), - ), - ).paddingAll(2), - child, - ], - )); - }, - )), + Text( + '${translate(label)}: ', + style: TextStyle(color: _disabledTextColor(context, enabled)), + ), + SizedBox( + width: 10, + ), + child, ], ).marginOnly(left: _kContentHSubMargin); } -Widget _futureBuilder( - {required Future? future, required Widget Function(dynamic data) hasData}) { - return FutureBuilder( - future: future, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.hasData) { - return hasData(snapshot.data!); - } else { - if (snapshot.hasError) { - debugPrint(snapshot.error.toString()); - } - return Container(); - } - }); -} - Widget _lock( bool locked, String label, @@ -1427,25 +1680,22 @@ Widget _lock( _LabeledTextField( BuildContext context, - String lable, + String label, TextEditingController controller, String errorText, bool enabled, bool secure) { return Row( children: [ - Spacer(flex: 1), + ConstrainedBox( + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate(label)}:', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 16, color: _disabledTextColor(context, enabled)), + ).marginOnly(right: 10)), Expanded( - flex: 4, - child: Text( - '${translate(lable)}:', - textAlign: TextAlign.right, - style: TextStyle(color: _disabledTextColor(context, enabled)), - ), - ), - Spacer(flex: 1), - Expanded( - flex: 10, child: TextField( controller: controller, enabled: enabled, @@ -1456,9 +1706,8 @@ _LabeledTextField( color: _disabledTextColor(context, enabled), )), ), - Spacer(flex: 1), ], - ); + ).marginOnly(bottom: 8); } // ignore: must_be_immutable @@ -1476,7 +1725,6 @@ class _ComboBox extends StatelessWidget { required this.values, required this.initialKey, required this.onChanged, - // ignore: unused_element this.enabled = true, }) : super(key: key); @@ -1489,19 +1737,29 @@ class _ComboBox extends StatelessWidget { var ref = values[index].obs; current = keys[index]; return Container( - decoration: BoxDecoration(border: Border.all(color: MyTheme.border)), - height: 30, + decoration: BoxDecoration( + border: Border.all( + color: enabled + ? MyTheme.color(context).border2 ?? MyTheme.border + : MyTheme.border, + ), + borderRadius: + BorderRadius.circular(8), //border raiuds of dropdown button + ), + height: 42, // should be the height of a TextField child: Obx(() => DropdownButton( isExpanded: true, value: ref.value, elevation: 16, - underline: Container( - height: 25, - ), + underline: Container(), + style: TextStyle( + color: enabled + ? Theme.of(context).textTheme.titleMedium?.color + : _disabledTextColor(context, enabled)), icon: const Icon( Icons.expand_more_sharp, size: 20, - ), + ).marginOnly(right: 15), onChanged: enabled ? (String? newValue) { if (newValue != null && newValue != ref.value) { @@ -1518,11 +1776,11 @@ class _ComboBox extends StatelessWidget { value, style: const TextStyle(fontSize: _kContentFontSize), overflow: TextOverflow.ellipsis, - ).marginOnly(left: 5), + ).marginOnly(left: 15), ); }).toList(), )), - ); + ).marginOnly(bottom: 5); } } @@ -1545,6 +1803,7 @@ void changeSocks5Proxy() async { var proxyController = TextEditingController(text: proxy); var userController = TextEditingController(text: username); var pwdController = TextEditingController(text: password); + RxBool obscure = true.obs; var isInProgress = false; gFFI.dialogManager.show((setState, close) { @@ -1590,78 +1849,66 @@ void changeSocks5Proxy() async { Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Hostname")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Hostname")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( decoration: InputDecoration( - border: const OutlineInputBorder(), errorText: proxyMsg.isNotEmpty ? proxyMsg : null), controller: proxyController, - focusNode: FocusNode()..requestFocus(), + autofocus: true, ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Username")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Username")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( child: TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), controller: userController, ), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Row( children: [ ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Password")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), + constraints: const BoxConstraints(minWidth: 140), + child: Text( + '${translate("Password")}:', + textAlign: TextAlign.right, + ).marginOnly(right: 10)), Expanded( - child: TextField( - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - controller: pwdController, - ), + child: Obx(() => TextField( + obscureText: obscure.value, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: () => obscure.value = !obscure.value, + icon: Icon(obscure.value + ? Icons.visibility_off + : Icons.visibility))), + controller: pwdController, + )), ), ], - ), - const SizedBox( - height: 8.0, - ), + ).marginOnly(bottom: 8), Offstage( offstage: !isInProgress, child: const LinearProgressIndicator()) ], ), ), actions: [ - TextButton(onPressed: close, child: Text(translate('Cancel'))), - TextButton(onPressed: submit, child: Text(translate('OK'))), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart index 57c7fe4b8..a642e2590 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -64,24 +64,18 @@ class _DesktopTabPageState extends State { @override Widget build(BuildContext context) { final tabWidget = Container( - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: DesktopTab( - controller: tabController, - tail: ActionIcon( - message: 'Settings', - icon: IconFont.menu, - onTap: DesktopTabPage.onAddSetting, - isClose: false, - ), - )); - }) - ]), - ); - return Platform.isMacOS + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + onTap: DesktopTabPage.onAddSetting, + isClose: false, + ), + ))); + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget : Obx( () => DragToResizeArea( diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 60b22a516..602dd5171 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -2,20 +2,23 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; +import 'package:flutter_hbb/desktop/widgets/dragable_divider.dart'; +import 'package:percent_indicator/percent_indicator.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/desktop/widgets/list_search_action_listener.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/file_model.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; + import '../../consts.dart'; import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; - import '../../common.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -46,8 +49,10 @@ enum MouseFocusScope { } class FileManagerPage extends StatefulWidget { - const FileManagerPage({Key? key, required this.id}) : super(key: key); + const FileManagerPage({Key? key, required this.id, this.forceRelay}) + : super(key: key); final String id; + final bool? forceRelay; @override State createState() => _FileManagerPageState(); @@ -55,53 +60,21 @@ class FileManagerPage extends StatefulWidget { 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(); - - /// [_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 - - ScrollController getBreadCrumbScrollController(bool isLocal) { - return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; - } - - GlobalKey getLocationBarKey(bool isLocal) { - return isLocal ? _locationBarKeyLocal : _locationBarKeyRemote; - } + final _overlayKeyState = OverlayKeyState(); late FFI _ffi; FileModel get model => _ffi.fileModel; - - SelectedItems getSelectedItems(bool isLocal) { - return isLocal ? _localSelectedItems : _remoteSelectedItems; - } + JobController get jobController => model.jobController; @override void initState() { super.initState(); _ffi = FFI(); - _ffi.start(widget.id, isFileTransfer: true); + _ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { _ffi.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); @@ -111,53 +84,716 @@ class _FileManagerPageState extends State 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(() { + model.close().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 + bool get wantKeepAlive => true; + @override Widget build(BuildContext context) { super.build(context); - return Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.dialogManager.setOverlayState(Overlay.of(context)); - return ChangeNotifierProvider.value( - value: _ffi.fileModel, - child: Consumer(builder: (context, model, child) { - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Row( - children: [ - Flexible(flex: 3, child: body(isLocal: true)), - Flexible(flex: 3, child: body(isLocal: false)), - Flexible(flex: 2, child: statusList()) - ], - ), - ); - })); + return Overlay(key: _overlayKeyState.key, initialEntries: [ + OverlayEntry(builder: (_) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: Row( + children: [ + Flexible( + flex: 3, + child: dropArea(FileManagerView( + model.localController, _ffi, _mouseFocusScope))), + Flexible( + flex: 3, + child: dropArea(FileManagerView( + model.remoteController, _ffi, _mouseFocusScope))), + Flexible(flex: 2, child: statusList()) + ], + ), + ); }) ]); } + Widget dropArea(FileManagerView fileView) { + return DropTarget( + onDragDone: (detail) => + handleDragDone(detail, fileView.controller.isLocal), + onDragEntered: (enter) { + _dropMaskVisible.value = true; + }, + onDragExited: (exit) { + _dropMaskVisible.value = false; + }, + child: fileView); + } + + Widget generateCard(Widget child) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(15.0), + ), + ), + child: child, + ); + } + + /// transfer status list + /// watch transfer status + Widget statusList() { + statusListView(List jobs) => ListView.builder( + controller: ScrollController(), + itemBuilder: (BuildContext context, int index) { + final item = jobs[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 5), + child: generateCard( + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Transform.rotate( + angle: item.isRemoteToLocal ? 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.fileName, + 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: () { + jobController.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: () { + jobController.jobTable.removeAt(index); + jobController.cancelJob(item.id); + }, + color: MyTheme.accent, + hoverColor: MyTheme.accent80, + ), + ], + ), + ], + ), + ], + ).paddingSymmetric(vertical: 10), + ), + ); + }, + itemCount: jobController.jobTable.length, + ); + + return PreferredSize( + preferredSize: const Size(200, double.infinity), + child: Container( + margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), + padding: const EdgeInsets.all(8.0), + child: Obx( + () => jobController.jobTable.isEmpty + ? generateCard( + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + "assets/transfer.svg", + color: Theme.of(context).tabBarTheme.labelColor, + height: 40, + ).paddingOnly(bottom: 10), + Text( + translate("No transfers in progress"), + textAlign: TextAlign.center, + textScaleFactor: 1.20, + style: TextStyle( + color: + Theme.of(context).tabBarTheme.labelColor), + ), + ], + ), + ), + ) + : statusListView(jobController.jobTable), + )), + ); + } + + void handleDragDone(DropDoneDetails details, bool isLocal) { + if (isLocal) { + // ignore local + return; + } + final items = SelectedItems(isLocal: false); + for (var file in details.files) { + final f = File(file.path); + items.add(Entry() + ..path = file.path + ..name = file.name + ..size = FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync()); + } + final otherSideData = model.localController.directoryData(); + model.remoteController.sendFiles(items, otherSideData); + } +} + +class FileManagerView extends StatefulWidget { + final FileController controller; + final FFI _ffi; + final Rx _mouseFocusScope; + + FileManagerView(this.controller, this._ffi, this._mouseFocusScope); + + @override + State createState() => _FileManagerViewState(); +} + +class _FileManagerViewState extends State { + final _locationStatus = LocationStatus.bread.obs; + final _locationNode = FocusNode(); + final _locationBarKey = GlobalKey(); + final _searchText = "".obs; + final _breadCrumbScroller = ScrollController(); + final _keyboardNode = FocusNode(); + final _listSearchBuffer = TimeoutStringBuffer(); + final _nameColWidth = kDesktopFileTransferNameColWidth.obs; + final _modifiedColWidth = kDesktopFileTransferModifiedColWidth.obs; + final _fileListScrollController = ScrollController(); + + /// [_lastClickTime], [_lastClickEntry] help to handle double click + var _lastClickTime = + DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000; + Entry? _lastClickEntry; + + FileController get controller => widget.controller; + bool get isLocal => widget.controller.isLocal; + FFI get _ffi => widget._ffi; + SelectedItems get selectedItems => controller.selectedItems; + + @override + void initState() { + super.initState(); + // register location listener + _locationNode.addListener(onLocationFocusChanged); + controller.directory.listen((e) => breadCrumbScrollToEnd()); + } + + @override + void dispose() { + _locationNode.removeListener(onLocationFocusChanged); + _locationNode.dispose(); + _keyboardNode.dispose(); + _breadCrumbScroller.dispose(); + _fileListScrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + headTools(), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: MouseRegion( + onEnter: (evt) { + widget._mouseFocusScope.value = isLocal + ? MouseFocusScope.local + : MouseFocusScope.remote; + _keyboardNode.requestFocus(); + }, + onExit: (evt) => + widget._mouseFocusScope.value = MouseFocusScope.none, + child: _buildFileList(context, _fileListScrollController), + )) + ], + ), + ), + ], + ), + ); + } + + void onLocationFocusChanged() { + debugPrint("focus changed on local"); + if (_locationNode.hasFocus) { + // ignore + } else { + // lost focus, change to bread + if (_locationStatus.value != LocationStatus.fileSearchBar) { + _locationStatus.value = LocationStatus.bread; + } + } + } + + Widget headTools() { + 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(); + controller.goBack(); + }, + ), + 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(); + controller.goToParentDirectory(); + }, + ), + ], + ), + 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) { + _locationNode.requestFocus(); + } + }); + }, + child: Obx( + () => Container( + child: Row( + children: [ + Expanded( + child: _locationStatus.value == + LocationStatus.bread + ? buildBread() + : buildPathLocation()), + ], + ), + ), + ), + ), + ), + ), + ), + ), + Obx(() { + switch (_locationStatus.value) { + case LocationStatus.bread: + return MenuButton( + onPressed: () { + _locationStatus.value = LocationStatus.fileSearchBar; + Future.delayed( + Duration.zero, () => _locationNode.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: () { + controller.refresh(); + }, + 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: () { + controller.goToHomeDirectory(); + }, + 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) { + controller.createDir(PathUtil.join( + controller.directory.value.path, + name.value.text, + controller.options.value.isWindows, + )); + close(); + } + } + + cancel() => close(false); + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset("assets/folder_new.svg", + color: MyTheme.accent), + Text( + translate("Create Folder"), + ).paddingOnly( + left: 10, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name", + ), + ), + controller: name, + autofocus: true, + ), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "Ok", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }); + }, + child: SvgPicture.asset( + "assets/folder_new.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + color: Theme.of(context).cardColor, + hoverColor: Theme.of(context).hoverColor, + ), + Obx(() => MenuButton( + onPressed: SelectedItems.valid(selectedItems.items) + ? () async { + await (controller + .removeAction(selectedItems)); + 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), + ], + ), + ), + Obx(() => ElevatedButton.icon( + style: ButtonStyle( + padding: MaterialStateProperty.all( + isLocal + ? EdgeInsets.only(left: 10) + : EdgeInsets.only(right: 10)), + backgroundColor: MaterialStateProperty.all( + selectedItems.items.isEmpty + ? MyTheme.accent80 + : MyTheme.accent, + ), + ), + onPressed: SelectedItems.valid(selectedItems.items) + ? () { + final otherSideData = + controller.getOtherSideDirectoryData(); + controller.sendFiles(selectedItems, otherSideData); + selectedItems.clear(); + } + : null, + icon: isLocal + ? Text( + translate('Send'), + textAlign: TextAlign.right, + style: TextStyle( + color: selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white, + ), + ) + : RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + "assets/arrow.svg", + color: selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white, + alignment: Alignment.bottomRight, + ), + ), + label: isLocal + ? SvgPicture.asset( + "assets/arrow.svg", + color: selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white, + ) + : Text( + translate('Receive'), + style: TextStyle( + color: selectedItems.items.isEmpty + ? Theme.of(context).brightness == + Brightness.light + ? MyTheme.grayBg + : MyTheme.darkGray + : Colors.white, + ), + ), + )), + ], + ).marginOnly(top: 8.0) + ], + ), + ); + } + Widget menu({bool isLocal = false}) { var menuPos = RelativeRect.fill; @@ -166,305 +802,276 @@ class _FileManagerPageState extends State switchType: SwitchType.scheckbox, text: translate("Show Hidden Files"), getter: () async { - return model.getCurrentShowHidden(isLocal); + return controller.options.value.isWindows; }, setter: (bool v) async { - model.toggleShowHidden(local: isLocal); + controller.toggleShowHidden(); }, padding: kDesktopMenuPadding, dismissOnClicked: true, ), MenuEntryButton( childBuilder: (style) => Text(translate("Select All"), style: style), - proc: () => setState(() => getSelectedItems(isLocal) - .selectAll(model.getCurrentDir(isLocal).entries)), + proc: () => setState(() => + selectedItems.selectAll(controller.directory.value.entries)), padding: kDesktopMenuPadding, dismissOnClicked: true), MenuEntryButton( childBuilder: (style) => Text(translate("Unselect All"), style: style), - proc: () => setState(() => getSelectedItems(isLocal).clear()), + proc: () => selectedItems.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: IconButton( - icon: const Icon(Icons.more_vert), - splashRadius: kDesktopIconButtonSplashRadius, - onPressed: () => mod_menu.showMenu( - context: context, - position: menuPos, - items: items - .map((e) => e.build( - context, - MenuConfig( - commonColor: CustomPopupMenuTheme.commonColor, - height: CustomPopupMenuTheme.height, - dividerHeight: CustomPopupMenuTheme.dividerHeight))) - .expand((i) => i) - .toList(), - elevation: 8, - ), - )); - } - - Widget body({bool isLocal = false}) { - final scrollController = ScrollController(); - return Container( - decoration: BoxDecoration(border: Border.all(color: Colors.black26)), - margin: const EdgeInsets.all(16.0), - padding: const EdgeInsets.all(8.0), - child: DropTarget( - 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: SingleChildScrollView( - controller: scrollController, - child: _buildDataTable(context, isLocal, scrollController), + 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 _buildDataTable( - BuildContext context, bool isLocal, ScrollController scrollController) { - const rowHeight = 25.0; - final fd = model.getCurrentDir(isLocal); + Widget _buildFileList( + BuildContext context, ScrollController scrollController) { + final fd = controller.directory.value; final entries = fd.entries; - final sortIndex = (SortBy style) { - switch (style) { - case SortBy.name: - return 0; - case SortBy.type: - return 0; - case SortBy.modified: - return 1; - case SortBy.size: - return 2; - } - }(model.getSortStyle(isLocal)); - final sortAscending = - isLocal ? model.localSortAscending : model.remoteSortAscending; - return MouseRegion( - onEnter: (evt) { - _mouseFocusScope.value = - isLocal ? MouseFocusScope.local : MouseFocusScope.remote; - if (isLocal) { - _keyboardNodeLocal.requestFocus(); - } else { - _keyboardNodeRemote.requestFocus(); + return ListSearchActionListener( + node: _keyboardNode, + buffer: _listSearchBuffer, + onNext: (buffer) { + debugPrint("searching next for $buffer"); + assert(buffer.length == 1); + assert(selectedItems.items.length <= 1); + var skipCount = 0; + if (selectedItems.items.isNotEmpty) { + final index = entries.indexOf(selectedItems.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) { + selectedItems.clear(); + return; + } + _jumpToEntry(isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight); }, - onExit: (evt) { - _mouseFocusScope.value = MouseFocusScope.none; + onSearch: (buffer) { + debugPrint("searching for $buffer"); + final selectedEntries = selectedItems; + final searchResult = entries + .where((element) => element.name.toLowerCase().startsWith(buffer)); + selectedEntries.clear(); + if (searchResult.isEmpty) { + selectedItems.clear(); + return; + } + _jumpToEntry(isLocal, searchResult.first, scrollController, + kDesktopFileTransferRowHeight); }, - child: ListSearchActionListener( - node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote, - buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote, - onNext: (buffer) { - debugPrint("searching next for $buffer"); - assert(buffer.length == 1); - final selectedEntries = getSelectedItems(isLocal); - assert(selectedEntries.length <= 1); - var skipCount = 0; - if (selectedEntries.items.isNotEmpty) { - final index = entries.indexOf(selectedEntries.items.first); - if (index < 0) { - return; - } - skipCount = index + 1; - } - var searchResult = entries - .skip(skipCount) - .where((element) => element.name.startsWith(buffer)); - if (searchResult.isEmpty) { - // cannot find next, lets restart search from head - searchResult = - entries.where((element) => element.name.startsWith(buffer)); - } - if (searchResult.isEmpty) { - setState(() { - getSelectedItems(isLocal).clear(); - }); - return; - } - _jumpToEntry( - isLocal, searchResult.first, scrollController, rowHeight, buffer); - }, - onSearch: (buffer) { - debugPrint("searching for $buffer"); - final selectedEntries = getSelectedItems(isLocal); - final searchResult = - entries.where((element) => element.name.startsWith(buffer)); - selectedEntries.clear(); - if (searchResult.isEmpty) { - setState(() { - getSelectedItems(isLocal).clear(); - }); - return; - } - _jumpToEntry( - isLocal, searchResult.first, scrollController, rowHeight, buffer); - }, - child: ObxValue( - (searchText) { - final filteredEntries = searchText.isNotEmpty - ? entries.where((element) { - return element.name.contains(searchText.value); - }).toList(growable: false) - : entries; - return DataTable( - key: ValueKey(isLocal ? 0 : 1), - showCheckboxColumn: false, - dataRowHeight: rowHeight, - headingRowHeight: 30, - horizontalMargin: 8, - columnSpacing: 8, - showBottomBorder: true, - sortColumnIndex: sortIndex, - sortAscending: sortAscending, - columns: [ - DataColumn( - label: Text( - translate("Name"), - ).marginSymmetric(horizontal: 4), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.name, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text( - translate("Modified"), - ), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.modified, - isLocal: isLocal, ascending: ascending); - }), - DataColumn( - label: Text(translate("Size")), - onSort: (columnIndex, ascending) { - model.changeSortStyle(SortBy.size, - isLocal: isLocal, ascending: ascending); - }), - ], - rows: filteredEntries.map((entry) { - final sizeStr = - entry.isFile ? readableFileSize(entry.size.toDouble()) : ""; - final lastModifiedStr = entry.isDrive - ? " " - : "${entry.lastModified().toString().replaceAll(".000", "")} "; - return DataRow( - key: ValueKey(entry.name), - onSelectChanged: (s) { - _onSelectedChanged(getSelectedItems(isLocal), - filteredEntries, entry, isLocal); - }, - selected: getSelectedItems(isLocal).contains(entry), - cells: [ - DataCell( - Container( - width: 200, - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: entry.name, - child: Row(children: [ - entry.isDrive - ? Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7)) - .paddingAll(4) - : Icon( - entry.isFile - ? Icons.feed_outlined - : Icons.folder, - size: 20, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7), - ).marginSymmetric(horizontal: 2), - Expanded( - child: Text(entry.name, - overflow: TextOverflow.ellipsis)) - ]), - )), - onTap: () { - final items = getSelectedItems(isLocal); - - // handle double click - if (_checkDoubleClick(entry)) { - openDirectory(entry.path, isLocal: isLocal); - items.clear(); - return; - } - _onSelectedChanged( - items, filteredEntries, entry, isLocal); - }, + child: Obx(() { + final entries = controller.directory.value.entries; + 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", "")} "; + return Padding( + padding: EdgeInsets.symmetric(vertical: 1), + child: Obx(() => Container( + decoration: BoxDecoration( + color: selectedItems.items.contains(entry) + ? 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: _nameColWidth.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 = selectedItems; + // handle double click + if (_checkDoubleClick(entry)) { + controller.openDirectory(entry.path); + items.clear(); + return; + } + _onSelectedChanged( + items, filteredEntries, entry, isLocal); + }, + ), + SizedBox( + width: 2.0, + ), + GestureDetector( + child: Obx( + () => SizedBox( + width: _modifiedColWidth.value, + child: Tooltip( + waitDuration: Duration(milliseconds: 500), + message: lastModifiedStr, + child: Text( + lastModifiedStr, + overflow: TextOverflow.ellipsis, + 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), + ), + ), + ), + ), + ], + ), ), - DataCell(FittedBox( - child: Tooltip( - waitDuration: Duration(milliseconds: 500), - message: lastModifiedStr, - child: Text( - lastModifiedStr, - style: TextStyle( - fontSize: 12, color: MyTheme.darkGray), - )))), - DataCell(Tooltip( - waitDuration: Duration(milliseconds: 500), - message: sizeStr, - child: Text( - sizeStr, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10, color: MyTheme.darkGray), - ))), - ]); - }).toList(growable: false), - ); - }, - isLocal ? _searchTextLocal : _searchTextRemote, - ), - ), + ), + ], + ))), + ); + }).toList(growable: false); + + return Column( + children: [ + // Header + Row( + children: [ + Expanded(child: _buildFileBrowserHeader(context)), + ], + ), + // Body + Expanded( + child: ListView.builder( + controller: scrollController, + itemExtent: kDesktopFileTransferRowHeight, + itemBuilder: (context, index) { + return rows[index]; + }, + itemCount: rows.length, + ), + ), + ], + ); + }), ); } + onSearchText(String searchText, bool isLocal) { + selectedItems.clear(); + _searchText.value = searchText; + } + void _jumpToEntry(bool isLocal, Entry entry, - ScrollController scrollController, double rowHeight, String buffer) { - final entries = model.getCurrentDir(isLocal).entries; + ScrollController scrollController, double rowHeight) { + final entries = controller.directory.value.entries; final index = entries.indexOf(entry); if (index == -1) { debugPrint("entry is not valid: ${entry.path}"); } - final selectedEntries = getSelectedItems(isLocal); - final searchResult = - entries.where((element) => element.name.startsWith(buffer)); + final selectedEntries = selectedItems; + final searchResult = entries.where((element) => element == entry); selectedEntries.clear(); if (searchResult.isEmpty) { return; @@ -474,10 +1081,8 @@ class _FileManagerPageState extends State entries.indexOf(searchResult.first) * rowHeight), scrollController.position.maxScrollExtent); scrollController.jumpTo(offset); - setState(() { - selectedEntries.add(isLocal, searchResult.first); - debugPrint("focused on ${searchResult.first.name}"); - }); + selectedEntries.add(searchResult.first); + debugPrint("focused on ${searchResult.first.name}"); } void _onSelectedChanged(SelectedItems selectedItems, List entries, @@ -487,10 +1092,10 @@ class _FileManagerPageState extends State final isShiftDown = RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft); if (isCtrlDown) { - if (selectedItems.contains(entry)) { + if (selectedItems.items.contains(entry)) { selectedItems.remove(entry); } else { - selectedItems.add(isLocal, entry); + selectedItems.add(entry); } } else if (isShiftDown) { final List indexGroup = []; @@ -504,10 +1109,10 @@ class _FileManagerPageState extends State selectedItems.clear(); entries .getRange(minIndex, maxIndex + 1) - .forEach((e) => selectedItems.add(isLocal, e)); + .forEach((e) => selectedItems.add(e)); } else { selectedItems.clear(); - selectedItems.add(isLocal, entry); + selectedItems.add(entry); } setState(() {}); } @@ -526,409 +1131,134 @@ class _FileManagerPageState extends State return false; } - /// transfer status list - /// watch transfer status - Widget statusList() { - return PreferredSize( - preferredSize: const Size(200, double.infinity), - child: Container( - margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration(border: Border.all(color: Colors.grey)), - child: Obx( - () => ListView.builder( - controller: ScrollController(), - itemBuilder: (BuildContext context, int index) { - final item = model.jobTable[index]; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Transform.rotate( - angle: item.isRemote ? pi : 0, - child: const Icon(Icons.send)), - const SizedBox( - width: 16.0, - ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Tooltip( - waitDuration: Duration(milliseconds: 500), - message: item.jobName, - child: Text( - item.jobName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - )), - Wrap( - children: [ - Text( - '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '), - Text( - '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), - Offstage( - offstage: - item.state != JobState.inProgress, - child: Text( - '${"${readableFileSize(item.speed)}/s"} ')), - Offstage( - offstage: item.totalSize <= 0, - child: Text( - '${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'), - ), - ], - ), - ], - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Offstage( - offstage: item.state != JobState.paused, - child: IconButton( - onPressed: () { - model.resumeJob(item.id); - }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.restart_alt_rounded)), - ), - IconButton( - icon: const Icon(Icons.close), - splashRadius: 1, - onPressed: () { - model.jobTable.removeAt(index); - model.cancelJob(item.id); - }, - ), - ], - ) - ], - ), - SizedBox( - height: 8.0, - ), - Divider( - height: 2.0, - ) - ], - ); - }, - itemCount: model.jobTable.length, - ), + Widget _buildFileBrowserHeader(BuildContext context) { + final padding = EdgeInsets.all(1.0); + return SizedBox( + height: kDesktopFileTransferHeaderHeight, + child: Row( + children: [ + Obx( + () => headerItemFunc( + _nameColWidth.value, SortBy.name, translate("Name")), ), - )); - } - - 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(color: Colors.blue), - padding: EdgeInsets.all(8.0), - child: FutureBuilder( - future: bind.sessionGetPlatform( - id: _ffi.id, isRemote: !isLocal), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { - return getPlatformImage('${snapshot.data}'); - } else { - return CircularProgressIndicator( - color: Colors.white, - ); - } - })), - Text(isLocal - ? translate("Local Computer") - : translate("Remote Computer")) - .marginOnly(left: 8.0) - ], - ), - preferredSize: Size(double.infinity, 70)), - // buttons - Row( - children: [ - Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back), - splashRadius: kDesktopIconButtonSplashRadius, - onPressed: () { - selectedItems.clear(); - model.goBack(isLocal: isLocal); - }, - ), - IconButton( - icon: const Icon(Icons.arrow_upward), - splashRadius: kDesktopIconButtonSplashRadius, - onPressed: () { - selectedItems.clear(); - model.goToParentDirectory(isLocal: isLocal); - }, - ), - ], - ), - Expanded( - child: GestureDetector( - onTap: () { - locationStatus.value = - locationStatus.value == LocationStatus.bread - ? LocationStatus.pathLocation - : LocationStatus.bread; - Future.delayed(Duration.zero, () { - if (locationStatus.value == LocationStatus.pathLocation) { - locationFocus.requestFocus(); - } - }); + 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")), + ), + DraggableDivider( + axis: Axis.vertical, + onPointerMove: (dx) { + _modifiedColWidth.value += dx; + _modifiedColWidth.value = min( + kDesktopFileTransferMaximumWidth, + max(kDesktopFileTransferMinimumWidth, + _modifiedColWidth.value)); }, - child: Obx(() => Container( - decoration: BoxDecoration( - border: Border.all( - color: locationStatus.value == LocationStatus.bread - ? Colors.black12 - : Theme.of(context) - .colorScheme - .primary - .withOpacity(0.5))), - child: Row( - children: [ - Expanded( - child: locationStatus.value == LocationStatus.bread - ? buildBread(isLocal) - : buildPathLocation(isLocal)), - ], - ))), - )), - Obx(() { - switch (locationStatus.value) { - case LocationStatus.bread: - return IconButton( - onPressed: () { - locationStatus.value = LocationStatus.fileSearchBar; - final focusNode = - isLocal ? _locationNodeLocal : _locationNodeRemote; - Future.delayed( - Duration.zero, () => focusNode.requestFocus()); - }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: Icon(Icons.search)); - case LocationStatus.pathLocation: - return IconButton( - color: Theme.of(context).disabledColor, - onPressed: null, - splashRadius: kDesktopIconButtonSplashRadius, - icon: Icon(Icons.close)); - case LocationStatus.fileSearchBar: - return IconButton( - color: Theme.of(context).disabledColor, - onPressed: () { - onSearchText("", isLocal); - locationStatus.value = LocationStatus.bread; - }, - splashRadius: 1, - icon: Icon(Icons.close)); - } - }), - IconButton( - onPressed: () { - model.refresh(isLocal: isLocal); - }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.refresh)), - ], - ), - Row( - textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, - children: [ - Expanded( - child: Row( - mainAxisAlignment: - isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, - children: [ - IconButton( - onPressed: () { - model.goHome(isLocal: isLocal); - }, - icon: const Icon(Icons.home_outlined), - splashRadius: kDesktopIconButtonSplashRadius, - ), - IconButton( - 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(); - } - } + padding: padding), + Expanded(child: headerItemFunc(null, SortBy.size, translate("Size"))) + ], + ), + ); + } - 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, - focusNode: FocusNode()..requestFocus(), - ), - ], - ), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))) - ], - onSubmit: submit, - onCancel: cancel, - ); - }); - }, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.create_new_folder_outlined)), - IconButton( - onPressed: validItems(selectedItems) - ? () async { - await (model.removeAction(selectedItems, - isLocal: isLocal)); - selectedItems.clear(); - } - : null, - splashRadius: kDesktopIconButtonSplashRadius, - icon: const Icon(Icons.delete_forever_outlined)), - menu(isLocal: isLocal), - ], - ), - ), - TextButton.icon( - onPressed: validItems(selectedItems) - ? () { - model.sendFiles(selectedItems, isRemote: !isLocal); - selectedItems.clear(); - } - : null, - icon: Transform.rotate( - angle: isLocal ? 0 : pi, - child: const Icon( - Icons.send, - ), + Widget headerItemFunc(double? width, SortBy sortBy, String name) { + 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!; + } + controller.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()) + ], ), - label: Text( - isLocal ? translate('Send') : translate('Receive'), - )), - ], - ).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; + ), + ), () { + if (controller.sortBy.value == sortBy) { + return controller.sortAscending.obs; + } else { + return Rx(null); } - } + }()); } - 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) { + Widget buildBread() { final items = getPathBreadCrumbItems(isLocal, (list) { var path = ""; for (var item in list) { - path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); + path = PathUtil.join(path, item, controller.options.value.isWindows); } - openDirectory(path, isLocal: isLocal); + controller.openDirectory(path); }); - final locationBarKey = getLocationBarKey(isLocal); return items.isEmpty ? Offstage() : Row( - key: locationBarKey, + 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: Icon(Icons.chevron_right), - overflow: ScrollableOverflow( - controller: - getBreadCrumbScrollController(isLocal)), - ))), + child: Listener( + // handle mouse wheel + onPointerSignal: (e) { + if (e is PointerScrollEvent) { + final sc = _breadCrumbScroller; + 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: _breadCrumbScroller, + ), + ), + ), + ), ActionIcon( message: "", - icon: Icons.arrow_drop_down, + icon: Icons.keyboard_arrow_down_rounded, onTap: () async { - final renderBox = locationBarKey.currentContext + final renderBox = _locationBarKey.currentContext ?.findRenderObject() as RenderBox; - locationBarKey.currentContext?.size; + _locationBarKey.currentContext?.size; final size = renderBox.size; final offset = renderBox.localToGlobal(Offset.zero); @@ -936,17 +1266,17 @@ class _FileManagerPageState extends State final x = offset.dx; final y = offset.dy + size.height + 1; - final isPeerWindows = model.getCurrentIsWindows(isLocal); + final isPeerWindows = controller.options.value.isWindows; final List menuItems = [ MenuEntryButton( childBuilder: (TextStyle? style) => isPeerWindows - ? buildWindowsThisPC(style) + ? buildWindowsThisPC(context, style) : Text( '/', style: style, ), proc: () { - openDirectory('/', isLocal: isLocal); + controller.openDirectory('/'); }, dismissOnClicked: true), MenuEntryDivider() @@ -957,8 +1287,9 @@ class _FileManagerPageState extends State loadingTag = _ffi.dialogManager.showLoading("Waiting"); } try { - final fd = - await model.fetchDirectory("/", isLocal, isLocal); + final showHidden = controller.options.value.showHidden; + final fd = await controller.fileFetcher + .fetchDirectory("/", isLocal, showHidden); for (var entry in fd.entries) { menuItems.add(MenuEntryButton( childBuilder: (TextStyle? style) => @@ -977,11 +1308,11 @@ class _FileManagerPageState extends State ) ]), proc: () { - openDirectory('${entry.name}\\', - isLocal: isLocal); + controller.openDirectory('${entry.name}\\'); }, dismissOnClicked: true)); } + menuItems.add(MenuEntryDivider()); } catch (e) { debugPrint("buildBread fetchDirectory err=$e"); } finally { @@ -990,7 +1321,6 @@ class _FileManagerPageState extends State } } } - menuItems.add(MenuEntryDivider()); mod_menu.showMenu( context: context, position: RelativeRect.fromLTRB(x, y, x, y), @@ -1013,128 +1343,100 @@ class _FileManagerPageState extends State ]); } - 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 path = controller.directory.value.path; final breadCrumbList = List.empty(growable: true); - final isWindows = model.getCurrentIsWindows(isLocal); + final isWindows = controller.options.value.isWindows; if (isWindows && path == '/') { breadCrumbList.add(BreadCrumbItem( content: TextButton( - child: buildWindowsThisPC(), + child: buildWindowsThisPC(context), 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( + breadCrumbList.addAll( + list.asMap().entries.map( + (e) => BreadCrumbItem( + content: TextButton( child: Text(e.value), style: ButtonStyle( - minimumSize: MaterialStateProperty.all(Size(0, 0))), - onPressed: () => onPressed(list.sublist(0, e.key + 1))) - .marginSymmetric(horizontal: 4)))); + minimumSize: MaterialStateProperty.all( + Size(0, 0), + ), + ), + onPressed: () => onPressed( + list.sublist(0, e.key + 1), + ), + ).marginSymmetric(horizontal: 4), + ), + ), + ); } return breadCrumbList; } - breadCrumbScrollToEnd(bool isLocal) { + breadCrumbScrollToEnd() { Future.delayed(Duration(milliseconds: 200), () { - final breadCrumbScroller = getBreadCrumbScrollController(isLocal); - if (breadCrumbScroller.hasClients) { - breadCrumbScroller.animateTo( - breadCrumbScroller.position.maxScrollExtent, + 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; + Widget buildPathLocation() { + final text = _locationStatus.value == LocationStatus.pathLocation + ? controller.directory.value.path + : _searchText.value; final textController = TextEditingController(text: text) ..selection = TextSelection.collapsed(offset: text.length); - return Row(children: [ - Icon( - locationStatus.value == LocationStatus.pathLocation - ? Icons.folder - : Icons.search, - color: Theme.of(context).hintColor, - ).paddingSymmetric(horizontal: 2), - Expanded( + return Row( + children: [ + SvgPicture.asset( + _locationStatus.value == LocationStatus.pathLocation + ? "assets/folder.svg" + : "assets/search.svg", + color: Theme.of(context).tabBarTheme.labelColor, + ), + Expanded( child: TextField( - focusNode: focusNode, - decoration: InputDecoration( - border: InputBorder.none, - isDense: true, - prefix: Padding(padding: EdgeInsets.only(left: 4.0))), - controller: textController, - onSubmitted: (path) { - openDirectory(path, isLocal: isLocal); - }, - onChanged: locationStatus.value == LocationStatus.fileSearchBar - ? (searchText) => onSearchText(searchText, isLocal) - : null, - )) - ]); + focusNode: _locationNode, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + prefix: Padding( + padding: EdgeInsets.only(left: 4.0), + ), + ), + controller: textController, + onSubmitted: (path) { + controller.openDirectory(path); + }, + 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(); - } - }); - } + // openDirectory(String path, {bool isLocal = false}) { + // model.openDirectory(path, isLocal: isLocal); + // } +} + +Widget buildWindowsThisPC(BuildContext context, [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) + ]); } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 7e07eaa9a..39958e88e 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -31,13 +31,21 @@ class _FileManagerTabPageState extends State { _FileManagerTabPageState(Map params) { Get.put(DesktopTabController(tabType: DesktopTabType.fileTransfer)); + tabController.onSelected = (_, id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: params['id'], label: params['id'], selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, onTabCloseButton: () => () => tabController.closeBy(params['id']), - page: FileManagerPage(key: ValueKey(params['id']), id: params['id']))); + page: FileManagerPage( + key: ValueKey(params['id']), + id: params['id'], + forceRelay: params['forceRelay'], + ))); } @override @@ -60,7 +68,11 @@ class _FileManagerTabPageState extends State { selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, onTabCloseButton: () => tabController.closeBy(id), - page: FileManagerPage(key: ValueKey(id), id: id))); + page: FileManagerPage( + key: ValueKey(id), + id: id, + forceRelay: args['forceRelay'], + ))); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { @@ -78,7 +90,7 @@ class _FileManagerTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).cardColor, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, @@ -86,7 +98,7 @@ class _FileManagerTabPageState extends State { labelGetter: DesktopTab.labelGetterAlias, )), ); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget : SubWindowDragToResizeArea( child: tabWidget, diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index e7bb28813..adc0df138 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -1,7 +1,11 @@ +import 'dart:io'; + import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:window_manager/window_manager.dart'; @@ -13,10 +17,55 @@ class InstallPage extends StatefulWidget { State createState() => _InstallPageState(); } -class _InstallPageState extends State with WindowListener { +class _InstallPageState extends State { + final tabController = DesktopTabController(tabType: DesktopTabType.main); + + @override + void initState() { + super.initState(); + Get.put(tabController); + const lable = "install"; + tabController.add(TabInfo( + key: lable, + label: lable, + closable: false, + page: _InstallPageBody( + key: const ValueKey(lable), + ))); + } + + @override + void dispose() { + super.dispose(); + Get.delete(); + } + + @override + Widget build(BuildContext context) { + return DragToResizeArea( + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + child: Container( + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab(controller: tabController)), + ), + ); + } +} + +class _InstallPageBody extends StatefulWidget { + const _InstallPageBody({Key? key}) : super(key: key); + + @override + State<_InstallPageBody> createState() => _InstallPageBodyState(); +} + +class _InstallPageBodyState extends State<_InstallPageBody> + with WindowListener { late final TextEditingController controller; final RxBool startmenu = true.obs; final RxBool desktopicon = true.obs; + final RxBool driverCert = true.obs; final RxBool showProgress = false.obs; final RxBool btnEnabled = true.obs; @@ -46,15 +95,19 @@ class _InstallPageState extends State with WindowListener { final double em = 13; final btnFontSize = 0.9 * em; final double button_radius = 6; + final isDarkTheme = MyTheme.currentThemeMode() == ThemeMode.dark; final buttonStyle = OutlinedButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(button_radius)), )); final inputBorder = OutlineInputBorder( borderRadius: BorderRadius.zero, - borderSide: BorderSide(color: Colors.black12)); + borderSide: + BorderSide(color: isDarkTheme ? Colors.white70 : Colors.black12)); + final textColor = isDarkTheme ? null : Colors.black87; + final dividerColor = isDarkTheme ? Colors.white70 : Colors.black87; return Scaffold( - backgroundColor: Colors.white, + backgroundColor: null, body: SingleChildScrollView( child: Column( children: [ @@ -91,30 +144,66 @@ class _InstallPageState extends State with WindowListener { style: buttonStyle, child: Text(translate('Change Path'), style: TextStyle( - color: Colors.black87, - fontSize: btnFontSize))) + color: textColor, fontSize: btnFontSize))) .marginOnly(left: em)) ], ).marginSymmetric(vertical: 2 * em), - Row( - children: [ - Obx(() => Checkbox( - value: startmenu.value, - onChanged: (b) { - if (b != null) startmenu.value = b; - })), - Text(translate('Create start menu shortcuts')) - ], + TextButton( + onPressed: () => startmenu.value = !startmenu.value, + child: Row( + children: [ + Obx(() => Checkbox( + value: startmenu.value, + onChanged: (b) { + if (b != null) startmenu.value = b; + })), + RichText( + text: TextSpan( + text: translate('Create start menu shortcuts'), + style: DefaultTextStyle.of(context).style, + ), + ), + ], + ), ), - Row( - children: [ - Obx(() => Checkbox( - value: desktopicon.value, - onChanged: (b) { - if (b != null) desktopicon.value = b; - })), - Text(translate('Create desktop icon')) - ], + TextButton( + onPressed: () => desktopicon.value = !desktopicon.value, + child: Row( + children: [ + Obx(() => Checkbox( + value: desktopicon.value, + onChanged: (b) { + if (b != null) desktopicon.value = b; + })), + RichText( + text: TextSpan( + text: translate('Create desktop icon'), + style: DefaultTextStyle.of(context).style, + ), + ), + ], + ), + ), + Offstage( + offstage: !Platform.isWindows, + child: TextButton( + onPressed: () => driverCert.value = !driverCert.value, + child: Row( + children: [ + Obx(() => Checkbox( + value: driverCert.value, + onChanged: (b) { + if (b != null) driverCert.value = b; + })), + RichText( + text: TextSpan( + text: translate('idd_driver_tip'), + style: DefaultTextStyle.of(context).style, + ), + ), + ], + ), + ), ), GestureDetector( onTap: () => launchUrlString('http://rustdesk.com/privacy'), @@ -127,8 +216,7 @@ class _InstallPageState extends State with WindowListener { )).marginOnly(top: 2 * em), Row(children: [Text(translate('agreement_tip'))]) .marginOnly(top: em), - Divider(color: Colors.black87) - .marginSymmetric(vertical: 0.5 * em), + Divider(color: dividerColor).marginSymmetric(vertical: 0.5 * em), Row( children: [ Expanded( @@ -143,8 +231,7 @@ class _InstallPageState extends State with WindowListener { style: buttonStyle, child: Text(translate('Cancel'), style: TextStyle( - color: Colors.black87, - fontSize: btnFontSize))) + color: textColor, fontSize: btnFontSize))) .marginOnly(right: 2 * em)), Obx(() => ElevatedButton( onPressed: btnEnabled.value ? install : null, @@ -167,8 +254,7 @@ class _InstallPageState extends State with WindowListener { style: buttonStyle, child: Text(translate('Run without install'), style: TextStyle( - color: Colors.black87, - fontSize: btnFontSize))) + color: textColor, fontSize: btnFontSize))) .marginOnly(left: 2 * em)), ), ], @@ -179,12 +265,47 @@ class _InstallPageState extends State with WindowListener { } 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); + do_install() { + btnEnabled.value = false; + showProgress.value = true; + String args = ''; + if (startmenu.value) args += ' startmenu'; + if (desktopicon.value) args += ' desktopicon'; + if (driverCert.value) args += ' driverCert'; + bind.installInstallMe(options: args, path: controller.text); + } + + if (driverCert.isTrue) { + final tag = 'install-info-install-cert-confirm'; + final btns = [ + dialogButton( + 'Cancel', + onPressed: () => gFFI.dialogManager.dismissByTag(tag), + isOutline: true, + ), + dialogButton( + 'OK', + onPressed: () { + gFFI.dialogManager.dismissByTag(tag); + do_install(); + }, + isOutline: false, + ), + ]; + gFFI.dialogManager.show( + (setState, close) => CustomAlertDialog( + title: null, + content: SelectionArea( + child: + msgboxContent('info', 'Warning', 'confirm_idd_driver_tip')), + actions: btns, + onCancel: close, + ), + tag: tag, + ); + } else { + do_install(); + } } void selectInstallPath() async { diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index b2458d096..06b2cf3a7 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -11,7 +11,7 @@ import 'package:wakelock/wakelock.dart'; const double _kColumn1Width = 30; const double _kColumn4Width = 100; -const double _kRowHeight = 50; +const double _kRowHeight = 60; const double _kTextLeftMargin = 20; class _PortForward { @@ -26,10 +26,12 @@ class _PortForward { } class PortForwardPage extends StatefulWidget { - const PortForwardPage({Key? key, required this.id, required this.isRDP}) + const PortForwardPage( + {Key? key, required this.id, required this.isRDP, this.forceRelay}) : super(key: key); final String id; final bool isRDP; + final bool? forceRelay; @override State createState() => _PortForwardPageState(); @@ -47,7 +49,7 @@ class _PortForwardPageState extends State void initState() { super.initState(); _ffi = FFI(); - _ffi.start(widget.id, isPortForward: true); + _ffi.start(widget.id, isPortForward: true, forceRelay: widget.forceRelay); Get.put(_ffi, tag: 'pf_${widget.id}'); if (!Platform.isLinux) { Wakelock.enable(); @@ -89,7 +91,7 @@ class _PortForwardPageState extends State Flexible( child: Container( decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, border: Border.all(width: 1, color: MyTheme.border)), child: widget.isRDP ? buildRdp(context) : buildTunnel(context), @@ -127,12 +129,12 @@ class _PortForwardPageState extends State } buildTunnel(BuildContext context) { - text(String lable) => Expanded( - child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin)); + text(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); return Theme( data: Theme.of(context) - .copyWith(backgroundColor: Theme.of(context).backgroundColor), + .copyWith(backgroundColor: Theme.of(context).colorScheme.background), child: Obx(() => ListView.builder( controller: ScrollController(), itemCount: pfs.length + 2, @@ -167,7 +169,8 @@ class _PortForwardPageState extends State return Container( height: _kRowHeight, - decoration: BoxDecoration(color: Theme.of(context).backgroundColor), + decoration: + BoxDecoration(color: Theme.of(context).colorScheme.background), child: Row(children: [ buildTunnelInputCell(context, controller: localPortController, @@ -179,36 +182,31 @@ class _PortForwardPageState extends State buildTunnelInputCell(context, controller: remotePortController, inputFormatters: portInputFormatter), - SizedBox( - width: _kColumn4Width, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - elevation: 0, side: const BorderSide(color: MyTheme.border)), - onPressed: () async { - int? localPort = int.tryParse(localPortController.text); - int? remotePort = int.tryParse(remotePortController.text); - if (localPort != null && - remotePort != null && - (remoteHostController.text.isEmpty || - remoteHostController.text.trim().isNotEmpty)) { - await bind.sessionAddPortForward( - id: 'pf_${widget.id}', - localPort: localPort, - remoteHost: remoteHostController.text.trim().isEmpty - ? 'localhost' - : remoteHostController.text.trim(), - remotePort: remotePort); - localPortController.clear(); - remoteHostController.clear(); - remotePortController.clear(); - refreshTunnelConfig(); - } - }, - child: Text( - translate('Add'), - ), - ).marginAll(10), - ), + ElevatedButton( + 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'), + ), + ).marginSymmetric(horizontal: 10), ]), ); } @@ -217,32 +215,21 @@ class _PortForwardPageState extends State {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).backgroundColor, - contentPadding: const EdgeInsets.all(10), - hintText: hint, - hintStyle: - TextStyle(color: Theme.of(context).hintColor, fontSize: 16)), - style: TextStyle(color: textColor, fontSize: 16), - ).marginAll(10), + child: Padding( + padding: const EdgeInsets.all(10.0), + child: TextField( + controller: controller, + inputFormatters: inputFormatters, + decoration: InputDecoration( + hintText: hint, + ))), ); } Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) { - text(String lable) => Expanded( - child: Text(lable, style: const TextStyle(fontSize: 20)) + text(String label) => Expanded( + child: Text(label, style: const TextStyle(fontSize: 20)) .marginOnly(left: _kTextLeftMargin)); return Container( @@ -252,7 +239,7 @@ class _PortForwardPageState extends State ? MyTheme.currentThemeMode() == ThemeMode.dark ? const Color(0xFF202020) : const Color(0xFFF4F5F6) - : Theme.of(context).backgroundColor), + : Theme.of(context).colorScheme.background), child: Row(children: [ text(pf.localPort.toString()), const SizedBox(width: _kColumn1Width), @@ -285,16 +272,16 @@ class _PortForwardPageState extends State } buildRdp(BuildContext context) { - text1(String lable) => Expanded( - child: Text(translate(lable)).marginOnly(left: _kTextLeftMargin)); - text2(String lable) => Expanded( + text1(String label) => Expanded( + child: Text(translate(label)).marginOnly(left: _kTextLeftMargin)); + text2(String label) => Expanded( child: Text( - lable, + label, style: const TextStyle(fontSize: 20), ).marginOnly(left: _kTextLeftMargin)); return Theme( data: Theme.of(context) - .copyWith(backgroundColor: Theme.of(context).backgroundColor), + .copyWith(backgroundColor: Theme.of(context).colorScheme.background), child: ListView.builder( controller: ScrollController(), itemCount: 2, @@ -313,8 +300,8 @@ class _PortForwardPageState extends State } else { return Container( height: _kRowHeight, - decoration: - BoxDecoration(color: Theme.of(context).backgroundColor), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background), child: Row(children: [ Expanded( child: Align( @@ -322,14 +309,9 @@ class _PortForwardPageState extends State 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), diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index d4c0a86f8..32f02c9b7 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -31,6 +31,10 @@ class _PortForwardTabPageState extends State { isRDP = params['isRDP']; tabController = Get.put(DesktopTabController(tabType: DesktopTabType.portForward)); + tabController.onSelected = (_, id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: params['id'], label: params['id'], @@ -40,6 +44,7 @@ class _PortForwardTabPageState extends State { key: ValueKey(params['id']), id: params['id'], isRDP: isRDP, + forceRelay: params['forceRelay'], ))); } @@ -68,7 +73,12 @@ class _PortForwardTabPageState extends State { label: id, selectedIcon: selectedIcon, unselectedIcon: unselectedIcon, - page: PortForwardPage(id: id, isRDP: isRDP))); + page: PortForwardPage( + key: ValueKey(args['id']), + id: id, + isRDP: isRDP, + forceRelay: args['forceRelay'], + ))); } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { @@ -86,7 +96,7 @@ class _PortForwardTabPageState extends State { decoration: BoxDecoration( border: Border.all(color: MyTheme.color(context).border!)), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, onWindowCloseButton: () async { @@ -97,13 +107,15 @@ class _PortForwardTabPageState extends State { labelGetter: DesktopTab.labelGetterAlias, )), ); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget - : SubWindowDragToResizeArea( - child: tabWidget, - resizeEdgeSize: stateGlobal.resizeEdgeSize.value, - windowId: stateGlobal.windowId, - ); + : Obx( + () => SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + windowId: stateGlobal.windowId, + ), + ); } void onRemoveId(String id) { diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 102dc784a..1bc09f137 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'dart:ui' as ui; import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_custom_cursor/cursor_manager.dart' @@ -18,25 +17,31 @@ import '../../consts.dart'; import '../../common/widgets/overlay.dart'; import '../../common/widgets/remote_input.dart'; import '../../common.dart'; -import '../../mobile/widgets/dialog.dart'; +import '../../common/widgets/dialog.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; -import '../widgets/remote_menubar.dart'; +import '../../utils/image.dart'; +import '../widgets/remote_toolbar.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.password, required this.menubarState, + this.switchUuid, + this.forceRelay, }) : super(key: key); final String id; + final String? password; final MenubarState menubarState; + final String? switchUuid; + final bool? forceRelay; final SimpleWrapper?> _lastState = SimpleWrapper(null); FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; @@ -59,6 +64,11 @@ class _RemotePageState extends State late RxBool _zoomCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; + late RxInt _textureId; + late int _textureKey; + final useTextureRender = bind.mainUseTextureRender(); + + final _blockableOverlayState = BlockableOverlayState(); final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); @@ -79,6 +89,8 @@ class _RemotePageState extends State _showRemoteCursor = ShowRemoteCursorState.find(id); _keyboardEnabled = KeyboardEnabledState.find(id); _remoteCursorMoved = RemoteCursorMovedState.find(id); + _textureKey = newTextureId; + _textureId = RxInt(-1); } void _removeStates(String id) { @@ -100,7 +112,12 @@ class _RemotePageState extends State showKBLayoutTypeChooserIfNeeded( _ffi.ffiModel.pi.platform, _ffi.dialogManager); }); - _ffi.start(widget.id); + _ffi.start( + widget.id, + password: widget.password, + switchUuid: widget.switchUuid, + forceRelay: widget.forceRelay, + ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); _ffi.dialogManager @@ -109,6 +126,18 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.enable(); } + // Register texture. + _textureId.value = -1; + if (useTextureRender) { + textureRenderer.createTexture(_textureKey).then((id) async { + debugPrint("id: $id, texture_key: $_textureKey"); + if (id != -1) { + final ptr = await textureRenderer.getTexturePtr(_textureKey); + platformFFI.registerTexture(widget.id, ptr); + _textureId.value = id; + } + }); + } _ffi.ffiModel.updateEventListener(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // Session option should be set after models.dart/FFI.start @@ -128,6 +157,13 @@ class _RemotePageState extends State // }); // _isCustomCursorInited = true; // } + + _ffi.dialogManager.setOverlayState(_blockableOverlayState); + _ffi.chatModel.setOverlayState(_blockableOverlayState); + // make remote page penetrable automatically, effective for chat over remote + _blockableOverlayState.onMiddleBlockedClick = () { + _blockableOverlayState.setMiddleBlocked(false); + }; } @override @@ -166,6 +202,10 @@ class _RemotePageState extends State @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); + if (useTextureRender) { + platformFFI.registerTexture(widget.id, 0); + textureRenderer.closeTexture(_textureKey); + } // ensure we leave this session, this is a double check bind.sessionEnterOrLeave(id: widget.id, enter: false); DesktopMultiWindow.removeListener(this); @@ -187,39 +227,50 @@ class _RemotePageState extends State Widget buildBody(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Overlay( - initialEntries: [ - OverlayEntry(builder: (context) { - _ffi.chatModel.setOverlayState(Overlay.of(context)); - _ffi.dialogManager.setOverlayState(Overlay.of(context)); - return Container( - color: Colors.black, - child: RawKeyFocusScope( - focusNode: _rawKeyFocusNode, - onFocusChange: (bool imageFocused) { - debugPrint( - "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); - // See [onWindowBlur]. - if (Platform.isWindows) { - if (_isWindowBlur) { - imageFocused = false; - Future.delayed(Duration.zero, () { - _rawKeyFocusNode.unfocus(); - }); - } - if (imageFocused) { - _ffi.inputModel.enterOrLeave(true); - } else { - _ffi.inputModel.enterOrLeave(false); - } - } - }, - inputModel: _ffi.inputModel, - child: getBodyForDesktop(context))); - }) - ], - )); + backgroundColor: Theme.of(context).colorScheme.background, + + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + body: BlockableOverlay( + state: _blockableOverlayState, + underlying: Container( + color: Colors.black, + child: RawKeyFocusScope( + focusNode: _rawKeyFocusNode, + onFocusChange: (bool imageFocused) { + debugPrint( + "onFocusChange(window active:${!_isWindowBlur}) $imageFocused"); + // See [onWindowBlur]. + if (Platform.isWindows) { + if (_isWindowBlur) { + imageFocused = false; + Future.delayed(Duration.zero, () { + _rawKeyFocusNode.unfocus(); + }); + } + if (imageFocused) { + _ffi.inputModel.enterOrLeave(true); + } else { + _ffi.inputModel.enterOrLeave(false); + } + } + }, + inputModel: _ffi.inputModel, + child: getBodyForDesktop(context))), + upperLayer: [ + OverlayEntry( + builder: (context) => RemoteMenubar( + id: widget.id, + ffi: _ffi, + state: widget.menubarState, + onEnterOrLeaveImageSetter: (func) => + _onEnterOrLeaveImage4Menubar = func, + onEnterOrLeaveImageCleaner: () => + _onEnterOrLeaveImage4Menubar = null, + )) + ], + ), + ); } @override @@ -259,6 +310,10 @@ class _RemotePageState extends State } void leaveView(PointerExitEvent evt) { + if (_ffi.ffiModel.keyboard()) { + _ffi.inputModel.tryMoveEdgeOnExit(evt.position); + } + _cursorOverImage.value = false; _firstEnterImage.value = false; if (_onEnterOrLeaveImage4Menubar != null) { @@ -274,6 +329,34 @@ class _RemotePageState extends State } } + Widget _buildRawPointerMouseRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return RawPointerMouseRegion( + onEnter: onEnter, + onExit: onExit, + onPointerDown: (event) { + // A double check for blur status. + // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. + // Sometimes the system does not send the necessary focus event to flutter. We should manually + // handle this inconsistent status by setting `_isWindowBlur` to false. So we can + // ensure the grab-key thread is running when our users are clicking the remote canvas. + if (_isWindowBlur) { + debugPrint( + "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); + _isWindowBlur = false; + } + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + }, + inputModel: _ffi.inputModel, + child: child, + ); + } + Widget getBodyForDesktop(BuildContext context) { var paints = [ MouseRegion(onEnter: (evt) { @@ -290,27 +373,10 @@ class _RemotePageState extends State cursorOverImage: _cursorOverImage, keyboardEnabled: _keyboardEnabled, remoteCursorMoved: _remoteCursorMoved, - listenerBuilder: (child) => RawPointerMouseRegion( - onEnter: enterView, - onExit: leaveView, - onPointerDown: (event) { - // A double check for blur status. - // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. - // Sometimes the system does not send the necessary focus event to flutter. We should manually - // handle this inconsistent status by setting `_isWindowBlur` to false. So we can - // ensure the grab-key thread is running when our users are clicking the remote canvas. - if (_isWindowBlur) { - debugPrint( - "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); - _isWindowBlur = false; - } - if (!_rawKeyFocusNode.hasFocus) { - _rawKeyFocusNode.requestFocus(); - } - }, - inputModel: _ffi.inputModel, - child: child, - ), + textureId: _textureId, + useTextureRender: useTextureRender, + listenerBuilder: (child) => + _buildRawPointerMouseRegion(child, enterView, leaveView), ); })) ]; @@ -323,14 +389,14 @@ class _RemotePageState extends State zoomCursor: _zoomCursor, )))); } - paints.add(QualityMonitor(_ffi.qualityMonitorModel)); - paints.add(RemoteMenubar( - id: widget.id, - ffi: _ffi, - state: widget.menubarState, - onEnterOrLeaveImageSetter: (func) => _onEnterOrLeaveImage4Menubar = func, - onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Menubar = null, - )); + paints.add( + Positioned( + top: 10, + right: 10, + child: _buildRawPointerMouseRegion( + QualityMonitor(_ffi.qualityMonitorModel), null, null), + ), + ); return Stack( children: paints, ); @@ -342,10 +408,12 @@ class _RemotePageState extends State class ImagePaint extends StatefulWidget { final String id; - final Rx zoomCursor; - final Rx cursorOverImage; - final Rx keyboardEnabled; - final Rx remoteCursorMoved; + final RxBool zoomCursor; + final RxBool cursorOverImage; + final RxBool keyboardEnabled; + final RxBool remoteCursorMoved; + final RxInt textureId; + final bool useTextureRender; final Widget Function(Widget)? listenerBuilder; ImagePaint( @@ -355,6 +423,8 @@ class ImagePaint extends StatefulWidget { required this.cursorOverImage, required this.keyboardEnabled, required this.remoteCursorMoved, + required this.textureId, + required this.useTextureRender, this.listenerBuilder}) : super(key: key); @@ -368,10 +438,10 @@ class _ImagePaintState extends State { final ScrollController _vertical = ScrollController(); String get id => widget.id; - Rx get zoomCursor => widget.zoomCursor; - Rx get cursorOverImage => widget.cursorOverImage; - Rx get keyboardEnabled => widget.keyboardEnabled; - Rx get remoteCursorMoved => widget.remoteCursorMoved; + RxBool get zoomCursor => widget.zoomCursor; + RxBool get cursorOverImage => widget.cursorOverImage; + RxBool get keyboardEnabled => widget.keyboardEnabled; + RxBool get remoteCursorMoved => widget.remoteCursorMoved; Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; @override @@ -380,63 +450,116 @@ class _ImagePaintState extends State { var c = Provider.of(context); final s = c.scale; - mouseRegion({child}) => Obx(() => MouseRegion( - cursor: cursorOverImage.isTrue - ? c.cursorEmbedded - ? SystemMouseCursors.none - : keyboardEnabled.isTrue - ? (() { - if (remoteCursorMoved.isTrue) { - _lastRemoteCursorMoved = true; - return SystemMouseCursors.none; - } else { - if (_lastRemoteCursorMoved) { - _lastRemoteCursorMoved = false; - _firstEnterImage.value = true; - } - return _buildCustomCursor(context, s); - } - }()) - : _buildDisabledCursor(context, s) - : MouseCursor.defer, - onHover: (evt) {}, - child: child)); + mouseRegion({child}) => Obx(() { + double getCursorScale() { + var c = Provider.of(context); + var cursorScale = 1.0; + if (Platform.isWindows) { + // debug win10 + final isViewAdaptive = + c.viewStyle.style == kRemoteViewStyleAdaptive; + if (zoomCursor.value && isViewAdaptive) { + cursorScale = s * c.devicePixelRatio; + } + } else { + final isViewOriginal = + c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { + cursorScale = s; + } + } + return cursorScale; + } - if (c.scrollStyle == ScrollStyle.scrollbar) { + 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 imageWidget = CustomPaint( - size: Size(imageWidth, imageHeight), - painter: ImagePainter(image: m.image, x: 0, y: 0, scale: 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: _buildCrossScrollbar(context, _buildListener(imageWidget), - Size(imageWidth, imageHeight))), - ); + 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 { - final imageWidget = CustomPaint( - size: Size(c.size.width, c.size.height), - painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), - ); - return mouseRegion(child: _buildListener(imageWidget)); + late final Widget imageWidget; + if (c.size.width > 0 && c.size.height > 0) { + if (widget.useTextureRender) { + imageWidget = Stack( + children: [ + Positioned( + left: c.x, + top: c.y, + width: c.getDisplayWidth() * s, + height: c.getDisplayHeight() * s, + child: Texture(textureId: widget.textureId.value), + ) + ], + ); + } else { + imageWidget = CustomPaint( + size: Size(c.size.width, c.size.height), + painter: + ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); + } + return mouseRegion(child: _buildListener(imageWidget)); + } else { + return Container(); + } } } @@ -445,7 +568,7 @@ class _ImagePaintState extends State { if (cache == null) { return MouseCursor.defer; } else { - final key = cache.updateGetKey(scale, zoomCursor.value); + final key = cache.updateGetKey(scale); if (!cursor.cachedKeys.contains(key)) { debugPrint("Register custom cursor with key $key"); // [Safety] @@ -565,24 +688,6 @@ class _ImagePaintState extends State { return widget; } - Widget _buildCrossScrollbar(BuildContext context, Widget child, Size size) { - var layoutSize = MediaQuery.of(context).size; - // If minimized, w or h may be negative here. - final w = layoutSize.width - kWindowBorderWidth * 2; - final h = - layoutSize.height - kWindowBorderWidth * 2 - kDesktopRemoteTabBarHeight; - layoutSize = Size( - w < 0 ? 0 : w, - h < 0 ? 0 : h, - ); - bool overflow = - layoutSize.width < size.width || layoutSize.height < size.height; - return overflow - ? Obx(() => - _buildCrossScrollbarFromLayout(context, child, layoutSize, size)) - : _buildCrossScrollbarFromLayout(context, child, layoutSize, size); - } - Widget _buildListener(Widget child) { if (listenerBuilder != null) { return listenerBuilder!(child); @@ -629,7 +734,8 @@ class CursorPaint extends StatelessWidget { double x = (m.x - hotx) * c.scale + cx; double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; - if (zoomCursor.isTrue) { + final isViewOriginal = c.viewStyle.style == kRemoteViewStyleOriginal; + if (zoomCursor.value || isViewOriginal) { x = m.x - hotx + cx / c.scale; y = m.y - hoty + cy / c.scale; scale = c.scale; @@ -645,40 +751,3 @@ class CursorPaint extends StatelessWidget { ); } } - -class ImagePainter extends CustomPainter { - ImagePainter({ - required this.image, - required this.x, - required this.y, - required this.scale, - }); - - ui.Image? image; - double x; - double y; - double scale; - - @override - void paint(Canvas canvas, Size size) { - if (image == null) return; - if (x.isNaN || y.isNaN) return; - canvas.scale(scale, scale); - // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 - // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html - var paint = Paint(); - if ((scale - 1.0).abs() > 0.001) { - paint.filterQuality = FilterQuality.medium; - if (scale > 10.00000) { - paint.filterQuality = FilterQuality.high; - } - } - canvas.drawImage( - image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate != this; - } -} diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 604787290..53774e36d 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -9,7 +9,7 @@ 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/remote_toolbar.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' as mod_menu; @@ -22,7 +22,7 @@ import 'package:bot_toast/bot_toast.dart'; import '../../models/platform_model.dart'; class _MenuTheme { - static const Color commonColor = MyTheme.accent; + static const Color blueColor = MyTheme.button; // kMinInteractiveDimension static const double height = 20.0; static const double dividerHeight = 12.0; @@ -38,9 +38,9 @@ class ConnectionTabPage extends StatefulWidget { } class _ConnectionTabPageState extends State { - final tabController = Get.put(DesktopTabController( - tabType: DesktopTabType.remoteScreen, - onSelected: (_, id) => bind.setCurSessionId(id: id))); + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.remoteScreen)); + final contentKey = UniqueKey(); static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData unselectedIcon = Icons.desktop_windows_outlined; @@ -54,6 +54,11 @@ class _ConnectionTabPageState extends State { final peerId = params['id']; if (peerId != null) { ConnectionTypeState.init(peerId); + tabController.onSelected = (_, id) { + bind.setCurSessionId(id: id); + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: peerId, label: peerId, @@ -63,7 +68,10 @@ class _ConnectionTabPageState extends State { page: RemotePage( key: ValueKey(peerId), id: peerId, + password: params['password'], menubarState: _menubarState, + switchUuid: params['switch_uuid'], + forceRelay: params['forceRelay'], ), )); _update_remote_count(); @@ -84,6 +92,7 @@ class _ConnectionTabPageState extends State { if (call.method == "new_remote_desktop") { final args = jsonDecode(call.arguments); final id = args['id']; + final switchUuid = args['switch_uuid']; window_on_top(windowId()); ConnectionTypeState.init(id); tabController.add(TabInfo( @@ -95,9 +104,14 @@ class _ConnectionTabPageState extends State { page: RemotePage( key: ValueKey(id), id: id, + password: args['password'], menubarState: _menubarState, + switchUuid: switchUuid, + forceRelay: args['forceRelay'], ), )); + } else if (call.method == kWindowDisableGrabKeyboard) { + stateGlobal.grabKeyboard = false; } else if (call.method == "onDestroy") { tabController.clear(); } else if (call.method == kWindowActionRebuild) { @@ -105,6 +119,9 @@ class _ConnectionTabPageState extends State { } _update_remote_count(); }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId()); + }); } @override @@ -123,7 +140,7 @@ class _ConnectionTabPageState extends State { width: stateGlobal.windowBorderWidth.value), ), child: Scaffold( - backgroundColor: Theme.of(context).backgroundColor, + backgroundColor: Theme.of(context).colorScheme.background, body: DesktopTab( controller: tabController, onWindowCloseButton: handleWindowCloseButton, @@ -187,13 +204,16 @@ class _ConnectionTabPageState extends State { ), ), ); - return Platform.isMacOS + return Platform.isMacOS || kUseCompatibleUiMode ? tabWidget - : SubWindowDragToResizeArea( - child: tabWidget, - resizeEdgeSize: stateGlobal.resizeEdgeSize.value, - windowId: stateGlobal.windowId, - ); + : Obx(() => SubWindowDragToResizeArea( + key: contentKey, + child: tabWidget, + // Specially configured for a better resize area and remote control. + childPadding: kDragToResizeAreaPadding, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + windowId: stateGlobal.windowId, + )); } // Note: Some dup code to ../widgets/remote_menubar @@ -231,96 +251,37 @@ class _ConnectionTabPageState extends State { padding: padding, ), MenuEntryDivider(), - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Scale original'), - value: kRemoteViewStyleOriginal, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Scale adaptive'), - value: kRemoteViewStyleAdaptive, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetViewStyle(id: key) ?? '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetViewStyle(id: key, value: newValue); - ffi.canvasModel.updateViewStyle(); - cancelFunc(); - }, - padding: padding, + RemoteMenuEntry.viewStyle( + key, + ffi, + padding, + dismissFunc: cancelFunc, ), ]); - if (!ffi.canvasModel.cursorEmbedded) { + if (!ffi.canvasModel.cursorEmbedded && + !ffi.ffiModel.viewOnly && + !pi.is_wayland) { menu.add(MenuEntryDivider()); - menu.add(() { - final state = ShowRemoteCursorState.find(key); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Show remote cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: key, value: 'show-remote-cursor'); - cancelFunc(); - }, - padding: padding, - ); - }()); + menu.add(RemoteMenuEntry.showRemoteCursor( + key, + padding, + dismissFunc: cancelFunc, + )); } - if (perms['keyboard'] != false) { + if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) { if (perms['clipboard'] != false) { - menu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate('Disable clipboard'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: key, arg: 'disable-clipboard'); - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: key, value: 'disable-clipboard'); - cancelFunc(); - }, - padding: padding, - )); + menu.add(RemoteMenuEntry.disableClipboard(key, padding, + dismissFunc: cancelFunc)); } - menu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Insert Lock'), - style: style, - ), - proc: () { - bind.sessionLockScreen(id: key); - cancelFunc(); - }, - padding: padding, - dismissOnClicked: true, - )); + menu.add( + RemoteMenuEntry.insertLock(key, padding, dismissFunc: cancelFunc)); - if (pi.platform == 'Linux' || pi.sasEnabled) { - menu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - '${translate("Insert")} Ctrl + Alt + Del', - style: style, - ), - proc: () { - bind.sessionCtrlAltDel(id: key); - cancelFunc(); - }, - padding: padding, - dismissOnClicked: true, - )); + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { + menu.add(RemoteMenuEntry.insertCtrlAltDel(key, padding, + dismissFunc: cancelFunc)); } } @@ -329,7 +290,7 @@ class _ConnectionTabPageState extends State { .map((entry) => entry.build( context, const MenuConfig( - commonColor: _MenuTheme.commonColor, + commonColor: _MenuTheme.blueColor, height: _MenuTheme.height, dividerHeight: _MenuTheme.dividerHeight, ))) diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index fa367f488..45591b79b 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -1,11 +1,13 @@ // original cm window in Sciter version. import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_hbb/utils/platform_channel.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; @@ -30,7 +32,12 @@ class _DesktopServerPageState extends State void initState() { gFFI.ffiModel.updateEventListener(""); windowManager.addListener(this); - tabController.onRemoved = (_, id) => onRemoveId(id); + tabController.onRemoved = (_, id) { + onRemoveId(id); + }; + tabController.onSelected = (_, id) { + windowManager.setTitle(getWindowNameWithId(id)); + }; super.initState(); } @@ -42,8 +49,14 @@ class _DesktopServerPageState extends State @override void onWindowClose() { - gFFI.serverModel.closeAll(); - gFFI.close(); + Future.wait([gFFI.serverModel.closeAll(), gFFI.close()]).then((_) { + if (Platform.isMacOS) { + RdPlatformChannel.instance.terminate(); + } else { + windowManager.setPreventClose(false); + windowManager.close(); + } + }); super.onWindowClose(); } @@ -63,26 +76,19 @@ class _DesktopServerPageState extends State ], child: Consumer( builder: (context, serverModel, child) => Container( - decoration: BoxDecoration( - border: - Border.all(color: MyTheme.color(context).border!)), - child: Overlay(initialEntries: [ - OverlayEntry(builder: (context) { - gFFI.dialogManager.setOverlayState(Overlay.of(context)); - return Scaffold( - backgroundColor: Theme.of(context).backgroundColor, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded(child: ConnectionManager()), - ], - ), - ), - ); - }) - ]), - ))); + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + ], + ), + ), + )))); } @override @@ -180,7 +186,7 @@ class ConnectionManagerState extends State { windowManager.startDragging(); }, child: Container( - color: Theme.of(context).backgroundColor, + color: Theme.of(context).colorScheme.background, ), ), ), @@ -516,6 +522,48 @@ class _CmControlPanel extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ + Offstage( + offstage: !client.inVoiceCall, + child: buildButton(context, + color: Colors.red, + onClick: () => closeVoiceCall(), + icon: Icon(Icons.phone_disabled_rounded, color: Colors.white), + text: "Stop voice call", + textColor: Colors.white), + ), + Offstage( + offstage: !client.incomingVoiceCall, + child: Row( + children: [ + Expanded( + child: buildButton(context, + color: MyTheme.accent, + onClick: () => handleVoiceCall(true), + icon: Icon(Icons.phone_enabled, color: Colors.white), + text: "Accept", + textColor: Colors.white), + ), + Expanded( + child: buildButton(context, + color: Colors.red, + onClick: () => handleVoiceCall(false), + icon: + Icon(Icons.phone_disabled_rounded, color: Colors.white), + text: "Dismiss", + textColor: Colors.white), + ) + ], + ), + ), + Offstage( + offstage: !client.fromSwitch, + child: buildButton(context, + color: Colors.purple, + onClick: () => handleSwitchBack(context), + icon: Icon(Icons.reply, color: Colors.white), + text: "Switch Sides", + textColor: Colors.white), + ), Offstage( offstage: !showElevation, child: buildButton(context, color: Colors.green[700], onClick: () { @@ -612,7 +660,7 @@ class _CmControlPanel extends StatelessWidget { .marginSymmetric(horizontal: showElevation ? 0 : bigMargin); } - buildButton( + Widget buildButton( BuildContext context, { required Color? color, required Function() onClick, @@ -674,6 +722,18 @@ class _CmControlPanel extends StatelessWidget { windowManager.close(); } } + + void handleSwitchBack(BuildContext context) { + bind.cmSwitchBack(connId: client.id); + } + + void handleVoiceCall(bool accept) { + bind.cmHandleIncomingVoiceCall(id: client.id, accept: accept); + } + + void closeVoiceCall() { + bind.cmCloseVoiceCall(id: client.id); + } } void checkClickTime(int id, Function() callback) async { diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart index e8361a652..64af41401 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,8 +1,11 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:provider/provider.dart'; /// multi-tab desktop remote screen @@ -10,7 +13,9 @@ class DesktopRemoteScreen extends StatelessWidget { final Map params; DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) { - bind.mainStartGrabKeyboard(); + if (!bind.mainStartGrabKeyboard()) { + stateGlobal.grabKeyboard = true; + } } @override @@ -23,6 +28,9 @@ class DesktopRemoteScreen extends StatelessWidget { ChangeNotifierProvider.value(value: gFFI.canvasModel), ], child: Scaffold( + // Set transparent background for padding the resize area out of the flutter view. + // This allows the wallpaper goes through our resize area. (Linux only now). + backgroundColor: Platform.isLinux ? Colors.transparent : null, body: ConnectionTabPage( params: params, ), diff --git a/flutter/lib/desktop/widgets/dragable_divider.dart b/flutter/lib/desktop/widgets/dragable_divider.dart new file mode 100644 index 000000000..3821b7e0d --- /dev/null +++ b/flutter/lib/desktop/widgets/dragable_divider.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; + +class DraggableDivider extends StatefulWidget { + final Axis axis; + final double thickness; + final Color color; + final Function(double)? onPointerMove; + final VoidCallback? onHover; + final EdgeInsets padding; + const DraggableDivider({ + super.key, + this.axis = Axis.horizontal, + this.thickness = 1.0, + this.color = const Color.fromARGB(200, 177, 175, 175), + this.onPointerMove, + this.padding = const EdgeInsets.symmetric(horizontal: 1.0), + this.onHover, + }); + + @override + State createState() => _DraggableDividerState(); +} + +class _DraggableDividerState extends State { + @override + Widget build(BuildContext context) { + return Listener( + onPointerMove: (event) { + final dl = + widget.axis == Axis.horizontal ? event.localDelta.dy : event.localDelta.dx; + widget.onPointerMove?.call(dl); + }, + onPointerHover: (event) => widget.onHover?.call(), + child: MouseRegion( + cursor: SystemMouseCursors.resizeLeftRight, + child: Padding( + padding: widget.padding, + child: Container( + decoration: BoxDecoration(color: widget.color), + width: widget.axis == Axis.horizontal + ? double.infinity + : widget.thickness, + height: widget.axis == Axis.horizontal + ? widget.thickness + : double.infinity, + ), + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart index cfbdb0c4e..90e72cd40 100644 --- a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -1,12 +1,13 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import '../../common.dart'; -typedef KBChoosedCallback = Future Function(String); +typedef KBChosenCallback = Future Function(String); const double _kImageMarginVertical = 6.0; const double _kImageMarginHorizontal = 10.0; @@ -25,12 +26,12 @@ const _kKBLayoutImageMap = { class _KBImage extends StatelessWidget { final String kbLayoutType; final double imageWidth; - final RxString choosedType; + final RxString chosenType; const _KBImage({ Key? key, required this.kbLayoutType, required this.imageWidth, - required this.choosedType, + required this.chosenType, }) : super(key: key); @override @@ -40,7 +41,7 @@ class _KBImage extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(_kBorderRadius), border: Border.all( - color: choosedType.value == kbLayoutType + color: chosenType.value == kbLayoutType ? _kImageBorderColor : Colors.transparent, width: _kImageBoarderWidth, @@ -66,13 +67,13 @@ class _KBImage extends StatelessWidget { class _KBChooser extends StatelessWidget { final String kbLayoutType; final double imageWidth; - final RxString choosedType; - final KBChoosedCallback cb; + final RxString chosenType; + final KBChosenCallback cb; const _KBChooser({ Key? key, required this.kbLayoutType, required this.imageWidth, - required this.choosedType, + required this.chosenType, required this.cb, }) : super(key: key); @@ -81,7 +82,7 @@ class _KBChooser extends StatelessWidget { onChanged(String? v) async { if (v != null) { if (await cb(v)) { - choosedType.value = v; + chosenType.value = v; } } } @@ -95,7 +96,7 @@ class _KBChooser extends StatelessWidget { child: _KBImage( kbLayoutType: kbLayoutType, imageWidth: imageWidth, - choosedType: choosedType, + chosenType: chosenType, ), style: TextButton.styleFrom(padding: EdgeInsets.zero), ), @@ -105,7 +106,7 @@ class _KBChooser extends StatelessWidget { Obx(() => Radio( splashRadius: 0, value: kbLayoutType, - groupValue: choosedType.value, + groupValue: chosenType.value, onChanged: onChanged, )), Text(kbLayoutType), @@ -121,14 +122,14 @@ class _KBChooser extends StatelessWidget { } class KBLayoutTypeChooser extends StatelessWidget { - final RxString choosedType; + final RxString chosenType; final double width; final double height; final double dividerWidth; - final KBChoosedCallback cb; + final KBChosenCallback cb; KBLayoutTypeChooser({ Key? key, - required this.choosedType, + required this.chosenType, required this.width, required this.height, required this.dividerWidth, @@ -147,7 +148,7 @@ class KBLayoutTypeChooser extends StatelessWidget { _KBChooser( kbLayoutType: _kKBLayoutTypeISO, imageWidth: imageWidth, - choosedType: choosedType, + chosenType: chosenType, cb: cb, ), VerticalDivider( @@ -156,7 +157,7 @@ class KBLayoutTypeChooser extends StatelessWidget { _KBChooser( kbLayoutType: _kKBLayoutTypeNotISO, imageWidth: imageWidth, - choosedType: choosedType, + chosenType: chosenType, cb: cb, ), ], @@ -170,14 +171,14 @@ RxString KBLayoutType = ''.obs; String getLocalPlatformForKBLayoutType(String peerPlatform) { String localPlatform = ''; - if (peerPlatform != 'Mac OS') { + if (peerPlatform != kPeerPlatformMacOS) { return localPlatform; } if (Platform.isWindows) { - localPlatform = 'Windows'; + localPlatform = kPeerPlatformWindows; } else if (Platform.isLinux) { - localPlatform = 'Linux'; + localPlatform = kPeerPlatformLinux; } // to-do: web desktop support ? return localPlatform; @@ -208,7 +209,7 @@ showKBLayoutTypeChooser( title: Text('${translate('Select local keyboard type')} ($localPlatform)'), content: KBLayoutTypeChooser( - choosedType: KBLayoutType, + chosenType: KBLayoutType, width: 360, height: 200, dividerWidth: 4.0, @@ -217,7 +218,7 @@ showKBLayoutTypeChooser( KBLayoutType.value = bind.getLocalKbLayoutType(); return v == KBLayoutType.value; }), - actions: [msgBoxButton(translate('Close'), close)], + actions: [dialogButton('Close', onPressed: close)], onCancel: close, ); }); diff --git a/flutter/lib/desktop/widgets/list_search_action_listener.dart b/flutter/lib/desktop/widgets/list_search_action_listener.dart index 9598c3400..36128bf26 100644 --- a/flutter/lib/desktop/widgets/list_search_action_listener.dart +++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart @@ -55,6 +55,7 @@ class TimeoutStringBuffer { } ListSearchAction input(String ch) { + ch = ch.toLowerCase(); final curr = DateTime.now(); try { if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) { diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart deleted file mode 100644 index 0736f0864..000000000 --- a/flutter/lib/desktop/widgets/login.dart +++ /dev/null @@ -1,521 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:flutter/material.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'; - -final _kMidButtonPadding = const EdgeInsets.fromLTRB(15, 0, 15, 0); - -class _IconOP extends StatelessWidget { - final String icon; - final double iconWidth; - const _IconOP({Key? key, required this.icon, required this.iconWidth}) - : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(horizontal: 4.0), - 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: [ - Expanded( - child: Container( - height: height, - padding: _kMidButtonPadding, - 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: Stack(children: [ - Center(child: Text('${translate("Continue with")} $op')), - Align( - alignment: Alignment.centerLeft, - child: SizedBox( - width: 120, - child: _IconOP( - icon: op, - iconWidth: iconWidth, - )), - ), - ]), - )), - ), - ) - ]); - } -} - -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: Row( - children: [ - Text( - _stateMsg, - style: TextStyle(fontSize: 12), - ), - SizedBox(width: 8), - Text( - _FailedMsg, - style: TextStyle( - 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: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: children, - )); - } -} - -class LoginWidgetUserPass extends StatelessWidget { - final String username; - final String pass; - final String usernameMsg; - final String passMsg; - final bool isInProgress; - final RxString curOP; - final Function(String, String) onLogin; - const LoginWidgetUserPass({ - Key? key, - required this.username, - required this.pass, - required this.usernameMsg, - required this.passMsg, - required this.isInProgress, - required this.curOP, - required this.onLogin, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - var userController = TextEditingController(text: username); - var pwdController = TextEditingController(text: pass); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - Container( - padding: _kMidButtonPadding, - child: Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - '${translate("Username")}:', - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: usernameMsg.isNotEmpty ? usernameMsg : null), - controller: userController, - focusNode: FocusNode()..requestFocus(), - ), - ), - ], - ), - ), - const SizedBox( - height: 8.0, - ), - Container( - padding: _kMidButtonPadding, - child: Row( - children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text('${translate("Password")}:') - .marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, - ), - Expanded( - child: TextField( - obscureText: true, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: passMsg.isNotEmpty ? passMsg : null), - controller: pwdController, - ), - ), - ], - ), - ), - const SizedBox( - height: 4.0, - ), - Offstage( - offstage: !isInProgress, child: const LinearProgressIndicator()), - const SizedBox( - height: 12.0, - ), - Row(children: [ - Expanded( - child: Container( - height: 38, - padding: _kMidButtonPadding, - child: Obx(() => ElevatedButton( - style: curOP.value.isEmpty || curOP.value == 'rustdesk' - ? null - : ElevatedButton.styleFrom( - primary: Colors.grey, - ), - child: Text( - translate('Login'), - style: TextStyle(fontSize: 16), - ), - onPressed: curOP.value.isEmpty || curOP.value == 'rustdesk' - ? () { - onLogin(userController.text, pwdController.text); - } - : null, - )), - ), - ), - ]), - ], - ); - } -} - -/// common login dialog for desktop -/// call this directly -Future loginDialog() async { - String username = ''; - var usernameMsg = ''; - String pass = ''; - var passMsg = ''; - var isInProgress = false; - var completer = Completer(); - final RxString curOP = ''.obs; - - gFFI.dialogManager.show((setState, close) { - cancel() { - isInProgress = false; - completer.complete(false); - close(); - } - - onLogin(String username0, String pass0) async { - setState(() { - usernameMsg = ''; - passMsg = ''; - isInProgress = true; - }); - cancel() { - curOP.value = ''; - if (isInProgress) { - setState(() { - isInProgress = false; - }); - } - } - - curOP.value = 'rustdesk'; - username = username0; - pass = pass0; - if (username.isEmpty) { - usernameMsg = translate('Username missed'); - cancel(); - return; - } - if (pass.isEmpty) { - passMsg = translate('Password missed'); - cancel(); - return; - } - try { - final resp = await gFFI.userModel.login(username, pass); - if (resp.containsKey('error')) { - passMsg = resp['error']; - cancel(); - return; - } - // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, - // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} - debugPrint('$resp'); - completer.complete(true); - } catch (err) { - debugPrintStack(label: err.toString()); - cancel(); - return; - } - close(); - } - - return CustomAlertDialog( - title: Text(translate('Login')), - content: ConstrainedBox( - constraints: const BoxConstraints(minWidth: 500), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox( - height: 8.0, - ), - LoginWidgetUserPass( - username: username, - pass: pass, - usernameMsg: usernameMsg, - passMsg: passMsg, - isInProgress: isInProgress, - curOP: curOP, - onLogin: onLogin, - ), - 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; - completer.complete(true); - close(); - }, - ), - ], - ), - ), - actions: [msgBoxButton(translate('Close'), cancel)], - onCancel: cancel, - ); - }); - return completer.future; -} diff --git a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart index a371e8f52..3e85cb296 100644 --- a/flutter/lib/desktop/widgets/material_mod_popup_menu.dart +++ b/flutter/lib/desktop/widgets/material_mod_popup_menu.dart @@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/menu_button.dart'; // Examples can assume: // enum Commands { heroAndScholar, hurricaneCame } @@ -790,6 +792,7 @@ class _PopupMenuRoute extends PopupRoute { _PopupMenuRoute({ required this.position, required this.items, + this.menuWrapper, this.initialValue, this.elevation, required this.barrierLabel, @@ -802,6 +805,7 @@ class _PopupMenuRoute extends PopupRoute { final RelativeRect position; final List> items; + final MenuWrapper? menuWrapper; final List itemSizes; final T? initialValue; final double? elevation; @@ -844,11 +848,14 @@ class _PopupMenuRoute extends PopupRoute { } } - final Widget menu = _PopupMenu( + Widget menu = _PopupMenu( route: this, semanticLabel: semanticLabel, constraints: constraints, ); + if (this.menuWrapper != null) { + menu = this.menuWrapper!(menu); + } final MediaQueryData mediaQuery = MediaQuery.of(context); return MediaQuery.removePadding( context: context, @@ -1035,6 +1042,7 @@ Future showMenu({ required BuildContext context, required RelativeRect position, required List> items, + MenuWrapper? menuWrapper, T? initialValue, double? elevation, String? semanticLabel, @@ -1062,6 +1070,7 @@ Future showMenu({ return navigator.push(_PopupMenuRoute( position: position, items: items, + menuWrapper: menuWrapper, initialValue: initialValue, elevation: elevation, semanticLabel: semanticLabel, @@ -1094,6 +1103,8 @@ typedef PopupMenuCanceled = void Function(); typedef PopupMenuItemBuilder = List> Function( BuildContext context); +typedef MenuWrapper = Widget Function(Widget child); + /// Displays a menu when pressed and calls [onSelected] when the menu is dismissed /// because an item was selected. The value passed to [onSelected] is the value of /// the selected menu item. @@ -1124,6 +1135,7 @@ class PopupMenuButton extends StatefulWidget { const PopupMenuButton({ Key? key, required this.itemBuilder, + this.menuWrapper, this.initialValue, this.onHover, this.onSelected, @@ -1151,6 +1163,9 @@ class PopupMenuButton extends StatefulWidget { /// Called when the button is pressed to create the items to show in the menu. final PopupMenuItemBuilder itemBuilder; + /// Menu wrapper. + final MenuWrapper? menuWrapper; + /// The value of the menu item, if any, that should be highlighted when the menu opens. final T? initialValue; @@ -1333,6 +1348,7 @@ class PopupMenuButtonState extends State> { context: context, elevation: widget.elevation ?? popupMenuTheme.elevation, items: items, + menuWrapper: widget.menuWrapper, initialValue: widget.initialValue, position: position, shape: widget.shape ?? popupMenuTheme.shape, @@ -1377,22 +1393,20 @@ class PopupMenuButtonState extends State> { onTap: widget.enabled ? showButtonMenu : null, onHover: widget.onHover, canRequestFocus: _canRequestFocus, - radius: widget.splashRadius, enableFeedback: enableFeedback, child: widget.child, ), ); } - return IconButton( - icon: widget.icon ?? Icon(Icons.adaptive.more), - padding: widget.padding, - splashRadius: widget.splashRadius, - iconSize: widget.iconSize ?? iconTheme.size ?? _kDefaultIconSize, + return MenuButton( + child: widget.icon ?? Icon(Icons.adaptive.more), tooltip: widget.tooltip ?? MaterialLocalizations.of(context).showMenuTooltip, onPressed: widget.enabled ? showButtonMenu : null, enableFeedback: enableFeedback, + color: MyTheme.button, + hoverColor: MyTheme.accent, ); } } diff --git a/flutter/lib/desktop/widgets/menu_button.dart b/flutter/lib/desktop/widgets/menu_button.dart new file mode 100644 index 000000000..17b160fed --- /dev/null +++ b/flutter/lib/desktop/widgets/menu_button.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +class MenuButton extends StatefulWidget { + final GestureTapCallback? onPressed; + final Color color; + final Color hoverColor; + final Color? splashColor; + final Widget child; + final String? tooltip; + final EdgeInsetsGeometry padding; + final bool enableFeedback; + const MenuButton({ + super.key, + required this.onPressed, + required this.color, + required this.hoverColor, + required this.child, + this.splashColor, + this.tooltip = "", + this.padding = const EdgeInsets.symmetric(horizontal: 3, vertical: 6), + this.enableFeedback = true, + }); + + @override + State createState() => _MenuButtonState(); +} + +class _MenuButtonState extends State { + bool _isHover = false; + final double _borderRadius = 8.0; + + @override + Widget build(BuildContext context) { + return Padding( + padding: widget.padding, + child: Tooltip( + message: widget.tooltip, + child: Material( + type: MaterialType.transparency, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_borderRadius), + color: _isHover ? widget.hoverColor : widget.color, + ), + child: InkWell( + hoverColor: widget.hoverColor, + onHover: (val) { + setState(() { + _isHover = val; + }); + }, + borderRadius: BorderRadius.circular(_borderRadius), + splashColor: widget.splashColor, + enableFeedback: widget.enableFeedback, + onTap: widget.onPressed, + child: widget.child, + ), + ), + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 20ab31ed9..9833dcbca 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -109,15 +109,28 @@ class MenuConfig { this.boxWidth}); } +typedef DismissCallback = Function(); + abstract class MenuEntryBase { bool dismissOnClicked; + DismissCallback? dismissCallback; RxBool? enabled; MenuEntryBase({ this.dismissOnClicked = false, this.enabled, + this.dismissCallback, }); List> build(BuildContext context, MenuConfig conf); + + enabledStyle(BuildContext context) => TextStyle( + color: Theme.of(context).textTheme.titleLarge?.color, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); + disabledStyle() => TextStyle( + color: Colors.grey, + fontSize: MenuConfig.fontSize, + fontWeight: FontWeight.normal); } class MenuEntryDivider extends MenuEntryBase { @@ -137,12 +150,14 @@ class MenuEntryRadioOption { String value; bool dismissOnClicked; RxBool? enabled; + DismissCallback? dismissCallback; MenuEntryRadioOption({ required this.text, required this.value, this.dismissOnClicked = false, this.enabled, + this.dismissCallback, }); } @@ -168,8 +183,13 @@ class MenuEntryRadios extends MenuEntryBase { required this.optionSetter, this.padding, dismissOnClicked = false, + dismissCallback, RxBool? enabled, - }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled) { + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + dismissCallback: dismissCallback, + ) { () async { _curOption.value = await curOptionGetter(); }(); @@ -189,54 +209,79 @@ class MenuEntryRadios extends MenuEntryBase { mod_menu.PopupMenuEntry _buildMenuItem( BuildContext context, MenuConfig conf, MenuEntryRadioOption opt) { + Widget getTextChild() { + final enabledTextChild = Text( + opt.text, + style: enabledStyle(context), + ); + final disabledTextChild = Text( + opt.text, + style: disabledStyle(), + ); + if (opt.enabled == null) { + return enabledTextChild; + } else { + return Obx( + () => opt.enabled!.isTrue ? enabledTextChild : disabledTextChild); + } + } + + final child = Container( + padding: padding, + alignment: AlignmentDirectional.centerStart, + constraints: + BoxConstraints(minHeight: conf.height, maxHeight: conf.height), + child: Row( + children: [ + getTextChild(), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Transform.scale( + scale: MenuConfig.iconScale, + child: Obx(() => opt.value == curOption.value + ? IconButton( + padding: + const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0), + hoverColor: Colors.transparent, + focusColor: Colors.transparent, + onPressed: () {}, + icon: Icon( + Icons.check, + color: (opt.enabled ?? true.obs).isTrue + ? conf.commonColor + : Colors.grey, + )) + : const SizedBox.shrink()), + ))), + ], + ), + ); + onPressed() { + if (opt.dismissOnClicked && Navigator.canPop(context)) { + Navigator.pop(context); + if (opt.dismissCallback != null) { + opt.dismissCallback!(); + } + } + setOption(opt.value); + } + return mod_menu.PopupMenuItem( padding: EdgeInsets.zero, height: conf.height, child: Container( - width: conf.boxWidth, - child: TextButton( - child: Container( - padding: padding, - alignment: AlignmentDirectional.centerStart, - constraints: BoxConstraints( - minHeight: conf.height, maxHeight: conf.height), - child: Row( - children: [ - Text( - opt.text, - style: TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: MenuConfig.iconScale, - child: Obx(() => opt.value == curOption.value - ? IconButton( - padding: const EdgeInsets.fromLTRB( - 8.0, 0.0, 8.0, 0.0), - hoverColor: Colors.transparent, - focusColor: Colors.transparent, - onPressed: () {}, - icon: Icon( - Icons.check, - color: conf.commonColor, - )) - : const SizedBox.shrink()), - ))), - ], - ), - ), - onPressed: () { - if (opt.dismissOnClicked && Navigator.canPop(context)) { - Navigator.pop(context); - } - setOption(opt.value); - }, - )), + width: conf.boxWidth, + child: opt.enabled == null + ? TextButton( + child: child, + onPressed: onPressed, + ) + : Obx(() => TextButton( + child: child, + onPressed: opt.enabled!.isTrue ? onPressed : null, + )), + ), ); } @@ -329,6 +374,9 @@ class MenuEntrySubRadios extends MenuEntryBase { onPressed: () { if (opt.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (opt.dismissCallback != null) { + opt.dismissCallback!(); + } } setOption(opt.value); }, @@ -390,7 +438,12 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { this.textStyle, this.padding, RxBool? enabled, - }) : super(dismissOnClicked: dismissOnClicked, enabled: enabled); + dismissCallback, + }) : super( + dismissOnClicked: dismissOnClicked, + enabled: enabled, + dismissCallback: dismissCallback, + ); RxBool get curOption; Future setOption(bool? option); @@ -432,6 +485,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } setOption(v); }, @@ -443,6 +499,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } setOption(v); }, @@ -454,6 +513,9 @@ abstract class MenuEntrySwitchBase extends MenuEntryBase { onPressed: () { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } setOption(!curOption.value); }, @@ -477,6 +539,7 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, + dismissCallback, }) : super( switchType: switchType, text: text, @@ -484,6 +547,7 @@ class MenuEntrySwitch extends MenuEntrySwitchBase { padding: padding, dismissOnClicked: dismissOnClicked, enabled: enabled, + dismissCallback: dismissCallback, ) { () async { _curOption.value = await getter(); @@ -520,12 +584,15 @@ class MenuEntrySwitch2 extends MenuEntrySwitchBase { EdgeInsets? padding, dismissOnClicked = false, RxBool? enabled, + dismissCallback, }) : super( - switchType: switchType, - text: text, - textStyle: textStyle, - padding: padding, - dismissOnClicked: dismissOnClicked); + switchType: switchType, + text: text, + textStyle: textStyle, + padding: padding, + dismissOnClicked: dismissOnClicked, + dismissCallback: dismissCallback, + ); @override RxBool get curOption => getter(); @@ -567,12 +634,9 @@ class MenuEntrySubMenu extends MenuEntryBase { const SizedBox(width: MenuConfig.midPadding), Obx(() => Text( text, - style: TextStyle( - color: super.enabled!.value - ? Theme.of(context).textTheme.titleLarge?.color - : Colors.grey, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal), + style: super.enabled!.value + ? enabledStyle(context) + : disabledStyle(), )), Expanded( child: Align( @@ -599,20 +663,14 @@ class MenuEntryButton extends MenuEntryBase { this.padding, dismissOnClicked = false, RxBool? enabled, + dismissCallback, }) : super( dismissOnClicked: dismissOnClicked, enabled: enabled, + dismissCallback: dismissCallback, ); Widget _buildChild(BuildContext context, MenuConfig conf) { - final enabledStyle = TextStyle( - color: Theme.of(context).textTheme.titleLarge?.color, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); - const disabledStyle = TextStyle( - color: Colors.grey, - fontSize: MenuConfig.fontSize, - fontWeight: FontWeight.normal); super.enabled ??= true.obs; return Obx(() => Container( width: conf.boxWidth, @@ -621,6 +679,9 @@ class MenuEntryButton extends MenuEntryBase { ? () { if (super.dismissOnClicked && Navigator.canPop(context)) { Navigator.pop(context); + if (super.dismissCallback != null) { + super.dismissCallback!(); + } } proc(); } @@ -631,7 +692,7 @@ class MenuEntryButton extends MenuEntryBase { constraints: BoxConstraints(minHeight: conf.height, maxHeight: conf.height), child: childBuilder( - super.enabled!.value ? enabledStyle : disabledStyle), + super.enabled!.value ? enabledStyle(context) : disabledStyle()), ), ))); } diff --git a/flutter/lib/desktop/widgets/refresh_wrapper.dart b/flutter/lib/desktop/widgets/refresh_wrapper.dart index 4f2795d71..b4ea14d01 100644 --- a/flutter/lib/desktop/widgets/refresh_wrapper.dart +++ b/flutter/lib/desktop/widgets/refresh_wrapper.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/main.dart'; import 'package:get/get.dart'; class RefreshWrapper extends StatefulWidget { final Widget Function(BuildContext context) builder; + const RefreshWrapper({super.key, required this.builder}); @override @@ -26,10 +28,12 @@ class RefreshWrapperState extends State { } rebuild() { - debugPrint("=====Global State Rebuild (win-${windowId ?? 'main'})====="); + debugPrint("=====Global State Rebuild (win-${kWindowId ?? 'main'})====="); if (Get.context != null) { (context as Element).visitChildren(_rebuildElement); } + // Synchronize the window theme of the system. + updateSystemWindowTheme(); setState(() {}); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart deleted file mode 100644 index 244822e19..000000000 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ /dev/null @@ -1,1487 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:math' as math; -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_hbb/models/chat_model.dart'; -import 'package:flutter_hbb/models/state_model.dart'; -import 'package:flutter_hbb/consts.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; -import 'package:get/get.dart'; -import 'package:provider/provider.dart'; -import 'package:debounce_throttle/debounce_throttle.dart'; -import 'package:desktop_multi_window/desktop_multi_window.dart'; -import 'package:window_size/window_size.dart' as window_size; - -import '../../common.dart'; -import '../../mobile/widgets/dialog.dart'; -import '../../models/model.dart'; -import '../../models/platform_model.dart'; -import '../../common/shared_state.dart'; -import './popup_menu.dart'; -import './material_mod_popup_menu.dart' as mod_menu; -import './kb_layout_type_chooser.dart'; - -class MenubarState { - final kStoreKey = 'remoteMenubarState'; - late RxBool show; - late RxBool _pin; - RxString viewStyle = RxString(kRemoteViewStyleOriginal); - - MenubarState() { - final s = bind.getLocalFlutterConfig(k: kStoreKey); - if (s.isEmpty) { - _initSet(false, false); - return; - } - - try { - final m = jsonDecode(s); - if (m == null) { - _initSet(false, false); - } else { - _initSet(m['pin'] ?? false, m['pin'] ?? false); - } - } catch (e) { - debugPrint('Failed to decode menubar state ${e.toString()}'); - _initSet(false, false); - } - } - - _initSet(bool s, bool p) { - // Show remubar when connection is established. - show = RxBool(true); - _pin = RxBool(p); - } - - bool get pin => _pin.value; - - switchShow() async { - show.value = !show.value; - } - - setShow(bool v) async { - if (show.value != v) { - show.value = v; - } - } - - switchPin() async { - _pin.value = !_pin.value; - // Save everytime changed, as this func will not be called frequently - await _savePin(); - } - - setPin(bool v) async { - if (_pin.value != v) { - _pin.value = v; - // Save everytime changed, as this func will not be called frequently - await _savePin(); - } - } - - _savePin() async { - bind.setLocalFlutterConfig( - k: kStoreKey, v: jsonEncode({'pin': _pin.value})); - } - - save() async { - await _savePin(); - } -} - -class _MenubarTheme { - static const Color commonColor = MyTheme.accent; - // kMinInteractiveDimension - static const double height = 20.0; - static const double dividerHeight = 12.0; -} - -class RemoteMenubar extends StatefulWidget { - final String id; - final FFI ffi; - final MenubarState state; - final Function(Function(bool)) onEnterOrLeaveImageSetter; - final Function() onEnterOrLeaveImageCleaner; - - const RemoteMenubar({ - Key? key, - required this.id, - required this.ffi, - required this.state, - required this.onEnterOrLeaveImageSetter, - required this.onEnterOrLeaveImageCleaner, - }) : super(key: key); - - @override - State createState() => _RemoteMenubarState(); -} - -class _RemoteMenubarState extends State { - late Debouncer _debouncerHide; - bool _isCursorOverImage = false; - window_size.Screen? _screen; - final _fractionX = 0.5.obs; - final _dragging = false.obs; - - int get windowId => stateGlobal.windowId; - - bool get isFullscreen => stateGlobal.fullscreen; - void _setFullscreen(bool v) { - stateGlobal.setFullscreen(v); - setState(() {}); - } - - RxBool get show => widget.state.show; - bool get pin => widget.state.pin; - - @override - initState() { - super.initState(); - - _debouncerHide = Debouncer( - Duration(milliseconds: 5000), - onChanged: _debouncerHideProc, - initialValue: 0, - ); - - widget.onEnterOrLeaveImageSetter((enter) { - if (enter) { - _debouncerHide.value = 0; - _isCursorOverImage = true; - } else { - _isCursorOverImage = false; - } - }); - } - - _debouncerHideProc(int v) { - if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) { - show.value = false; - } - } - - @override - dispose() { - super.dispose(); - - widget.onEnterOrLeaveImageCleaner(); - } - - @override - Widget build(BuildContext context) { - // No need to use future builder here. - _updateScreen(); - return Align( - alignment: Alignment.topCenter, - child: Obx(() => show.value - ? _buildMenubar(context) - : _buildDraggableShowHide(context)), - ); - } - - Widget _buildDraggableShowHide(BuildContext context) { - return Obx(() { - if (show.isTrue && _dragging.isFalse) { - _debouncerHide.value = 1; - } - return Align( - alignment: FractionalOffset(_fractionX.value, 0), - child: Offstage( - offstage: _dragging.isTrue, - child: _DraggableShowHide( - dragging: _dragging, - fractionX: _fractionX, - show: show, - ), - ), - ); - }); - } - - _updateScreen() async { - final v = await rustDeskWinManager.call( - WindowType.Main, kWindowGetWindowInfo, ''); - final String valueStr = v; - if (valueStr.isEmpty) { - _screen = null; - } else { - final screenMap = jsonDecode(valueStr); - _screen = window_size.Screen( - Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'], - screenMap['frame']['r'], screenMap['frame']['b']), - Rect.fromLTRB( - screenMap['visibleFrame']['l'], - screenMap['visibleFrame']['t'], - screenMap['visibleFrame']['r'], - screenMap['visibleFrame']['b']), - screenMap['scaleFactor']); - } - } - - Widget _buildMenubar(BuildContext context) { - final List menubarItems = []; - if (!isWebDesktop) { - menubarItems.add(_buildPinMenubar(context)); - menubarItems.add(_buildFullscreen(context)); - if (widget.ffi.ffiModel.isPeerAndroid) { - menubarItems.add(IconButton( - tooltip: translate('Mobile Actions'), - color: _MenubarTheme.commonColor, - icon: const Icon(Icons.build), - onPressed: () { - widget.ffi.dialogManager - .toggleMobileActionsOverlay(ffi: widget.ffi); - }, - )); - } - } - menubarItems.add(_buildMonitor(context)); - menubarItems.add(_buildControl(context)); - menubarItems.add(_buildDisplay(context)); - menubarItems.add(_buildKeyboard(context)); - if (!isWeb) { - menubarItems.add(_buildChat(context)); - } - menubarItems.add(_buildRecording(context)); - menubarItems.add(_buildClose(context)); - return PopupMenuTheme( - data: const PopupMenuThemeData( - textStyle: TextStyle(color: _MenubarTheme.commonColor)), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: MyTheme.border), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: menubarItems, - )), - _buildDraggableShowHide(context), - ])); - } - - Widget _buildPinMenubar(BuildContext context) { - return Obx(() => IconButton( - tooltip: translate(pin ? 'Unpin menubar' : 'Pin menubar'), - onPressed: () { - widget.state.switchPin(); - }, - icon: Obx(() => Transform.rotate( - angle: pin ? math.pi / 4 : 0, - child: Icon( - Icons.push_pin, - color: pin ? _MenubarTheme.commonColor : Colors.grey, - ))), - )); - } - - Widget _buildFullscreen(BuildContext context) { - return IconButton( - tooltip: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'), - onPressed: () { - _setFullscreen(!isFullscreen); - }, - icon: isFullscreen - ? const Icon( - Icons.fullscreen_exit, - color: _MenubarTheme.commonColor, - ) - : const Icon( - Icons.fullscreen, - color: _MenubarTheme.commonColor, - ), - ); - } - - Widget _buildChat(BuildContext context) { - return IconButton( - tooltip: translate('Chat'), - onPressed: () { - widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); - widget.ffi.chatModel.toggleChatOverlay(); - }, - icon: const Icon( - Icons.message, - color: _MenubarTheme.commonColor, - ), - ); - } - - Widget _buildMonitor(BuildContext context) { - final pi = widget.ffi.ffiModel.pi; - return mod_menu.PopupMenuButton( - tooltip: translate('Select Monitor'), - padding: EdgeInsets.zero, - position: mod_menu.PopupMenuPosition.under, - icon: Stack( - alignment: Alignment.center, - children: [ - const Icon( - Icons.personal_video, - color: _MenubarTheme.commonColor, - ), - Padding( - padding: const EdgeInsets.only(bottom: 3.9), - child: Obx(() { - RxInt display = CurrentDisplayState.find(widget.id); - return Text( - '${display.value + 1}/${pi.displays.length}', - style: const TextStyle( - color: _MenubarTheme.commonColor, fontSize: 8), - ); - }), - ) - ], - ), - itemBuilder: (BuildContext context) { - final List rowChildren = []; - for (int i = 0; i < pi.displays.length; i++) { - rowChildren.add( - Stack( - alignment: Alignment.center, - children: [ - const Icon( - Icons.personal_video, - color: _MenubarTheme.commonColor, - ), - TextButton( - child: Container( - alignment: AlignmentDirectional.center, - constraints: - const BoxConstraints(minHeight: _MenubarTheme.height), - child: Padding( - padding: const EdgeInsets.only(bottom: 2.5), - child: Text( - (i + 1).toString(), - style: - const TextStyle(color: _MenubarTheme.commonColor), - ), - )), - onPressed: () { - RxInt display = CurrentDisplayState.find(widget.id); - if (display.value != i) { - bind.sessionSwitchDisplay(id: widget.id, value: i); - } - }, - ) - ], - ), - ); - } - return >[ - mod_menu.PopupMenuItem( - height: _MenubarTheme.height, - padding: EdgeInsets.zero, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: rowChildren), - ) - ]; - }, - ); - } - - Widget _buildControl(BuildContext context) { - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: const Icon( - Icons.bolt, - color: _MenubarTheme.commonColor, - ), - tooltip: translate('Control Actions'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getControlMenu(context) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.commonColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _buildDisplay(BuildContext context) { - return FutureBuilder(future: () async { - widget.state.viewStyle.value = - await bind.sessionGetViewStyle(id: widget.id) ?? ''; - final supportedHwcodec = - await bind.sessionSupportedHwcodec(id: widget.id); - return {'supportedHwcodec': supportedHwcodec}; - }(), builder: (context, snapshot) { - if (snapshot.hasData) { - return Obx(() { - final remoteCount = RemoteCountState.find().value; - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: const Icon( - Icons.tv, - color: _MenubarTheme.commonColor, - ), - tooltip: translate('Display Settings'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => - _getDisplayMenu(snapshot.data!, remoteCount) - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.commonColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - }); - } else { - return const Offstage(); - } - }); - } - - Widget _buildKeyboard(BuildContext context) { - FfiModel ffiModel = Provider.of(context); - if (ffiModel.permissions['keyboard'] == false) { - return Offstage(); - } - return mod_menu.PopupMenuButton( - padding: EdgeInsets.zero, - icon: const Icon( - Icons.keyboard, - color: _MenubarTheme.commonColor, - ), - tooltip: translate('Keyboard Settings'), - position: mod_menu.PopupMenuPosition.under, - itemBuilder: (BuildContext context) => _getKeyboardMenu() - .map((entry) => entry.build( - context, - const MenuConfig( - commonColor: _MenubarTheme.commonColor, - height: _MenubarTheme.height, - dividerHeight: _MenubarTheme.dividerHeight, - ))) - .expand((i) => i) - .toList(), - ); - } - - Widget _buildRecording(BuildContext context) { - return Consumer(builder: ((context, value, child) { - if (value.permissions['recording'] != false) { - return Consumer( - builder: (context, value, child) => IconButton( - tooltip: value.start - ? translate('Stop session recording') - : translate('Start session recording'), - onPressed: () => value.toggle(), - icon: Icon( - value.start - ? Icons.pause_circle_filled - : Icons.videocam_outlined, - color: _MenubarTheme.commonColor, - ), - )); - } else { - return Offstage(); - } - })); - } - - Widget _buildClose(BuildContext context) { - return IconButton( - tooltip: translate('Close'), - onPressed: () { - clientClose(widget.id, widget.ffi.dialogManager); - }, - icon: const Icon( - Icons.close, - color: _MenubarTheme.commonColor, - ), - ); - } - - List> _getControlMenu(BuildContext context) { - final pi = widget.ffi.ffiModel.pi; - final perms = widget.ffi.ffiModel.permissions; - const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); - final List> displayMenu = []; - displayMenu.addAll([ - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - alignment: AlignmentDirectional.center, - height: _MenubarTheme.height, - child: Row( - children: [ - Text( - translate('OS Password'), - style: style, - ), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: 0.8, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.edit), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - } - showSetOSPassword( - widget.id, false, widget.ffi.dialogManager); - })), - )) - ], - )), - proc: () { - showSetOSPassword(widget.id, false, widget.ffi.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Transfer File'), - style: style, - ), - proc: () { - connect(context, widget.id, isFileTransfer: true); - }, - padding: padding, - dismissOnClicked: true, - ), - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('TCP Tunneling'), - style: style, - ), - padding: padding, - proc: () { - connect(context, widget.id, isTcpTunneling: true); - }, - dismissOnClicked: true, - ), - ]); - // {handler.get_audit_server() &&
  • {translate('Note')}
  • } - final auditServer = - bind.sessionGetAuditServerSync(id: widget.id, typ: "conn"); - if (auditServer.isNotEmpty) { - displayMenu.add( - MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Note'), - style: style, - ), - proc: () { - showAuditDialog(widget.id, widget.ffi.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - ), - ); - } - displayMenu.add(MenuEntryDivider()); - if (perms['keyboard'] != false) { - if (pi.platform == 'Linux' || pi.sasEnabled) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - '${translate("Insert")} Ctrl + Alt + Del', - style: style, - ), - proc: () { - bind.sessionCtrlAltDel(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - )); - } - } - if (perms['restart'] != false && - (pi.platform == 'Linux' || - pi.platform == 'Windows' || - pi.platform == 'Mac OS')) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Restart Remote Device'), - style: style, - ), - proc: () { - showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); - }, - padding: padding, - dismissOnClicked: true, - )); - } - - if (perms['keyboard'] != false) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Insert Lock'), - style: style, - ), - proc: () { - bind.sessionLockScreen(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - )); - - if (pi.platform == 'Windows') { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Obx(() => Text( - translate( - '${BlockInputState.find(widget.id).value ? 'Unb' : 'B'}lock user input'), - style: style, - )), - proc: () { - RxBool blockInput = BlockInputState.find(widget.id); - bind.sessionToggleOption( - id: widget.id, - value: '${blockInput.value ? 'un' : ''}block-input'); - blockInput.value = !blockInput.value; - }, - padding: padding, - dismissOnClicked: true, - )); - } - } - - if (pi.version.isNotEmpty) { - displayMenu.add(MenuEntryButton( - childBuilder: (TextStyle? style) => Text( - translate('Refresh'), - style: style, - ), - proc: () { - bind.sessionRefresh(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - )); - } - - if (!isWebDesktop) { - // if (perms['keyboard'] != false && perms['clipboard'] != false) { - // displayMenu.add(MenuEntryButton( - // childBuilder: (TextStyle? style) => Text( - // translate('Paste'), - // style: style, - // ), - // proc: () { - // () async { - // ClipboardData? data = - // await Clipboard.getData(Clipboard.kTextPlain); - // if (data != null && data.text != null) { - // bind.sessionInputString(id: widget.id, value: data.text ?? ''); - // } - // }(); - // }, - // padding: padding, - // dismissOnClicked: true, - // )); - // } - } - - return displayMenu; - } - - bool _isWindowCanBeAdjusted(int remoteCount) { - if (remoteCount != 1) { - return false; - } - if (_screen == null) { - return false; - } - double scale = _screen!.scaleFactor; - double selfWidth = _screen!.visibleFrame.width; - double selfHeight = _screen!.visibleFrame.height; - if (isFullscreen) { - selfWidth = _screen!.frame.width; - selfHeight = _screen!.frame.height; - } - - final canvasModel = widget.ffi.canvasModel; - final displayWidth = canvasModel.getDisplayWidth(); - final displayHeight = canvasModel.getDisplayHeight(); - final requiredWidth = displayWidth + - (canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2); - final requiredHeight = displayHeight + - (canvasModel.tabBarHeight + canvasModel.windowBorderWidth * 2); - return selfWidth > (requiredWidth * scale) && - selfHeight > (requiredHeight * scale); - } - - List> _getDisplayMenu( - dynamic futureData, int remoteCount) { - const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); - final displayMenu = [ - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Scale original'), - value: kRemoteViewStyleOriginal, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Scale adaptive'), - value: kRemoteViewStyleAdaptive, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async { - // null means peer id is not found, which there's no need to care about - final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; - widget.state.viewStyle.value = viewStyle; - return viewStyle; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetViewStyle(id: widget.id, value: newValue); - widget.state.viewStyle.value = newValue; - widget.ffi.canvasModel.updateViewStyle(); - }, - padding: padding, - dismissOnClicked: true, - ), - MenuEntryDivider(), - MenuEntryRadios( - text: translate('Image Quality'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('Good image quality'), - value: kRemoteImageQualityBest, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Balanced'), - value: kRemoteImageQualityBalanced, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Optimize reaction time'), - value: kRemoteImageQualityLow, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Custom'), - value: kRemoteImageQualityCustom, - dismissOnClicked: true), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetImageQuality(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - if (oldValue != newValue) { - await bind.sessionSetImageQuality(id: widget.id, value: newValue); - } - - double qualityInitValue = 50; - double fpsInitValue = 30; - bool qualitySet = false; - bool fpsSet = false; - setCustomValues({double? quality, double? fps}) async { - if (quality != null) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: quality.toInt()); - } - if (fps != null) { - fpsSet = true; - await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); - } - if (!qualitySet) { - qualitySet = true; - await bind.sessionSetCustomImageQuality( - id: widget.id, value: qualityInitValue.toInt()); - } - if (!fpsSet) { - fpsSet = true; - await bind.sessionSetCustomFps( - id: widget.id, fps: fpsInitValue.toInt()); - } - } - - if (newValue == kRemoteImageQualityCustom) { - final btnClose = msgBoxButton(translate('Close'), () async { - await setCustomValues(); - widget.ffi.dialogManager.dismissAll(); - }); - - // quality - final quality = - await bind.sessionGetCustomImageQuality(id: widget.id); - qualityInitValue = quality != null && quality.isNotEmpty - ? quality[0].toDouble() - : 50.0; - const qualityMinValue = 10.0; - const qualityMaxValue = 100.0; - if (qualityInitValue < qualityMinValue) { - qualityInitValue = qualityMinValue; - } - if (qualityInitValue > qualityMaxValue) { - qualityInitValue = qualityMaxValue; - } - final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final debouncerQuality = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(quality: v); - }, - initialValue: qualityInitValue, - ); - final qualitySlider = Obx(() => Row( - children: [ - Slider( - value: qualitySliderValue.value, - min: qualityMinValue, - max: qualityMaxValue, - divisions: 90, - onChanged: (double value) { - qualitySliderValue.value = value; - debouncerQuality.value = value; - }, - ), - SizedBox( - width: 90, - child: Obx(() => Text( - '${qualitySliderValue.value.round()}% Bitrate', - style: const TextStyle(fontSize: 15), - ))) - ], - )); - // fps - final fpsOption = - await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); - fpsInitValue = - fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; - if (fpsInitValue < 10 || fpsInitValue > 120) { - fpsInitValue = 30; - } - final RxDouble fpsSliderValue = RxDouble(fpsInitValue); - final debouncerFps = Debouncer( - Duration(milliseconds: 1000), - onChanged: (double v) { - setCustomValues(fps: v); - }, - initialValue: qualityInitValue, - ); - bool? direct; - try { - direct = ConnectionTypeState.find(widget.id).direct.value == - ConnectionType.strDirect; - } catch (_) {} - final fpsSlider = Offstage( - offstage: - (await bind.mainIsUsingPublicServer() && direct != true) || - (await bind.versionToNumber( - v: widget.ffi.ffiModel.pi.version) < - await bind.versionToNumber(v: '1.2.0')), - child: Row( - children: [ - Obx((() => Slider( - value: fpsSliderValue.value, - min: 10, - max: 120, - divisions: 22, - onChanged: (double value) { - fpsSliderValue.value = value; - debouncerFps.value = value; - }, - ))), - SizedBox( - width: 90, - child: Obx(() { - final fps = fpsSliderValue.value.round(); - String text; - if (fps < 100) { - text = '$fps FPS'; - } else { - text = '$fps FPS'; - } - return Text( - text, - style: const TextStyle(fontSize: 15), - ); - })) - ], - ), - ); - - final content = Column( - children: [qualitySlider, fpsSlider], - ); - msgBoxCommon(widget.ffi.dialogManager, 'Custom Image Quality', - content, [btnClose]); - } - }, - padding: padding, - ), - MenuEntryDivider(), - ]; - - if (widget.state.viewStyle.value == kRemoteViewStyleOriginal) { - displayMenu.insert( - 2, - MenuEntryRadios( - text: translate('Scroll Style'), - optionsGetter: () => [ - MenuEntryRadioOption( - text: translate('ScrollAuto'), - value: kRemoteScrollStyleAuto, - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: translate('Scrollbar'), - value: kRemoteScrollStyleBar, - dismissOnClicked: true, - ), - ], - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetScrollStyle(id: widget.id) ?? '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetScrollStyle(id: widget.id, value: newValue); - widget.ffi.canvasModel.updateScrollStyle(); - }, - padding: padding, - dismissOnClicked: true, - )); - displayMenu.insert(3, MenuEntryDivider()); - } - - if (_isWindowCanBeAdjusted(remoteCount)) { - displayMenu.insert( - 0, - MenuEntryDivider(), - ); - displayMenu.insert( - 0, - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - child: Text( - translate('Adjust Window'), - style: style, - )), - proc: () { - () async { - await _updateScreen(); - if (_screen != null) { - _setFullscreen(false); - double scale = _screen!.scaleFactor; - final wndRect = - await WindowController.fromWindowId(windowId).getFrame(); - final mediaSize = MediaQueryData.fromWindow(ui.window).size; - // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. - // https://stackoverflow.com/a/7561083 - double magicWidth = - wndRect.right - wndRect.left - mediaSize.width * scale; - double magicHeight = - wndRect.bottom - wndRect.top - mediaSize.height * scale; - - final canvasModel = widget.ffi.canvasModel; - final width = (canvasModel.getDisplayWidth() + - canvasModel.windowBorderWidth * 2) * - scale + - magicWidth; - final height = (canvasModel.getDisplayHeight() + - canvasModel.tabBarHeight + - canvasModel.windowBorderWidth * 2) * - scale + - magicHeight; - double left = wndRect.left + (wndRect.width - width) / 2; - double top = wndRect.top + (wndRect.height - height) / 2; - - Rect frameRect = _screen!.frame; - if (!isFullscreen) { - frameRect = _screen!.visibleFrame; - } - if (left < frameRect.left) { - left = frameRect.left; - } - if (top < frameRect.top) { - top = frameRect.top; - } - if ((left + width) > frameRect.right) { - left = frameRect.right - width; - } - if ((top + height) > frameRect.bottom) { - top = frameRect.bottom - height; - } - await WindowController.fromWindowId(windowId) - .setFrame(Rect.fromLTWH(left, top, width, height)); - } - }(); - }, - padding: padding, - dismissOnClicked: true, - ), - ); - } - - /// Show Codec Preference - if (bind.mainHasHwcodec()) { - final List codecs = []; - try { - final Map codecsJson = jsonDecode(futureData['supportedHwcodec']); - final h264 = codecsJson['h264'] ?? false; - final h265 = codecsJson['h265'] ?? false; - codecs.add(h264); - codecs.add(h265); - } catch (e) { - debugPrint("Show Codec Preference err=$e"); - } - if (codecs.length == 2 && (codecs[0] || codecs[1])) { - displayMenu.add(MenuEntryRadios( - text: translate('Codec Preference'), - optionsGetter: () { - final list = [ - MenuEntryRadioOption( - text: translate('Auto'), - value: 'auto', - dismissOnClicked: true, - ), - MenuEntryRadioOption( - text: 'VP9', - value: 'vp9', - dismissOnClicked: true, - ), - ]; - if (codecs[0]) { - list.add(MenuEntryRadioOption( - text: 'H264', - value: 'h264', - dismissOnClicked: true, - )); - } - if (codecs[1]) { - list.add(MenuEntryRadioOption( - text: 'H265', - value: 'h265', - dismissOnClicked: true, - )); - } - return list; - }, - curOptionGetter: () async => - // null means peer id is not found, which there's no need to care about - await bind.sessionGetOption( - id: widget.id, arg: 'codec-preference') ?? - '', - optionSetter: (String oldValue, String newValue) async { - await bind.sessionPeerOption( - id: widget.id, name: 'codec-preference', value: newValue); - bind.sessionChangePreferCodec(id: widget.id); - }, - padding: padding, - dismissOnClicked: true, - )); - } - } - - /// Show remote cursor - if (!widget.ffi.canvasModel.cursorEmbedded) { - displayMenu.add(() { - final state = ShowRemoteCursorState.find(widget.id); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Show remote cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption( - id: widget.id, value: 'show-remote-cursor'); - }, - padding: padding, - dismissOnClicked: true, - ); - }()); - } - - /// Show remote cursor scaling with image - if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { - displayMenu.add(() { - final opt = 'zoom-cursor'; - final state = PeerBoolOption.find(widget.id, opt); - return MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Zoom cursor'), - getter: () { - return state; - }, - setter: (bool v) async { - state.value = v; - await bind.sessionToggleOption(id: widget.id, value: opt); - }, - padding: padding, - dismissOnClicked: true, - ); - }()); - } - - /// Show quality monitor - displayMenu.add(MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate('Show quality monitor'), - getter: () async { - return bind.sessionGetToggleOptionSync( - id: widget.id, arg: 'show-quality-monitor'); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'show-quality-monitor'); - widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); - }, - padding: padding, - dismissOnClicked: true, - )); - - final perms = widget.ffi.ffiModel.permissions; - final pi = widget.ffi.ffiModel.pi; - - if (perms['audio'] != false) { - displayMenu - .add(_createSwitchMenuEntry('Mute', 'disable-audio', padding, true)); - } - - if (Platform.isWindows && - pi.platform == 'Windows' && - perms['file'] != false) { - displayMenu.add(_createSwitchMenuEntry( - 'Allow file copy and paste', 'enable-file-transfer', padding, true)); - } - - if (perms['keyboard'] != false) { - if (perms['clipboard'] != false) { - displayMenu.add(_createSwitchMenuEntry( - 'Disable clipboard', 'disable-clipboard', padding, true)); - } - displayMenu.add(_createSwitchMenuEntry( - 'Lock after session end', 'lock-after-session-end', padding, true)); - if (pi.features.privacyMode) { - displayMenu.add(MenuEntrySwitch2( - switchType: SwitchType.scheckbox, - text: translate('Privacy mode'), - getter: () { - return PrivacyModeState.find(widget.id); - }, - setter: (bool v) async { - await bind.sessionToggleOption( - id: widget.id, value: 'privacy-mode'); - }, - padding: padding, - dismissOnClicked: true, - )); - } - } - return displayMenu; - } - - List> _getKeyboardMenu() { - final List> keyboardMenu = [ - MenuEntryRadios( - text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'), - MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), - ], - curOptionGetter: () async { - return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; - }, - optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); - widget.ffi.canvasModel.updateViewStyle(); - }, - ) - ]; - final localPlatform = - getLocalPlatformForKBLayoutType(widget.ffi.ffiModel.pi.platform); - if (localPlatform != '') { - keyboardMenu.add(MenuEntryDivider()); - keyboardMenu.add( - MenuEntryButton( - childBuilder: (TextStyle? style) => Container( - alignment: AlignmentDirectional.center, - height: _MenubarTheme.height, - child: Row( - children: [ - Obx(() => RichText( - text: TextSpan( - text: '${translate('Local keyboard type')}: ', - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: KBLayoutType.value, - style: TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - )), - Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Transform.scale( - scale: 0.8, - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.settings), - onPressed: () { - if (Navigator.canPop(context)) { - Navigator.pop(context); - } - showKBLayoutTypeChooser( - localPlatform, widget.ffi.dialogManager); - }, - ), - ), - )) - ], - )), - proc: () {}, - padding: EdgeInsets.zero, - dismissOnClicked: false, - ), - ); - } - return keyboardMenu; - } - - MenuEntrySwitch _createSwitchMenuEntry( - String text, String option, EdgeInsets? padding, bool dismissOnClicked) { - return MenuEntrySwitch( - switchType: SwitchType.scheckbox, - text: translate(text), - getter: () async { - return bind.sessionGetToggleOptionSync(id: widget.id, arg: option); - }, - setter: (bool v) async { - await bind.sessionToggleOption(id: widget.id, value: option); - }, - padding: padding, - dismissOnClicked: dismissOnClicked, - ); - } -} - -void showSetOSPassword( - String id, bool login, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var password = await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; - var autoLogin = await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; - controller.text = password; - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - bind.sessionPeerOption(id: id, name: 'os-password', value: text); - bind.sessionPeerOption( - id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); - if (text != '' && login) { - bind.sessionInputOsPassword(id: id, value: text); - } - close(); - } - - return CustomAlertDialog( - title: Text(translate('OS Password')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Auto Login'), - ), - value: autoLogin, - onChanged: (v) { - if (v == null) return; - setState(() => autoLogin = v); - }, - ), - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: close, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - -void showAuditDialog(String id, dialogManager) async { - final controller = TextEditingController(); - dialogManager.show((setState, close) { - submit() { - var text = controller.text.trim(); - if (text != '') { - bind.sessionSendNote(id: id, note: text); - } - close(); - } - - late final focusNode = FocusNode( - onKey: (FocusNode node, RawKeyEvent evt) { - if (evt.logicalKey.keyLabel == 'Enter') { - if (evt is RawKeyDownEvent) { - int pos = controller.selection.base.offset; - controller.text = - '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; - controller.selection = - TextSelection.fromPosition(TextPosition(offset: pos + 1)); - } - return KeyEventResult.handled; - } - if (evt.logicalKey.keyLabel == 'Esc') { - if (evt is RawKeyDownEvent) { - close(); - } - return KeyEventResult.handled; - } else { - return KeyEventResult.ignored; - } - }, - ); - - return CustomAlertDialog( - title: Text(translate('Note')), - content: SizedBox( - width: 250, - height: 120, - child: TextField( - autofocus: true, - keyboardType: TextInputType.multiline, - textInputAction: TextInputAction.newline, - decoration: const InputDecoration.collapsed( - hintText: 'input note here', - ), - // inputFormatters: [ - // LengthLimitingTextInputFormatter(16), - // // FilteringTextInputFormatter(RegExp(r'[a-zA-z][a-zA-z0-9\_]*'), allow: true) - // ], - maxLines: null, - maxLength: 256, - controller: controller, - focusNode: focusNode, - )), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: close, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), - ], - onSubmit: submit, - onCancel: close, - ); - }); -} - -class _DraggableShowHide extends StatefulWidget { - final RxDouble fractionX; - final RxBool dragging; - final RxBool show; - const _DraggableShowHide({ - Key? key, - required this.fractionX, - required this.dragging, - required this.show, - }) : super(key: key); - - @override - State<_DraggableShowHide> createState() => __DraggableShowHideState(); -} - -class __DraggableShowHideState extends State<_DraggableShowHide> { - Offset position = Offset.zero; - Size size = Size.zero; - - Widget _buildDraggable(BuildContext context) { - return Draggable( - axis: Axis.horizontal, - child: Icon( - Icons.drag_indicator, - size: 15, - ), - feedback: widget, - onDragStarted: (() { - final RenderObject? renderObj = context.findRenderObject(); - if (renderObj != null) { - final RenderBox renderBox = renderObj as RenderBox; - size = renderBox.size; - position = renderBox.localToGlobal(Offset.zero); - } - widget.dragging.value = true; - }), - onDragEnd: (details) { - final mediaSize = MediaQueryData.fromWindow(ui.window).size; - widget.fractionX.value += - (details.offset.dx - position.dx) / (mediaSize.width - size.width); - if (widget.fractionX.value < 0.35) { - widget.fractionX.value = 0.35; - } - if (widget.fractionX.value > 0.65) { - widget.fractionX.value = 0.65; - } - widget.dragging.value = false; - }, - ); - } - - @override - Widget build(BuildContext context) { - final ButtonStyle buttonStyle = ButtonStyle( - minimumSize: MaterialStateProperty.all(const Size(0, 0)), - padding: MaterialStateProperty.all(EdgeInsets.zero), - ); - final child = Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildDraggable(context), - TextButton( - onPressed: () => setState(() { - widget.show.value = !widget.show.value; - }), - child: Obx((() => Icon( - widget.show.isTrue ? Icons.expand_less : Icons.expand_more, - size: 15, - ))), - ), - ], - ); - return TextButtonTheme( - data: TextButtonThemeData(style: buttonStyle), - child: Container( - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: MyTheme.border), - ), - child: SizedBox( - height: 15, - child: child, - ), - ), - ); - } -} diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart new file mode 100644 index 000000000..45a1a55bc --- /dev/null +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -0,0 +1,2314 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:window_size/window_size.dart' as window_size; + +import '../../common.dart'; +import '../../common/widgets/dialog.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../../common/shared_state.dart'; +import './popup_menu.dart'; +import './kb_layout_type_chooser.dart'; + +const _kKeyLegacyMode = 'legacy'; +const _kKeyMapMode = 'map'; +const _kKeyTranslateMode = 'translate'; + +class MenubarState { + final kStoreKey = 'remoteMenubarState'; + late RxBool show; + late RxBool _pin; + RxString viewStyle = RxString(kRemoteViewStyleOriginal); + + MenubarState() { + final s = bind.getLocalFlutterConfig(k: kStoreKey); + if (s.isEmpty) { + _initSet(false, false); + return; + } + + try { + final m = jsonDecode(s); + if (m == null) { + _initSet(false, false); + } else { + _initSet(m['pin'] ?? false, m['pin'] ?? false); + } + } catch (e) { + debugPrint('Failed to decode menubar state ${e.toString()}'); + _initSet(false, false); + } + } + + _initSet(bool s, bool p) { + // Show remubar when connection is established. + show = RxBool(true); + _pin = RxBool(p); + } + + bool get pin => _pin.value; + + switchShow() async { + show.value = !show.value; + } + + setShow(bool v) async { + if (show.value != v) { + show.value = v; + } + } + + switchPin() async { + _pin.value = !_pin.value; + // Save everytime changed, as this func will not be called frequently + await _savePin(); + } + + setPin(bool v) async { + if (_pin.value != v) { + _pin.value = v; + // Save everytime changed, as this func will not be called frequently + await _savePin(); + } + } + + _savePin() async { + bind.setLocalFlutterConfig( + k: kStoreKey, v: jsonEncode({'pin': _pin.value})); + } + + save() async { + await _savePin(); + } +} + +class _MenubarTheme { + 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; + + static const double buttonSize = 32; + static const double buttonHMargin = 3; + static const double buttonVMargin = 6; + static const double iconRadius = 8; + static const double elevation = 3; +} + +typedef DismissFunc = void Function(); + +class RemoteMenuEntry { + static MenuEntryRadios viewStyle( + String remoteId, + FFI ffi, + EdgeInsets padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + RxString? rxViewStyle, + }) { + return MenuEntryRadios( + text: translate('Ratio'), + optionsGetter: () => [ + MenuEntryRadioOption( + text: translate('Scale original'), + value: kRemoteViewStyleOriginal, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ), + MenuEntryRadioOption( + text: translate('Scale adaptive'), + value: kRemoteViewStyleAdaptive, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ), + ], + curOptionGetter: () async { + // null means peer id is not found, which there's no need to care about + final viewStyle = await bind.sessionGetViewStyle(id: remoteId) ?? ''; + if (rxViewStyle != null) { + rxViewStyle.value = viewStyle; + } + return viewStyle; + }, + optionSetter: (String oldValue, String newValue) async { + await bind.sessionSetViewStyle(id: remoteId, value: newValue); + if (rxViewStyle != null) { + rxViewStyle.value = newValue; + } + ffi.canvasModel.updateViewStyle(); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } + + static MenuEntrySwitch2 showRemoteCursor( + String remoteId, + EdgeInsets padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + final state = ShowRemoteCursorState.find(remoteId); + final optKey = 'show-remote-cursor'; + return MenuEntrySwitch2( + switchType: SwitchType.scheckbox, + text: translate('Show remote cursor'), + getter: () { + return state; + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: remoteId, value: optKey); + state.value = + bind.sessionGetToggleOptionSync(id: remoteId, arg: optKey); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } + + static MenuEntrySwitch disableClipboard( + String remoteId, + EdgeInsets? padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return createSwitchMenuEntry( + remoteId, + 'Disable clipboard', + 'disable-clipboard', + padding, + true, + dismissCallback: dismissCallback, + ); + } + + static MenuEntrySwitch createSwitchMenuEntry( + String remoteId, + String text, + String option, + EdgeInsets? padding, + bool dismissOnClicked, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate(text), + getter: () async { + return bind.sessionGetToggleOptionSync(id: remoteId, arg: option); + }, + setter: (bool v) async { + await bind.sessionToggleOption(id: remoteId, value: option); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: dismissOnClicked, + dismissCallback: dismissCallback, + ); + } + + static MenuEntryButton insertLock( + String remoteId, + EdgeInsets? padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Insert Lock'), + style: style, + ), + proc: () { + bind.sessionLockScreen(id: remoteId); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } + + static insertCtrlAltDel( + String remoteId, + EdgeInsets? padding, { + DismissFunc? dismissFunc, + DismissCallback? dismissCallback, + }) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + '${translate("Insert")} Ctrl + Alt + Del', + style: style, + ), + proc: () { + bind.sessionCtrlAltDel(id: remoteId); + if (dismissFunc != null) { + dismissFunc(); + } + }, + padding: padding, + dismissOnClicked: true, + dismissCallback: dismissCallback, + ); + } +} + +class RemoteMenubar extends StatefulWidget { + final String id; + final FFI ffi; + final MenubarState state; + final Function(Function(bool)) onEnterOrLeaveImageSetter; + final Function() onEnterOrLeaveImageCleaner; + + RemoteMenubar({ + Key? key, + required this.id, + required this.ffi, + required this.state, + required this.onEnterOrLeaveImageSetter, + required this.onEnterOrLeaveImageCleaner, + }) : super(key: key); + + @override + State createState() => _RemoteMenubarState(); +} + +class _RemoteMenubarState extends State { + late Debouncer _debouncerHide; + bool _isCursorOverImage = false; + final _fractionX = 0.5.obs; + final _dragging = false.obs; + + int get windowId => stateGlobal.windowId; + + bool get isFullscreen => stateGlobal.fullscreen; + void _setFullscreen(bool v) { + stateGlobal.setFullscreen(v); + setState(() {}); + } + + RxBool get show => widget.state.show; + bool get pin => widget.state.pin; + + @override + initState() { + super.initState(); + + _debouncerHide = Debouncer( + Duration(milliseconds: 5000), + onChanged: _debouncerHideProc, + initialValue: 0, + ); + + widget.onEnterOrLeaveImageSetter((enter) { + if (enter) { + _debouncerHide.value = 0; + _isCursorOverImage = true; + } else { + _isCursorOverImage = false; + } + }); + } + + _debouncerHideProc(int v) { + if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) { + show.value = false; + } + } + + @override + dispose() { + super.dispose(); + + widget.onEnterOrLeaveImageCleaner(); + } + + @override + Widget build(BuildContext context) { + // No need to use future builder here. + return Align( + alignment: Alignment.topCenter, + child: Obx(() => show.value + ? _buildToolbar(context) + : _buildDraggableShowHide(context)), + ); + } + + Widget _buildDraggableShowHide(BuildContext context) { + return Obx(() { + if (show.isTrue && _dragging.isFalse) { + _debouncerHide.value = 1; + } + return Align( + alignment: FractionalOffset(_fractionX.value, 0), + child: Offstage( + offstage: _dragging.isTrue, + child: Material( + elevation: _MenubarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, + child: _DraggableShowHide( + dragging: _dragging, + fractionX: _fractionX, + show: show, + ), + ), + ), + ); + }); + } + + Widget _buildToolbar(BuildContext context) { + final List toolbarItems = []; + if (!isWebDesktop) { + toolbarItems.add(_PinMenu(state: widget.state)); + toolbarItems.add( + _FullscreenMenu(state: widget.state, setFullscreen: _setFullscreen)); + toolbarItems.add(_MobileActionMenu(ffi: widget.ffi)); + } + + if (PrivacyModeState.find(widget.id).isFalse && + widget.ffi.ffiModel.pi.displays.length > 1) { + toolbarItems.add( + bind.mainGetUserDefaultOption(key: 'show_monitors_toolbar') == 'Y' + ? _MultiMonitorMenu(id: widget.id, ffi: widget.ffi) + : _MonitorMenu(id: widget.id, ffi: widget.ffi), + ); + } + + toolbarItems + .add(_ControlMenu(id: widget.id, ffi: widget.ffi, state: widget.state)); + toolbarItems.add(_DisplayMenu( + id: widget.id, + ffi: widget.ffi, + state: widget.state, + setFullscreen: _setFullscreen, + )); + toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + if (!isWeb) { + toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); + toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); + } + toolbarItems.add(_RecordMenu()); + toolbarItems.add(_CloseMenu(id: widget.id, ffi: widget.ffi)); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Material( + elevation: _MenubarTheme.elevation, + shadowColor: MyTheme.color(context).shadow, + borderRadius: BorderRadius.all(Radius.circular(4.0)), + color: Theme.of(context) + .menuBarTheme + .style + ?.backgroundColor + ?.resolve(MaterialState.values.toSet()), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Theme( + data: themeData(), + child: Row( + children: [ + SizedBox(width: _MenubarTheme.buttonHMargin * 2), + ...toolbarItems, + SizedBox(width: _MenubarTheme.buttonHMargin * 2) + ], + ), + ), + ), + ), + _buildDraggableShowHide(context), + ], + ); + } + + ThemeData themeData() { + return Theme.of(context).copyWith( + menuButtonTheme: MenuButtonThemeData( + style: ButtonStyle( + minimumSize: MaterialStatePropertyAll(Size(64, 36)), + textStyle: MaterialStatePropertyAll( + TextStyle(fontWeight: FontWeight.normal), + ), + ), + ), + dividerTheme: DividerThemeData(space: 4), + menuBarTheme: MenuBarThemeData( + style: MenuStyle( + padding: MaterialStatePropertyAll(EdgeInsets.zero), + elevation: MaterialStatePropertyAll(0), + shape: MaterialStatePropertyAll(BeveledRectangleBorder()), + ).copyWith( + backgroundColor: + Theme.of(context).menuBarTheme.style?.backgroundColor)), + ); + } +} + +class _PinMenu extends StatelessWidget { + final MenubarState state; + const _PinMenu({Key? key, required this.state}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () => _IconMenuButton( + assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg", + tooltip: state.pin ? 'Unpin menubar' : 'Pin menubar', + onPressed: state.switchPin, + color: state.pin ? _MenubarTheme.blueColor : Colors.grey[800]!, + hoverColor: + state.pin ? _MenubarTheme.hoverBlueColor : Colors.grey[850]!, + ), + ); + } +} + +class _FullscreenMenu extends StatelessWidget { + final MenubarState state; + final Function(bool) setFullscreen; + bool get isFullscreen => stateGlobal.fullscreen; + const _FullscreenMenu( + {Key? key, required this.state, required this.setFullscreen}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconMenuButton( + assetName: + isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg", + tooltip: isFullscreen ? 'Exit Fullscreen' : 'Fullscreen', + onPressed: () => setFullscreen(!isFullscreen), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ); + } +} + +class _MobileActionMenu extends StatelessWidget { + final FFI ffi; + const _MobileActionMenu({Key? key, required this.ffi}) : super(key: key); + + @override + Widget build(BuildContext context) { + if (!ffi.ffiModel.isPeerAndroid) return Offstage(); + return _IconMenuButton( + assetName: 'assets/actions_mobile.svg', + tooltip: 'Mobile Actions', + onPressed: () => ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi), + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ); + } +} + +class _MonitorMenu extends StatelessWidget { + final String id; + final FFI ffi; + const _MonitorMenu({Key? key, required this.id, required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconSubmenuButton( + tooltip: 'Select Monitor', + icon: icon(), + ffi: ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuStyle: MenuStyle( + padding: + MaterialStatePropertyAll(EdgeInsets.symmetric(horizontal: 6))), + menuChildren: [Row(children: displays(context))]); + } + + icon() { + final pi = ffi.ffiModel.pi; + return Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/screen.svg", + color: Colors.white, + ), + Obx(() { + RxInt display = CurrentDisplayState.find(id); + return Text( + '${display.value + 1}/${pi.displays.length}', + style: const TextStyle( + color: _MenubarTheme.blueColor, + fontSize: 8, + fontWeight: FontWeight.bold, + ), + ); + }), + ], + ); + } + + List displays(BuildContext context) { + final List rowChildren = []; + final pi = ffi.ffiModel.pi; + for (int i = 0; i < pi.displays.length; i++) { + rowChildren.add(_IconMenuButton( + topLevel: false, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + tooltip: "", + hMargin: 6, + vMargin: 12, + icon: Container( + alignment: AlignmentDirectional.center, + constraints: const BoxConstraints(minHeight: _MenubarTheme.height), + child: Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/screen.svg", + color: Colors.white, + ), + Text( + (i + 1).toString(), + style: TextStyle( + color: _MenubarTheme.blueColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + onPressed: () { + _menuDismissCallback(ffi); + RxInt display = CurrentDisplayState.find(id); + if (display.value != i) { + bind.sessionSwitchDisplay(id: id, value: i); + } + }, + )); + } + return rowChildren; + } +} + +class _ControlMenu extends StatelessWidget { + final String id; + final FFI ffi; + final MenubarState state; + _ControlMenu( + {Key? key, required this.id, required this.ffi, required this.state}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconSubmenuButton( + tooltip: 'Control Actions', + svg: "assets/actions.svg", + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + ffi: ffi, + menuChildren: [ + requestElevation(), + osPassword(), + transferFile(context), + tcpTunneling(context), + note(), + Divider(), + ctrlAltDel(), + restart(), + insertLock(), + blockUserInput(), + switchSides(), + refresh(), + ]); + } + + requestElevation() { + final visible = ffi.elevationModel.showRequestMenu; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Request Elevation')), + ffi: ffi, + onPressed: () => showRequestElevationDialog(id, ffi.dialogManager)); + } + + osPassword() { + return _MenuItemButton( + child: Text(translate('OS Password')), + trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)), + ffi: ffi, + onPressed: () => _showSetOSPassword(id, false, ffi.dialogManager)); + } + + _showSetOSPassword( + String id, bool login, OverlayDialogManager dialogManager) async { + final controller = TextEditingController(); + var password = + await bind.sessionGetOption(id: id, arg: 'os-password') ?? ''; + var autoLogin = + await bind.sessionGetOption(id: id, arg: 'auto-login') != ''; + controller.text = password; + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: 'os-password', value: text); + bind.sessionPeerOption( + id: id, name: 'auto-login', value: autoLogin ? 'Y' : ''); + if (text != '' && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + } + + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('OS Password')).paddingOnly(left: 10), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + transferFile(BuildContext context) { + return _MenuItemButton( + child: Text(translate('Transfer File')), + ffi: ffi, + onPressed: () => connect(context, id, isFileTransfer: true)); + } + + tcpTunneling(BuildContext context) { + return _MenuItemButton( + child: Text(translate('TCP Tunneling')), + ffi: ffi, + onPressed: () => connect(context, id, isTcpTunneling: true)); + } + + note() { + final auditServer = bind.sessionGetAuditServerSync(id: id, typ: "conn"); + final visible = auditServer.isNotEmpty; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Note')), + ffi: ffi, + onPressed: () => _showAuditDialog(id, ffi.dialogManager), + ); + } + + _showAuditDialog(String id, dialogManager) async { + final controller = TextEditingController(); + dialogManager.show((setState, close) { + submit() { + var text = controller.text.trim(); + if (text != '') { + bind.sessionSendNote(id: id, note: text); + } + close(); + } + + late final focusNode = FocusNode( + onKey: (FocusNode node, RawKeyEvent evt) { + if (evt.logicalKey.keyLabel == 'Enter') { + if (evt is RawKeyDownEvent) { + int pos = controller.selection.base.offset; + controller.text = + '${controller.text.substring(0, pos)}\n${controller.text.substring(pos)}'; + controller.selection = + TextSelection.fromPosition(TextPosition(offset: pos + 1)); + } + return KeyEventResult.handled; + } + if (evt.logicalKey.keyLabel == 'Esc') { + if (evt is RawKeyDownEvent) { + close(); + } + return KeyEventResult.handled; + } else { + return KeyEventResult.ignored; + } + }, + ); + + return CustomAlertDialog( + title: Text(translate('Note')), + content: SizedBox( + width: 250, + height: 120, + child: TextField( + autofocus: true, + keyboardType: TextInputType.multiline, + textInputAction: TextInputAction.newline, + decoration: const InputDecoration.collapsed( + hintText: 'input note here', + ), + maxLines: null, + maxLength: 256, + controller: controller, + focusNode: focusNode, + )), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit) + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + ctrlAltDel() { + final perms = ffi.ffiModel.permissions; + final viewOnly = ffi.ffiModel.viewOnly; + final pi = ffi.ffiModel.pi; + final visible = !viewOnly && + perms['keyboard'] != false && + (pi.platform == kPeerPlatformLinux || pi.sasEnabled); + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text('${translate("Insert")} Ctrl + Alt + Del'), + ffi: ffi, + onPressed: () => bind.sessionCtrlAltDel(id: id)); + } + + restart() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['restart'] != false && + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS); + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Restart Remote Device')), + ffi: ffi, + onPressed: () => showRestartRemoteDevice(pi, id, ffi.dialogManager)); + } + + insertLock() { + final perms = ffi.ffiModel.permissions; + final viewOnly = ffi.ffiModel.viewOnly; + final visible = !viewOnly && perms['keyboard'] != false; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Insert Lock')), + ffi: ffi, + onPressed: () => bind.sessionLockScreen(id: id)); + } + + blockUserInput() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = + perms['keyboard'] != false && pi.platform == kPeerPlatformWindows; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Obx(() => Text(translate( + '${BlockInputState.find(id).value ? 'Unb' : 'B'}lock user input'))), + ffi: ffi, + onPressed: () { + RxBool blockInput = BlockInputState.find(id); + bind.sessionToggleOption( + id: id, value: '${blockInput.value ? 'un' : ''}block-input'); + blockInput.value = !blockInput.value; + }); + } + + switchSides() { + final perms = ffi.ffiModel.permissions; + final pi = ffi.ffiModel.pi; + final visible = perms['keyboard'] != false && + pi.platform != kPeerPlatformAndroid && + pi.platform != kPeerPlatformMacOS && + version_cmp(pi.version, '1.2.0') >= 0; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Switch Sides')), + ffi: ffi, + onPressed: () => _showConfirmSwitchSidesDialog(id, ffi.dialogManager)); + } + + void _showConfirmSwitchSidesDialog( + String id, OverlayDialogManager dialogManager) async { + dialogManager.show((setState, close) { + submit() async { + await bind.sessionSwitchSides(id: id); + closeConnection(id: id); + } + + return CustomAlertDialog( + content: msgboxContent('info', 'Switch Sides', + 'Please confirm if you want to share your desktop?'), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); + } + + refresh() { + final pi = ffi.ffiModel.pi; + final visible = pi.version.isNotEmpty; + if (!visible) return Offstage(); + return _MenuItemButton( + child: Text(translate('Refresh')), + ffi: ffi, + onPressed: () => bind.sessionRefresh(id: id)); + } +} + +class _DisplayMenu extends StatefulWidget { + final String id; + final FFI ffi; + final MenubarState state; + final Function(bool) setFullscreen; + _DisplayMenu( + {Key? key, + required this.id, + required this.ffi, + required this.state, + required this.setFullscreen}) + : super(key: key); + + @override + State<_DisplayMenu> createState() => _DisplayMenuState(); +} + +class _DisplayMenuState extends State<_DisplayMenu> { + window_size.Screen? _screen; + + bool get isFullscreen => stateGlobal.fullscreen; + + int get windowId => stateGlobal.windowId; + + Map get perms => widget.ffi.ffiModel.permissions; + + PeerInfo get pi => widget.ffi.ffiModel.pi; + + @override + Widget build(BuildContext context) { + _updateScreen(); + return _IconSubmenuButton( + tooltip: 'Display Settings', + svg: "assets/display.svg", + ffi: widget.ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [ + adjustWindow(), + viewStyle(), + scrollStyle(), + imageQuality(), + codec(), + resolutions(), + Divider(), + showRemoteCursor(), + zoomCursor(), + showQualityMonitor(), + mute(), + fileCopyAndPaste(), + disableClipboard(), + lockAfterSessionEnd(), + privacyMode(), + swapKey(), + ]); + } + + adjustWindow() { + final visible = _isWindowCanBeAdjusted(); + if (!visible) return Offstage(); + return Column( + children: [ + _MenuItemButton( + child: Text(translate('Adjust Window')), + onPressed: _doAdjustWindow, + ffi: widget.ffi), + Divider(), + ], + ); + } + + _doAdjustWindow() async { + await _updateScreen(); + if (_screen != null) { + widget.setFullscreen(false); + double scale = _screen!.scaleFactor; + final wndRect = await WindowController.fromWindowId(windowId).getFrame(); + final mediaSize = MediaQueryData.fromWindow(ui.window).size; + // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect. + // https://stackoverflow.com/a/7561083 + double magicWidth = + wndRect.right - wndRect.left - mediaSize.width * scale; + double magicHeight = + wndRect.bottom - wndRect.top - mediaSize.height * scale; + + final canvasModel = widget.ffi.canvasModel; + final width = (canvasModel.getDisplayWidth() * canvasModel.scale + + CanvasModel.leftToEdge + + CanvasModel.rightToEdge) * + scale + + magicWidth; + final height = (canvasModel.getDisplayHeight() * canvasModel.scale + + CanvasModel.topToEdge + + CanvasModel.bottomToEdge) * + scale + + magicHeight; + double left = wndRect.left + (wndRect.width - width) / 2; + double top = wndRect.top + (wndRect.height - height) / 2; + + Rect frameRect = _screen!.frame; + if (!isFullscreen) { + frameRect = _screen!.visibleFrame; + } + if (left < frameRect.left) { + left = frameRect.left; + } + if (top < frameRect.top) { + top = frameRect.top; + } + if ((left + width) > frameRect.right) { + left = frameRect.right - width; + } + if ((top + height) > frameRect.bottom) { + top = frameRect.bottom - height; + } + await WindowController.fromWindowId(windowId) + .setFrame(Rect.fromLTWH(left, top, width, height)); + } + } + + _updateScreen() async { + final v = await rustDeskWinManager.call( + WindowType.Main, kWindowGetWindowInfo, ''); + final String valueStr = v; + if (valueStr.isEmpty) { + _screen = null; + } else { + final screenMap = jsonDecode(valueStr); + _screen = window_size.Screen( + Rect.fromLTRB(screenMap['frame']['l'], screenMap['frame']['t'], + screenMap['frame']['r'], screenMap['frame']['b']), + Rect.fromLTRB( + screenMap['visibleFrame']['l'], + screenMap['visibleFrame']['t'], + screenMap['visibleFrame']['r'], + screenMap['visibleFrame']['b']), + screenMap['scaleFactor']); + } + } + + _isWindowCanBeAdjusted() { + if (widget.state.viewStyle.value != kRemoteViewStyleOriginal) { + return false; + } + final remoteCount = RemoteCountState.find().value; + if (remoteCount != 1) { + return false; + } + if (_screen == null) { + return false; + } + final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor; + double selfWidth = _screen!.visibleFrame.width; + double selfHeight = _screen!.visibleFrame.height; + if (isFullscreen) { + selfWidth = _screen!.frame.width; + selfHeight = _screen!.frame.height; + } + + final canvasModel = widget.ffi.canvasModel; + final displayWidth = canvasModel.getDisplayWidth(); + final displayHeight = canvasModel.getDisplayHeight(); + final requiredWidth = + CanvasModel.leftToEdge + displayWidth + CanvasModel.rightToEdge; + final requiredHeight = + CanvasModel.topToEdge + displayHeight + CanvasModel.bottomToEdge; + return selfWidth > (requiredWidth * scale) && + selfHeight > (requiredHeight * scale); + } + + viewStyle() { + return futureBuilder(future: () async { + final viewStyle = await bind.sessionGetViewStyle(id: widget.id) ?? ''; + widget.state.viewStyle.value = viewStyle; + return viewStyle; + }(), hasData: (data) { + final groupValue = data as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetViewStyle(id: widget.id, value: value); + widget.state.viewStyle.value = value; + widget.ffi.canvasModel.updateViewStyle(); + } + + return Column(children: [ + _RadioMenuButton( + child: Text(translate('Scale original')), + value: kRemoteViewStyleOriginal, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Scale adaptive')), + value: kRemoteViewStyleAdaptive, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + Divider(), + ]); + }); + } + + scrollStyle() { + final visible = widget.state.viewStyle.value == kRemoteViewStyleOriginal; + if (!visible) return Offstage(); + return futureBuilder(future: () async { + final scrollStyle = await bind.sessionGetScrollStyle(id: widget.id) ?? ''; + return scrollStyle; + }(), hasData: (data) { + final groupValue = data as String; + onChange(String? value) async { + if (value == null) return; + await bind.sessionSetScrollStyle(id: widget.id, value: value); + widget.ffi.canvasModel.updateScrollStyle(); + } + + final enabled = widget.ffi.canvasModel.imageOverflow.value; + return Column(children: [ + _RadioMenuButton( + child: Text(translate('ScrollAuto')), + value: kRemoteScrollStyleAuto, + groupValue: groupValue, + onChanged: enabled ? (value) => onChange(value) : null, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Scrollbar')), + value: kRemoteScrollStyleBar, + groupValue: groupValue, + onChanged: enabled ? (value) => onChange(value) : null, + ffi: widget.ffi, + ), + Divider(), + ]); + }); + } + + imageQuality() { + return futureBuilder(future: () async { + final imageQuality = + await bind.sessionGetImageQuality(id: widget.id) ?? ''; + return imageQuality; + }(), hasData: (data) { + final groupValue = data as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetImageQuality(id: widget.id, value: value); + } + + return _SubmenuButton( + ffi: widget.ffi, + child: Text(translate('Image Quality')), + menuChildren: [ + _RadioMenuButton( + child: Text(translate('Good image quality')), + value: kRemoteImageQualityBest, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Balanced')), + value: kRemoteImageQualityBalanced, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Optimize reaction time')), + value: kRemoteImageQualityLow, + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('Custom')), + value: kRemoteImageQualityCustom, + groupValue: groupValue, + onChanged: (value) { + onChanged(value); + _customImageQualityDialog(); + }, + ffi: widget.ffi, + ), + ], + ); + }); + } + + _customImageQualityDialog() async { + double qualityInitValue = 50; + double fpsInitValue = 30; + bool qualitySet = false; + bool fpsSet = false; + setCustomValues({double? quality, double? fps}) async { + if (quality != null) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: quality.toInt()); + } + if (fps != null) { + fpsSet = true; + await bind.sessionSetCustomFps(id: widget.id, fps: fps.toInt()); + } + if (!qualitySet) { + qualitySet = true; + await bind.sessionSetCustomImageQuality( + id: widget.id, value: qualityInitValue.toInt()); + } + if (!fpsSet) { + fpsSet = true; + await bind.sessionSetCustomFps( + id: widget.id, fps: fpsInitValue.toInt()); + } + } + + final btnClose = dialogButton('Close', onPressed: () async { + await setCustomValues(); + widget.ffi.dialogManager.dismissAll(); + }); + + // quality + final quality = await bind.sessionGetCustomImageQuality(id: widget.id); + qualityInitValue = + quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0; + const qualityMinValue = 10.0; + const qualityMaxValue = 100.0; + if (qualityInitValue < qualityMinValue) { + qualityInitValue = qualityMinValue; + } + if (qualityInitValue > qualityMaxValue) { + qualityInitValue = qualityMaxValue; + } + final RxDouble qualitySliderValue = RxDouble(qualityInitValue); + final debouncerQuality = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(quality: v); + }, + initialValue: qualityInitValue, + ); + final qualitySlider = Obx(() => Row( + children: [ + Slider( + value: qualitySliderValue.value, + min: qualityMinValue, + max: qualityMaxValue, + divisions: 18, + onChanged: (double value) { + qualitySliderValue.value = value; + debouncerQuality.value = value; + }, + ), + SizedBox( + width: 40, + child: Text( + '${qualitySliderValue.value.round()}%', + style: const TextStyle(fontSize: 15), + )), + SizedBox( + width: 50, + child: Text( + translate('Bitrate'), + style: const TextStyle(fontSize: 15), + )) + ], + )); + // fps + final fpsOption = + await bind.sessionGetOption(id: widget.id, arg: 'custom-fps'); + fpsInitValue = fpsOption == null ? 30 : double.tryParse(fpsOption) ?? 30; + if (fpsInitValue < 10 || fpsInitValue > 120) { + fpsInitValue = 30; + } + final RxDouble fpsSliderValue = RxDouble(fpsInitValue); + final debouncerFps = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(fps: v); + }, + initialValue: qualityInitValue, + ); + bool? direct; + try { + direct = ConnectionTypeState.find(widget.id).direct.value == + ConnectionType.strDirect; + } catch (_) {} + final fpsSlider = Offstage( + offstage: (await bind.mainIsUsingPublicServer() && direct != true) || + version_cmp(pi.version, '1.2.0') < 0, + child: Row( + children: [ + Obx((() => Slider( + value: fpsSliderValue.value, + min: 10, + max: 120, + divisions: 22, + onChanged: (double value) { + fpsSliderValue.value = value; + debouncerFps.value = value; + }, + ))), + SizedBox( + width: 40, + child: Obx(() => Text( + '${fpsSliderValue.value.round()}', + style: const TextStyle(fontSize: 15), + ))), + SizedBox( + width: 50, + child: Text( + translate('FPS'), + style: const TextStyle(fontSize: 15), + )) + ], + ), + ); + + final content = Column( + children: [qualitySlider, fpsSlider], + ); + msgBoxCommon( + widget.ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); + } + + codec() { + return futureBuilder(future: () async { + final supportedHwcodec = + await bind.sessionSupportedHwcodec(id: widget.id); + final codecPreference = + await bind.sessionGetOption(id: widget.id, arg: 'codec-preference') ?? + ''; + return { + 'supportedHwcodec': supportedHwcodec, + 'codecPreference': codecPreference + }; + }(), hasData: (data) { + final List codecs = []; + try { + final Map codecsJson = jsonDecode(data['supportedHwcodec']); + final h264 = codecsJson['h264'] ?? false; + final h265 = codecsJson['h265'] ?? false; + codecs.add(h264); + codecs.add(h265); + } catch (e) { + debugPrint("Show Codec Preference err=$e"); + } + final visible = bind.mainHasHwcodec() && + codecs.length == 2 && + (codecs[0] || codecs[1]); + if (!visible) return Offstage(); + final groupValue = data['codecPreference'] as String; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionPeerOption( + id: widget.id, name: 'codec-preference', value: value); + bind.sessionChangePreferCodec(id: widget.id); + } + + return _SubmenuButton( + ffi: widget.ffi, + child: Text(translate('Codec')), + menuChildren: [ + _RadioMenuButton( + child: Text(translate('Auto')), + value: 'auto', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('VP9')), + value: 'vp9', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('H264')), + value: 'h264', + groupValue: groupValue, + onChanged: codecs[0] ? onChanged : null, + ffi: widget.ffi, + ), + _RadioMenuButton( + child: Text(translate('H265')), + value: 'h265', + groupValue: groupValue, + onChanged: codecs[1] ? onChanged : null, + ffi: widget.ffi, + ), + ]); + }); + } + + resolutions() { + final resolutions = widget.ffi.ffiModel.pi.resolutions; + final visible = widget.ffi.ffiModel.permissions["keyboard"] != false && + resolutions.length > 1; + if (!visible) return Offstage(); + final display = widget.ffi.ffiModel.display; + final groupValue = "${display.width}x${display.height}"; + onChanged(String? value) async { + if (value == null) return; + final list = value.split('x'); + if (list.length == 2) { + final w = int.tryParse(list[0]); + final h = int.tryParse(list[1]); + if (w != null && h != null) { + await bind.sessionChangeResolution( + id: widget.id, width: w, height: h); + Future.delayed(Duration(seconds: 3), () async { + final display = widget.ffi.ffiModel.display; + if (w == display.width && h == display.height) { + if (_isWindowCanBeAdjusted()) { + _doAdjustWindow(); + } + } + }); + } + } + } + + return _SubmenuButton( + ffi: widget.ffi, + menuChildren: resolutions + .map((e) => _RadioMenuButton( + value: '${e.width}x${e.height}', + groupValue: groupValue, + onChanged: onChanged, + ffi: widget.ffi, + child: Text('${e.width}x${e.height}'))) + .toList(), + child: Text(translate("Resolution"))); + } + + showRemoteCursor() { + if (widget.ffi.ffiModel.pi.platform == kPeerPlatformAndroid) { + return Offstage(); + } + final ffiModel = widget.ffi.ffiModel; + final visible = + !widget.ffi.canvasModel.cursorEmbedded && !ffiModel.pi.is_wayland; + if (!visible) return Offstage(); + final enabled = !ffiModel.viewOnly; + final state = ShowRemoteCursorState.find(widget.id); + final option = 'show-remote-cursor'; + return _CheckboxMenuButton( + value: state.value, + onChanged: enabled + ? (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + state.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + } + : null, + ffi: widget.ffi, + child: Text(translate('Show remote cursor'))); + } + + zoomCursor() { + if (widget.ffi.ffiModel.pi.platform == kPeerPlatformAndroid) { + return Offstage(); + } + final visible = widget.state.viewStyle.value != kRemoteViewStyleOriginal; + if (!visible) return Offstage(); + final option = 'zoom-cursor'; + final peerState = PeerBoolOption.find(widget.id, option); + return _CheckboxMenuButton( + value: peerState.value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + peerState.value = + bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + }, + ffi: widget.ffi, + child: Text(translate('Zoom cursor'))); + } + + showQualityMonitor() { + final option = 'show-quality-monitor'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: widget.id, value: option); + widget.ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + }, + ffi: widget.ffi, + child: Text(translate('Show quality monitor'))); + } + + mute() { + final visible = perms['audio'] != false; + if (!visible) return Offstage(); + final option = 'disable-audio'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Mute'))); + } + + fileCopyAndPaste() { + final visible = Platform.isWindows && + pi.platform == kPeerPlatformWindows && + perms['file'] != false; + if (!visible) return Offstage(); + final option = 'enable-file-transfer'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Allow file copy and paste'))); + } + + disableClipboard() { + final ffiModel = widget.ffi.ffiModel; + final visible = perms['keyboard'] != false && perms['clipboard'] != false; + if (!visible) return Offstage(); + final enabled = !ffiModel.viewOnly; + final option = 'disable-clipboard'; + var value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + if (ffiModel.viewOnly) value = true; + return _CheckboxMenuButton( + value: value, + onChanged: enabled + ? (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + } + : null, + ffi: widget.ffi, + child: Text(translate('Disable clipboard'))); + } + + lockAfterSessionEnd() { + final visible = perms['keyboard'] != false; + if (!visible) return Offstage(); + final option = 'lock-after-session-end'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Lock after session end'))); + } + + privacyMode() { + bool visible = perms['keyboard'] != false && pi.features.privacyMode; + if (!visible) return Offstage(); + final option = 'privacy-mode'; + final rxValue = PrivacyModeState.find(widget.id); + return _CheckboxMenuButton( + value: rxValue.value, + onChanged: (value) { + if (value == null) return; + if (widget.ffi.ffiModel.pi.currentDisplay != 0) { + msgBox( + widget.id, + 'custom-nook-nocancel-hasclose', + 'info', + 'Please switch to Display 1 first', + '', + widget.ffi.dialogManager); + return; + } + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Privacy mode'))); + } + + swapKey() { + final visible = perms['keyboard'] != false && + ((Platform.isMacOS && pi.platform != kPeerPlatformMacOS) || + (!Platform.isMacOS && pi.platform == kPeerPlatformMacOS)); + if (!visible) return Offstage(); + final option = 'allow_swap_key'; + final value = bind.sessionGetToggleOptionSync(id: widget.id, arg: option); + return _CheckboxMenuButton( + value: value, + onChanged: (value) { + if (value == null) return; + bind.sessionToggleOption(id: widget.id, value: option); + }, + ffi: widget.ffi, + child: Text(translate('Swap control-command key'))); + } +} + +class _KeyboardMenu extends StatelessWidget { + final String id; + final FFI ffi; + _KeyboardMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + PeerInfo get pi => ffi.ffiModel.pi; + + @override + Widget build(BuildContext context) { + var ffiModel = Provider.of(context); + if (ffiModel.permissions['keyboard'] == false) return Offstage(); + if (stateGlobal.grabKeyboard) { + if (bind.sessionIsKeyboardModeSupported(id: id, mode: _kKeyMapMode)) { + bind.sessionSetKeyboardMode(id: id, value: _kKeyMapMode); + } else if (bind.sessionIsKeyboardModeSupported( + id: id, mode: _kKeyLegacyMode)) { + bind.sessionSetKeyboardMode(id: id, value: _kKeyLegacyMode); + } + return Offstage(); + } + return _IconSubmenuButton( + tooltip: 'Keyboard Settings', + svg: "assets/keyboard.svg", + ffi: ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [ + mode(), + localKeyboardType(), + Divider(), + view_mode(), + ]); + } + + mode() { + return futureBuilder(future: () async { + return await bind.sessionGetKeyboardMode(id: id) ?? _kKeyLegacyMode; + }(), hasData: (data) { + final groupValue = data as String; + List modes = [ + KeyboardModeMenu(key: _kKeyLegacyMode, menu: 'Legacy mode'), + KeyboardModeMenu(key: _kKeyMapMode, menu: 'Map mode'), + KeyboardModeMenu(key: _kKeyTranslateMode, menu: 'Translate mode'), + ]; + List<_RadioMenuButton> list = []; + final enabled = !ffi.ffiModel.viewOnly; + onChanged(String? value) async { + if (value == null) return; + await bind.sessionSetKeyboardMode(id: id, value: value); + } + + for (KeyboardModeMenu mode in modes) { + if (bind.sessionIsKeyboardModeSupported(id: id, mode: mode.key)) { + if (pi.is_wayland && mode.key != _kKeyMapMode) { + continue; + } + var text = translate(mode.menu); + if (mode.key == _kKeyTranslateMode) { + text = '$text beta'; + } + list.add(_RadioMenuButton( + child: Text(text), + value: mode.key, + groupValue: groupValue, + onChanged: enabled ? onChanged : null, + ffi: ffi, + )); + } + } + return Column(children: list); + }); + } + + localKeyboardType() { + final localPlatform = getLocalPlatformForKBLayoutType(pi.platform); + final visible = localPlatform != ''; + if (!visible) return Offstage(); + final enabled = !ffi.ffiModel.viewOnly; + return Column( + children: [ + Divider(), + _MenuItemButton( + child: Text( + '${translate('Local keyboard type')}: ${KBLayoutType.value}'), + trailingIcon: const Icon(Icons.settings), + ffi: ffi, + onPressed: enabled + ? () => showKBLayoutTypeChooser(localPlatform, ffi.dialogManager) + : null, + ) + ], + ); + } + + view_mode() { + final ffiModel = ffi.ffiModel; + final enabled = version_cmp(pi.version, '1.2.0') >= 0 && + ffiModel.permissions["keyboard"] != false; + return _CheckboxMenuButton( + value: ffiModel.viewOnly, + onChanged: enabled + ? (value) async { + if (value == null) return; + await bind.sessionToggleOption(id: id, value: 'view-only'); + ffiModel.setViewOnly(id, value); + } + : null, + ffi: ffi, + child: Text(translate('View Mode'))); + } +} + +class _ChatMenu extends StatefulWidget { + final String id; + final FFI ffi; + _ChatMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + State<_ChatMenu> createState() => _ChatMenuState(); +} + +class _ChatMenuState extends State<_ChatMenu> { + // Using in StatelessWidget got `Looking up a deactivated widget's ancestor is unsafe`. + final chatButtonKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return _IconSubmenuButton( + tooltip: 'Chat', + key: chatButtonKey, + svg: 'assets/chat.svg', + ffi: widget.ffi, + color: _MenubarTheme.blueColor, + hoverColor: _MenubarTheme.hoverBlueColor, + menuChildren: [textChat(), voiceCall()]); + } + + textChat() { + return _MenuItemButton( + child: Text(translate('Text chat')), + ffi: widget.ffi, + onPressed: () { + RenderBox? renderBox = + chatButtonKey.currentContext?.findRenderObject() as RenderBox?; + + Offset? initPos; + if (renderBox != null) { + final pos = renderBox.localToGlobal(Offset.zero); + initPos = Offset(pos.dx, pos.dy + _MenubarTheme.dividerHeight); + } + + widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID); + widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos); + }); + } + + voiceCall() { + return _MenuItemButton( + child: Text(translate('Voice call')), + ffi: widget.ffi, + onPressed: () => bind.sessionRequestVoiceCall(id: widget.id), + ); + } +} + +class _VoiceCallMenu extends StatelessWidget { + final String id; + final FFI ffi; + _VoiceCallMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx( + () { + final String tooltip; + final String icon; + switch (ffi.chatModel.voiceCallStatus.value) { + case VoiceCallStatus.waitingForResponse: + tooltip = "Waiting"; + icon = "assets/call_wait.svg"; + break; + case VoiceCallStatus.connected: + tooltip = "Disconnect"; + icon = "assets/call_end.svg"; + break; + default: + return Offstage(); + } + return _IconMenuButton( + assetName: icon, + tooltip: tooltip, + onPressed: () => bind.sessionCloseVoiceCall(id: id), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor); + }, + ); + } +} + +class _RecordMenu extends StatelessWidget { + const _RecordMenu({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + var ffi = Provider.of(context); + final visible = ffi.permissions['recording'] != false; + if (!visible) return Offstage(); + return Consumer( + builder: (context, value, child) => _IconMenuButton( + assetName: 'assets/rec.svg', + tooltip: + value.start ? 'Stop session recording' : 'Start session recording', + onPressed: () => value.toggle(), + color: value.start ? _MenubarTheme.redColor : _MenubarTheme.blueColor, + hoverColor: value.start + ? _MenubarTheme.hoverRedColor + : _MenubarTheme.hoverBlueColor, + ), + ); + } +} + +class _CloseMenu extends StatelessWidget { + final String id; + final FFI ffi; + const _CloseMenu({Key? key, required this.id, required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _IconMenuButton( + assetName: 'assets/close.svg', + tooltip: 'Close', + onPressed: () => clientClose(id, ffi.dialogManager), + color: _MenubarTheme.redColor, + hoverColor: _MenubarTheme.hoverRedColor, + ); + } +} + +class _IconMenuButton extends StatefulWidget { + final String? assetName; + final Widget? icon; + final String? tooltip; + final Color color; + final Color hoverColor; + final VoidCallback? onPressed; + final double? hMargin; + final double? vMargin; + final bool topLevel; + const _IconMenuButton({ + Key? key, + this.assetName, + this.icon, + this.tooltip, + required this.color, + required this.hoverColor, + required this.onPressed, + this.hMargin, + this.vMargin, + this.topLevel = true, + }) : super(key: key); + + @override + State<_IconMenuButton> createState() => _IconMenuButtonState(); +} + +class _IconMenuButtonState extends State<_IconMenuButton> { + bool hover = false; + + @override + Widget build(BuildContext context) { + assert(widget.assetName != null || widget.icon != null); + final icon = widget.icon ?? + SvgPicture.asset( + widget.assetName!, + color: Colors.white, + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + ); + final button = SizedBox( + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + child: MenuItemButton( + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.transparent), + padding: MaterialStatePropertyAll(EdgeInsets.zero), + overlayColor: MaterialStatePropertyAll(Colors.transparent)), + onHover: (value) => setState(() { + hover = value; + }), + onPressed: widget.onPressed, + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_MenubarTheme.iconRadius), + color: hover ? widget.hoverColor : widget.color, + ), + child: icon)), + ), + ).marginSymmetric( + horizontal: widget.hMargin ?? _MenubarTheme.buttonHMargin, + vertical: widget.vMargin ?? _MenubarTheme.buttonVMargin); + if (widget.topLevel) { + return MenuBar(children: [button]); + } else { + return button; + } + } +} + +class _IconSubmenuButton extends StatefulWidget { + final String tooltip; + final String? svg; + final Widget? icon; + final Color color; + final Color hoverColor; + final List menuChildren; + final MenuStyle? menuStyle; + final FFI ffi; + + _IconSubmenuButton( + {Key? key, + this.svg, + this.icon, + required this.tooltip, + required this.color, + required this.hoverColor, + required this.menuChildren, + required this.ffi, + this.menuStyle}) + : super(key: key); + + @override + State<_IconSubmenuButton> createState() => _IconSubmenuButtonState(); +} + +class _IconSubmenuButtonState extends State<_IconSubmenuButton> { + bool hover = false; + + @override + Widget build(BuildContext context) { + assert(widget.svg != null || widget.icon != null); + final icon = widget.icon ?? + SvgPicture.asset( + widget.svg!, + color: Colors.white, + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + ); + final button = SizedBox( + width: _MenubarTheme.buttonSize, + height: _MenubarTheme.buttonSize, + child: SubmenuButton( + menuStyle: widget.menuStyle, + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll(Colors.transparent), + padding: MaterialStatePropertyAll(EdgeInsets.zero), + overlayColor: MaterialStatePropertyAll(Colors.transparent)), + onHover: (value) => setState(() { + hover = value; + }), + child: Material( + type: MaterialType.transparency, + child: Ink( + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(_MenubarTheme.iconRadius), + color: hover ? widget.hoverColor : widget.color, + ), + child: icon)), + menuChildren: widget.menuChildren + .map((e) => _buildPointerTrackWidget(e, widget.ffi)) + .toList())); + return MenuBar(children: [ + button.marginSymmetric( + horizontal: _MenubarTheme.buttonHMargin, + vertical: _MenubarTheme.buttonVMargin) + ]); + } +} + +class _SubmenuButton extends StatelessWidget { + final List menuChildren; + final Widget? child; + final FFI ffi; + const _SubmenuButton({ + Key? key, + required this.menuChildren, + required this.child, + required this.ffi, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SubmenuButton( + key: key, + child: child, + menuChildren: + menuChildren.map((e) => _buildPointerTrackWidget(e, ffi)).toList(), + ); + } +} + +class _MenuItemButton extends StatelessWidget { + final VoidCallback? onPressed; + final Widget? trailingIcon; + final Widget? child; + final FFI ffi; + _MenuItemButton( + {Key? key, + this.onPressed, + this.trailingIcon, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MenuItemButton( + key: key, + onPressed: onPressed != null + ? () { + _menuDismissCallback(ffi); + onPressed?.call(); + } + : null, + trailingIcon: trailingIcon, + child: child); + } +} + +class _CheckboxMenuButton extends StatelessWidget { + final bool? value; + final ValueChanged? onChanged; + final Widget? child; + final FFI ffi; + const _CheckboxMenuButton( + {Key? key, + required this.value, + required this.onChanged, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return CheckboxMenuButton( + key: key, + value: value, + child: child, + onChanged: onChanged != null + ? (bool? value) { + _menuDismissCallback(ffi); + onChanged?.call(value); + } + : null, + ); + } +} + +class _RadioMenuButton extends StatelessWidget { + final T value; + final T? groupValue; + final ValueChanged? onChanged; + final Widget? child; + final FFI ffi; + const _RadioMenuButton( + {Key? key, + required this.value, + required this.groupValue, + required this.onChanged, + required this.child, + required this.ffi}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return RadioMenuButton( + value: value, + groupValue: groupValue, + child: child, + onChanged: onChanged != null + ? (T? value) { + _menuDismissCallback(ffi); + onChanged?.call(value); + } + : null, + ); + } +} + +class _DraggableShowHide extends StatefulWidget { + final RxDouble fractionX; + final RxBool dragging; + final RxBool show; + const _DraggableShowHide({ + Key? key, + required this.fractionX, + required this.dragging, + required this.show, + }) : super(key: key); + + @override + State<_DraggableShowHide> createState() => _DraggableShowHideState(); +} + +class _DraggableShowHideState extends State<_DraggableShowHide> { + Offset position = Offset.zero; + Size size = Size.zero; + + Widget _buildDraggable(BuildContext context) { + return Draggable( + axis: Axis.horizontal, + child: Icon( + Icons.drag_indicator, + size: 20, + color: MyTheme.color(context).drag_indicator, + ), + feedback: widget, + onDragStarted: (() { + final RenderObject? renderObj = context.findRenderObject(); + if (renderObj != null) { + final RenderBox renderBox = renderObj as RenderBox; + size = renderBox.size; + position = renderBox.localToGlobal(Offset.zero); + } + widget.dragging.value = true; + }), + onDragEnd: (details) { + final mediaSize = MediaQueryData.fromWindow(ui.window).size; + widget.fractionX.value += + (details.offset.dx - position.dx) / (mediaSize.width - size.width); + if (widget.fractionX.value < 0.35) { + widget.fractionX.value = 0.35; + } + if (widget.fractionX.value > 0.65) { + widget.fractionX.value = 0.65; + } + widget.dragging.value = false; + }, + ); + } + + @override + Widget build(BuildContext context) { + final ButtonStyle buttonStyle = ButtonStyle( + minimumSize: MaterialStateProperty.all(const Size(0, 0)), + padding: MaterialStateProperty.all(EdgeInsets.zero), + ); + final child = Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildDraggable(context), + TextButton( + onPressed: () => setState(() { + widget.show.value = !widget.show.value; + }), + child: Obx((() => Icon( + widget.show.isTrue ? Icons.expand_less : Icons.expand_more, + size: 20, + ))), + ), + ], + ); + return TextButtonTheme( + data: TextButtonThemeData(style: buttonStyle), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .menuBarTheme + .style + ?.backgroundColor + ?.resolve(MaterialState.values.toSet()), + borderRadius: BorderRadius.vertical( + bottom: Radius.circular(5), + ), + ), + child: SizedBox( + height: 20, + child: child, + ), + ), + ); + } +} + +class KeyboardModeMenu { + final String key; + final String menu; + + KeyboardModeMenu({required this.key, required this.menu}); +} + +_menuDismissCallback(FFI ffi) => ffi.inputModel.refreshMousePos(); + +Widget _buildPointerTrackWidget(Widget child, FFI ffi) { + return Listener( + onPointerHover: (PointerHoverEvent e) => + ffi.inputModel.lastMousePos = e.position, + child: MouseRegion( + child: child, + ), + ); +} + +class _MultiMonitorMenu extends StatelessWidget { + final String id; + final FFI ffi; + + const _MultiMonitorMenu({ + Key? key, + required this.id, + required this.ffi, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final List rowChildren = []; + final pi = ffi.ffiModel.pi; + + for (int i = 0; i < pi.displays.length; i++) { + rowChildren.add( + Obx(() { + RxInt display = CurrentDisplayState.find(id); + return _IconMenuButton( + topLevel: false, + color: i == display.value + ? _MenubarTheme.blueColor + : Colors.grey[800]!, + hoverColor: i == display.value + ? _MenubarTheme.hoverBlueColor + : Colors.grey[850]!, + icon: Container( + alignment: AlignmentDirectional.center, + constraints: + const BoxConstraints(minHeight: _MenubarTheme.height), + child: Stack( + alignment: Alignment.center, + children: [ + SvgPicture.asset( + "assets/screen.svg", + color: Colors.white, + ), + Obx( + () => Text( + (i + 1).toString(), + style: TextStyle( + color: i == display.value + ? _MenubarTheme.blueColor + : Colors.grey[800]!, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + onPressed: () { + if (display.value != i) { + bind.sessionSwitchDisplay(id: id, value: i); + } + }, + ); + }), + ); + } + return Row(children: rowChildren); + } +} diff --git a/flutter/lib/desktop/widgets/scroll_wrapper.dart b/flutter/lib/desktop/widgets/scroll_wrapper.dart index 32ed149e5..c5bc3394b 100644 --- a/flutter/lib/desktop/widgets/scroll_wrapper.dart +++ b/flutter/lib/desktop/widgets/scroll_wrapper.dart @@ -14,6 +14,7 @@ class DesktopScrollWrapper extends StatelessWidget { return ImprovedScrolling( scrollController: scrollController, enableCustomMouseWheelScrolling: true, + // enableKeyboardScrolling: true, // strange behavior on mac customMouseWheelScrollConfig: CustomMouseWheelScrollConfig( scrollDuration: kDefaultScrollDuration, scrollCurve: Curves.linearToEaseOut, diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index f6a5da819..2186e879e 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -1,23 +1,23 @@ -import 'dart:io'; import 'dart:async'; +import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; +import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; -import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; import 'package:scroll_pos/scroll_pos.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:bot_toast/bot_toast.dart'; import '../../utils/multi_window_manager.dart'; @@ -53,6 +53,7 @@ enum DesktopTabType { remoteScreen, fileTransfer, portForward, + install, } class DesktopTabState { @@ -234,7 +235,7 @@ class DesktopTab extends StatelessWidget { Key? key, required this.controller, this.showLogo = true, - this.showTitle = true, + this.showTitle = false, this.showMinimize = true, this.showMaximize = true, this.showClose = true, @@ -249,8 +250,9 @@ class DesktopTab extends StatelessWidget { this.unSelectedTabBackgroundColor, }) : super(key: key) { tabType = controller.tabType; - isMainWindow = - tabType == DesktopTabType.main || tabType == DesktopTabType.cm; + isMainWindow = tabType == DesktopTabType.main || + tabType == DesktopTabType.cm || + tabType == DesktopTabType.install; } static RxString labelGetterAlias(String peerId) { @@ -278,7 +280,6 @@ class DesktopTab extends StatelessWidget { ), const Divider( height: 1, - thickness: 1, ), ], ), @@ -327,14 +328,32 @@ class DesktopTab extends StatelessWidget { )); } + List _tabWidgets = []; Widget _buildPageView() { return _buildBlock( child: Obx(() => PageView( controller: state.value.pageController, physics: NeverScrollableScrollPhysics(), - children: state.value.tabs - .map((tab) => tab.page) - .toList(growable: false)))); + children: () { + /// to-do refactor, separate connection state and UI state for remote session. + /// [workaround] PageView children need an immutable list, after it has been passed into PageView + final tabLen = state.value.tabs.length; + if (tabLen == _tabWidgets.length) { + return _tabWidgets; + } else if (_tabWidgets.isNotEmpty && + tabLen == _tabWidgets.length + 1) { + /// On add. Use the previous list(pointer) to prevent item's state init twice. + /// *[_tabWidgets.isNotEmpty] means TabsWindow(remote_tab_page or file_manager_tab_page) opened before, but was hidden. In this case, we have to reload, otherwise the child can't be built. + _tabWidgets.add(state.value.tabs.last.page); + return _tabWidgets; + } else { + /// On remove or change. Use new list(pointer) to reload list children so that items loading order is normal. + /// the Widgets in list must enable [AutomaticKeepAliveClientMixin] + final newList = state.value.tabs.map((v) => v.page).toList(); + _tabWidgets = newList; + return newList; + } + }()))); } /// Check whether to show ListView @@ -343,7 +362,8 @@ class DesktopTab extends StatelessWidget { /// - hide single item when only has one item (home) on [DesktopTabPage]. bool isHideSingleItem() { return state.value.tabs.length == 1 && - controller.tabType == DesktopTabType.main; + (controller.tabType == DesktopTabType.main || + controller.tabType == DesktopTabType.install); } Widget _buildBar() { @@ -374,7 +394,7 @@ class DesktopTab extends StatelessWidget { width: 78, )), Offstage( - offstage: kUseCompatibleUiMode, + offstage: kUseCompatibleUiMode || Platform.isMacOS, child: Row(children: [ Offstage( offstage: !showLogo, @@ -486,7 +506,7 @@ class WindowActionPanelState extends State } }); } else { - final wc = WindowController.fromWindowId(windowId!); + final wc = WindowController.fromWindowId(kWindowId!); wc.isMaximized().then((maximized) { debugPrint("isMaximized $maximized"); if (widget.isMaximized.value != maximized) { @@ -505,12 +525,18 @@ class WindowActionPanelState extends State super.dispose(); } + void _setMaximize(bool maximize) { + stateGlobal.setMaximize(maximize); + setState(() {}); + } + @override void onWindowMaximize() { // catch maximize from system if (!widget.isMaximized.value) { widget.isMaximized.value = true; } + _setMaximize(true); super.onWindowMaximize(); } @@ -520,6 +546,7 @@ class WindowActionPanelState extends State if (widget.isMaximized.value) { widget.isMaximized.value = false; } + _setMaximize(false); super.onWindowUnmaximize(); } @@ -527,13 +554,28 @@ class WindowActionPanelState extends State void onWindowClose() async { // hide window on close if (widget.isMainWindow) { + if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) { + await rustDeskWinManager.unregisterActiveWindow(kMainWindowId); + } + // macOS specific workaround, the window is not hiding when in fullscreen. + if (Platform.isMacOS && await windowManager.isFullScreen()) { + await windowManager.setFullScreen(false); + await Future.delayed(Duration(seconds: 1)); + } await windowManager.hide(); - rustDeskWinManager.unregisterActiveWindow(0); } else { - widget.onClose?.call(); - await WindowController.fromWindowId(windowId!).hide(); - rustDeskWinManager - .call(WindowType.Main, kWindowEventHide, {"id": windowId!}); + // it's safe to hide the subwindow + final controller = WindowController.fromWindowId(kWindowId!); + if (Platform.isMacOS && await controller.isFullScreen()) { + await controller.setFullscreen(false); + await Future.delayed(Duration(seconds: 1)); + } + await controller.hide(); + await Future.wait([ + rustDeskWinManager + .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}), + widget.onClose?.call() ?? Future.microtask(() => null) + ]); } super.onWindowClose(); } @@ -549,7 +591,7 @@ class WindowActionPanelState extends State child: Row( children: [ Offstage( - offstage: !widget.showMinimize, + offstage: !widget.showMinimize || Platform.isMacOS, child: ActionIcon( message: 'Minimize', icon: IconFont.min, @@ -557,16 +599,16 @@ class WindowActionPanelState extends State if (widget.isMainWindow) { windowManager.minimize(); } else { - WindowController.fromWindowId(windowId!).minimize(); + WindowController.fromWindowId(kWindowId!).minimize(); } }, isClose: false, )), Offstage( - offstage: !widget.showMaximize, + offstage: !widget.showMaximize || Platform.isMacOS, child: Obx(() => ActionIcon( message: - widget.isMaximized.value ? "Restore" : "Maximize", + widget.isMaximized.value ? 'Restore' : 'Maximize', icon: widget.isMaximized.value ? IconFont.restore : IconFont.max, @@ -574,7 +616,7 @@ class WindowActionPanelState extends State isClose: false, ))), Offstage( - offstage: !widget.showClose, + offstage: !widget.showClose || Platform.isMacOS, child: ActionIcon( message: 'Close', icon: IconFont.close, @@ -587,7 +629,7 @@ class WindowActionPanelState extends State if (widget.isMainWindow) { await windowManager.close(); } else { - await WindowController.fromWindowId(windowId!) + await WindowController.fromWindowId(kWindowId!) .close(); } }); @@ -616,7 +658,7 @@ void startDragging(bool isMainWindow) { if (isMainWindow) { windowManager.startDragging(); } else { - WindowController.fromWindowId(windowId!).startDragging(); + WindowController.fromWindowId(kWindowId!).startDragging(); } } @@ -632,7 +674,7 @@ Future toggleMaximize(bool isMainWindow) async { return true; } } else { - final wc = WindowController.fromWindowId(windowId!); + final wc = WindowController.fromWindowId(kWindowId!); if (await wc.isMaximized()) { wc.unmaximize(); return false; @@ -681,8 +723,8 @@ Future closeConfirmDialog() async { ]), // confirm checkbox actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -719,7 +761,8 @@ class _ListView extends StatelessWidget { /// - hide single item when only has one item (home) on [DesktopTabPage]. bool isHideSingleItem() { return state.value.tabs.length == 1 && - controller.tabType == DesktopTabType.main; + controller.tabType == DesktopTabType.main || + controller.tabType == DesktopTabType.install; } @override @@ -759,7 +802,8 @@ class _ListView extends StatelessWidget { tabBuilder: tabBuilder, tabMenuBuilder: tabMenuBuilder, maxLabelWidth: maxLabelWidth, - selectedTabBackgroundColor: selectedTabBackgroundColor, + selectedTabBackgroundColor: selectedTabBackgroundColor ?? + MyTheme.tabbar(context).selectedTabBackgroundColor, unSelectedTabBackgroundColor: unSelectedTabBackgroundColor, ); }).toList())); @@ -900,11 +944,11 @@ class _TabState extends State<_Tab> with RestorationMixin { children: [ _buildTabContent(), Obx((() => _CloseButton( - visiable: hover.value && widget.closable, + visible: hover.value && widget.closable, tabSelected: isSelected, onClose: () => widget.onClose(), ))) - ])).paddingSymmetric(horizontal: 10), + ])).paddingOnly(left: 10, right: 5), Offstage( offstage: !showDivider, child: VerticalDivider( @@ -912,7 +956,6 @@ class _TabState extends State<_Tab> with RestorationMixin { indent: _kDividerIndent, endIndent: _kDividerIndent, color: MyTheme.tabbar(context).dividerColor, - thickness: 1, ), ) ], @@ -932,13 +975,13 @@ class _TabState extends State<_Tab> with RestorationMixin { } class _CloseButton extends StatelessWidget { - final bool visiable; + final bool visible; final bool tabSelected; final Function onClose; const _CloseButton({ Key? key, - required this.visiable, + required this.visible, required this.tabSelected, required this.onClose, }) : super(key: key); @@ -948,9 +991,10 @@ class _CloseButton extends StatelessWidget { return SizedBox( width: _kIconSize, child: Offstage( - offstage: !visiable, + offstage: !visible, child: InkWell( - customBorder: const RoundedRectangleBorder(), + hoverColor: MyTheme.tabbar(context).closeHoverColor, + customBorder: const CircleBorder(), onTap: () => onClose(), child: Icon( Icons.close, @@ -960,7 +1004,7 @@ class _CloseButton extends StatelessWidget { : MyTheme.tabbar(context).unSelectedIconColor, ), ), - )).paddingOnly(left: 5); + )).paddingOnly(left: 10); } } @@ -1049,6 +1093,8 @@ class TabbarTheme extends ThemeExtension { final Color? unSelectedIconColor; final Color? dividerColor; final Color? hoverColor; + final Color? closeHoverColor; + final Color? selectedTabBackgroundColor; const TabbarTheme( {required this.selectedTabIconColor, @@ -1058,27 +1104,33 @@ class TabbarTheme extends ThemeExtension { required this.selectedIconColor, required this.unSelectedIconColor, required this.dividerColor, - required this.hoverColor}); + required this.hoverColor, + required this.closeHoverColor, + required this.selectedTabBackgroundColor}); static const light = TabbarTheme( selectedTabIconColor: MyTheme.accent, unSelectedTabIconColor: Color.fromARGB(255, 162, 203, 241), - selectedTextColor: Color.fromARGB(255, 26, 26, 26), - unSelectedTextColor: Color.fromARGB(255, 96, 96, 96), + selectedTextColor: Colors.black, + unSelectedTextColor: Color.fromARGB(255, 112, 112, 112), selectedIconColor: Color.fromARGB(255, 26, 26, 26), unSelectedIconColor: Color.fromARGB(255, 96, 96, 96), dividerColor: Color.fromARGB(255, 238, 238, 238), - hoverColor: Color.fromARGB(51, 158, 158, 158)); + hoverColor: Color.fromARGB(51, 158, 158, 158), + closeHoverColor: Color.fromARGB(255, 224, 224, 224), + selectedTabBackgroundColor: Color.fromARGB(255, 240, 240, 240)); static const dark = TabbarTheme( selectedTabIconColor: MyTheme.accent, unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98), selectedTextColor: Color.fromARGB(255, 255, 255, 255), - unSelectedTextColor: Color.fromARGB(255, 207, 207, 207), - selectedIconColor: Color.fromARGB(255, 215, 215, 215), + unSelectedTextColor: Color.fromARGB(255, 192, 192, 192), + selectedIconColor: Color.fromARGB(255, 192, 192, 192), unSelectedIconColor: Color.fromARGB(255, 255, 255, 255), dividerColor: Color.fromARGB(255, 64, 64, 64), - hoverColor: Colors.black26); + hoverColor: Colors.black26, + closeHoverColor: Colors.black, + selectedTabBackgroundColor: Colors.black26); @override ThemeExtension copyWith({ @@ -1090,6 +1142,8 @@ class TabbarTheme extends ThemeExtension { Color? unSelectedIconColor, Color? dividerColor, Color? hoverColor, + Color? closeHoverColor, + Color? selectedTabBackgroundColor, }) { return TabbarTheme( selectedTabIconColor: selectedTabIconColor ?? this.selectedTabIconColor, @@ -1101,6 +1155,9 @@ class TabbarTheme extends ThemeExtension { unSelectedIconColor: unSelectedIconColor ?? this.unSelectedIconColor, dividerColor: dividerColor ?? this.dividerColor, hoverColor: hoverColor ?? this.hoverColor, + closeHoverColor: closeHoverColor ?? this.closeHoverColor, + selectedTabBackgroundColor: + selectedTabBackgroundColor ?? this.selectedTabBackgroundColor, ); } @@ -1125,6 +1182,9 @@ class TabbarTheme extends ThemeExtension { Color.lerp(unSelectedIconColor, other.unSelectedIconColor, t), dividerColor: Color.lerp(dividerColor, other.dividerColor, t), hoverColor: Color.lerp(hoverColor, other.hoverColor, t), + closeHoverColor: Color.lerp(closeHoverColor, other.closeHoverColor, t), + selectedTabBackgroundColor: Color.lerp( + selectedTabBackgroundColor, other.selectedTabBackgroundColor, t), ); } diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart index 475b4cb86..38e4d917b 100644 --- a/flutter/lib/desktop/widgets/titlebar_widget.dart +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -24,47 +24,8 @@ class DesktopTitleBar extends StatelessWidget { Expanded( child: child ?? Offstage(), ) - // const WindowButtons() ], ), ); } -} - -// final buttonColors = WindowButtonColors( -// iconNormal: const Color(0xFF805306), -// mouseOver: const Color(0xFFF6A00C), -// mouseDown: const Color(0xFF805306), -// iconMouseOver: const Color(0xFF805306), -// iconMouseDown: const Color(0xFFFFD500)); -// -// final closeButtonColors = WindowButtonColors( -// mouseOver: const Color(0xFFD32F2F), -// mouseDown: const Color(0xFFB71C1C), -// iconNormal: const Color(0xFF805306), -// iconMouseOver: Colors.white); -// -// class WindowButtons extends StatelessWidget { -// const WindowButtons({Key? key}) : super(key: key); -// -// @override -// Widget build(BuildContext context) { -// return Row( -// children: [ -// MinimizeWindowButton(colors: buttonColors, onPressed: () { -// windowManager.minimize(); -// },), -// MaximizeWindowButton(colors: buttonColors, onPressed: () async { -// if (await windowManager.isMaximized()) { -// windowManager.restore(); -// } else { -// windowManager.maximize(); -// } -// },), -// CloseWindowButton(colors: closeButtonColors, onPressed: () { -// windowManager.close(); -// },), -// ], -// ); -// } -// } +} \ No newline at end of file diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6d09ef139..164de04d6 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,22 +1,23 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:bot_toast/bot_toast.dart'; import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; -import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart'; +import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:bot_toast/bot_toast.dart'; // import 'package:window_manager/window_manager.dart'; @@ -26,13 +27,18 @@ import 'mobile/pages/home_page.dart'; import 'mobile/pages/server_page.dart'; import 'models/platform_model.dart'; -int? windowId; -late List bootArgs; +/// Basic window and launch properties. +int? kWindowId; +WindowType? kWindowType; +late List kBootArgs; + +/// Uni links. +StreamSubscription? _uniLinkSubscription; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); - bootArgs = List.from(args); + kBootArgs = List.from(args); if (!isDesktop) { runMobileApp(); @@ -40,10 +46,10 @@ Future main(List args) async { } // main window if (args.isNotEmpty && args.first == 'multi_window') { - windowId = int.parse(args[1]); - stateGlobal.setWindowId(windowId!); + kWindowId = int.parse(args[1]); + stateGlobal.setWindowId(kWindowId!); if (!Platform.isMacOS) { - WindowController.fromWindowId(windowId!).showTitleBar(false); + WindowController.fromWindowId(kWindowId!).showTitleBar(false); } final argument = args[2].isEmpty ? {} @@ -51,15 +57,16 @@ Future main(List args) async { int type = argument['type'] ?? -1; // to-do: No need to parse window id ? // Because stateGlobal.windowId is a global value. - argument['windowId'] = windowId; - WindowType wType = type.windowType; - switch (wType) { + argument['windowId'] = kWindowId; + kWindowType = type.windowType; + final windowName = getWindowName(); + switch (kWindowType) { case WindowType.RemoteDesktop: desktopType = DesktopType.remote; runMultiWindow( argument, kAppTypeDesktopRemote, - 'RustDesk - Remote Desktop', + windowName, ); break; case WindowType.FileTransfer: @@ -67,7 +74,7 @@ Future main(List args) async { runMultiWindow( argument, kAppTypeDesktopFileTransfer, - 'RustDesk - File Transfer', + windowName, ); break; case WindowType.PortForward: @@ -75,7 +82,7 @@ Future main(List args) async { runMultiWindow( argument, kAppTypeDesktopPortForward, - 'RustDesk - Port Forward', + windowName, ); break; default: @@ -105,11 +112,12 @@ Future initEnv(String appType) async { await initGlobalFFI(); // await Firebase.initializeApp(); _registerEventHandler(); + // Update the system theme. + updateSystemWindowTheme(); } void runMainApp(bool startService) async { // register uni links - initUniLinks(); await initEnv(kAppTypeMain); // trigger connection status updater await bind.mainCheckConnectStatus(); @@ -119,27 +127,32 @@ void runMainApp(bool startService) async { } gFFI.userModel.refreshCurrentUser(); runApp(App()); - // restore the location of the main window before window hide or show - await restoreWindowPosition(WindowType.Main); - // check the startup argument, if we successfully handle the argument, we keep the main window hidden. - if (checkArguments()) { - windowManager.hide(); - } else { - windowManager.show(); - windowManager.focus(); - // move registration of active main window here to prevent async visible check. - rustDeskWinManager.registerActiveWindow(kWindowMainId); - } - // set window option + // Set window option. WindowOptions windowOptions = getHiddenTitleBarWindowOptions(); windowManager.waitUntilReadyToShow(windowOptions, () async { + // Restore the location of the main window before window hide or show. + await restoreWindowPosition(WindowType.Main); + // Check the startup argument, if we successfully handle the argument, we keep the main window hidden. + final handledByUniLinks = await initUniLinks(); + debugPrint( + "handled by uni links: $handledByUniLinks"); + if (handledByUniLinks || checkArguments()) { + windowManager.hide(); + } else { + windowManager.show(); + windowManager.focus(); + // Move registration of active main window here to prevent from async visible check. + rustDeskWinManager.registerActiveWindow(kWindowMainId); + } windowManager.setOpacity(1); + windowManager.setTitle(getWindowName()); }); } void runMobileApp() async { await initEnv(kAppTypeMain); if (isAndroid) androidChannelInit(); + platformFFI.syncAndroidServiceAppDirConfigPath(); runApp(App()); } @@ -150,7 +163,7 @@ void runMultiWindow( ) async { await initEnv(appType); // set prevent close to true, we handle close event manually - WindowController.fromWindowId(windowId!).setPreventClose(true); + WindowController.fromWindowId(kWindowId!).setPreventClose(true); late Widget widget; switch (appType) { case kAppTypeDesktopRemote: @@ -179,27 +192,30 @@ void runMultiWindow( ); // we do not hide titlebar on win7 because of the frame overflow. if (kUseCompatibleUiMode) { - WindowController.fromWindowId(windowId!).showTitleBar(true); + WindowController.fromWindowId(kWindowId!).showTitleBar(true); } switch (appType) { case kAppTypeDesktopRemote: await restoreWindowPosition(WindowType.RemoteDesktop, - windowId: windowId!); + windowId: kWindowId!); break; case kAppTypeDesktopFileTransfer: - await restoreWindowPosition(WindowType.FileTransfer, windowId: windowId!); + await restoreWindowPosition(WindowType.FileTransfer, + windowId: kWindowId!); break; case kAppTypeDesktopPortForward: - await restoreWindowPosition(WindowType.PortForward, windowId: windowId!); + await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; default: // no such appType exit(0); } + // show window from hidden status + WindowController.fromWindowId(kWindowId!).show(); } void runConnectionManagerScreen(bool hide) async { - await initEnv(kAppTypeMain); + await initEnv(kAppTypeConnectionManager); _runApp( '', const DesktopServerPage(), @@ -210,12 +226,15 @@ void runConnectionManagerScreen(bool hide) async { } else { showCmWindow(); } + // Start the uni links handler and redirect links to Native, not for Flutter. + _uniLinkSubscription = listenUniLinks(handleByFlutter: false); } void showCmWindow() { WindowOptions windowOptions = getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); windowManager.waitUntilReadyToShow(windowOptions, () async { + bind.mainHideDocker(); await windowManager.show(); await Future.wait([windowManager.focus(), windowManager.setOpacity(1)]); // ensure initial window size to be changed @@ -229,6 +248,7 @@ void hideCmWindow() { getHiddenTitleBarWindowOptions(size: kConnectionManagerWindowSize); windowManager.setOpacity(0); windowManager.waitUntilReadyToShow(windowOptions, () async { + bind.mainHideDocker(); await windowManager.hide(); }); } @@ -270,17 +290,20 @@ void _runApp( void runInstallPage() async { await windowManager.ensureInitialized(); await initEnv(kAppTypeMain); - _runApp('', const InstallPage(), ThemeMode.light); - windowManager.waitUntilReadyToShow( - WindowOptions(size: Size(800, 600), center: true), () async { + _runApp('', const InstallPage(), MyTheme.currentThemeMode()); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(size: Size(800, 600), center: true); + windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.show(); windowManager.focus(); windowManager.setOpacity(1); windowManager.setAlignment(Alignment.center); // ensure + windowManager.setTitle(getWindowName()); }); } -WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { +WindowOptions getHiddenTitleBarWindowOptions( + {Size? size, bool center = false}) { var defaultTitleBarStyle = TitleBarStyle.hidden; // we do not hide titlebar on win7 because of the frame overflow. if (kUseCompatibleUiMode) { @@ -288,7 +311,7 @@ WindowOptions getHiddenTitleBarWindowOptions({Size? size}) { } return WindowOptions( size: size, - center: false, + center: center, backgroundColor: Colors.transparent, skipTaskbar: false, titleBarStyle: defaultTitleBarStyle, @@ -318,6 +341,8 @@ class _AppState extends State { to = ThemeMode.light; } Get.changeThemeMode(to); + // Synchronize the window theme of the system. + updateSystemWindowTheme(); if (desktopType == DesktopType.main) { bind.mainChangeTheme(dark: to.toShortString()); } @@ -337,6 +362,7 @@ class _AppState extends State { ChangeNotifierProvider.value(value: gFFI.imageModel), ChangeNotifierProvider.value(value: gFFI.cursorModel), ChangeNotifierProvider.value(value: gFFI.canvasModel), + ChangeNotifierProvider.value(value: gFFI.peerTabModel), ], child: GetMaterialApp( navigatorKey: globalKey, diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 957910324..6fce887bf 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -7,9 +7,8 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; -import '../../common/widgets/address_book.dart'; +import '../../common/widgets/login.dart'; import '../../common/widgets/peer_tab_page.dart'; -import '../../common/widgets/peers_view.dart'; import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; @@ -258,7 +257,7 @@ class _WebMenuState extends State { } if (value == 'login') { if (gFFI.userModel.userName.value.isEmpty) { - showLogin(gFFI.dialogManager); + loginDialog(); } else { gFFI.userModel.logOut(); } diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index 549a44b78..30e0661f6 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -3,12 +3,12 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; import 'package:flutter_hbb/models/file_model.dart'; -import 'package:provider/provider.dart'; +import 'package:get/get.dart'; import 'package:toggle_switch/toggle_switch.dart'; import 'package:wakelock/wakelock.dart'; import '../../common.dart'; -import '../widgets/dialog.dart'; +import '../../common/widgets/dialog.dart'; class FileManagerPage extends StatefulWidget { FileManagerPage({Key? key, required this.id}) : super(key: key); @@ -18,10 +18,51 @@ class FileManagerPage extends StatefulWidget { State createState() => _FileManagerPageState(); } +enum SelectMode { local, remote, none } + +extension SelectModeEq on SelectMode { + bool eq(bool? currentIsLocal) { + if (currentIsLocal == null) { + return false; + } + if (currentIsLocal) { + return this == SelectMode.local; + } else { + return this == SelectMode.remote; + } + } +} + +extension SelectModeExt on Rx { + void toggle(bool currentIsLocal) { + switch (value) { + case SelectMode.local: + value = SelectMode.none; + break; + case SelectMode.remote: + value = SelectMode.none; + break; + case SelectMode.none: + if (currentIsLocal) { + value = SelectMode.local; + } else { + value = SelectMode.remote; + } + break; + } + } +} + class _FileManagerPageState extends State { final model = gFFI.fileModel; - final _selectedItems = SelectedItems(); - final _breadCrumbScroller = ScrollController(); + final selectMode = SelectMode.none.obs; + + var showLocal = true; + + FileController get currentFileController => + showLocal ? model.localController : model.remoteController; + FileDirectory get currentDir => currentFileController.directory.value; + DirectoryOptions get currentOptions => currentFileController.options.value; @override void initState() { @@ -32,13 +73,12 @@ class _FileManagerPageState extends State { .showLoading(translate('Connecting...'), onCancel: closeConnection); }); gFFI.ffiModel.updateEventListener(widget.id); - model.onDirChanged = (_) => breadCrumbScrollToEnd(); Wakelock.enable(); } @override void dispose() { - model.onClose().whenComplete(() { + model.close().whenComplete(() { gFFI.close(); gFFI.dialogManager.dismissAll(); Wakelock.disable(); @@ -47,293 +87,455 @@ class _FileManagerPageState extends State { } @override - Widget build(BuildContext context) => ChangeNotifierProvider.value( - value: model, - child: Consumer(builder: (_context, _model, _child) { - return WillPopScope( - onWillPop: () async { - if (model.selectMode) { - model.toggleSelectMode(); - } else { - model.goBack(); + Widget build(BuildContext context) => WillPopScope( + onWillPop: () async { + if (selectMode.value != SelectMode.none) { + selectMode.value = SelectMode.none; + setState(() {}); + } else { + currentFileController.goBack(); + } + return false; + }, + child: Scaffold( + // backgroundColor: MyTheme.grayBg, + appBar: AppBar( + leading: Row(children: [ + IconButton( + icon: Icon(Icons.close), + onPressed: () => clientClose(widget.id, gFFI.dialogManager)), + ]), + centerTitle: true, + title: ToggleSwitch( + initialLabelIndex: showLocal ? 0 : 1, + activeBgColor: [MyTheme.idColor], + inactiveBgColor: Theme.of(context).brightness == Brightness.light + ? MyTheme.grayBg + : null, + inactiveFgColor: Theme.of(context).brightness == Brightness.light + ? Colors.black54 + : null, + totalSwitches: 2, + minWidth: 100, + fontSize: 15, + iconSize: 18, + labels: [translate("Local"), translate("Remote")], + icons: [Icons.phone_android_sharp, Icons.screen_share], + onToggle: (index) { + final current = showLocal ? 0 : 1; + if (index != current) { + setState(() => showLocal = !showLocal); } - return false; }, - child: Scaffold( - // backgroundColor: MyTheme.grayBg, - appBar: AppBar( - leading: Row(children: [ - IconButton( - icon: Icon(Icons.close), - onPressed: () => - clientClose(widget.id, gFFI.dialogManager)), - ]), - centerTitle: true, - title: ToggleSwitch( - initialLabelIndex: model.isLocal ? 0 : 1, - activeBgColor: [MyTheme.idColor], - inactiveBgColor: - Theme.of(context).brightness == Brightness.light - ? MyTheme.grayBg - : null, - inactiveFgColor: - Theme.of(context).brightness == Brightness.light - ? Colors.black54 - : null, - totalSwitches: 2, - minWidth: 100, - fontSize: 15, - iconSize: 18, - labels: [translate("Local"), translate("Remote")], - icons: [Icons.phone_android_sharp, Icons.screen_share], - onToggle: (index) { - final current = model.isLocal ? 0 : 1; - if (index != current) { - model.togglePage(); - } - }, - ), - actions: [ - PopupMenuButton( - icon: Icon(Icons.more_vert), - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Row( - children: [ - Icon(Icons.refresh, - color: Theme.of(context).iconTheme.color), - SizedBox(width: 5), - Text(translate("Refresh File")) - ], - ), - value: "refresh", - ), - PopupMenuItem( - enabled: model.currentDir.path != "/", - child: Row( - children: [ - Icon(Icons.check, - color: Theme.of(context).iconTheme.color), - SizedBox(width: 5), - Text(translate("Multi Select")) - ], - ), - value: "select", - ), - PopupMenuItem( - enabled: model.currentDir.path != "/", - child: Row( - children: [ - Icon(Icons.folder_outlined, - color: Theme.of(context).iconTheme.color), - SizedBox(width: 5), - Text(translate("Create Folder")) - ], - ), - value: "folder", - ), - PopupMenuItem( - enabled: model.currentDir.path != "/", - child: Row( - children: [ - Icon( - model.getCurrentShowHidden() - ? Icons.check_box_outlined - : Icons.check_box_outline_blank, - color: Theme.of(context).iconTheme.color), - SizedBox(width: 5), - Text(translate("Show Hidden Files")) - ], - ), - value: "hidden", - ) - ]; - }, - onSelected: (v) { - if (v == "refresh") { - model.refresh(); - } else if (v == "select") { - _selectedItems.clear(); - model.toggleSelectMode(); - } else if (v == "folder") { - final name = TextEditingController(); - gFFI.dialogManager - .show((setState, close) => CustomAlertDialog( - title: Text(translate("Create Folder")), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - decoration: InputDecoration( - labelText: translate( - "Please enter the folder name"), - ), - controller: name, - ), - ], + ), + actions: [ + PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.refresh, + color: Theme.of(context).iconTheme.color), + SizedBox(width: 5), + Text(translate("Refresh File")) + ], + ), + value: "refresh", + ), + PopupMenuItem( + enabled: currentDir.path != "/", + child: Row( + children: [ + Icon(Icons.check, + color: Theme.of(context).iconTheme.color), + SizedBox(width: 5), + Text(translate("Multi Select")) + ], + ), + value: "select", + ), + PopupMenuItem( + enabled: currentDir.path != "/", + child: Row( + children: [ + Icon(Icons.folder_outlined, + color: Theme.of(context).iconTheme.color), + SizedBox(width: 5), + Text(translate("Create Folder")) + ], + ), + value: "folder", + ), + PopupMenuItem( + enabled: currentDir.path != "/", + child: Row( + children: [ + Icon( + currentOptions.showHidden + ? Icons.check_box_outlined + : Icons.check_box_outline_blank, + color: Theme.of(context).iconTheme.color), + SizedBox(width: 5), + Text(translate("Show Hidden Files")) + ], + ), + value: "hidden", + ) + ]; + }, + onSelected: (v) { + if (v == "refresh") { + currentFileController.refresh(); + } else if (v == "select") { + model.localController.selectedItems.clear(); + model.remoteController.selectedItems.clear(); + selectMode.toggle(showLocal); + setState(() {}); + } else if (v == "folder") { + final name = TextEditingController(); + gFFI.dialogManager + .show((setState, close) => CustomAlertDialog( + title: Text(translate("Create Folder")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + decoration: InputDecoration( + labelText: translate( + "Please enter the folder name"), ), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () => close(false), - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: () { - if (name.value.text.isNotEmpty) { - model.createDir(PathUtil.join( - model.currentDir.path, - name.value.text, - model - .getCurrentIsWindows())); - close(); - } - }, - child: Text(translate("OK"))) - ])); - } else if (v == "hidden") { - model.toggleShowHidden(); - } - }), - ], + controller: name, + ), + ], + ), + actions: [ + dialogButton("Cancel", + onPressed: () => close(false), + isOutline: true), + dialogButton("OK", onPressed: () { + if (name.value.text.isNotEmpty) { + currentFileController.createDir( + PathUtil.join( + currentDir.path, + name.value.text, + currentOptions.isWindows)); + close(); + } + }) + ])); + } else if (v == "hidden") { + currentFileController.toggleShowHidden(); + } + }), + ], + ), + body: showLocal + ? FileManagerView( + controller: model.localController, + selectMode: selectMode, + ) + : FileManagerView( + controller: model.remoteController, + selectMode: selectMode, ), - body: body(), - bottomSheet: bottomSheet(), - )); - })); + bottomSheet: bottomSheet(), + )); - bool showCheckBox() { - if (!model.selectMode) { - return false; - } - return !_selectedItems.isOtherPage(model.isLocal); + Widget? bottomSheet() { + return Obx(() { + final selectedItems = getActiveSelectedItems(); + final jobTable = model.jobController.jobTable; + + final localLabel = selectedItems?.isLocal == null + ? "" + : " [${selectedItems!.isLocal ? translate("Local") : translate("Remote")}]"; + if (!(selectMode.value == SelectMode.none)) { + final selectedItemsLen = + "${selectedItems?.items.length ?? 0} ${translate("items")}"; + if (selectedItems == null || + selectedItems.items.isEmpty || + selectMode.value.eq(showLocal)) { + return BottomSheetBody( + leading: Icon(Icons.check), + title: translate("Selected"), + text: selectedItemsLen + localLabel, + onCanceled: () { + selectedItems?.items.clear(); + selectMode.value = SelectMode.none; + setState(() {}); + }, + actions: [ + IconButton( + icon: Icon(Icons.compare_arrows), + onPressed: () => setState(() => showLocal = !showLocal), + ), + IconButton( + icon: Icon(Icons.delete_forever), + onPressed: selectedItems != null + ? () async { + if (selectedItems.items.isNotEmpty) { + await currentFileController + .removeAction(selectedItems); + selectedItems.items.clear(); + selectMode.value = SelectMode.none; + } + } + : null, + ) + ]); + } else { + return BottomSheetBody( + leading: Icon(Icons.input), + title: translate("Paste here?"), + text: selectedItemsLen + localLabel, + onCanceled: () { + selectedItems.items.clear(); + selectMode.value = SelectMode.none; + setState(() {}); + }, + actions: [ + IconButton( + icon: Icon(Icons.compare_arrows), + onPressed: () => setState(() => showLocal = !showLocal), + ), + IconButton( + icon: Icon(Icons.paste), + onPressed: () { + selectMode.value = SelectMode.none; + final otherSide = showLocal + ? model.remoteController + : model.localController; + final thisSideData = + DirectoryData(currentDir, currentOptions); + otherSide.sendFiles(selectedItems, thisSideData); + selectedItems.items.clear(); + selectMode.value = SelectMode.none; + }, + ) + ]); + } + } + + if (jobTable.isEmpty) { + return Offstage(); + } + + switch (jobTable.last.state) { + case JobState.inProgress: + return BottomSheetBody( + leading: CircularProgressIndicator(), + title: translate("Waiting"), + text: + "${translate("Speed")}: ${readableFileSize(jobTable.last.speed)}/s", + onCanceled: () { + model.jobController.cancelJob(jobTable.last.id); + jobTable.clear(); + }, + ); + case JobState.done: + return BottomSheetBody( + leading: Icon(Icons.check), + title: "${translate("Successful")}!", + text: jobTable.last.display(), + onCanceled: () => jobTable.clear(), + ); + case JobState.error: + return BottomSheetBody( + leading: Icon(Icons.error), + title: "${translate("Error")}!", + text: "", + onCanceled: () => jobTable.clear(), + ); + case JobState.none: + break; + case JobState.paused: + // TODO: Handle this case. + break; + } + return Offstage(); + }); } - Widget body() { - final isLocal = model.isLocal; - final fd = model.currentDir; - final entries = fd.entries; + SelectedItems? getActiveSelectedItems() { + final localSelectedItems = model.localController.selectedItems; + final remoteSelectedItems = model.remoteController.selectedItems; + + if (localSelectedItems.items.isNotEmpty && + remoteSelectedItems.items.isNotEmpty) { + // assert unreachable + debugPrint("Wrong SelectedItems state, reset"); + localSelectedItems.clear(); + remoteSelectedItems.clear(); + } + + if (localSelectedItems.items.isEmpty && remoteSelectedItems.items.isEmpty) { + return null; + } + + if (localSelectedItems.items.length > remoteSelectedItems.items.length) { + return localSelectedItems; + } else { + return remoteSelectedItems; + } + } +} + +class FileManagerView extends StatefulWidget { + final FileController controller; + final Rx selectMode; + + FileManagerView({required this.controller, required this.selectMode}); + + @override + State createState() => _FileManagerViewState(); +} + +class _FileManagerViewState extends State { + final _listScrollController = ScrollController(); + final _breadCrumbScroller = ScrollController(); + + bool get isLocal => widget.controller.isLocal; + FileController get controller => widget.controller; + SelectedItems get _selectedItems => widget.controller.selectedItems; + + @override + void initState() { + super.initState(); + controller.directory.listen((e) => breadCrumbScrollToEnd()); + } + + @override + Widget build(BuildContext context) { return Column(children: [ headTools(), - Expanded( - child: ListView.builder( - controller: ScrollController(), - itemCount: entries.length + 1, - itemBuilder: (context, index) { - if (index >= entries.length) { - return listTail(); - } - var selected = false; - if (model.selectMode) { - selected = _selectedItems.contains(entries[index]); - } + Expanded(child: Obx(() { + final entries = controller.directory.value.entries; + return ListView.builder( + controller: _listScrollController, + itemCount: entries.length + 1, + itemBuilder: (context, index) { + if (index >= entries.length) { + return listTail(); + } + var selected = false; + if (widget.selectMode.value != SelectMode.none) { + selected = _selectedItems.items.contains(entries[index]); + } - final sizeStr = entries[index].isFile - ? readableFileSize(entries[index].size.toDouble()) - : ""; - return Card( - child: ListTile( - leading: entries[index].isDrive - ? Padding( - padding: EdgeInsets.symmetric(vertical: 8), - child: Image( - image: iconHardDrive, - fit: BoxFit.scaleDown, - color: Theme.of(context) - .iconTheme - .color - ?.withOpacity(0.7))) - : Icon( - entries[index].isFile - ? Icons.feed_outlined - : Icons.folder, - size: 40), - title: Text(entries[index].name), - selected: selected, - subtitle: entries[index].isDrive - ? null - : Text( - "${entries[index].lastModified().toString().replaceAll(".000", "")} $sizeStr", - style: TextStyle(fontSize: 12, color: MyTheme.darkGray), - ), - trailing: entries[index].isDrive - ? null - : showCheckBox() - ? Checkbox( - value: selected, - onChanged: (v) { - if (v == null) return; - if (v && !selected) { - _selectedItems.add(isLocal, entries[index]); - } else if (!v && selected) { - _selectedItems.remove(entries[index]); - } - setState(() {}); - }) - : PopupMenuButton( - icon: Icon(Icons.more_vert), - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Text(translate("Delete")), - value: "delete", - ), - PopupMenuItem( - child: Text(translate("Multi Select")), - value: "multi_select", - ), - PopupMenuItem( - child: Text(translate("Properties")), - value: "properties", - enabled: false, - ) - ]; - }, - onSelected: (v) { - if (v == "delete") { - final items = SelectedItems(); - items.add(isLocal, entries[index]); - model.removeAction(items); - } else if (v == "multi_select") { - _selectedItems.clear(); - model.toggleSelectMode(); - } - }), - onTap: () { - if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) { - if (selected) { - _selectedItems.remove(entries[index]); - } else { - _selectedItems.add(isLocal, entries[index]); + final sizeStr = entries[index].isFile + ? readableFileSize(entries[index].size.toDouble()) + : ""; + + final showCheckBox = () { + return widget.selectMode.value != SelectMode.none && + widget.selectMode.value.eq(controller.selectedItems.isLocal); + }(); + return Card( + child: ListTile( + leading: entries[index].isDrive + ? Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Image( + image: iconHardDrive, + fit: BoxFit.scaleDown, + color: Theme.of(context) + .iconTheme + .color + ?.withOpacity(0.7))) + : Icon( + entries[index].isFile + ? Icons.feed_outlined + : Icons.folder, + size: 40), + title: Text(entries[index].name), + selected: selected, + subtitle: entries[index].isDrive + ? null + : Text( + "${entries[index].lastModified().toString().replaceAll(".000", "")} $sizeStr", + style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + ), + trailing: entries[index].isDrive + ? null + : showCheckBox + ? Checkbox( + value: selected, + onChanged: (v) { + if (v == null) return; + if (v && !selected) { + _selectedItems.add(entries[index]); + } else if (!v && selected) { + _selectedItems.remove(entries[index]); + } + setState(() {}); + }) + : PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text(translate("Delete")), + value: "delete", + ), + PopupMenuItem( + child: Text(translate("Multi Select")), + value: "multi_select", + ), + PopupMenuItem( + child: Text(translate("Properties")), + value: "properties", + enabled: false, + ) + ]; + }, + onSelected: (v) { + if (v == "delete") { + final items = SelectedItems(isLocal: isLocal); + items.add(entries[index]); + controller.removeAction(items); + } else if (v == "multi_select") { + _selectedItems.clear(); + widget.selectMode.toggle(isLocal); + setState(() {}); + } + }), + onTap: () { + if (showCheckBox) { + if (selected) { + _selectedItems.remove(entries[index]); + } else { + _selectedItems.add(entries[index]); + } + setState(() {}); + return; } - setState(() {}); - return; - } - if (entries[index].isDirectory || entries[index].isDrive) { - model.openDirectory(entries[index].path); - } else { - // Perform file-related tasks. - } - }, - onLongPress: entries[index].isDrive - ? null - : () { - _selectedItems.clear(); - model.toggleSelectMode(); - if (model.selectMode) { - _selectedItems.add(isLocal, entries[index]); - } - setState(() {}); - }, - ), - ); - }, - )) + if (entries[index].isDirectory || entries[index].isDrive) { + controller.openDirectory(entries[index].path); + } else { + // Perform file-related tasks. + } + }, + onLongPress: entries[index].isDrive + ? null + : () { + _selectedItems.clear(); + widget.selectMode.toggle(isLocal); + if (widget.selectMode.value != SelectMode.none) { + _selectedItems.add(entries[index]); + } + setState(() {}); + }, + ), + ); + }, + ); + })) ]); } - breadCrumbScrollToEnd() { + void breadCrumbScrollToEnd() { Future.delayed(Duration(milliseconds: 200), () { if (_breadCrumbScroller.hasClients) { _breadCrumbScroller.animateTo( @@ -347,35 +549,39 @@ class _FileManagerPageState extends State { Widget headTools() => Container( child: Row( children: [ - Expanded( - child: BreadCrumb( - items: getPathBreadCrumbItems(() => model.goHome(), (list) { - var path = ""; - if (model.currentHome.startsWith(list[0])) { - // absolute path - for (var item in list) { - path = PathUtil.join(path, item, model.getCurrentIsWindows()); + Expanded(child: Obx(() { + final home = controller.options.value.home; + final isWindows = controller.options.value.isWindows; + return BreadCrumb( + items: getPathBreadCrumbItems(controller.shortPath, isWindows, + () => controller.goToHomeDirectory(), (list) { + var path = ""; + if (home.startsWith(list[0])) { + // absolute path + for (var item in list) { + path = PathUtil.join(path, item, isWindows); + } + } else { + path += home; + for (var item in list) { + path = PathUtil.join(path, item, isWindows); + } } - } else { - path += model.currentHome; - for (var item in list) { - path = PathUtil.join(path, item, model.getCurrentIsWindows()); - } - } - model.openDirectory(path); - }), - divider: Icon(Icons.chevron_right), - overflow: ScrollableOverflow(controller: _breadCrumbScroller), - )), + controller.openDirectory(path); + }), + divider: Icon(Icons.chevron_right), + overflow: ScrollableOverflow(controller: _breadCrumbScroller), + ); + })), Row( children: [ IconButton( icon: Icon(Icons.arrow_back), - onPressed: model.goBack, + onPressed: controller.goBack, ), IconButton( icon: Icon(Icons.arrow_upward), - onPressed: model.goToParentDirectory, + onPressed: controller.goToParentDirectory, ), PopupMenuButton( icon: Icon(Icons.sort), @@ -387,123 +593,37 @@ class _FileManagerPageState extends State { )) .toList(); }, - onSelected: model.changeSortStyle), + onSelected: controller.changeSortStyle), ], ) ], )); - Widget listTail() { - return Container( - height: 100, - child: Column( - children: [ - Padding( - padding: EdgeInsets.fromLTRB(30, 5, 30, 0), - child: Text( - model.currentDir.path, - style: TextStyle(color: MyTheme.darkGray), - ), - ), - Padding( - padding: EdgeInsets.all(2), - child: Text( - "${translate("Total")}: ${model.currentDir.entries.length} ${translate("items")}", - style: TextStyle(color: MyTheme.darkGray), - ), - ) - ], - ), - ); - } - - Widget? bottomSheet() { - final state = model.jobState; - final isOtherPage = _selectedItems.isOtherPage(model.isLocal); - final selectedItemsLen = "${_selectedItems.length} ${translate("items")}"; - final local = _selectedItems.isLocal == null - ? "" - : " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]"; - - if (model.selectMode) { - if (_selectedItems.length == 0 || !isOtherPage) { - return BottomSheetBody( - leading: Icon(Icons.check), - title: translate("Selected"), - text: selectedItemsLen + local, - onCanceled: () => model.toggleSelectMode(), - actions: [ - IconButton( - icon: Icon(Icons.compare_arrows), - onPressed: model.togglePage, + Widget listTail() => Obx(() => Container( + height: 100, + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(30, 5, 30, 0), + child: Text( + controller.directory.value.path, + style: TextStyle(color: MyTheme.darkGray), ), - IconButton( - icon: Icon(Icons.delete_forever), - onPressed: () { - if (_selectedItems.length > 0) { - model.removeAction(_selectedItems); - } - }, - ) - ]); - } else { - return BottomSheetBody( - leading: Icon(Icons.input), - title: translate("Paste here?"), - text: selectedItemsLen + local, - onCanceled: () => model.toggleSelectMode(), - actions: [ - IconButton( - icon: Icon(Icons.compare_arrows), - onPressed: model.togglePage, + ), + Padding( + padding: EdgeInsets.all(2), + child: Text( + "${translate("Total")}: ${controller.directory.value.entries.length} ${translate("items")}", + style: TextStyle(color: MyTheme.darkGray), ), - IconButton( - icon: Icon(Icons.paste), - onPressed: () { - model.toggleSelectMode(); - model.sendFiles(_selectedItems); - }, - ) - ]); - } - } + ) + ], + ), + )); - switch (state) { - case JobState.inProgress: - return BottomSheetBody( - leading: CircularProgressIndicator(), - title: translate("Waiting"), - text: - "${translate("Speed")}: ${readableFileSize(model.jobProgress.speed)}/s", - onCanceled: () => model.cancelJob(model.jobProgress.id), - ); - case JobState.done: - return BottomSheetBody( - leading: Icon(Icons.check), - title: "${translate("Successful")}!", - text: model.jobProgress.display(), - onCanceled: () => model.jobReset(), - ); - case JobState.error: - return BottomSheetBody( - leading: Icon(Icons.error), - title: "${translate("Error")}!", - text: "", - onCanceled: () => model.jobReset(), - ); - case JobState.none: - break; - case JobState.paused: - // TODO: Handle this case. - break; - } - return null; - } - - List getPathBreadCrumbItems( + List getPathBreadCrumbItems(String shortPath, bool isWindows, void Function() onHome, void Function(List) onPressed) { - final path = model.currentShortPath; - final list = PathUtil.split(path, model.getCurrentIsWindows()); + final list = PathUtil.split(shortPath, isWindows); final breadCrumbList = [ BreadCrumbItem( content: IconButton( diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 97ce6268d..8f4efaece 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -7,16 +7,19 @@ import 'package:flutter/services.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; import '../../common.dart'; import '../../common/widgets/overlay.dart'; +import '../../common/widgets/dialog.dart'; import '../../common/widgets/remote_input.dart'; import '../../models/input_model.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; +import '../../utils/image.dart'; import '../widgets/dialog.dart'; import '../widgets/gestures.dart'; @@ -32,17 +35,16 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State { - Timer? _interval; Timer? _timer; bool _showBar = !isWebDesktop; - double _bottom = 0; + bool _showGestureHelp = false; String _value = ''; double _scale = 1; double _mouseScrollIntegral = 0; // mouse scroll speed controller Orientation? _currentOrientation; - var _more = true; - var _fn = false; + final keyboardVisibilityController = KeyboardVisibilityController(); + late final StreamSubscription keyboardSubscription; final FocusNode _mobileFocusNode = FocusNode(); final FocusNode _physicalFocusNode = FocusNode(); var _showEdit = false; // use soft keyboard @@ -57,14 +59,14 @@ class _RemotePageState extends State { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); - _interval = - Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); Wakelock.enable(); _physicalFocusNode.requestFocus(); gFFI.ffiModel.updateEventListener(widget.id); gFFI.inputModel.listenToMouse(true); gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id); + keyboardSubscription = + keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged); } @override @@ -75,47 +77,26 @@ class _RemotePageState extends State { _mobileFocusNode.dispose(); _physicalFocusNode.dispose(); gFFI.close(); - _interval?.cancel(); _timer?.cancel(); gFFI.dialogManager.dismissAll(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); Wakelock.disable(); + keyboardSubscription.cancel(); super.dispose(); } - void resetTool() { - inputModel.resetModifiers(); - } - - bool isKeyboardShown() { - return _bottom >= 100; - } - - // crash on web before widget initiated. - void intervalUnsafe() { - var v = MediaQuery.of(context).viewInsets.bottom; - if (v != _bottom) { - resetTool(); - setState(() { - _bottom = v; - if (v < 100) { - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, - overlays: []); - // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard - if (gFFI.chatModel.chatWindowOverlayEntry == null && - gFFI.ffiModel.pi.version.isNotEmpty) { - gFFI.invokeMethod("enable_soft_keyboard", false); - } - } - }); + void onSoftKeyboardChanged(bool visible) { + if (!visible) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard + if (gFFI.chatModel.chatWindowOverlayEntry == null && + gFFI.ffiModel.pi.version.isNotEmpty) { + gFFI.invokeMethod("enable_soft_keyboard", false); + } } - } - - void interval() { - try { - intervalUnsafe(); - } catch (e) {} + // update for Scaffold + setState(() {}); } // handle mobile virtual keyboard @@ -218,8 +199,9 @@ class _RemotePageState extends State { @override Widget build(BuildContext context) { final pi = Provider.of(context).pi; - final hideKeyboard = isKeyboardShown() && _showEdit; - final showActionButton = !_showBar || hideKeyboard; + final keyboardIsVisible = + keyboardVisibilityController.isVisible && _showEdit; + final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp; final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; return WillPopScope( @@ -228,29 +210,40 @@ class _RemotePageState extends State { return false; }, child: getRawPointerAndKeyBody(Scaffold( - // resizeToAvoidBottomInset: true, + // workaround for https://github.com/rustdesk/rustdesk/issues/3131 + floatingActionButtonLocation: keyboardIsVisible + ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) + : null, floatingActionButton: !showActionButton ? null : FloatingActionButton( - mini: !hideKeyboard, + mini: !keyboardIsVisible, child: Icon( - hideKeyboard ? Icons.expand_more : Icons.expand_less), + (keyboardIsVisible || _showGestureHelp) + ? Icons.expand_more + : Icons.expand_less, + color: Colors.white, + ), backgroundColor: MyTheme.accent, onPressed: () { setState(() { - if (hideKeyboard) { + if (keyboardIsVisible) { _showEdit = false; gFFI.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); _physicalFocusNode.requestFocus(); + } else if (_showGestureHelp) { + _showGestureHelp = false; } else { _showBar = !_showBar; } }); }), - bottomNavigationBar: _showBar && pi.displays.isNotEmpty - ? getBottomAppBar(keyboard) - : null, + bottomNavigationBar: _showGestureHelp + ? getGestureHelp() + : (_showBar && pi.displays.isNotEmpty + ? getBottomAppBar(keyboard) + : null), body: Overlay( initialEntries: [ OverlayEntry(builder: (context) { @@ -340,7 +333,8 @@ class _RemotePageState extends State { icon: Icon(gFFI.ffiModel.touchMode ? Icons.touch_app : Icons.mouse), - onPressed: changeTouchMode, + onPressed: () => setState( + () => _showGestureHelp = !_showGestureHelp), ), ]) + (isWeb @@ -492,13 +486,18 @@ class _RemotePageState extends State { } Widget getBodyForMobile() { + final keyboardIsVisible = keyboardVisibilityController.isVisible; return Container( color: MyTheme.canvasColor, child: Stack(children: () { final paints = [ ImagePaint(), - QualityMonitor(gFFI.qualityMonitorModel), - getHelpTools(), + Positioned( + top: 10, + right: 10, + child: QualityMonitor(gFFI.qualityMonitorModel), + ), + KeyHelpTools(requestShow: (keyboardIsVisible || _showGestureHelp)), SizedBox( width: 0, height: 0, @@ -571,17 +570,18 @@ class _RemotePageState extends State { child: Text(translate('Reset canvas')), value: 'reset_canvas')); } if (perms['keyboard'] != false) { - more.add(PopupMenuItem( - child: Text(translate('Physical Keyboard Input Mode')), - value: 'input-mode')); - if (pi.platform == 'Linux' || pi.sasEnabled) { + // * Currently mobile does not enable map mode + // more.add(PopupMenuItem( + // child: Text(translate('Physical Keyboard Input Mode')), + // value: 'input-mode')); + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { more.add(PopupMenuItem( child: Text('${translate('Insert')} Ctrl + Alt + Del'), value: 'cad')); } more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); - if (pi.platform == 'Windows' && + if (pi.platform == kPeerPlatformWindows && await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != true) { more.add(PopupMenuItem( @@ -591,9 +591,9 @@ class _RemotePageState extends State { } } if (perms["restart"] != false && - (pi.platform == "Linux" || - pi.platform == "Windows" || - pi.platform == "Mac OS")) { + (pi.platform == kPeerPlatformLinux || + pi.platform == kPeerPlatformWindows || + pi.platform == kPeerPlatformMacOS)) { more.add(PopupMenuItem( child: Text(translate('Restart Remote Device')), value: 'restart')); } @@ -628,8 +628,9 @@ class _RemotePageState extends State { ); if (value == 'cad') { bind.sessionCtrlAltDel(id: widget.id); - } else if (value == 'input-mode') { - changePhysicalKeyboardInputMode(); + // * Currently mobile does not enable map mode + // } else if (value == 'input-mode') { + // changePhysicalKeyboardInputMode(); } else if (value == 'lock') { bind.sessionLockScreen(id: widget.id); } else if (value == 'block-input') { @@ -666,94 +667,110 @@ class _RemotePageState extends State { }(); } - void changeTouchMode() { - setState(() => _showEdit = false); - showModalBottomSheet( - // backgroundColor: MyTheme.grayBg, - isScrollControlled: true, - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(5))), - builder: (context) => DraggableScrollableSheet( - expand: false, - builder: (context, scrollController) { - return SingleChildScrollView( - controller: ScrollController(), - padding: EdgeInsets.symmetric(vertical: 10), - child: GestureHelp( - touchMode: gFFI.ffiModel.touchMode, - onTouchModeChange: (t) { - gFFI.ffiModel.toggleTouchMode(); - final v = gFFI.ffiModel.touchMode ? 'Y' : ''; - bind.sessionPeerOption( - id: widget.id, name: "touch", value: v); - })); - })); + /// aka changeTouchMode + BottomAppBar getGestureHelp() { + return BottomAppBar( + child: SingleChildScrollView( + controller: ScrollController(), + padding: EdgeInsets.symmetric(vertical: 10), + child: GestureHelp( + touchMode: gFFI.ffiModel.touchMode, + onTouchModeChange: (t) { + gFFI.ffiModel.toggleTouchMode(); + final v = gFFI.ffiModel.touchMode ? 'Y' : ''; + bind.sessionPeerOption( + id: widget.id, name: "touch", value: v); + }))); } - void changePhysicalKeyboardInputMode() async { - var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; - gFFI.dialogManager.show((setState, close) { - void setMode(String? v) async { - await bind.sessionPeerOption( - id: widget.id, name: "keyboard-mode", value: v ?? ""); - setState(() => current = v ?? ''); - Future.delayed(Duration(milliseconds: 300), close); - } + // * Currently mobile does not enable map mode + // void changePhysicalKeyboardInputMode() async { + // var current = await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + // gFFI.dialogManager.show((setState, close) { + // void setMode(String? v) async { + // await bind.sessionSetKeyboardMode(id: widget.id, value: v ?? ""); + // setState(() => current = v ?? ''); + // Future.delayed(Duration(milliseconds: 300), close); + // } + // + // return CustomAlertDialog( + // title: Text(translate('Physical Keyboard Input Mode')), + // content: Column(mainAxisSize: MainAxisSize.min, children: [ + // getRadio('Legacy mode', 'legacy', current, setMode, + // contentPadding: EdgeInsets.zero), + // getRadio('Map mode', 'map', current, setMode, + // contentPadding: EdgeInsets.zero), + // ])); + // }, clickMaskDismiss: true); + // } +} - return CustomAlertDialog( - title: Text(translate('Physical Keyboard Input Mode')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - getRadio('Legacy mode', 'legacy', current, setMode, - contentPadding: EdgeInsets.zero), - getRadio('Map mode', 'map', current, setMode, - contentPadding: EdgeInsets.zero), - ])); - }, clickMaskDismiss: true); +class KeyHelpTools extends StatefulWidget { + /// need to show by external request, etc [keyboardIsVisible] or [changeTouchMode] + final bool requestShow; + + KeyHelpTools({required this.requestShow}); + + @override + State createState() => _KeyHelpToolsState(); +} + +class _KeyHelpToolsState extends State { + var _more = true; + var _fn = false; + var _pin = false; + final _keyboardVisibilityController = KeyboardVisibilityController(); + + InputModel get inputModel => gFFI.inputModel; + + Widget wrap(String text, void Function() onPressed, + {bool? active, IconData? icon}) { + return TextButton( + style: TextButton.styleFrom( + minimumSize: Size(0, 0), + padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75), + //adds padding inside the button + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + //limits the touch area to the button area + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5.0), + ), + backgroundColor: active == true ? MyTheme.accent80 : null, + ), + child: icon != null + ? Icon(icon, size: 14, color: Colors.white) + : Text(translate(text), + style: TextStyle(color: Colors.white, fontSize: 11)), + onPressed: onPressed); } - Widget getHelpTools() { - final keyboard = isKeyboardShown(); - if (!keyboard) { - return SizedBox(); + @override + Widget build(BuildContext context) { + final hasModifierOn = inputModel.ctrl || + inputModel.alt || + inputModel.shift || + inputModel.command; + + if (!_pin && !hasModifierOn && !widget.requestShow) { + return Offstage(); } final size = MediaQuery.of(context).size; - wrap(String text, void Function() onPressed, - [bool? active, IconData? icon]) { - return TextButton( - style: TextButton.styleFrom( - minimumSize: Size(0, 0), - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 9.75), - //adds padding inside the button - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - //limits the touch area to the button area - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5.0), - ), - backgroundColor: active == true ? MyTheme.accent80 : null, - ), - child: icon != null - ? Icon(icon, size: 17, color: Colors.white) - : Text(translate(text), - style: TextStyle(color: Colors.white, fontSize: 11)), - onPressed: onPressed); - } final pi = gFFI.ffiModel.pi; - final isMac = pi.platform == "Mac OS"; + final isMac = pi.platform == kPeerPlatformMacOS; final modifiers = [ wrap('Ctrl ', () { setState(() => inputModel.ctrl = !inputModel.ctrl); - }, inputModel.ctrl), + }, active: inputModel.ctrl), wrap(' Alt ', () { setState(() => inputModel.alt = !inputModel.alt); - }, inputModel.alt), + }, active: inputModel.alt), wrap('Shift', () { setState(() => inputModel.shift = !inputModel.shift); - }, inputModel.shift), + }, active: inputModel.shift), wrap(isMac ? ' Cmd ' : ' Win ', () { setState(() => inputModel.command = !inputModel.command); - }, inputModel.command), + }, active: inputModel.command), ]; final keys = [ wrap( @@ -766,7 +783,14 @@ class _RemotePageState extends State { } }, ), - _fn), + active: _fn), + wrap( + '', + () => setState( + () => _pin = !_pin, + ), + active: _pin, + icon: Icons.push_pin), wrap( ' ... ', () => setState( @@ -777,7 +801,7 @@ class _RemotePageState extends State { } }, ), - _more), + active: _more), ]; final fn = [ SizedBox(width: 9999), @@ -802,6 +826,9 @@ class _RemotePageState extends State { wrap('End', () { inputModel.inputKey('VK_END'); }), + wrap('Ins', () { + inputModel.inputKey('VK_INSERT'); + }), wrap('Del', () { inputModel.inputKey('VK_DELETE'); }), @@ -814,16 +841,16 @@ class _RemotePageState extends State { SizedBox(width: 9999), wrap('', () { inputModel.inputKey('VK_LEFT'); - }, false, Icons.keyboard_arrow_left), + }, icon: Icons.keyboard_arrow_left), wrap('', () { inputModel.inputKey('VK_UP'); - }, false, Icons.keyboard_arrow_up), + }, icon: Icons.keyboard_arrow_up), wrap('', () { inputModel.inputKey('VK_DOWN'); - }, false, Icons.keyboard_arrow_down), + }, icon: Icons.keyboard_arrow_down), wrap('', () { inputModel.inputKey('VK_RIGHT'); - }, false, Icons.keyboard_arrow_right), + }, icon: Icons.keyboard_arrow_right), wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () { sendPrompt(isMac, 'VK_C'); }), @@ -838,14 +865,15 @@ class _RemotePageState extends State { return Container( color: Color(0xAA000000), padding: EdgeInsets.only( - top: keyboard ? 24 : 4, left: 0, right: 0, bottom: 8), + top: _keyboardVisibilityController.isVisible ? 24 : 4, bottom: 8), child: Wrap( spacing: space, runSpacing: space, children: [SizedBox(width: 9999)] + - (keyboard - ? modifiers + keys + (_fn ? fn : []) + (_more ? more : []) - : modifiers), + modifiers + + keys + + (_fn ? fn : []) + + (_more ? more : []), )); } } @@ -889,32 +917,6 @@ class CursorPaint extends StatelessWidget { } } -class ImagePainter extends CustomPainter { - ImagePainter({ - required this.image, - required this.x, - required this.y, - required this.scale, - }); - - ui.Image? image; - double x; - double y; - double scale; - - @override - void paint(Canvas canvas, Size size) { - if (image == null) return; - canvas.scale(scale, scale); - canvas.drawImage(image!, Offset(x, y), Paint()); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return oldDelegate != this; - } -} - void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { String quality = @@ -995,7 +997,7 @@ void showOptions( } more.add(getToggle( id, setState, 'lock-after-session-end', 'Lock after session end')); - if (pi.platform == 'Windows') { + if (pi.platform == kPeerPlatformWindows) { more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); } } @@ -1058,7 +1060,7 @@ void showOptions( final toggles = [ getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'), ]; - if (!gFFI.canvasModel.cursorEmbedded) { + if (!gFFI.canvasModel.cursorEmbedded && !pi.is_wayland) { toggles.insert(0, getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor')); } @@ -1098,15 +1100,9 @@ void showSetOSPassword( ), ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton( + 'OK', onPressed: () { var text = controller.text.trim(); bind.sessionPeerOption(id: id, name: "os-password", value: text); @@ -1117,7 +1113,6 @@ void showSetOSPassword( } close(); }, - child: Text(translate('OK')), ), ]); }); @@ -1137,3 +1132,16 @@ void sendPrompt(bool isMac, String key) { gFFI.inputModel.ctrl = old; } } + +class FABLocation extends FloatingActionButtonLocation { + FloatingActionButtonLocation location; + double offsetX; + double offsetY; + FABLocation(this.location, this.offsetX, this.offsetY); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + final offset = location.getOffset(scaffoldGeometry); + return Offset(offset.dx + offsetX, offset.dy + offsetY); + } +} diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index abccdf683..ae61c91a7 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -1,11 +1,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; +import 'package:get/get.dart'; import 'package:provider/provider.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; +import '../../consts.dart'; import '../../models/platform_model.dart'; import '../../models/server_model.dart'; import 'home_page.dart'; @@ -40,14 +43,14 @@ class ServerPage extends StatefulWidget implements PageShape { value: "setTemporaryPasswordLength", enabled: gFFI.serverModel.verificationMethod != kUsePermanentPassword, - child: Text(translate("Set temporary password length")), + child: Text(translate("One-time password length")), ), const PopupMenuDivider(), PopupMenuItem( padding: const EdgeInsets.symmetric(horizontal: 0.0), value: kUseTemporaryPassword, child: ListTile( - title: Text(translate("Use temporary password")), + title: Text(translate("Use one-time password")), trailing: Icon( Icons.check, color: gFFI.serverModel.verificationMethod == @@ -138,9 +141,11 @@ class _ServerPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ - ServerInfo(), - const PermissionChecker(), + gFFI.serverModel.isStart + ? ServerInfo() + : ServiceNotRunningNotification(), const ConnectionManager(), + const PermissionChecker(), SizedBox.fromSize(size: const Size(0, 15.0)), ], ), @@ -150,14 +155,42 @@ class _ServerPageState extends State { } void checkService() async { - gFFI.invokeMethod("check_service"); // jvm - // for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page - if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { - PermissionManager.complete("file", await PermissionManager.check("file")); + gFFI.invokeMethod("check_service"); + // for Android 10/11, request MANAGE_EXTERNAL_STORAGE permission from system setting page + if (AndroidPermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { + AndroidPermissionManager.complete(kManageExternalStorage, + await AndroidPermissionManager.check(kManageExternalStorage)); debugPrint("file permission finished"); } } +class ServiceNotRunningNotification extends StatelessWidget { + ServiceNotRunningNotification({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final serverModel = Provider.of(context); + + return PaddingCard( + title: translate("Service is not running"), + titleIcon: + const Icon(Icons.warning_amber_sharp, color: Colors.redAccent), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("android_start_service_tip"), + style: + const TextStyle(fontSize: 12, color: MyTheme.darkGray)) + .marginOnly(bottom: 8), + ElevatedButton.icon( + icon: const Icon(Icons.play_arrow), + onPressed: serverModel.toggleService, + label: Text(translate("Start Service"))) + ], + )); + } +} + class ServerInfo extends StatelessWidget { final model = gFFI.serverModel; final emptyController = TextEditingController(text: "-"); @@ -167,78 +200,104 @@ class ServerInfo extends StatelessWidget { @override Widget build(BuildContext context) { final isPermanent = model.verificationMethod == kUsePermanentPassword; - return model.isStart - ? PaddingCard( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - readOnly: true, - style: const TextStyle( - fontSize: 25.0, - fontWeight: FontWeight.bold, - color: MyTheme.accent), - controller: model.serverId, - decoration: InputDecoration( - icon: const Icon(Icons.perm_identity), - labelText: translate("ID"), - labelStyle: const TextStyle( - fontWeight: FontWeight.bold, color: MyTheme.accent80), - ), - onSaved: (String? value) {}, + final serverModel = Provider.of(context); + + const Color colorPositive = Colors.green; + const Color colorNegative = Colors.red; + const double iconMarginRight = 15; + const double iconSize = 24; + const TextStyle textStyleHeading = TextStyle( + fontSize: 16.0, fontWeight: FontWeight.bold, color: Colors.grey); + const TextStyle textStyleValue = + TextStyle(fontSize: 25.0, fontWeight: FontWeight.bold); + + void copyToClipboard(String value) { + Clipboard.setData(ClipboardData(text: value)); + showToast(translate('Copied')); + } + + Widget ConnectionStateNotification() { + if (serverModel.connectStatus == -1) { + return Row(children: [ + const Icon(Icons.warning_amber_sharp, + color: colorNegative, size: iconSize) + .marginOnly(right: iconMarginRight), + Expanded(child: Text(translate('not_ready_status'))) + ]); + } else if (serverModel.connectStatus == 0) { + return Row(children: [ + SizedBox(width: 20, height: 20, child: CircularProgressIndicator()) + .marginOnly(left: 4, right: iconMarginRight), + Expanded(child: Text(translate('connecting_status'))) + ]); + } else { + return Row(children: [ + const Icon(Icons.check, color: colorPositive, size: iconSize) + .marginOnly(right: iconMarginRight), + Expanded(child: Text(translate('Ready'))) + ]); + } + } + + return PaddingCard( + title: translate('Your Device'), + child: Column( + // ID + children: [ + Row(children: [ + const Icon(Icons.perm_identity, + color: Colors.grey, size: iconSize) + .marginOnly(right: iconMarginRight), + Text( + translate('ID'), + style: textStyleHeading, + ) + ]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text( + model.serverId.value.text, + style: textStyleValue, ), - TextFormField( - readOnly: true, - style: const TextStyle( - fontSize: 25.0, - fontWeight: FontWeight.bold, - color: MyTheme.accent), - controller: isPermanent ? emptyController : model.serverPasswd, - decoration: InputDecoration( - icon: const Icon(Icons.lock), - labelText: translate("Password"), - labelStyle: const TextStyle( - fontWeight: FontWeight.bold, color: MyTheme.accent80), - suffix: isPermanent - ? null - : IconButton( - icon: const Icon(Icons.refresh), - onPressed: () => - bind.mainUpdateTemporaryPassword())), - onSaved: (String? value) {}, + IconButton( + visualDensity: VisualDensity.compact, + icon: Icon(Icons.copy_outlined), + onPressed: () { + copyToClipboard(model.serverId.value.text.trim()); + }) + ]).marginOnly(left: 39, bottom: 10), + // Password + Row(children: [ + const Icon(Icons.lock_outline, color: Colors.grey, size: iconSize) + .marginOnly(right: iconMarginRight), + Text( + translate('One-time Password'), + style: textStyleHeading, + ) + ]), + Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + Text( + isPermanent ? '-' : model.serverPasswd.value.text, + style: textStyleValue, ), - ], - )) - : PaddingCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Center( - child: Row( - children: [ - const Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 24), - const SizedBox(width: 10), - Expanded( - child: Text( - translate("Service is not running"), - style: const TextStyle( - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 18, - color: MyTheme.accent, - ), - )) - ], - )), - const SizedBox(height: 5), - Center( - child: Text( - translate("android_start_service_tip"), - style: const TextStyle(fontSize: 12, color: MyTheme.darkGray), - )) - ], - )); + isPermanent + ? SizedBox.shrink() + : Row(children: [ + IconButton( + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.refresh), + onPressed: () => bind.mainUpdateTemporaryPassword()), + IconButton( + visualDensity: VisualDensity.compact, + icon: Icon(Icons.copy_outlined), + onPressed: () { + copyToClipboard( + model.serverPasswd.value.text.trim()); + }) + ]) + ]).marginOnly(left: 40, bottom: 15), + ConnectionStateNotification() + ], + )); } } @@ -254,78 +313,37 @@ class _PermissionCheckerState extends State { Widget build(BuildContext context) { final serverModel = Provider.of(context); final hasAudioPermission = androidVersion >= 30; - final String status; - if (serverModel.connectStatus == -1) { - status = 'not_ready_status'; - } else if (serverModel.connectStatus == 0) { - status = 'connecting_status'; - } else { - status = 'Ready'; - } return PaddingCard( title: translate("Permissions"), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - PermissionRow(translate("Screen Capture"), serverModel.mediaOk, - serverModel.toggleService), - PermissionRow(translate("Input Control"), serverModel.inputOk, - serverModel.toggleInput), - PermissionRow(translate("Transfer File"), serverModel.fileOk, - serverModel.toggleFile), - hasAudioPermission - ? PermissionRow(translate("Audio Capture"), serverModel.audioOk, - serverModel.toggleAudio) - : Text( - "* ${translate("android_version_audio_tip")}", + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + serverModel.mediaOk + ? ElevatedButton.icon( + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(Colors.red)), + icon: const Icon(Icons.stop), + onPressed: serverModel.toggleService, + label: Text(translate("Stop service"))) + .marginOnly(bottom: 8) + : SizedBox.shrink(), + PermissionRow(translate("Screen Capture"), serverModel.mediaOk, + serverModel.toggleService), + PermissionRow(translate("Input Control"), serverModel.inputOk, + serverModel.toggleInput), + PermissionRow(translate("Transfer File"), serverModel.fileOk, + serverModel.toggleFile), + hasAudioPermission + ? PermissionRow(translate("Audio Capture"), serverModel.audioOk, + serverModel.toggleAudio) + : Row(children: [ + Icon(Icons.info_outline).marginOnly(right: 15), + Expanded( + child: Text( + translate("android_version_audio_tip"), style: const TextStyle(color: MyTheme.darkGray), - ), - const SizedBox(height: 8), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - flex: 0, - child: serverModel.mediaOk - ? ElevatedButton.icon( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Colors.red)), - icon: const Icon(Icons.stop), - onPressed: serverModel.toggleService, - label: Text(translate("Stop service"))) - : ElevatedButton.icon( - icon: const Icon(Icons.play_arrow), - onPressed: serverModel.toggleService, - label: Text(translate("Start Service")))), - Expanded( - child: serverModel.mediaOk - ? Row( - children: [ - Expanded( - flex: 0, - child: Padding( - padding: const EdgeInsets.only( - left: 20, right: 5), - child: Icon(Icons.circle, - color: serverModel.connectStatus > 0 - ? Colors.greenAccent - : Colors.deepOrangeAccent, - size: 10))), - Expanded( - child: Text(translate(status), - softWrap: true, - style: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.w500, - color: MyTheme.accent80))) - ], - ) - : const SizedBox.shrink()) - ], - ), - ], - )); + )) + ]) + ])); } } @@ -339,37 +357,14 @@ class PermissionRow extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - flex: 5, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: - Text(name, style: Theme.of(context).textTheme.labelLarge))), - Expanded( - flex: 2, - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text(isOk ? translate("ON") : translate("OFF"), - style: TextStyle( - fontSize: 16.0, - color: isOk ? Colors.green : Colors.grey))), - ), - Expanded( - flex: 3, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerRight, - child: TextButton( - onPressed: onPressed, - child: Text( - translate(isOk ? "CLOSE" : "OPEN"), - style: const TextStyle(fontWeight: FontWeight.bold), - )))), - ], - ); + return SwitchListTile( + visualDensity: VisualDensity.compact, + contentPadding: EdgeInsets.all(0), + title: Text(name), + value: isOk, + onChanged: (bool value) { + onPressed(); + }); } } @@ -386,70 +381,66 @@ class ConnectionManager extends StatelessWidget { ? "File Connection" : "Screen Connection"), titleIcon: client.isFileTransfer - ? Icons.folder_outlined - : Icons.mobile_screen_share, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: ClientInfo(client)), - Expanded( - flex: -1, - child: client.isFileTransfer || !client.authorized - ? const SizedBox.shrink() - : IconButton( - onPressed: () { - gFFI.chatModel.changeCurrentID(client.id); - final bar = - navigationBarKey.currentWidget; - if (bar != null) { - bar as BottomNavigationBar; - bar.onTap!(1); - } - }, - icon: const Icon( - Icons.chat, - color: MyTheme.accent, - ))) - ], - ), - client.authorized - ? const SizedBox.shrink() - : Text( - translate("android_new_connection_tip"), - style: Theme.of(globalKey.currentContext!) - .textTheme - .bodyMedium, - ), - client.authorized - ? ElevatedButton.icon( - style: ButtonStyle( - backgroundColor: - MaterialStateProperty.all(Colors.red)), - icon: const Icon(Icons.close), - onPressed: () { - bind.cmCloseConnection(connId: client.id); - gFFI.invokeMethod( - "cancel_notification", client.id); - }, - label: Text(translate("Close"))) - : Row(children: [ - TextButton( - child: Text(translate("Dismiss")), - onPressed: () { - serverModel.sendLoginResponse(client, false); - }), - const SizedBox(width: 20), - ElevatedButton( - child: Text(translate("Accept")), - onPressed: () { - serverModel.sendLoginResponse(client, true); - }), - ]), - ], - ))) + ? Icon(Icons.folder_outlined) + : Icon(Icons.mobile_screen_share), + child: Column(children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: ClientInfo(client)), + Expanded( + flex: -1, + child: client.isFileTransfer || !client.authorized + ? const SizedBox.shrink() + : IconButton( + onPressed: () { + gFFI.chatModel.changeCurrentID(client.id); + final bar = navigationBarKey.currentWidget; + if (bar != null) { + bar as BottomNavigationBar; + bar.onTap!(1); + } + }, + icon: const Icon(Icons.chat))) + ], + ), + client.authorized + ? const SizedBox.shrink() + : Text( + translate("android_new_connection_tip"), + style: Theme.of(context).textTheme.bodyMedium, + ).marginOnly(bottom: 5), + client.authorized + ? Container( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + style: ButtonStyle( + backgroundColor: + MaterialStatePropertyAll(Colors.red)), + icon: const Icon(Icons.close), + onPressed: () { + bind.cmCloseConnection(connId: client.id); + gFFI.invokeMethod( + "cancel_notification", client.id); + }, + label: Text(translate("Disconnect")))) + : Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + child: Text(translate("Dismiss")), + onPressed: () { + serverModel.sendLoginResponse( + client, false); + }).marginOnly(right: 15), + ElevatedButton.icon( + icon: const Icon(Icons.check), + label: Text(translate("Accept")), + onPressed: () { + serverModel.sendLoginResponse(client, true); + }), + ]), + ]))) .toList()); } } @@ -459,7 +450,7 @@ class PaddingCard extends StatelessWidget { : super(key: key); final String? title; - final IconData? titleIcon; + final Icon? titleIcon; final Widget child; @override @@ -469,23 +460,16 @@ class PaddingCard extends StatelessWidget { children.insert( 0, Padding( - padding: const EdgeInsets.symmetric(vertical: 5.0), + padding: const EdgeInsets.fromLTRB(0, 5, 0, 8), child: Row( children: [ - titleIcon != null - ? Padding( - padding: const EdgeInsets.only(right: 10), - child: - Icon(titleIcon, color: MyTheme.accent, size: 30)) - : const SizedBox.shrink(), - Text( - title!, - style: const TextStyle( - fontFamily: 'WorkSans', - fontWeight: FontWeight.bold, - fontSize: 20, - color: MyTheme.accent, - ), + titleIcon?.marginOnly(right: 10) ?? const SizedBox.shrink(), + Expanded( + child: Text(title!, + style: Theme.of(context) + .textTheme + .titleLarge + ?.merge(TextStyle(fontWeight: FontWeight.bold))), ) ], ))); @@ -493,12 +477,14 @@ class PaddingCard extends StatelessWidget { return SizedBox( width: double.maxFinite, child: Card( - margin: const EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(13), + ), + margin: const EdgeInsets.fromLTRB(12.0, 10.0, 12.0, 0), child: Padding( padding: - const EdgeInsets.symmetric(vertical: 15.0, horizontal: 30.0), + const EdgeInsets.symmetric(vertical: 15.0, horizontal: 20.0), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, children: children, ), ), @@ -514,7 +500,7 @@ class ClientInfo extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + child: Column(children: [ Row( children: [ Expanded( @@ -522,21 +508,19 @@ class ClientInfo extends StatelessWidget { child: Padding( padding: const EdgeInsets.only(right: 12), child: CircleAvatar( - backgroundColor: - str2color(client.name).withOpacity(0.7), + backgroundColor: str2color( + client.name, + Theme.of(context).brightness == Brightness.light + ? 255 + : 150), child: Text(client.name[0])))), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(client.name, - style: const TextStyle( - color: MyTheme.idColor, fontSize: 18)), + Text(client.name, style: const TextStyle(fontSize: 18)), const SizedBox(width: 8), - Text(client.peerId, - style: const TextStyle( - color: MyTheme.idColor, fontSize: 10)) + Text(client.peerId, style: const TextStyle(fontSize: 10)) ])) ], ), @@ -567,7 +551,7 @@ void androidChannelInit() { { var type = arguments["type"] as String; var result = arguments["result"] as bool; - PermissionManager.complete(type, result); + AndroidPermissionManager.complete(type, result); break; } case "on_media_projection_canceled": diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 9637ecb40..c19601956 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -9,6 +9,8 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; +import '../../common/widgets/login.dart'; +import '../../consts.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; @@ -30,18 +32,20 @@ class SettingsPage extends StatefulWidget implements PageShape { } const url = 'https://rustdesk.com/'; -final _hasIgnoreBattery = androidVersion >= 26; -var _ignoreBatteryOpt = false; -var _enableAbr = false; -var _denyLANDiscovery = false; -var _onlyWhiteList = false; -var _enableDirectIPAccess = false; -var _enableRecordSession = false; -var _autoRecordIncomingSession = false; -var _localIP = ""; -var _directAccessPort = ""; class _SettingsState extends State with WidgetsBindingObserver { + final _hasIgnoreBattery = androidVersion >= 26; + var _ignoreBatteryOpt = false; + var _enableStartOnBoot = false; + var _enableAbr = false; + var _denyLANDiscovery = false; + var _onlyWhiteList = false; + var _enableDirectIPAccess = false; + var _enableRecordSession = false; + var _autoRecordIncomingSession = false; + var _localIP = ""; + var _directAccessPort = ""; + @override void initState() { super.initState(); @@ -49,11 +53,34 @@ class _SettingsState extends State with WidgetsBindingObserver { () async { var update = false; + if (_hasIgnoreBattery) { - update = await updateIgnoreBatteryStatus(); + if (await checkAndUpdateIgnoreBatteryStatus()) { + update = true; + } } - final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N"; + if (await checkAndUpdateStartOnBoot()) { + update = true; + } + + // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW + var enableStartOnBoot = + await gFFI.invokeMethod(AndroidChannel.kGetStartOnBootOpt); + if (enableStartOnBoot) { + if (!await canStartOnBoot()) { + enableStartOnBoot = false; + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false); + } + } + + if (enableStartOnBoot != _enableStartOnBoot) { + update = true; + _enableStartOnBoot = enableStartOnBoot; + } + + final enableAbrRes = option2bool( + "enable-abr", await bind.mainGetOption(key: "enable-abr")); if (enableAbrRes != _enableAbr) { update = true; _enableAbr = enableAbrRes; @@ -124,15 +151,18 @@ class _SettingsState extends State with WidgetsBindingObserver { void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { () async { - if (await updateIgnoreBatteryStatus()) { + final ibs = await checkAndUpdateIgnoreBatteryStatus(); + final sob = await checkAndUpdateStartOnBoot(); + if (ibs || sob) { setState(() {}); } }(); } } - Future updateIgnoreBatteryStatus() async { - final res = await PermissionManager.check("ignore_battery_optimizations"); + Future checkAndUpdateIgnoreBatteryStatus() async { + final res = await AndroidPermissionManager.check( + kRequestIgnoreBatteryOptimizations); if (_ignoreBatteryOpt != res) { _ignoreBatteryOpt = res; return true; @@ -141,6 +171,18 @@ class _SettingsState extends State with WidgetsBindingObserver { } } + Future checkAndUpdateStartOnBoot() async { + if (!await canStartOnBoot() && _enableStartOnBoot) { + _enableStartOnBoot = false; + debugPrint( + "checkAndUpdateStartOnBoot and set _enableStartOnBoot -> false"); + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, false); + return true; + } else { + return false; + } + } + @override Widget build(BuildContext context) { Provider.of(context); @@ -264,7 +306,8 @@ class _SettingsState extends State with WidgetsBindingObserver { ]), onToggle: (v) async { if (v) { - PermissionManager.request("ignore_battery_optimizations"); + await AndroidPermissionManager.request( + kRequestIgnoreBatteryOptimizations); } else { final res = await gFFI.dialogManager .show((setState, close) => CustomAlertDialog( @@ -272,21 +315,53 @@ class _SettingsState extends State with WidgetsBindingObserver { content: Text(translate( "android_open_battery_optimizations_tip")), actions: [ - TextButton( - onPressed: () => close(), - child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), - child: - Text(translate("Open System Setting"))), + dialogButton("Cancel", + onPressed: () => close(), isOutline: true), + dialogButton( + "Open System Setting", + onPressed: () => close(true), + ), ], )); if (res == true) { - PermissionManager.request("application_details_settings"); + AndroidPermissionManager.startAction( + kActionApplicationDetailsSettings); } } })); } + enhancementsTiles.add(SettingsTile.switchTile( + initialValue: _enableStartOnBoot, + title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text("${translate('Start on Boot')} (beta)"), + Text( + '* ${translate('Start the screen sharing service on boot, requires special permissions')}', + style: Theme.of(context).textTheme.bodySmall), + ]), + onToggle: (toValue) async { + if (toValue) { + // 1. request kIgnoreBatteryOptimizations + if (!await AndroidPermissionManager.check( + kRequestIgnoreBatteryOptimizations)) { + if (!await AndroidPermissionManager.request( + kRequestIgnoreBatteryOptimizations)) { + return; + } + } + + // 2. request kSystemAlertWindow + if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { + if (!await AndroidPermissionManager.request(kSystemAlertWindow)) { + return; + } + } + + // (Optional) 3. request input permission + } + setState(() => _enableStartOnBoot = toValue); + + gFFI.invokeMethod(AndroidChannel.kSetStartOnBootOpt, toValue); + })); return SettingsList( sections: [ @@ -300,7 +375,7 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.person), onPressed: (context) { if (gFFI.userModel.userName.value.isEmpty) { - showLogin(gFFI.dialogManager); + loginDialog(); } else { gFFI.userModel.logOut(); } @@ -322,8 +397,13 @@ class _SettingsState extends State with WidgetsBindingObserver { showLanguageSettings(gFFI.dialogManager); }), SettingsTile.navigation( - title: Text(translate('Dark Theme')), - leading: Icon(Icons.dark_mode), + title: Text(translate( + Theme.of(context).brightness == Brightness.light + ? 'Dark Theme' + : 'Light Theme')), + leading: Icon(Theme.of(context).brightness == Brightness.light + ? Icons.dark_mode + : Icons.light_mode), onPressed: (context) { showThemeSettings(gFFI.dialogManager); }, @@ -387,6 +467,17 @@ class _SettingsState extends State with WidgetsBindingObserver { ], ); } + + Future canStartOnBoot() async { + // start on boot depends on ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS and SYSTEM_ALERT_WINDOW + if (_hasIgnoreBattery && !_ignoreBatteryOpt) { + return false; + } + if (!await AndroidPermissionManager.check(kSystemAlertWindow)) { + return false; + } + return true; + } } void showServerSettings(OverlayDialogManager dialogManager) async { @@ -397,7 +488,7 @@ void showServerSettings(OverlayDialogManager dialogManager) async { void showLanguageSettings(OverlayDialogManager dialogManager) async { try { final langs = json.decode(await bind.mainGetLangs()) as List; - var lang = await bind.mainGetLocalOption(key: "lang"); + var lang = bind.mainGetLocalOption(key: "lang"); dialogManager.show((setState, close) { setLang(v) { if (lang != v) { @@ -482,77 +573,6 @@ void showAbout(OverlayDialogManager dialogManager) { }, clickMaskDismiss: true, backDismiss: true); } -void showLogin(OverlayDialogManager dialogManager) { - final passwordController = TextEditingController(); - final nameController = TextEditingController(); - var loading = false; - var error = ''; - dialogManager.show((setState, close) { - return CustomAlertDialog( - title: Text(translate('Login')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - TextField( - autofocus: true, - autocorrect: false, - enableSuggestions: false, - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Username'), - ), - controller: nameController, - ), - PasswordWidget(controller: passwordController, autoFocus: false), - ]), - actions: (loading - ? [CircularProgressIndicator()] - : (error != "" - ? [ - Text(translate(error), - style: TextStyle(color: Colors.red)) - ] - : [])) + - [ - TextButton( - style: flatButtonStyle, - onPressed: loading - ? null - : () { - close(); - setState(() { - loading = false; - }); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: loading - ? null - : () async { - final name = nameController.text.trim(); - final pass = passwordController.text.trim(); - if (name != "" && pass != "") { - setState(() { - loading = true; - }); - final resp = await gFFI.userModel.login(name, pass); - setState(() { - loading = false; - }); - if (resp.containsKey('error')) { - error = resp['error']; - return; - } - } - close(); - }, - child: Text(translate('OK')), - ), - ], - ); - }); -} - class ScanButton extends StatelessWidget { @override Widget build(BuildContext context) { diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 2df80d9fd..b281895ff 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,46 +1,19 @@ import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import '../../common.dart'; -import '../../models/model.dart'; import '../../models/platform_model.dart'; -void clientClose(String id, OverlayDialogManager dialogManager) { - msgBox(id, '', 'Close', 'Are you sure to close the connection?', '', - dialogManager); -} - -void showSuccess() { +void _showSuccess() { showToast(translate("Successful")); } -void showError() { +void _showError() { showToast(translate("Error")); } -void showRestartRemoteDevice( - PeerInfo pi, String id, OverlayDialogManager dialogManager) async { - final res = - await dialogManager.show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Restart Remote Device")), - ]), - content: Text( - "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), - actions: [ - TextButton( - onPressed: () => close(), child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), child: Text(translate("OK"))), - ], - )); - if (res == true) bind.sessionRestartRemoteDevice(id: id); -} - void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); @@ -48,8 +21,26 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { var validateLength = false; var validateSame = false; dialogManager.show((setState, close) { + submit() async { + close(); + dialogManager.showLoading(translate("Waiting")); + if (await gFFI.serverModel.setPermanentPassword(p0.text)) { + dialogManager.dismissAll(); + _showSuccess(); + } else { + dialogManager.dismissAll(); + _showError(); + } + } + return CustomAlertDialog( - title: Text(translate('Set your own password')), + title: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.password_rounded, color: MyTheme.accent), + Text(translate('Set your own password')).paddingOnly(left: 10), + ], + ), content: Form( autovalidateMode: AutovalidateMode.onUserInteraction, child: Column(mainAxisSize: MainAxisSize.min, children: [ @@ -94,30 +85,19 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { }, ), ])), + onCancel: close, + onSubmit: (validateLength && validateSame) ? submit : null, actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), + dialogButton( + 'Cancel', + icon: Icon(Icons.close_rounded), + onPressed: close, + isOutline: true, ), - TextButton( - style: flatButtonStyle, - onPressed: (validateLength && validateSame) - ? () async { - close(); - dialogManager.showLoading(translate("Waiting")); - if (await gFFI.serverModel.setPermanentPassword(p0.text)) { - dialogManager.dismissAll(); - showSuccess(); - } else { - dialogManager.dismissAll(); - showError(); - } - } - : null, - child: Text(translate('OK')), + dialogButton( + 'OK', + icon: Icon(Icons.done_rounded), + onPressed: (validateLength && validateSame) ? submit : null, ), ], ); @@ -142,12 +122,12 @@ void setTemporaryPasswordLengthDialog( bind.mainUpdateTemporaryPassword(); Future.delayed(Duration(milliseconds: 200), () { close(); - showSuccess(); + _showSuccess(); }); } return CustomAlertDialog( - title: Text(translate("Set temporary password length")), + title: Text(translate("Set one-time password length")), content: Column( mainAxisSize: MainAxisSize.min, children: @@ -158,85 +138,6 @@ void setTemporaryPasswordLengthDialog( }, backDismiss: true, clickMaskDismiss: true); } -void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { - final controller = TextEditingController(); - var remember = await bind.sessionGetRemember(id: id) ?? false; - dialogManager.dismissAll(); - dialogManager.show((setState, close) { - cancel() { - close(); - closeConnection(); - } - - submit() { - var text = controller.text.trim(); - if (text == '') return; - gFFI.login(id, text, remember); - close(); - dialogManager.showLoading(translate('Logging in...'), - onCancel: closeConnection); - } - - return CustomAlertDialog( - title: Text(translate('Password Required')), - content: Column(mainAxisSize: MainAxisSize.min, children: [ - PasswordWidget(controller: controller), - CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate('Remember password'), - ), - value: remember, - onChanged: (v) { - if (v != null) { - setState(() => remember = v); - } - }, - ), - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), - ], - onSubmit: submit, - onCancel: cancel, - ); - }); -} - -void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { - dialogManager.show((setState, close) => CustomAlertDialog( - title: Text(translate('Wrong Password')), - content: Text(translate('Do you want to enter again?')), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - closeConnection(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: () { - enterPasswordDialog(id, dialogManager); - }, - child: Text(translate('Retry')), - ), - ])); -} - void showServerSettingsWithValue( ServerConfig serverConfig, OverlayDialogManager dialogManager) async { Map oldOptions = jsonDecode(await bind.mainGetOptions()); @@ -320,15 +221,11 @@ void showServerSettingsWithValue( child: LinearProgressIndicator()) ])), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + dialogButton( + 'OK', onPressed: () async { setState(() { idServerMsg = null; @@ -360,7 +257,6 @@ void showServerSettingsWithValue( isInProgress = false; }); }, - child: Text(translate('OK')), ), ], ); @@ -375,63 +271,3 @@ Future validateAsync(String value) async { final res = await bind.mainTestIfValidServer(server: value); return res.isEmpty ? null : res; } - -class PasswordWidget extends StatefulWidget { - PasswordWidget({Key? key, required this.controller, this.autoFocus = true}) - : super(key: key); - - final TextEditingController controller; - final bool autoFocus; - - @override - State createState() => _PasswordWidgetState(); -} - -class _PasswordWidgetState extends State { - bool _passwordVisible = false; - final _focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - if (widget.autoFocus) { - Timer(Duration(milliseconds: 50), () => _focusNode.requestFocus()); - } - } - - @override - void dispose() { - _focusNode.unfocus(); - _focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return TextField( - focusNode: _focusNode, - controller: widget.controller, - obscureText: !_passwordVisible, - //This will obscure text dynamically - keyboardType: TextInputType.visiblePassword, - decoration: InputDecoration( - labelText: translate('Password'), - hintText: translate('Enter your password'), - // Here is key idea - suffixIcon: IconButton( - icon: Icon( - // Based on passwordVisible state choose the icon - _passwordVisible ? Icons.visibility : Icons.visibility_off, - color: Theme.of(context).primaryColorDark, - ), - onPressed: () { - // Update the state i.e. toggle the state of passwordVisible variable - setState(() { - _passwordVisible = !_passwordVisible; - }); - }, - ), - ), - ); - } -} diff --git a/flutter/lib/mobile/widgets/gesture_help.dart b/flutter/lib/mobile/widgets/gesture_help.dart index 37cc77c8f..bc31ae2c4 100644 --- a/flutter/lib/mobile/widgets/gesture_help.dart +++ b/flutter/lib/mobile/widgets/gesture_help.dart @@ -2,8 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:toggle_switch/toggle_switch.dart'; -import '../../models/model.dart'; - class GestureIcons { static const String _family = 'gestureicons'; @@ -79,7 +77,10 @@ class _GestureHelpState extends State { children: [ ToggleSwitch( initialLabelIndex: _selectedIndex, - inactiveBgColor: MyTheme.darkGray, + activeFgColor: Colors.white, + inactiveFgColor: Colors.white60, + activeBgColor: [MyTheme.accent], + inactiveBgColor: Theme.of(context).hintColor, totalSwitches: 2, minWidth: 150, fontSize: 15, @@ -188,7 +189,7 @@ class GestureInfo extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - width: this.width, + width: width, child: Column( children: [ Icon( @@ -199,11 +200,14 @@ class GestureInfo extends StatelessWidget { SizedBox(height: 6), Text(fromText, textAlign: TextAlign.center, - style: TextStyle(fontSize: 9, color: Colors.grey)), + style: + TextStyle(fontSize: 9, color: Theme.of(context).hintColor)), SizedBox(height: 3), Text(toText, textAlign: TextAlign.center, - style: TextStyle(fontSize: 12, color: Colors.black)) + style: TextStyle( + fontSize: 12, + color: Theme.of(context).textTheme.bodySmall?.color)) ], )); } diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index d8a0e8f99..175c8424b 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -27,8 +27,7 @@ class AbModel { abError.value = ""; final api = "${await bind.mainGetApiServer()}/api/ab/get"; try { - final resp = - await http.post(Uri.parse(api), headers: await getHttpHeaders()); + final resp = await http.post(Uri.parse(api), headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { @@ -102,7 +101,7 @@ class AbModel { Future pushAb() async { abLoading.value = true; final api = "${await bind.mainGetApiServer()}/api/ab"; - var authHeaders = await getHttpHeaders(); + var authHeaders = getHttpHeaders(); authHeaders['Content-Type'] = "application/json"; final peersJsonData = peers.map((e) => e.toJson()).toList(); final body = jsonEncode({ diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 18a0be279..9db5a1571 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,7 +1,11 @@ +import 'dart:async'; + import 'package:dash_chat_2/dash_chat_2.dart'; import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; import '../consts.dart'; @@ -26,18 +30,20 @@ class MessageBody { class ChatModel with ChangeNotifier { static final clientModeID = -1; - /// _overlayState: - /// Desktop: store session overlay by using [setOverlayState]. - /// Mobile: always null, use global overlay. - /// see [_getOverlayState] in [showChatIconOverlay] or [showChatWindowOverlay] - OverlayState? _overlayState; OverlayEntry? chatIconOverlayEntry; OverlayEntry? chatWindowOverlayEntry; + bool isConnManager = false; + RxBool isWindowFocus = true.obs; + BlockableOverlayState? _blockableOverlayState; + final Rx _voiceCallStatus = Rx(VoiceCallStatus.notStarted); + + Rx get voiceCallStatus => _voiceCallStatus; + final ChatUser me = ChatUser( id: "", - firstName: "Me", + firstName: translate("Me"), ); late final Map _messages = {}..[clientModeID] = @@ -52,6 +58,19 @@ class ChatModel with ChangeNotifier { bool get isShowCMChatPage => _isShowCMChatPage; + void setOverlayState(BlockableOverlayState blockableOverlayState) { + _blockableOverlayState = blockableOverlayState; + + _blockableOverlayState!.addMiddleBlockedListener((v) { + if (!v) { + isWindowFocus.value = false; + if (isWindowFocus.value) { + isWindowFocus.toggle(); + } + } + }); + } + final WeakReference parent; ChatModel(this.parent); @@ -68,20 +87,6 @@ class ChatModel with ChangeNotifier { } } - setOverlayState(OverlayState? os) { - _overlayState = os; - } - - OverlayState? _getOverlayState() { - if (_overlayState == null) { - if (globalKey.currentState == null || - globalKey.currentState!.overlay == null) return null; - return globalKey.currentState!.overlay; - } else { - return _overlayState; - } - } - showChatIconOverlay({Offset offset = const Offset(200, 50)}) { if (chatIconOverlayEntry != null) { chatIconOverlayEntry!.remove(); @@ -94,7 +99,7 @@ class ChatModel with ChangeNotifier { } } - final overlayState = _getOverlayState(); + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { @@ -126,23 +131,35 @@ class ChatModel with ChangeNotifier { } } - showChatWindowOverlay() { + showChatWindowOverlay({Offset? chatInitPos}) { if (chatWindowOverlayEntry != null) return; - final overlayState = _getOverlayState(); + isWindowFocus.value = true; + _blockableOverlayState?.setMiddleBlocked(true); + + final overlayState = _blockableOverlayState?.state; if (overlayState == null) return; final overlay = OverlayEntry(builder: (context) { - return DraggableChatWindow( - position: const Offset(20, 80), - width: 250, - height: 350, - chatModel: this); + return Listener( + onPointerDown: (_) { + if (!isWindowFocus.value) { + isWindowFocus.value = true; + _blockableOverlayState?.setMiddleBlocked(true); + } + }, + child: DraggableChatWindow( + position: chatInitPos ?? Offset(20, 80), + width: 250, + height: 350, + chatModel: this)); }); overlayState.insert(overlay); chatWindowOverlayEntry = overlay; + requestChatInputFocus(); } hideChatWindowOverlay() { if (chatWindowOverlayEntry != null) { + _blockableOverlayState?.setMiddleBlocked(false); chatWindowOverlayEntry!.remove(); chatWindowOverlayEntry = null; return; @@ -152,13 +169,13 @@ class ChatModel with ChangeNotifier { _isChatOverlayHide() => ((!isDesktop && chatIconOverlayEntry == null) || chatWindowOverlayEntry == null); - toggleChatOverlay() { + toggleChatOverlay({Offset? chatInitPos}) { if (_isChatOverlayHide()) { gFFI.invokeMethod("enable_soft_keyboard", true); if (!isDesktop) { showChatIconOverlay(); } - showChatWindowOverlay(); + showChatWindowOverlay(chatInitPos: chatInitPos); } else { hideChatIconOverlay(); hideChatWindowOverlay(); @@ -188,6 +205,7 @@ class ChatModel with ChangeNotifier { await windowManager.setSizeAlignment( kConnectionManagerWindowSize, Alignment.topRight); } else { + requestChatInputFocus(); await windowManager.show(); await windowManager.setSizeAlignment(Size(600, 400), Alignment.topRight); _isShowCMChatPage = !_isShowCMChatPage; @@ -285,11 +303,48 @@ class ChatModel with ChangeNotifier { close() { hideChatIconOverlay(); hideChatWindowOverlay(); - _overlayState = null; notifyListeners(); } resetClientMode() { _messages[clientModeID]?.clear(); } + + void requestChatInputFocus() { + Timer(Duration(milliseconds: 100), () { + if (inputNode.hasListeners && inputNode.canRequestFocus) { + inputNode.requestFocus(); + } + }); + } + + void onVoiceCallWaiting() { + _voiceCallStatus.value = VoiceCallStatus.waitingForResponse; + } + + void onVoiceCallStarted() { + _voiceCallStatus.value = VoiceCallStatus.connected; + } + + void onVoiceCallClosed(String reason) { + _voiceCallStatus.value = VoiceCallStatus.notStarted; + } + + void onVoiceCallIncoming() { + if (isConnManager) { + _voiceCallStatus.value = VoiceCallStatus.incoming; + } + } + + void closeVoiceCall(String id) { + bind.sessionCloseVoiceCall(id: id); + } +} + +enum VoiceCallStatus { + notStarted, + waitingForResponse, + connected, + // Connection manager only. + incoming } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index c08d2e623..6efcc6f30 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -4,9 +4,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/utils/event_loop.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; +import '../consts.dart'; import 'model.dart'; import 'platform_model.dart'; @@ -23,294 +25,99 @@ enum SortBy { } } -class FileModel extends ChangeNotifier { - /// mobile, current selected page show on mobile screen - var _isSelectedLocal = false; - - /// mobile, select mode state - var _selectMode = false; - - final _localOption = DirectoryOption(); - final _remoteOption = DirectoryOption(); - - List localHistory = []; - List remoteHistory = []; - - var _jobId = 0; - - final _jobProgress = JobProgress(); // from rust update - - /// JobTable - final _jobTable = List.empty(growable: true).obs; - - /// `isLocal` bool - Function(bool)? onDirChanged; - - RxList get jobTable => _jobTable; - - bool get isLocal => _isSelectedLocal; - - bool get selectMode => _selectMode; - - JobProgress get jobProgress => _jobProgress; - - JobState get jobState => _jobProgress.state; - - SortBy _sortStyle = SortBy.name; - - SortBy get sortStyle => _sortStyle; - - SortBy _localSortStyle = SortBy.name; - - bool _localSortAscending = true; - - bool _remoteSortAscending = true; - - SortBy _remoteSortStyle = SortBy.name; - - bool get localSortAscending => _localSortAscending; - - SortBy getSortStyle(bool isLocal) { - return isLocal ? _localSortStyle : _remoteSortStyle; +class JobID { + int _count = 0; + int next() { + _count++; + return _count; } +} - FileDirectory _currentLocalDir = FileDirectory(); - - FileDirectory get currentLocalDir => _currentLocalDir; - - FileDirectory _currentRemoteDir = FileDirectory(); - - FileDirectory get currentRemoteDir => _currentRemoteDir; - - FileDirectory get currentDir => - _isSelectedLocal ? currentLocalDir : currentRemoteDir; - - FileDirectory getCurrentDir(bool isLocal) { - return isLocal ? currentLocalDir : currentRemoteDir; - } - - String getCurrentShortPath(bool isLocal) { - final currentDir = getCurrentDir(isLocal); - final currentHome = getCurrentHome(isLocal); - if (currentDir.path.startsWith(currentHome)) { - var path = currentDir.path.replaceFirst(currentHome, ""); - if (path.isEmpty) return ""; - if (path[0] == "/" || path[0] == "\\") { - // remove more '/' or '\' - path = path.replaceFirst(path[0], ""); - } - return path; - } else { - return currentDir.path.replaceFirst(currentHome, ""); - } - } - - String get currentHome => - _isSelectedLocal ? _localOption.home : _remoteOption.home; - - String getCurrentHome(bool isLocal) { - return isLocal ? _localOption.home : _remoteOption.home; - } - - int getJob(int id) { - return jobTable.indexWhere((element) => element.id == id); - } - - String get currentShortPath { - if (currentDir.path.startsWith(currentHome)) { - var path = currentDir.path.replaceFirst(currentHome, ""); - if (path.isEmpty) return ""; - if (path[0] == "/" || path[0] == "\\") { - // remove more '/' or '\' - path = path.replaceFirst(path[0], ""); - } - return path; - } else { - return currentDir.path.replaceFirst(currentHome, ""); - } - } - - String shortPath(bool isLocal) { - final dir = isLocal ? currentLocalDir : currentRemoteDir; - if (dir.path.startsWith(currentHome)) { - var path = dir.path.replaceFirst(currentHome, ""); - if (path.isEmpty) return ""; - if (path[0] == "/" || path[0] == "\\") { - // remove more '/' or '\' - path = path.replaceFirst(path[0], ""); - } - return path; - } else { - return dir.path.replaceFirst(currentHome, ""); - } - } - - bool getCurrentShowHidden([bool? isLocal]) { - final isLocal_ = isLocal ?? _isSelectedLocal; - return isLocal_ ? _localOption.showHidden : _remoteOption.showHidden; - } - - bool getCurrentIsWindows([bool? isLocal]) { - final isLocal_ = isLocal ?? _isSelectedLocal; - return isLocal_ ? _localOption.isWindows : _remoteOption.isWindows; - } - - final _fileFetcher = FileFetcher(); - - final _jobResultListener = JobResultListener>(); +typedef GetSessionID = String Function(); +class FileModel { final WeakReference parent; + // late final String sessionID; + late final FileFetcher fileFetcher; + late final JobController jobController; - FileModel(this.parent); + late final FileController localController; + late final FileController remoteController; - toggleSelectMode() { - if (jobState == JobState.inProgress) { - return; - } - _selectMode = !_selectMode; - notifyListeners(); + late final GetSessionID getSessionID; + String get sessionID => getSessionID(); + late final FileDialogEventLoop evtLoop; + + FileModel(this.parent) { + getSessionID = () => parent.target?.id ?? ""; + fileFetcher = FileFetcher(getSessionID); + jobController = JobController(getSessionID); + localController = FileController( + isLocal: true, + getSessionID: getSessionID, + rootState: parent, + jobController: jobController, + fileFetcher: fileFetcher, + getOtherSideDirectoryData: () => remoteController.directoryData()); + remoteController = FileController( + isLocal: false, + getSessionID: getSessionID, + rootState: parent, + jobController: jobController, + fileFetcher: fileFetcher, + getOtherSideDirectoryData: () => localController.directoryData()); + evtLoop = FileDialogEventLoop(); } - togglePage() { - _isSelectedLocal = !_isSelectedLocal; - notifyListeners(); + Future onReady() async { + await evtLoop.onReady(); + await localController.onReady(); + await remoteController.onReady(); } - toggleShowHidden({bool? showHidden, bool? local}) { - final isLocal = local ?? _isSelectedLocal; - if (isLocal) { - _localOption.showHidden = showHidden ?? !_localOption.showHidden; - } else { - _remoteOption.showHidden = showHidden ?? !_remoteOption.showHidden; - } - refresh(isLocal: local); + Future close() async { + await evtLoop.close(); + parent.target?.dialogManager.dismissAll(); + await localController.close(); + await remoteController.close(); } - tryUpdateJobProgress(Map evt) { - try { - int id = int.parse(evt['id']); - if (!isDesktop) { - _jobProgress.id = id; - _jobProgress.fileNum = int.parse(evt['file_num']); - _jobProgress.speed = double.parse(evt['speed']); - _jobProgress.finishedSize = int.parse(evt['finished_size']); - } else { - // Desktop uses jobTable - // id = index + 1 - final jobIndex = getJob(id); - if (jobIndex >= 0 && _jobTable.length > jobIndex) { - final job = _jobTable[jobIndex]; - job.fileNum = int.parse(evt['file_num']); - job.speed = double.parse(evt['speed']); - job.finishedSize = int.parse(evt['finished_size']); - debugPrint("update job $id with $evt"); - } - } - notifyListeners(); - } catch (e) { - debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}"); - } + Future refreshAll() async { + await localController.refresh(); + await remoteController.refresh(); } - receiveFileDir(Map evt) { + void receiveFileDir(Map evt) { if (evt['is_local'] == "false") { - // init remote home, the connection will automatic read remote home when established, - try { - final fd = FileDirectory.fromJson(jsonDecode(evt['value'])); - fd.format(_remoteOption.isWindows, sort: _sortStyle); - if (fd.id > 0) { - final jobIndex = getJob(fd.id); - if (jobIndex != -1) { - final job = jobTable[jobIndex]; - var totalSize = 0; - var fileCount = fd.entries.length; - for (var element in fd.entries) { - totalSize += element.size; - } - job.totalSize = totalSize; - job.fileCount = fileCount; - debugPrint("update receive details:${fd.path}"); - } - } else if (_remoteOption.home.isEmpty) { - _remoteOption.home = fd.path; - debugPrint("init remote home:${fd.path}"); - _currentRemoteDir = fd; - } - } catch (e) { - debugPrint("receiveFileDir err=$e"); - } + // init remote home, the remote connection will send one dir event when established. TODO opt + remoteController.initDirAndHome(evt); } - _fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); - notifyListeners(); + fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); } - jobDone(Map evt) async { - if (_jobResultListener.isListening) { - _jobResultListener.complete(evt); - return; - } - if (!isDesktop) { - _selectMode = false; - _jobProgress.state = JobState.done; - } else { - int id = int.parse(evt['id']); - final jobIndex = getJob(id); - if (jobIndex != -1) { - final job = jobTable[jobIndex]; - job.finishedSize = job.totalSize; - job.state = JobState.done; - job.fileNum = int.parse(evt['file_num']); - } - } - await Future.wait([ - refresh(isLocal: false), - refresh(isLocal: true), - ]); + Future postOverrideFileConfirm(Map evt) async { + evtLoop.pushEvent( + _FileDialogEvent(WeakReference(this), FileDialogType.overwrite, evt)); } - jobError(Map evt) { - final err = evt['err'].toString(); - if (!isDesktop) { - if (_jobResultListener.isListening) { - _jobResultListener.complete(evt); - return; - } - _selectMode = false; - _jobProgress.clear(); - _jobProgress.err = err; - _jobProgress.state = JobState.error; - _jobProgress.fileNum = int.parse(evt['file_num']); - if (err == "skipped") { - _jobProgress.state = JobState.done; - _jobProgress.finishedSize = _jobProgress.totalSize; - } - } else { - int jobIndex = getJob(int.parse(evt['id'])); - if (jobIndex != -1) { - final job = jobTable[jobIndex]; - job.state = JobState.error; - job.err = err; - job.fileNum = int.parse(evt['file_num']); - if (err == "skipped") { - job.state = JobState.done; - job.finishedSize = job.totalSize; - } - } - } - debugPrint("jobError $evt"); - notifyListeners(); - } - - overrideFileConfirm(Map evt) async { - final resp = await showFileConfirmDialog( - translate("Overwrite"), "${evt['read_path']}", true); + Future overrideFileConfirm(Map evt, + {bool? overrideConfirm, bool skip = false}) async { + // If `skip == true`, it means to skip this file without showing dialog. + // Because `resp` may be null after the user operation or the last remembered operation, + // and we should distinguish them. + final resp = overrideConfirm ?? + (!skip + ? await showFileConfirmDialog(translate("Overwrite"), + "${evt['read_path']}", true, evt['is_identical'] == "true") + : null); final id = int.tryParse(evt['id']) ?? 0; if (false == resp) { - final jobIndex = getJob(id); + final jobIndex = jobController.getJob(id); if (jobIndex != -1) { - cancelJob(id); - final job = jobTable[jobIndex]; + await jobController.cancelJob(id); + final job = jobController.jobTable[jobIndex]; job.state = JobState.done; + jobController.jobTable.refresh(); } } else { var need_override = false; @@ -321,368 +128,28 @@ class FileModel extends ChangeNotifier { // overwrite need_override = true; } - bind.sessionSetConfirmOverrideFile( - id: parent.target?.id ?? "", + // Update the loop config. + if (fileConfirmCheckboxRemember) { + evtLoop.setSkip(!need_override); + } + await bind.sessionSetConfirmOverrideFile( + id: sessionID, actId: id, fileNum: int.parse(evt['file_num']), needOverride: need_override, remember: fileConfirmCheckboxRemember, isUpload: evt['is_upload'] == "true"); } - } - - jobReset() { - _jobProgress.clear(); - notifyListeners(); - } - - onReady() async { - _localOption.home = await bind.mainGetHomeDir(); - _localOption.showHidden = (await bind.sessionGetPeerOption( - id: parent.target?.id ?? "", name: "local_show_hidden")) - .isNotEmpty; - _localOption.isWindows = Platform.isWindows; - - _remoteOption.showHidden = (await bind.sessionGetPeerOption( - id: parent.target?.id ?? "", name: "remote_show_hidden")) - .isNotEmpty; - _remoteOption.isWindows = - parent.target?.ffiModel.pi.platform.toLowerCase() == "windows"; - - await Future.delayed(Duration(milliseconds: 100)); - - final local = (await bind.sessionGetPeerOption( - id: parent.target?.id ?? "", name: "local_dir")); - final remote = (await bind.sessionGetPeerOption( - id: parent.target?.id ?? "", name: "remote_dir")); - openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true); - openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false); - await Future.delayed(Duration(seconds: 1)); - if (_currentLocalDir.path.isEmpty) { - openDirectory(_localOption.home, isLocal: true); + // Update the loop config. + if (fileConfirmCheckboxRemember) { + evtLoop.setOverrideConfirm(resp); } - if (_currentRemoteDir.path.isEmpty) { - openDirectory(_remoteOption.home, isLocal: false); - } - } - - Future onClose() async { - parent.target?.dialogManager.dismissAll(); - jobReset(); - - onDirChanged = null; - - // save config - Map msgMap = {}; - - msgMap["local_dir"] = _currentLocalDir.path; - msgMap["local_show_hidden"] = _localOption.showHidden ? "Y" : ""; - msgMap["remote_dir"] = _currentRemoteDir.path; - msgMap["remote_show_hidden"] = _remoteOption.showHidden ? "Y" : ""; - final id = parent.target?.id ?? ""; - for (final msg in msgMap.entries) { - await bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); - } - _currentLocalDir.clear(); - _currentRemoteDir.clear(); - _localOption.clear(); - _remoteOption.clear(); - } - - Future refresh({bool? isLocal}) async { - if (isDesktop) { - isLocal = isLocal ?? _isSelectedLocal; - isLocal - ? await openDirectory(currentLocalDir.path, isLocal: isLocal) - : await openDirectory(currentRemoteDir.path, isLocal: isLocal); - } else { - await openDirectory(currentDir.path); - } - } - - openDirectory(String path, {bool? isLocal, bool isBack = false}) async { - isLocal = isLocal ?? _isSelectedLocal; - if (path == ".") { - refresh(isLocal: isLocal); - return; - } - if (path == "..") { - goToParentDirectory(isLocal: isLocal); - return; - } - if (!isBack) { - pushHistory(isLocal); - } - final showHidden = getCurrentShowHidden(isLocal); - final isWindows = getCurrentIsWindows(isLocal); - // process /C:\ -> C:\ on Windows - if (isWindows && path.length > 1 && path[0] == '/') { - path = path.substring(1); - if (path[path.length - 1] != '\\') { - path = "$path\\"; - } - } - try { - final fd = await _fileFetcher.fetchDirectory(path, isLocal, showHidden); - fd.format(isWindows, sort: _sortStyle); - if (isLocal) { - _currentLocalDir = fd; - } else { - _currentRemoteDir = fd; - } - notifyListeners(); - onDirChanged?.call(isLocal); - } catch (e) { - debugPrint("Failed to openDirectory $path: $e"); - } - } - - Future fetchDirectory(path, isLocal, showHidden) async { - return await _fileFetcher.fetchDirectory(path, isLocal, showHidden); - } - - void pushHistory(bool isLocal) { - final history = isLocal ? localHistory : remoteHistory; - final currPath = isLocal ? currentLocalDir.path : currentRemoteDir.path; - if (history.isNotEmpty && history.last == currPath) { - return; - } - history.add(currPath); - } - - goHome({bool? isLocal}) { - isLocal = isLocal ?? _isSelectedLocal; - openDirectory(getCurrentHome(isLocal), isLocal: isLocal); - } - - goBack({bool? isLocal}) { - isLocal = isLocal ?? _isSelectedLocal; - final history = isLocal ? localHistory : remoteHistory; - if (history.isEmpty) return; - final path = history.removeAt(history.length - 1); - if (path.isEmpty) return; - final currPath = isLocal ? currentLocalDir.path : currentRemoteDir.path; - if (currPath == path) { - goBack(isLocal: isLocal); - return; - } - openDirectory(path, isLocal: isLocal, isBack: true); - } - - goToParentDirectory({bool? isLocal}) { - isLocal = isLocal ?? _isSelectedLocal; - final isWindows = - isLocal ? _localOption.isWindows : _remoteOption.isWindows; - final currDir = isLocal ? currentLocalDir : currentRemoteDir; - var parent = PathUtil.dirname(currDir.path, isWindows); - // specially for C:\, D:\, goto '/' - if (parent == currDir.path && isWindows) { - openDirectory('/', isLocal: isLocal); - return; - } - openDirectory(parent, isLocal: isLocal); - } - - /// isRemote only for desktop now, [isRemote == true] means [remote -> local] - sendFiles(SelectedItems items, {bool isRemote = false}) { - if (isDesktop) { - // desktop sendFiles - final toPath = isRemote ? currentLocalDir.path : currentRemoteDir.path; - final isWindows = - isRemote ? _localOption.isWindows : _remoteOption.isWindows; - final showHidden = - isRemote ? _localOption.showHidden : _remoteOption.showHidden; - for (var from in items.items) { - final jobId = ++_jobId; - _jobTable.add(JobProgress() - ..jobName = from.path - ..totalSize = from.size - ..state = JobState.inProgress - ..id = jobId - ..isRemote = isRemote); - bind.sessionSendFiles( - id: '${parent.target?.id}', - actId: _jobId, - path: from.path, - to: PathUtil.join(toPath, from.name, isWindows), - fileNum: 0, - includeHidden: showHidden, - isRemote: isRemote); - debugPrint( - "path:${from.path}, toPath:$toPath, to:${PathUtil.join(toPath, from.name, isWindows)}"); - } - } else { - if (items.isLocal == null) { - debugPrint("Failed to sendFiles ,wrong path state"); - return; - } - _jobProgress.state = JobState.inProgress; - final toPath = - items.isLocal! ? currentRemoteDir.path : currentLocalDir.path; - final isWindows = - items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows; - final showHidden = - items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden; - items.items.forEach((from) async { - _jobId++; - await bind.sessionSendFiles( - id: '${parent.target?.id}', - actId: _jobId, - path: from.path, - to: PathUtil.join(toPath, from.name, isWindows), - fileNum: 0, - includeHidden: showHidden, - isRemote: !(items.isLocal!)); - }); - } - } - - bool removeCheckboxRemember = false; - - removeAction(SelectedItems items, {bool? isLocal}) async { - isLocal = isLocal ?? _isSelectedLocal; - removeCheckboxRemember = false; - if (items.isLocal == null) { - debugPrint("Failed to removeFile, wrong path state"); - return; - } - final isWindows = - items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows; - await Future.forEach(items.items, (Entry item) async { - _jobId++; - var title = ""; - var content = ""; - late final List entries; - if (item.isFile) { - title = translate("Are you sure you want to delete this file?"); - content = item.name; - entries = [item]; - } else if (item.isDirectory) { - title = translate("Not an empty directory"); - parent.target?.dialogManager.showLoading(translate("Waiting")); - final fd = await _fileFetcher.fetchDirectoryRecursive( - _jobId, item.path, items.isLocal!, true); - if (fd.path.isEmpty) { - fd.path = item.path; - } - fd.format(isWindows); - parent.target?.dialogManager.dismissAll(); - if (fd.entries.isEmpty) { - final confirm = await showRemoveDialog( - translate( - "Are you sure you want to delete this empty directory?"), - item.name, - false); - if (confirm == true) { - sendRemoveEmptyDir(item.path, 0, items.isLocal!); - } - return; - } - entries = fd.entries; - } else { - entries = []; - } - - for (var i = 0; i < entries.length; i++) { - final dirShow = item.isDirectory - ? "${translate("Are you sure you want to delete the file of this directory?")}\n" - : ""; - final count = entries.length > 1 ? "${i + 1}/${entries.length}" : ""; - content = "$dirShow$count \n${entries[i].path}"; - final confirm = - await showRemoveDialog(title, content, item.isDirectory); - try { - if (confirm == true) { - sendRemoveFile(entries[i].path, i, items.isLocal!); - final res = await _jobResultListener.start(); - // handle remove res; - if (item.isDirectory && - res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i, items.isLocal!); - } - } - if (removeCheckboxRemember) { - if (confirm == true) { - for (var j = i + 1; j < entries.length; j++) { - sendRemoveFile(entries[j].path, j, items.isLocal!); - final res = await _jobResultListener.start(); - if (item.isDirectory && - res['file_num'] == (entries.length - 1).toString()) { - sendRemoveEmptyDir(item.path, i, items.isLocal!); - } - } - } - break; - } - } catch (e) { - print("remove error: $e"); - } - } - }); - _selectMode = false; - refresh(isLocal: isLocal); - } - - Future showRemoveDialog( - String title, String content, bool showCheckbox) async { - return await parent.target?.dialogManager.show( - (setState, Function(bool v) close) { - cancel() => close(false); - submit() => close(true); - return CustomAlertDialog( - title: Row( - children: [ - const Icon(Icons.warning, color: Colors.red), - const SizedBox(width: 20), - Text(title) - ], - ), - contentBoxConstraints: - BoxConstraints(minHeight: 100, minWidth: 400, maxWidth: 400), - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text(content), - const SizedBox(height: 5), - Text(translate("This is irreversible!"), - style: const TextStyle(fontWeight: FontWeight.bold)), - showCheckbox - ? CheckboxListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - controlAffinity: ListTileControlAffinity.leading, - title: Text( - translate("Do this for all conflicts"), - ), - value: removeCheckboxRemember, - onChanged: (v) { - if (v == null) return; - setState(() => removeCheckboxRemember = v); - }, - ) - : const SizedBox.shrink() - ]), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))), - ], - onSubmit: submit, - onCancel: cancel, - ); - }, useAnimation: false); } bool fileConfirmCheckboxRemember = false; Future showFileConfirmDialog( - String title, String content, bool showCheckbox) async { + String title, String content, bool showCheckbox, bool isIdentical) async { fileConfirmCheckboxRemember = false; return await parent.target?.dialogManager.show( (setState, Function(bool? v) close) { @@ -691,9 +158,10 @@ class FileModel extends ChangeNotifier { return CustomAlertDialog( title: Row( children: [ - const Icon(Icons.warning, color: Colors.red), - const SizedBox(width: 20), - Text(title) + const Icon(Icons.warning_rounded, color: Colors.red), + Text(title).paddingOnly( + left: 10, + ), ], ), contentBoxConstraints: @@ -706,6 +174,17 @@ class FileModel extends ChangeNotifier { style: const TextStyle(fontWeight: FontWeight.bold)), const SizedBox(height: 5), Text(content), + Offstage( + offstage: !isIdentical, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12), + Text(translate("identical_file_tip"), + style: const TextStyle(fontWeight: FontWeight.w500)) + ], + ), + ), showCheckbox ? CheckboxListTile( contentPadding: const EdgeInsets.all(0), @@ -723,18 +202,406 @@ class FileModel extends ChangeNotifier { : const SizedBox.shrink() ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: () => close(null), - child: Text(translate("Skip"))), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))), + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "Skip", + icon: Icon(Icons.navigate_next_rounded), + onPressed: () => close(null), + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), + ], + onSubmit: submit, + onCancel: cancel, + ); + }, useAnimation: false); + } +} + +class DirectoryData { + final DirectoryOptions options; + final FileDirectory directory; + DirectoryData(this.directory, this.options); +} + +class FileController { + final bool isLocal; + final GetSessionID getSessionID; + String get sessionID => getSessionID(); + + final FileFetcher fileFetcher; + + final options = DirectoryOptions().obs; + final directory = FileDirectory().obs; + + final history = RxList.empty(growable: true); + final sortBy = SortBy.name.obs; + var sortAscending = true; + final JobController jobController; + final WeakReference rootState; + + final DirectoryData Function() getOtherSideDirectoryData; + late final SelectedItems selectedItems = SelectedItems(isLocal: isLocal); + + FileController( + {required this.isLocal, + required this.getSessionID, + required this.rootState, + required this.jobController, + required this.fileFetcher, + required this.getOtherSideDirectoryData}); + + String get homePath => options.value.home; + OverlayDialogManager? get dialogManager => rootState.target?.dialogManager; + + String get shortPath { + final dirPath = directory.value.path; + if (dirPath.startsWith(homePath)) { + var path = dirPath.replaceFirst(homePath, ""); + if (path.isEmpty) return ""; + if (path[0] == "/" || path[0] == "\\") { + // remove more '/' or '\' + path = path.replaceFirst(path[0], ""); + } + return path; + } else { + return dirPath.replaceFirst(homePath, ""); + } + } + + DirectoryData directoryData() { + return DirectoryData(directory.value, options.value); + } + + Future onReady() async { + if (isLocal) { + options.value.home = await bind.mainGetHomeDir(); + } + options.value.showHidden = (await bind.sessionGetPeerOption( + id: sessionID, + name: isLocal ? "local_show_hidden" : "remote_show_hidden")) + .isNotEmpty; + options.value.isWindows = isLocal + ? Platform.isWindows + : rootState.target?.ffiModel.pi.platform == kPeerPlatformWindows; + + await Future.delayed(Duration(milliseconds: 100)); + + final dir = (await bind.sessionGetPeerOption( + id: sessionID, name: isLocal ? "local_dir" : "remote_dir")); + openDirectory(dir.isEmpty ? options.value.home : dir); + + await Future.delayed(Duration(seconds: 1)); + + if (directory.value.path.isEmpty) { + openDirectory(options.value.home); + } + } + + Future close() async { + // save config + Map msgMap = {}; + msgMap[isLocal ? "local_dir" : "remote_dir"] = directory.value.path; + msgMap[isLocal ? "local_show_hidden" : "remote_show_hidden"] = + options.value.showHidden ? "Y" : ""; + for (final msg in msgMap.entries) { + await bind.sessionPeerOption( + id: sessionID, name: msg.key, value: msg.value); + } + directory.value.clear(); + options.value.clear(); + } + + void toggleShowHidden({bool? showHidden}) { + options.value.showHidden = showHidden ?? !options.value.showHidden; + refresh(); + } + + void changeSortStyle(SortBy sort, {bool? isLocal, bool ascending = true}) { + sortBy.value = sort; + sortAscending = ascending; + directory.update((dir) { + dir?.changeSortStyle(sort, ascending: ascending); + }); + } + + Future refresh() async { + await openDirectory(directory.value.path); + } + + Future openDirectory(String path, {bool isBack = false}) async { + if (path == ".") { + refresh(); + return; + } + if (path == "..") { + goToParentDirectory(); + return; + } + if (!isBack) { + pushHistory(); + } + final showHidden = options.value.showHidden; + final isWindows = options.value.isWindows; + // process /C:\ -> C:\ on Windows + if (isWindows && path.length > 1 && path[0] == '/') { + path = path.substring(1); + if (path[path.length - 1] != '\\') { + path = "$path\\"; + } + } + try { + final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden); + fd.format(isWindows, sort: sortBy.value); + directory.value = fd; + } catch (e) { + debugPrint("Failed to openDirectory $path: $e"); + } + } + + void pushHistory() { + if (history.isNotEmpty && history.last == directory.value.path) { + return; + } + history.add(directory.value.path); + } + + void goToHomeDirectory() { + openDirectory(homePath); + } + + void goBack() { + if (history.isEmpty) return; + final path = history.removeAt(history.length - 1); + if (path.isEmpty) return; + if (directory.value.path == path) { + goBack(); + return; + } + openDirectory(path, isBack: true); + } + + void goToParentDirectory() { + final isWindows = options.value.isWindows; + final dirPath = directory.value.path; + var parent = PathUtil.dirname(dirPath, isWindows); + // specially for C:\, D:\, goto '/' + if (parent == dirPath && isWindows) { + openDirectory('/'); + return; + } + openDirectory(parent); + } + + // TODO deprecated this + void initDirAndHome(Map evt) { + try { + final fd = FileDirectory.fromJson(jsonDecode(evt['value'])); + fd.format(options.value.isWindows, sort: sortBy.value); + if (fd.id > 0) { + final jobIndex = jobController.getJob(fd.id); + if (jobIndex != -1) { + final job = jobController.jobTable[jobIndex]; + var totalSize = 0; + var fileCount = fd.entries.length; + for (var element in fd.entries) { + totalSize += element.size; + } + job.totalSize = totalSize; + job.fileCount = fileCount; + debugPrint("update receive details: ${fd.path}"); + jobController.jobTable.refresh(); + } + } else if (options.value.home.isEmpty) { + options.value.home = fd.path; + debugPrint("init remote home: ${fd.path}"); + directory.value = fd; + } + } catch (e) { + debugPrint("initDirAndHome err=$e"); + } + } + + /// sendFiles from current side (FileController.isLocal) to other side (SelectedItems). + void sendFiles(SelectedItems items, DirectoryData otherSideData) { + /// ignore wrong items side status + if (items.isLocal != isLocal) { + return; + } + + // alias + final isRemoteToLocal = !isLocal; + + final toPath = otherSideData.directory.path; + final isWindows = otherSideData.options.isWindows; + final showHidden = otherSideData.options.showHidden; + for (var from in items.items) { + final jobID = jobController.add(from, isRemoteToLocal); + bind.sessionSendFiles( + id: sessionID, + actId: jobID, + path: from.path, + to: PathUtil.join(toPath, from.name, isWindows), + fileNum: 0, + includeHidden: showHidden, + isRemote: isRemoteToLocal); + debugPrint( + "path: ${from.path}, toPath: $toPath, to: ${PathUtil.join(toPath, from.name, isWindows)}"); + } + } + + bool _removeCheckboxRemember = false; + + Future removeAction(SelectedItems items) async { + _removeCheckboxRemember = false; + if (items.isLocal != isLocal) { + debugPrint("Failed to removeFile, wrong files"); + return; + } + final isWindows = options.value.isWindows; + await Future.forEach(items.items, (Entry item) async { + final jobID = JobController.jobID.next(); + var title = ""; + var content = ""; + late final List entries; + if (item.isFile) { + title = translate("Are you sure you want to delete this file?"); + content = item.name; + entries = [item]; + } else if (item.isDirectory) { + title = translate("Not an empty directory"); + dialogManager?.showLoading(translate("Waiting")); + final fd = await fileFetcher.fetchDirectoryRecursive( + jobID, item.path, items.isLocal, true); + if (fd.path.isEmpty) { + fd.path = item.path; + } + fd.format(isWindows); + dialogManager?.dismissAll(); + if (fd.entries.isEmpty) { + final confirm = await showRemoveDialog( + translate( + "Are you sure you want to delete this empty directory?"), + item.name, + false); + if (confirm == true) { + sendRemoveEmptyDir(item.path, 0); + } + return; + } + entries = fd.entries; + } else { + entries = []; + } + + for (var i = 0; i < entries.length; i++) { + final dirShow = item.isDirectory + ? "${translate("Are you sure you want to delete the file of this directory?")}\n" + : ""; + final count = entries.length > 1 ? "${i + 1}/${entries.length}" : ""; + content = "$dirShow\n\n${entries[i].path}".trim(); + final confirm = await showRemoveDialog( + count.isEmpty ? title : "$title ($count)", + content, + item.isDirectory, + ); + try { + if (confirm == true) { + sendRemoveFile(entries[i].path, i); + final res = await jobController.jobResultListener.start(); + // handle remove res; + if (item.isDirectory && + res['file_num'] == (entries.length - 1).toString()) { + sendRemoveEmptyDir(item.path, i); + } + } + if (_removeCheckboxRemember) { + if (confirm == true) { + for (var j = i + 1; j < entries.length; j++) { + sendRemoveFile(entries[j].path, j); + final res = await jobController.jobResultListener.start(); + if (item.isDirectory && + res['file_num'] == (entries.length - 1).toString()) { + sendRemoveEmptyDir(item.path, i); + } + } + } + break; + } + } catch (e) { + print("remove error: $e"); + } + } + }); + refresh(); + } + + Future showRemoveDialog( + String title, String content, bool showCheckbox) async { + return await dialogManager?.show((setState, Function(bool v) close) { + cancel() => close(false); + submit() => close(true); + return CustomAlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.warning_rounded, color: Colors.red), + Text(title).paddingOnly( + left: 10, + ), + ], + ), + contentBoxConstraints: + BoxConstraints(minHeight: 100, minWidth: 400, maxWidth: 400), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(content), + Text( + translate("This is irreversible!"), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.red, + ), + ).paddingOnly(top: 20), + showCheckbox + ? CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Do this for all conflicts"), + ), + value: _removeCheckboxRemember, + onChanged: (v) { + if (v == null) return; + setState(() => _removeCheckboxRemember = v); + }, + ) + : const SizedBox.shrink() + ], + ), + actions: [ + dialogButton( + "Cancel", + icon: Icon(Icons.close_rounded), + onPressed: cancel, + isOutline: true, + ), + dialogButton( + "OK", + icon: Icon(Icons.done_rounded), + onPressed: submit, + ), ], onSubmit: submit, onCancel: cancel, @@ -742,64 +609,163 @@ class FileModel extends ChangeNotifier { }, useAnimation: false); } - sendRemoveFile(String path, int fileNum, bool isLocal) { + void sendRemoveFile(String path, int fileNum) { bind.sessionRemoveFile( - id: '${parent.target?.id}', - actId: _jobId, + id: sessionID, + actId: JobController.jobID.next(), path: path, isRemote: !isLocal, fileNum: fileNum); } - sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { - final history = isLocal ? localHistory : remoteHistory; + void sendRemoveEmptyDir(String path, int fileNum) { history.removeWhere((element) => element.contains(path)); bind.sessionRemoveAllEmptyDirs( - id: '${parent.target?.id}', - actId: _jobId, + id: sessionID, + actId: JobController.jobID.next(), path: path, isRemote: !isLocal); } - createDir(String path, {bool? isLocal}) async { - isLocal = isLocal ?? this.isLocal; - _jobId++; + Future createDir(String path) async { bind.sessionCreateDir( - id: '${parent.target?.id}', - actId: _jobId, + id: sessionID, + actId: JobController.jobID.next(), path: path, isRemote: !isLocal); } +} - cancelJob(int id) async { - bind.sessionCancelJob(id: '${parent.target?.id}', actId: id); - jobReset(); +class JobController { + static final JobID jobID = JobID(); + final jobTable = List.empty(growable: true).obs; + final jobResultListener = JobResultListener>(); + final GetSessionID getSessionID; + String get sessionID => getSessionID(); + + JobController(this.getSessionID); + + int getJob(int id) { + return jobTable.indexWhere((element) => element.id == id); } - changeSortStyle(SortBy sort, {bool? isLocal, bool ascending = true}) { - _sortStyle = sort; - if (isLocal == null) { - // compatible for mobile logic - _currentLocalDir.changeSortStyle(sort, ascending: ascending); - _currentRemoteDir.changeSortStyle(sort, ascending: ascending); - _localSortStyle = sort; - _localSortAscending = ascending; - _remoteSortStyle = sort; - _remoteSortAscending = ascending; - } else if (isLocal) { - _currentLocalDir.changeSortStyle(sort, ascending: ascending); - _localSortStyle = sort; - _localSortAscending = ascending; - } else { - _currentRemoteDir.changeSortStyle(sort, ascending: ascending); - _remoteSortStyle = sort; - _remoteSortAscending = ascending; + // JobProgress? getJob(int id) { + // return jobTable.firstWhere((element) => element.id == id); + // } + + // return jobID + int add(Entry from, bool isRemoteToLocal) { + final jobID = JobController.jobID.next(); + jobTable.add(JobProgress() + ..fileName = path.basename(from.path) + ..jobName = from.path + ..totalSize = from.size + ..state = JobState.inProgress + ..id = jobID + ..isRemoteToLocal = isRemoteToLocal); + return jobID; + } + + void tryUpdateJobProgress(Map evt) { + try { + int id = int.parse(evt['id']); + // id = index + 1 + final jobIndex = getJob(id); + if (jobIndex >= 0 && jobTable.length > jobIndex) { + final job = jobTable[jobIndex]; + job.fileNum = int.parse(evt['file_num']); + job.speed = double.parse(evt['speed']); + job.finishedSize = int.parse(evt['finished_size']); + debugPrint("update job $id with $evt"); + jobTable.refresh(); + } + } catch (e) { + debugPrint("Failed to tryUpdateJobProgress, evt: ${evt.toString()}"); } - notifyListeners(); } - initFileFetcher() { - _fileFetcher.id = parent.target?.id; + void jobDone(Map evt) async { + if (jobResultListener.isListening) { + jobResultListener.complete(evt); + return; + } + + int id = int.parse(evt['id']); + final jobIndex = getJob(id); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.finishedSize = job.totalSize; + job.state = JobState.done; + job.fileNum = int.parse(evt['file_num']); + jobTable.refresh(); + } + } + + void jobError(Map evt) { + final err = evt['err'].toString(); + int jobIndex = getJob(int.parse(evt['id'])); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.state = JobState.error; + job.err = err; + job.fileNum = int.parse(evt['file_num']); + if (err == "skipped") { + job.state = JobState.done; + job.finishedSize = job.totalSize; + } + jobTable.refresh(); + } + debugPrint("jobError $evt"); + } + + Future cancelJob(int id) async { + await bind.sessionCancelJob(id: sessionID, actId: id); + } + + void loadLastJob(Map evt) { + debugPrint("load last job: $evt"); + Map jobDetail = json.decode(evt['value']); + // int id = int.parse(jobDetail['id']); + String remote = jobDetail['remote']; + String to = jobDetail['to']; + bool showHidden = jobDetail['show_hidden']; + int fileNum = jobDetail['file_num']; + bool isRemote = jobDetail['is_remote']; + final currJobId = JobController.jobID.next(); + String fileName = path.basename(isRemote ? remote : to); + var jobProgress = JobProgress() + ..fileName = fileName + ..jobName = isRemote ? remote : to + ..id = currJobId + ..isRemoteToLocal = isRemote + ..fileNum = fileNum + ..remote = remote + ..to = to + ..showHidden = showHidden + ..state = JobState.paused; + jobTable.add(jobProgress); + bind.sessionAddJob( + id: sessionID, + isRemote: isRemote, + includeHidden: showHidden, + actId: currJobId, + path: isRemote ? remote : to, + to: isRemote ? to : remote, + fileNum: fileNum, + ); + } + + void resumeJob(int jobId) { + final jobIndex = getJob(jobId); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + bind.sessionResumeJob( + id: sessionID, actId: job.id, isRemote: job.isRemoteToLocal); + job.state = JobState.inProgress; + jobTable.refresh(); + } else { + debugPrint("jobId $jobId is not exists"); + } } void updateFolderFiles(Map evt) { @@ -813,55 +779,9 @@ class FileModel extends ChangeNotifier { final job = jobTable[jobIndex]; job.fileCount = num_entries; job.totalSize = total_size.toInt(); + jobTable.refresh(); } debugPrint("update folder files: $info"); - notifyListeners(); - } - - bool get remoteSortAscending => _remoteSortAscending; - - void loadLastJob(Map evt) { - debugPrint("load last job: $evt"); - Map jobDetail = json.decode(evt['value']); - // int id = int.parse(jobDetail['id']); - String remote = jobDetail['remote']; - String to = jobDetail['to']; - bool showHidden = jobDetail['show_hidden']; - int fileNum = jobDetail['file_num']; - bool isRemote = jobDetail['is_remote']; - final currJobId = _jobId++; - var jobProgress = JobProgress() - ..jobName = isRemote ? remote : to - ..id = currJobId - ..isRemote = isRemote - ..fileNum = fileNum - ..remote = remote - ..to = to - ..showHidden = showHidden - ..state = JobState.paused; - jobTable.add(jobProgress); - bind.sessionAddJob( - id: '${parent.target?.id}', - isRemote: isRemote, - includeHidden: showHidden, - actId: currJobId, - path: isRemote ? remote : to, - to: isRemote ? to : remote, - fileNum: fileNum, - ); - } - - resumeJob(int jobId) { - final jobIndex = getJob(jobId); - if (jobIndex != -1) { - final job = jobTable[jobIndex]; - bind.sessionResumeJob( - id: '${parent.target?.id}', actId: job.id, isRemote: job.isRemote); - job.state = JobState.inProgress; - } else { - debugPrint("jobId $jobId is not exists"); - } - notifyListeners(); } } @@ -910,10 +830,10 @@ class FileFetcher { Map> remoteTasks = {}; Map> readRecursiveTasks = {}; - String? id; + final GetSessionID getSessionID; + String get sessionID => getSessionID(); - // if id == null, means to fetch global FFI - FFI get _ffi => ffi(id ?? ""); + FileFetcher(this.getSessionID); Future registerReadTask(bool isLocal, String path) { // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later @@ -927,23 +847,23 @@ class FileFetcher { Timer(Duration(seconds: 2), () { tasks.remove(path); if (c.isCompleted) return; - c.completeError("Failed to read dir,timeout"); + c.completeError("Failed to read dir, timeout"); }); return c.future; } - Future registerReadRecursiveTask(int id) { + Future registerReadRecursiveTask(int actID) { final tasks = readRecursiveTasks; - if (tasks.containsKey(id)) { + if (tasks.containsKey(actID)) { throw "Failed to registerRemoveTask, already have same ReadRecursive job"; } final c = Completer(); - tasks[id] = c; + tasks[actID] = c; Timer(Duration(seconds: 2), () { - tasks.remove(id); + tasks.remove(actID); if (c.isCompleted) return; - c.completeError("Failed to read dir,timeout"); + c.completeError("Failed to read dir, timeout"); }); return c.future; } @@ -967,7 +887,7 @@ class FileFetcher { completer?.complete(fd); } } catch (e) { - debugPrint("tryCompleteJob err :$e"); + debugPrint("tryCompleteJob err: $e"); } } @@ -976,12 +896,12 @@ class FileFetcher { try { if (isLocal) { final res = await bind.sessionReadLocalDirSync( - id: id ?? "", path: path, showHidden: showHidden); + id: sessionID, path: path, showHidden: showHidden); final fd = FileDirectory.fromJson(jsonDecode(res)); return fd; } else { await bind.sessionReadRemoteDir( - id: id ?? "", path: path, includeHidden: showHidden); + id: sessionID, path: path, includeHidden: showHidden); return registerReadTask(isLocal, path); } } catch (e) { @@ -990,16 +910,16 @@ class FileFetcher { } Future fetchDirectoryRecursive( - int id, String path, bool isLocal, bool showHidden) async { + int actID, String path, bool isLocal, bool showHidden) async { // TODO test Recursive is show hidden default? try { await bind.sessionReadDirRecursive( - id: _ffi.id, - actId: id, + id: sessionID, + actId: actID, path: path, isRemote: !isLocal, showHidden: showHidden); - return registerReadRecursiveTask(id); + return registerReadRecursiveTask(actID); } catch (e) { return Future.error(e); } @@ -1096,8 +1016,12 @@ class JobProgress { var finishedSize = 0; var totalSize = 0; var fileCount = 0; - var isRemote = false; + // [isRemote == true] means [remote -> local] + // var isRemote = false; + // to-do use enum + var isRemoteToLocal = false; var jobName = ""; + var fileName = ""; var remote = ""; var to = ""; var showHidden = false; @@ -1110,6 +1034,7 @@ class JobProgress { speed = 0; finishedSize = 0; jobName = ""; + fileName = ""; fileCount = 0; remote = ""; to = ""; @@ -1151,12 +1076,12 @@ class PathUtil { } } -class DirectoryOption { +class DirectoryOptions { String home; bool showHidden; bool isWindows; - DirectoryOption( + DirectoryOptions( {this.home = "", this.showHidden = false, this.isWindows = false}); clear() { @@ -1167,53 +1092,37 @@ class DirectoryOption { } class SelectedItems { - bool? _isLocal; - final List _items = []; + final bool isLocal; + final items = RxList.empty(growable: true); - List get items => _items; + SelectedItems({required this.isLocal}); - int get length => _items.length; - - bool? get isLocal => _isLocal; - - add(bool isLocal, Entry e) { + void add(Entry e) { if (e.isDrive) return; - _isLocal ??= isLocal; - if (_isLocal != null && _isLocal != isLocal) { - return; - } - if (!_items.contains(e)) { - _items.add(e); + if (!items.contains(e)) { + items.add(e); } } - bool contains(Entry e) { - return _items.contains(e); + void remove(Entry e) { + items.remove(e); } - remove(Entry e) { - _items.remove(e); - if (_items.isEmpty) { - _isLocal = null; - } - } - - bool isOtherPage(bool currentIsLocal) { - if (_isLocal == null) { - return false; - } else { - return _isLocal != currentIsLocal; - } - } - - clear() { - _items.clear(); - _isLocal = null; + void clear() { + items.clear(); } void selectAll(List entries) { - _items.clear(); - _items.addAll(entries); + items.clear(); + items.addAll(entries); + } + + static bool valid(RxList items) { + if (items.isNotEmpty) { + // exclude DirDrive type + return items.any((item) => !item.isDrive); + } + return false; } } @@ -1299,3 +1208,74 @@ List _sortList(List list, SortBy sortType, bool ascending) { } return []; } + +/// Define a general queue which can accepts different dialog type. +/// +/// [Visibility] +/// The `_FileDialogType` and `_DialogEvent` are invisible for other models. +enum FileDialogType { overwrite, unknown } + +class _FileDialogEvent extends BaseEvent> { + WeakReference fileModel; + bool? _overrideConfirm; + bool _skip = false; + + _FileDialogEvent(this.fileModel, super.type, super.data); + + void setOverrideConfirm(bool? confirm) { + _overrideConfirm = confirm; + } + + void setSkip(bool skip) { + _skip = skip; + } + + @override + EventCallback>? findCallback(FileDialogType type) { + final model = fileModel.target; + if (model == null) { + return null; + } + switch (type) { + case FileDialogType.overwrite: + return (data) async { + return await model.overrideFileConfirm(data, + overrideConfirm: _overrideConfirm, skip: _skip); + }; + default: + debugPrint("Unknown event type: $type with $data"); + return null; + } + } +} + +class FileDialogEventLoop + extends BaseEventLoop> { + bool? _overrideConfirm; + bool _skip = false; + + @override + Future onPreConsume( + BaseEvent> evt) async { + var event = evt as _FileDialogEvent; + event.setOverrideConfirm(_overrideConfirm); + event.setSkip(_skip); + debugPrint( + "FileDialogEventLoop: consuming"); + } + + @override + Future onEventsClear() { + _overrideConfirm = null; + _skip = false; + return super.onEventsClear(); + } + + void setOverrideConfirm(bool? confirm) { + _overrideConfirm = confirm; + } + + void setSkip(bool skip) { + _skip = skip; + } +} diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index f220d62f1..5e2b85f90 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -35,7 +35,7 @@ class GroupModel { await reset(); if (gFFI.userModel.userName.isEmpty || (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { - statePeerTab.check(); + gFFI.peerTabModel.check_dynamic_tabs(); return; } userLoading.value = true; @@ -59,7 +59,7 @@ class GroupModel { if (gFFI.userModel.isAdmin.isFalse) 'grp': gFFI.userModel.groupName.value, }); - final resp = await http.get(uri, headers: await getHttpHeaders()); + final resp = await http.get(uri, headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { @@ -82,7 +82,7 @@ class GroupModel { userLoadError.value = err.toString(); } finally { userLoading.value = false; - statePeerTab.check(); + gFFI.peerTabModel.check_dynamic_tabs(); } } @@ -110,7 +110,7 @@ class GroupModel { 'grp': gFFI.userModel.groupName.value, 'target_user': username }); - final resp = await http.get(uri, headers: await getHttpHeaders()); + final resp = await http.get(uri, headers: getHttpHeaders()); if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") { Map json = jsonDecode(resp.body); if (json.containsKey('error')) { diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index b488f30f3..310a3cadb 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -17,6 +17,10 @@ import './state_model.dart'; /// Mouse button enum. enum MouseButtons { left, right, wheel } +const _kMouseEventDown = 'mousedown'; +const _kMouseEventUp = 'mouseup'; +const _kMouseEventMove = 'mousemove'; + extension ToString on MouseButtons { String get value { switch (this) { @@ -41,12 +45,16 @@ class InputModel { var command = false; // trackpad - var trackpadScrollDistance = Offset.zero; + final _trackpadSpeed = 0.02; + var _trackpadLastDelta = Offset.zero; + var _trackpadScrollUnsent = Offset.zero; + var _stopFling = true; Timer? _flingTimer; + final _flingBaseDelay = 10; // mouse final isPhysicalMouse = false.obs; - int _lastMouseDownButtons = 0; + int _lastButtons = 0; Offset lastMousePos = Offset.zero; get id => parent.target?.id ?? ""; @@ -54,9 +62,16 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - bind.sessionGetKeyboardMode(id: id).then((result) { - keyboardMode = result.toString(); - }); + if (!stateGlobal.grabKeyboard) { + return KeyEventResult.handled; + } + + // * Currently mobile does not enable map mode + if (isDesktop) { + bind.sessionGetKeyboardMode(id: id).then((result) { + keyboardMode = result.toString(); + }); + } final key = e.logicalKey; if (e is RawKeyDownEvent) { @@ -89,10 +104,9 @@ class InputModel { } } - if (keyboardMode == 'map') { + // * Currently mobile does not enable map mode + if (isDesktop && keyboardMode == 'map') { mapKeyboardMode(e); - } else if (keyboardMode == 'translate') { - legacyKeyboardMode(e); } else { legacyKeyboardMode(e); } @@ -115,8 +129,11 @@ class InputModel { keyCode = newData.keyCode; } else if (e.data is RawKeyEventDataLinux) { RawKeyEventDataLinux newData = e.data as RawKeyEventDataLinux; - scanCode = newData.scanCode; - keyCode = newData.keyCode; + // scanCode and keyCode of RawKeyEventDataLinux are incorrect. + // 1. scanCode means keycode + // 2. keyCode means keysym + scanCode = 0; + keyCode = newData.scanCode; } else if (e.data is RawKeyEventDataAndroid) { RawKeyEventDataAndroid newData = e.data as RawKeyEventDataAndroid; scanCode = newData.scanCode + 8; @@ -131,16 +148,33 @@ class InputModel { } else { down = false; } - inputRawKey(e.character ?? "", keyCode, scanCode, down); + inputRawKey(e.character ?? '', keyCode, scanCode, down); } /// Send raw Key Event void inputRawKey(String name, int keyCode, int scanCode, bool down) { + const capslock = 1; + const numlock = 2; + const scrolllock = 3; + int lockModes = 0; + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.capsLock)) { + lockModes |= (1 << capslock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.numLock)) { + lockModes |= (1 << numlock); + } + if (HardwareKeyboard.instance.lockModesEnabled + .contains(KeyboardLockMode.scrollLock)) { + lockModes |= (1 << scrolllock); + } bind.sessionHandleFlutterKeyEvent( id: id, name: name, keycode: keyCode, scancode: scanCode, + lockModes: lockModes, downOrUp: down); } @@ -183,20 +217,42 @@ class InputModel { Map getEvent(PointerEvent evt, String type) { final Map out = {}; - out['type'] = type; out['x'] = evt.position.dx; out['y'] = evt.position.dy; if (alt) out['alt'] = 'true'; if (shift) out['shift'] = 'true'; if (ctrl) out['ctrl'] = 'true'; if (command) out['command'] = 'true'; - out['buttons'] = evt - .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) - if (evt.buttons != 0) { - _lastMouseDownButtons = evt.buttons; + + // Check update event type and set buttons to be sent. + int buttons = _lastButtons; + if (type == _kMouseEventMove) { + // flutter may emit move event if one button is pressed and another button + // is pressing or releasing. + if (evt.buttons != _lastButtons) { + // For simplicity + // Just consider 3 - 1 ((Left + Right buttons) - Left button) + // Do not consider 2 - 1 (Right button - Left button) + // or 6 - 5 ((Right + Mid buttons) - (Left + Mid buttons)) + // and so on + buttons = evt.buttons - _lastButtons; + if (buttons > 0) { + type = _kMouseEventDown; + } else { + type = _kMouseEventUp; + buttons = -buttons; + } + } } else { - out['buttons'] = _lastMouseDownButtons; + if (evt.buttons != 0) { + buttons = evt.buttons; + } } + _lastButtons = evt.buttons; + + out['buttons'] = buttons; + out['type'] = type; + return out; } @@ -255,16 +311,16 @@ class InputModel { } void onPointHoverImage(PointerHoverEvent e) { + _stopFling = true; if (e.kind != ui.PointerDeviceKind.mouse) return; if (!isPhysicalMouse.value) { isPhysicalMouse.value = true; } if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousemove')); + handleMouse(getEvent(e, _kMouseEventMove)); } } -/* int _signOrZero(num x) { if (x == 0) { return 0; @@ -273,17 +329,33 @@ class InputModel { } } - void onPointerPanZoomStart(PointerPanZoomStartEvent e) {} + void onPointerPanZoomStart(PointerPanZoomStartEvent e) { + _stopFling = true; + } // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures // TODO(support zoom in/out) void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { var delta = e.panDelta; - trackpadScrollDistance += delta; + _trackpadLastDelta = delta; + _trackpadScrollUnsent += (delta * _trackpadSpeed); + var x = _trackpadScrollUnsent.dx.truncate(); + var y = _trackpadScrollUnsent.dy.truncate(); + _trackpadScrollUnsent -= Offset(_trackpadScrollUnsent.dx - x.toDouble(), + _trackpadScrollUnsent.dy - y.toDouble()); + + if (x == 0 && y == 0) { + x = delta.dx > 1 ? 1 : (delta.dx < -1 ? -1 : 0); + y = delta.dy > 1 ? 1 : (delta.dy < -1 ? -1 : 0); + if (x.abs() > y.abs()) { + y = 0; + } else { + x = 0; + } + } + bind.sessionSendMouse( - id: id, - msg: - '{"type": "trackpad", "x": "${delta.dx.toInt()}", "y": "${delta.dy.toInt()}"}'); + id: id, msg: '{"type": "trackpad", "x": "$x", "y": "$y"}'); } // Simple simulation for fling. @@ -306,40 +378,88 @@ class InputModel { }); } - void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { - var x = _signOrZero(trackpadScrollDistance.dx); - var y = _signOrZero(trackpadScrollDistance.dy); - var dx = trackpadScrollDistance.dx.abs() ~/ 40; - var dy = trackpadScrollDistance.dy.abs() ~/ 40; - _scheduleFling(x, y, dx, dy); + void _scheduleFling2(double x, double y, int delay) { + if ((x ==0 && y == 0) || _stopFling) { + return; + } - trackpadScrollDistance = Offset.zero; + _flingTimer = Timer(Duration(milliseconds: delay), () { + if (_stopFling) { + return; + } + + final d = 0.95; + x *= d; + y *= d; + final dx0 = x * _trackpadSpeed * 2; + final dy0 = y * _trackpadSpeed * 2; + + // Try set delta (x,y) and delay. + var dx = dx0.toInt(); + var dy = dy0.toInt(); + var delay = _flingBaseDelay; + + // Try set min delta (x,y), and increase delay. + if (dx == 0 && dy == 0) { + final thr = 25; + var vx = thr; + var vy = thr; + if (dx0 != 0) { + vx = 1.0 ~/ dx0.abs(); + } + if (dy0 != 0) { + vy = 1.0 ~/ dy0.abs(); + } + if (vx < vy && vx < thr) { + delay *= vx; + dx = dx0 > 0 ? 1 : (dx0 < 0 ? -1 : 0); + } else if (vy < thr) { + delay *= vy; + dy = dy0 > 0 ? 1 : (dy0 < 0 ? -1 : 0); + } + } + + if (dx == 0 && dy == 0) { + return; + } + + bind.sessionSendMouse( + id: id, msg: '{"type": "trackpad", "x": "$dx", "y": "$dy"}'); + _scheduleFling2(x, y, delay); + }); + } + + void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { + _stopFling = false; + _trackpadScrollUnsent = Offset.zero; + _scheduleFling2(_trackpadLastDelta.dx, _trackpadLastDelta.dy, _flingBaseDelay); + _trackpadLastDelta = Offset.zero; } -*/ void onPointDownImage(PointerDownEvent e) { debugPrint("onPointDownImage"); + _stopFling = true; if (e.kind != ui.PointerDeviceKind.mouse) { if (isPhysicalMouse.value) { isPhysicalMouse.value = false; } } if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousedown')); + handleMouse(getEvent(e, _kMouseEventDown)); } } void onPointUpImage(PointerUpEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mouseup')); + handleMouse(getEvent(e, _kMouseEventUp)); } } void onPointMoveImage(PointerMoveEvent e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousemove')); + handleMouse(getEvent(e, _kMouseEventMove)); } } @@ -362,7 +482,57 @@ class InputModel { } } - void handleMouse(Map evt) { + void refreshMousePos() => handleMouse({ + 'x': lastMousePos.dx, + 'y': lastMousePos.dy, + 'buttons': 0, + 'type': _kMouseEventMove, + }); + + void tryMoveEdgeOnExit(Offset pos) => handleMouse( + { + 'x': pos.dx, + 'y': pos.dy, + 'buttons': 0, + 'type': _kMouseEventMove, + }, + onExit: true, + ); + + int trySetNearestRange(int v, int min, int max, int n) { + if (v < min && v >= min - n) { + v = min; + } + if (v > max && v <= max + n) { + v = max; + } + return v; + } + + Offset setNearestEdge(double x, double y, Display d) { + double left = x - d.x; + double right = d.x + d.width - 1 - x; + double top = y - d.y; + double bottom = d.y + d.height - 1 - y; + if (left < right && left < top && left < bottom) { + x = d.x; + } + if (right < left && right < top && right < bottom) { + x = d.x + d.width - 1; + } + if (top < left && top < right && top < bottom) { + y = d.y; + } + if (bottom < left && bottom < right && bottom < top) { + y = d.y + d.height - 1; + } + return Offset(x, y); + } + + void handleMouse( + Map evt, { + bool onExit = false, + }) { double x = evt['x']; double y = max(0.0, evt['y']); final cursorModel = parent.target!.cursorModel; @@ -388,31 +558,34 @@ class InputModel { var type = ''; var isMove = false; switch (evt['type']) { - case 'mousedown': + case _kMouseEventDown: type = 'down'; break; - case 'mouseup': + case _kMouseEventUp: type = 'up'; break; - case 'mousemove': + case _kMouseEventMove: isMove = true; break; default: return; } evt['type'] = type; - if (isDesktop) { - y = y - stateGlobal.tabBarHeight; - } + y -= CanvasModel.topToEdge; + x -= CanvasModel.leftToEdge; final canvasModel = parent.target!.canvasModel; + final nearThr = 3; + var nearRight = (canvasModel.size.width - x) < nearThr; + var nearBottom = (canvasModel.size.height - y) < nearThr; + final ffiModel = parent.target!.ffiModel; if (isMove) { canvasModel.moveDesktopMouse(x, y); } final d = ffiModel.display; + final imageWidth = d.width * canvasModel.scale; + final imageHeight = d.height * canvasModel.scale; if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { - final imageWidth = d.width * canvasModel.scale; - final imageHeight = d.height * canvasModel.scale; x += imageWidth * canvasModel.scrollX; y += imageHeight * canvasModel.scrollY; @@ -430,25 +603,72 @@ class InputModel { x /= canvasModel.scale; y /= canvasModel.scale; + if (canvasModel.scale > 0 && canvasModel.scale < 1) { + final step = 1.0 / canvasModel.scale - 1; + if (nearRight) { + x += step; + } + if (nearBottom) { + y += step; + } + } x += d.x; y += d.y; - if (type != '') { - x = 0; - y = 0; + + if (onExit) { + final pos = setNearestEdge(x, y, d); + x = pos.dx; + y = pos.dy; } - evt['x'] = '${x.round()}'; - evt['y'] = '${y.round()}'; + + var evtX = 0; + var evtY = 0; + try { + evtX = x.round(); + evtY = y.round(); + } catch (e) { + debugPrintStack( + label: 'canvasModel.scale value ${canvasModel.scale}, $e'); + return; + } + + int minX = d.x.toInt(); + int maxX = (d.x + d.width).toInt() - 1; + int minY = d.y.toInt(); + int maxY = (d.y + d.height).toInt() - 1; + evtX = trySetNearestRange(evtX, minX, maxX, 5); + evtY = trySetNearestRange(evtY, minY, maxY, 5); + if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) { + // If left mouse up, no early return. + if (evt['buttons'] != kPrimaryMouseButton || type != 'up') { + return; + } + } + + if (type != '') { + evtX = 0; + evtY = 0; + } + + evt['x'] = '$evtX'; + evt['y'] = '$evtY'; var buttons = ''; switch (evt['buttons']) { - case 1: + case kPrimaryMouseButton: buttons = 'left'; break; - case 2: + case kSecondaryMouseButton: buttons = 'right'; break; - case 4: + case kMiddleMouseButton: buttons = 'wheel'; break; + case kBackMouseButton: + buttons = 'back'; + break; + case kForwardMouseButton: + buttons = 'forward'; + break; } evt['buttons'] = buttons; bind.sessionSendMouse(id: id, msg: json.encode(evt)); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 0f7099bc8..701fae48c 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; -import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; @@ -13,24 +12,25 @@ import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/group_model.dart'; +import 'package:flutter_hbb/models/peer_tab_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/common/shared_state.dart'; -import 'package:flutter_hbb/utils/multi_window_manager.dart'; import 'package:tuple/tuple.dart'; import 'package:image/image.dart' as img2; import 'package:flutter_custom_cursor/cursor_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; import '../common.dart'; -import '../common/shared_state.dart'; import '../utils/image.dart' as img; -import '../mobile/widgets/dialog.dart'; +import '../common/widgets/dialog.dart'; import 'input_model.dart'; import 'platform_model.dart'; typedef HandleMsgBox = Function(Map evt, String id); +typedef ReconnectHandle = Function(OverlayDialogManager, String, bool); final _waitForImage = {}; class FfiModel with ChangeNotifier { @@ -44,6 +44,7 @@ class FfiModel with ChangeNotifier { bool _touchMode = false; Timer? _timer; var _reconnects = 1; + bool _viewOnly = false; WeakReference parent; Map get permissions => _permissions; @@ -60,7 +61,9 @@ class FfiModel with ChangeNotifier { bool get touchMode => _touchMode; - bool get isPeerAndroid => _pi.platform == 'Android'; + bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid; + + bool get viewOnly => _viewOnly; set inputBlocked(v) { _inputBlocked = v; @@ -137,6 +140,8 @@ class FfiModel with ChangeNotifier { handleMsgBox(evt, peerId); } else if (name == 'peer_info') { handlePeerInfo(evt, peerId); + } else if (name == 'sync_peer_info') { + handleSyncPeerInfo(evt, peerId); } else if (name == 'connection_ready') { setConnectionType( peerId, evt['secure'] == 'true', evt['direct'] == 'true'); @@ -151,7 +156,7 @@ class FfiModel with ChangeNotifier { } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - parent.target?.ffiModel.updatePermission(evt, peerId); + updatePermission(evt, peerId); } else if (name == 'chat_client_mode') { parent.target?.chatModel .receive(ChatModel.clientModeID, evt['text'] ?? ''); @@ -161,17 +166,18 @@ class FfiModel with ChangeNotifier { } else if (name == 'file_dir') { parent.target?.fileModel.receiveFileDir(evt); } else if (name == 'job_progress') { - parent.target?.fileModel.tryUpdateJobProgress(evt); + parent.target?.fileModel.jobController.tryUpdateJobProgress(evt); } else if (name == 'job_done') { - parent.target?.fileModel.jobDone(evt); + parent.target?.fileModel.jobController.jobDone(evt); + parent.target?.fileModel.refreshAll(); } else if (name == 'job_error') { - parent.target?.fileModel.jobError(evt); + parent.target?.fileModel.jobController.jobError(evt); } else if (name == 'override_file_confirm') { - parent.target?.fileModel.overrideFileConfirm(evt); + parent.target?.fileModel.postOverrideFileConfirm(evt); } else if (name == 'load_last_job') { - parent.target?.fileModel.loadLastJob(evt); + parent.target?.fileModel.jobController.loadLastJob(evt); } else if (name == 'update_folder_files') { - parent.target?.fileModel.updateFolderFiles(evt); + parent.target?.fileModel.jobController.updateFolderFiles(evt); } else if (name == 'add_connection') { parent.target?.serverModel.addConnection(evt); } else if (name == 'on_client_remove') { @@ -183,13 +189,9 @@ class FfiModel with ChangeNotifier { } else if (name == 'update_privacy_mode') { updatePrivacyMode(evt, peerId); } else if (name == 'new_connection') { - var arg = evt['peer_id'].toString(); - if (arg.startsWith(kUniLinksPrefix)) { - parseRustdeskUri(arg); - } else { - Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(arg); - }); + var uni_links = evt['uni_links'].toString(); + if (uni_links.startsWith(kUniLinksPrefix)) { + parseRustdeskUri(uni_links); } } else if (name == 'alias') { handleAliasChanged(evt); @@ -198,6 +200,32 @@ class FfiModel with ChangeNotifier { parent.target?.serverModel.setShowElevation(show); } else if (name == 'cancel_msgbox') { cancelMsgBox(evt, peerId); + } else if (name == 'switch_back') { + final peer_id = evt['peer_id'].toString(); + await bind.sessionSwitchSides(id: peer_id); + closeConnection(id: peer_id); + } else if (name == 'portable_service_running') { + parent.target?.elevationModel.onPortableServiceRunning(evt); + } else if (name == "on_url_scheme_received") { + final url = evt['url'].toString(); + parseRustdeskUri(url); + } else if (name == "on_voice_call_waiting") { + // Waiting for the response from the peer. + parent.target?.chatModel.onVoiceCallWaiting(); + } else if (name == "on_voice_call_started") { + // Voice call is connected. + parent.target?.chatModel.onVoiceCallStarted(); + } else if (name == "on_voice_call_closed") { + // Voice call is closed with reason. + final reason = evt['reason'].toString(); + parent.target?.chatModel.onVoiceCallClosed(reason); + } else if (name == "on_voice_call_incoming") { + // Voice call is requested by the peer. + parent.target?.chatModel.onVoiceCallIncoming(); + } else if (name == "update_voice_call_state") { + parent.target?.serverModel.updateVoiceCallState(evt); + } else { + debugPrint("Unknown event name: $name"); } }; } @@ -214,30 +242,35 @@ class FfiModel with ChangeNotifier { } } - handleSwitchDisplay(Map evt, String peerId) { - final oldOrientation = _display.width > _display.height; - var old = _pi.currentDisplay; - _pi.currentDisplay = int.parse(evt['display']); - _display.x = double.parse(evt['x']); - _display.y = double.parse(evt['y']); - _display.width = int.parse(evt['width']); - _display.height = int.parse(evt['height']); - _display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; - if (old != _pi.currentDisplay) { - parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); + _updateCurDisplay(String peerId, Display newDisplay) { + if (newDisplay != _display) { + if (newDisplay.x != _display.x || newDisplay.y != _display.y) { + parent.target?.cursorModel + .updateDisplayOrigin(newDisplay.x, newDisplay.y); + } + _display = newDisplay; + _updateSessionWidthHeight(peerId); } + } + + handleSwitchDisplay(Map evt, String peerId) { + _pi.currentDisplay = int.parse(evt['display']); + var newDisplay = Display(); + newDisplay.x = double.parse(evt['x']); + newDisplay.y = double.parse(evt['y']); + newDisplay.width = int.parse(evt['width']); + newDisplay.height = int.parse(evt['height']); + newDisplay.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; + + _updateCurDisplay(peerId, newDisplay); try { CurrentDisplayState.find(peerId).value = _pi.currentDisplay; } catch (e) { // } - - // remote is mobile, and orientation changed - if ((_display.width > _display.height) != oldOrientation) { - gFFI.canvasModel.updateViewStyle(); - } parent.target?.recordingModel.onSwitchDisplay(); + handleResolutions(peerId, evt["resolutions"]); notifyListeners(); } @@ -257,15 +290,27 @@ class FfiModel with ChangeNotifier { final text = evt['text']; final link = evt['link']; if (type == 're-input-password') { - wrongPasswordDialog(id, dialogManager); + wrongPasswordDialog(id, dialogManager, type, title, text); } else if (type == 'input-password') { enterPasswordDialog(id, dialogManager); + } else if (type == 'xsession-login' || type == 'xsession-re-login') { + // to-do + } else if (type == 'xsession-login-password' || + type == 'xsession-login-password') { + // to-do } else if (type == 'restarting') { showMsgBox(id, type, title, text, link, false, dialogManager, hasCancel: false); } else if (type == 'wait-remote-accept-nook') { - msgBoxCommon(dialogManager, title, Text(translate(text)), - [msgBoxButton("Cancel", closeConnection)]); + showWaitAcceptDialog(id, type, title, text, dialogManager); + } else if (type == 'on-uac' || type == 'on-foreground-elevated') { + showOnBlockDialog(id, type, title, text, dialogManager); + } else if (type == 'wait-uac') { + showWaitUacDialog(id, dialogManager, type); + } else if (type == 'elevation-error') { + showElevationError(id, type, title, text, dialogManager); + } else if (type == "relay-hint") { + showRelayHintDialog(id, type, title, text, dialogManager); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(id, type, title, text, link, hasRetry, dialogManager); @@ -276,14 +321,12 @@ class FfiModel with ChangeNotifier { showMsgBox(String id, String type, String title, String text, String link, bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { - msgBox(id, type, title, text, link, dialogManager, hasCancel: hasCancel); + msgBox(id, type, title, text, link, dialogManager, + hasCancel: hasCancel, reconnect: reconnect); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - bind.sessionReconnect(id: id); - clearPermissions(); - dialogManager.showLoading(translate('Connecting...'), - onCancel: closeConnection); + reconnect(dialogManager, id, false); }); _reconnects *= 2; } else { @@ -291,6 +334,57 @@ class FfiModel with ChangeNotifier { } } + void reconnect( + OverlayDialogManager dialogManager, String id, bool forceRelay) { + bind.sessionReconnect(id: id, forceRelay: forceRelay); + clearPermissions(); + dialogManager.showLoading(translate('Connecting...'), + onCancel: closeConnection); + } + + void showRelayHintDialog(String id, String type, String title, String text, + OverlayDialogManager dialogManager) { + dialogManager.show(tag: '$id-$type', (setState, close) { + onClose() { + closeConnection(); + close(); + } + + final style = + ElevatedButton.styleFrom(backgroundColor: Colors.green[700]); + return CustomAlertDialog( + title: null, + content: msgboxContent(type, title, + "${translate(text)}\n\n${translate('relay_hint_tip')}"), + actions: [ + dialogButton('Close', onPressed: onClose, isOutline: true), + dialogButton('Retry', + onPressed: () => reconnect(dialogManager, id, false)), + dialogButton('Connect via relay', + onPressed: () => reconnect(dialogManager, id, true), + buttonStyle: style), + dialogButton('Always connect via relay', onPressed: () { + const option = 'force-always-relay'; + bind.sessionPeerOption( + id: id, name: option, value: bool2option(option, true)); + reconnect(dialogManager, id, true); + }, buttonStyle: style), + ], + onCancel: onClose, + ); + }); + } + + _updateSessionWidthHeight(String id) { + parent.target?.canvasModel.updateViewStyle(); + if (display.width <= 0 || display.height <= 0) { + debugPrintStack( + label: 'invalid display size (${display.width},${display.height})'); + } else { + bind.sessionSetSize(id: id, width: display.width, height: display.height); + } + } + /// Handle the peer info event based on [evt]. handlePeerInfo(Map evt, String peerId) async { // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs) @@ -310,10 +404,12 @@ class FfiModel with ChangeNotifier { // } + final connType = parent.target?.connType; + if (isPeerAndroid) { _touchMode = true; - if (parent.target != null && - parent.target!.connType == ConnType.defaultConn && + if (connType == ConnType.defaultConn && + parent.target != null && parent.target!.ffiModel.permissions['keyboard'] != false) { Timer( const Duration(milliseconds: 100), @@ -325,10 +421,9 @@ class FfiModel with ChangeNotifier { await bind.sessionGetOption(id: peerId, arg: 'touch-mode') != ''; } - if (parent.target != null && - parent.target!.connType == ConnType.fileTransfer) { + if (connType == ConnType.fileTransfer) { parent.target?.fileModel.onReady(); - } else { + } else if (connType == ConnType.defaultConn) { _pi.displays = []; List displays = json.decode(evt['displays']); for (int i = 0; i < displays.length; ++i) { @@ -341,8 +436,10 @@ class FfiModel with ChangeNotifier { d.cursorEmbedded = d0['cursor_embedded'] == 1; _pi.displays.add(d); } + stateGlobal.displaysCount.value = _pi.displays.length; if (_pi.currentDisplay < _pi.displays.length) { _display = _pi.displays[_pi.currentDisplay]; + _updateSessionWidthHeight(peerId); } if (displays.isNotEmpty) { parent.target?.dialogManager.showLoading( @@ -353,6 +450,70 @@ class FfiModel with ChangeNotifier { } Map features = json.decode(evt['features']); _pi.features.privacyMode = features['privacy_mode'] == 1; + handleResolutions(peerId, evt["resolutions"]); + parent.target?.elevationModel.onPeerInfo(_pi); + } + if (connType == ConnType.defaultConn) { + setViewOnly(peerId, + bind.sessionGetToggleOptionSync(id: peerId, arg: 'view-only')); + } + if (connType == ConnType.defaultConn) { + final platform_additions = evt['platform_additions']; + if (platform_additions != null && platform_additions != '') { + try { + _pi.platform_additions = json.decode(platform_additions); + } catch (e) { + debugPrint('Failed to decode platform_additions $e'); + } + } + } + notifyListeners(); + } + + handleResolutions(String id, dynamic resolutions) { + try { + final List dynamicArray = jsonDecode(resolutions as String); + List arr = List.empty(growable: true); + for (int i = 0; i < dynamicArray.length; i++) { + var width = dynamicArray[i]["width"]; + var height = dynamicArray[i]["height"]; + if (width is int && width > 0 && height is int && height > 0) { + arr.add(Resolution(width, height)); + } + } + arr.sort((a, b) { + if (b.width != a.width) { + return b.width - a.width; + } else { + return b.height - a.height; + } + }); + _pi.resolutions = arr; + } catch (e) { + debugPrint("Failed to parse resolutions:$e"); + } + } + + /// Handle the peer info synchronization event based on [evt]. + handleSyncPeerInfo(Map evt, String peerId) async { + if (evt['displays'] != null) { + List displays = json.decode(evt['displays']); + List newDisplays = []; + for (int i = 0; i < displays.length; ++i) { + Map d0 = displays[i]; + var d = Display(); + d.x = d0['x'].toDouble(); + d.y = d0['y'].toDouble(); + d.width = d0['width']; + d.height = d0['height']; + d.cursorEmbedded = d0['cursor_embedded'] == 1; + newDisplays.add(d); + } + _pi.displays = newDisplays; + stateGlobal.displaysCount.value = _pi.displays.length; + if (_pi.currentDisplay >= 0 && _pi.currentDisplay < _pi.displays.length) { + _updateCurDisplay(peerId, _pi.displays[_pi.currentDisplay]); + } } notifyListeners(); } @@ -376,6 +537,27 @@ class FfiModel with ChangeNotifier { // } } + + void setViewOnly(String id, bool value) { + if (version_cmp(_pi.version, '1.2.0') < 0) return; + // tmp fix for https://github.com/rustdesk/rustdesk/pull/3706#issuecomment-1481242389 + // because below rx not used in mobile version, so not initialized, below code will cause crash + // current our flutter code quality is fucking shit now. !!!!!!!!!!!!!!!! + try { + if (value) { + ShowRemoteCursorState.find(id).value = value; + } else { + ShowRemoteCursorState.find(id).value = + bind.sessionGetToggleOptionSync(id: id, arg: 'show-remote-cursor'); + } + } catch (e) { + // + } + if (_viewOnly != value) { + _viewOnly = value; + notifyListeners(); + } + } } class ImageModel with ChangeNotifier { @@ -387,29 +569,33 @@ class ImageModel with ChangeNotifier { WeakReference parent; - final List _callbacksOnFirstImage = []; + final List callbacksOnFirstImage = []; ImageModel(this.parent); - addCallbackOnFirstImage(Function(String) cb) => - _callbacksOnFirstImage.add(cb); + addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb); onRgba(Uint8List rgba) { if (_waitForImage[id]!) { _waitForImage[id] = false; parent.target?.dialogManager.dismissAll(); if (isDesktop) { - for (final cb in _callbacksOnFirstImage) { + for (final cb in callbacksOnFirstImage) { cb(id); } } } + final pid = parent.target?.id; - ui.decodeImageFromPixels( + img.decodeImageFromPixels( rgba, parent.target?.ffiModel.display.width ?? 0, parent.target?.ffiModel.display.height ?? 0, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { + isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, + onPixelsCopied: () { + // Unlock the rgba memory from rust codes. + platformFFI.nextRgba(id); + }).then((image) { if (parent.target?.id != pid) return; try { // my throw exception, because the listener maybe already dispose @@ -478,13 +664,28 @@ class ViewStyle { final int displayWidth; final int displayHeight; ViewStyle({ - this.style = '', - this.width = 0.0, - this.height = 0.0, - this.displayWidth = 0, - this.displayHeight = 0, + required this.style, + required this.width, + required this.height, + required this.displayWidth, + required this.displayHeight, }); + static defaultViewStyle() { + final desktop = (isDesktop || isWebDesktop); + final w = + desktop ? kDesktopDefaultDisplayWidth : kMobileDefaultDisplayWidth; + final h = + desktop ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; + return ViewStyle( + style: '', + width: w.toDouble(), + height: h.toDouble(), + displayWidth: w, + displayHeight: h, + ); + } + static int _double2Int(double v) => (v * 100).round().toInt(); @override @@ -513,9 +714,14 @@ class ViewStyle { double get scale { double s = 1.0; if (style == kRemoteViewStyleAdaptive) { - final s1 = width / displayWidth; - final s2 = height / displayHeight; - s = s1 < s2 ? s1 : s2; + if (width != 0 && + height != 0 && + displayWidth != 0 && + displayHeight != 0) { + final s1 = width / displayWidth; + final s2 = height / displayHeight; + s = s1 < s2 ? s1 : s2; + } } return s; } @@ -528,6 +734,8 @@ class CanvasModel with ChangeNotifier { double _y = 0; // image scale double _scale = 1.0; + double _devicePixelRatio = 1.0; + Size _size = Size.zero; // the tabbar over the image // double tabBarHeight = 0.0; // the window border's width @@ -539,7 +747,9 @@ class CanvasModel with ChangeNotifier { // scroll offset y percent double _scrollY = 0.0; ScrollStyle _scrollStyle = ScrollStyle.scrollauto; - ViewStyle _lastViewStyle = ViewStyle(); + ViewStyle _lastViewStyle = ViewStyle.defaultViewStyle(); + + final _imageOverflow = false.obs; WeakReference parent; @@ -548,8 +758,11 @@ class CanvasModel with ChangeNotifier { double get x => _x; double get y => _y; double get scale => _scale; + double get devicePixelRatio => _devicePixelRatio; + Size get size => _size; ScrollStyle get scrollStyle => _scrollStyle; ViewStyle get viewStyle => _lastViewStyle; + RxBool get imageOverflow => _imageOverflow; _resetScroll() => setScrollPercent(0.0, 0.0); @@ -561,19 +774,40 @@ class CanvasModel with ChangeNotifier { double get scrollX => _scrollX; double get scrollY => _scrollY; + static double get leftToEdge => (isDesktop || isWebDesktop) + ? windowBorderWidth + kDragToResizeAreaPadding.left + : 0; + static double get rightToEdge => (isDesktop || isWebDesktop) + ? windowBorderWidth + kDragToResizeAreaPadding.right + : 0; + static double get topToEdge => (isDesktop || isWebDesktop) + ? tabBarHeight + windowBorderWidth + kDragToResizeAreaPadding.top + : 0; + static double get bottomToEdge => (isDesktop || isWebDesktop) + ? windowBorderWidth + kDragToResizeAreaPadding.bottom + : 0; + updateViewStyle() async { + Size getSize() { + final size = MediaQueryData.fromWindow(ui.window).size; + // If minimized, w or h may be negative here. + double w = size.width - leftToEdge - rightToEdge; + double h = size.height - topToEdge - bottomToEdge; + return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); + } + final style = await bind.sessionGetViewStyle(id: id); if (style == null) { return; } - final sizeWidth = size.width; - final sizeHeight = size.height; + + _size = getSize(); final displayWidth = getDisplayWidth(); final displayHeight = getDisplayHeight(); final viewStyle = ViewStyle( style: style, - width: sizeWidth, - height: sizeHeight, + width: size.width, + height: size.height, displayWidth: displayWidth, displayHeight: displayHeight, ); @@ -585,9 +819,16 @@ class CanvasModel with ChangeNotifier { } _lastViewStyle = viewStyle; _scale = viewStyle.scale; - _x = (sizeWidth - displayWidth * _scale) / 2; - _y = (sizeHeight - displayHeight * _scale) / 2; + + _devicePixelRatio = ui.window.devicePixelRatio; + if (kIgnoreDpi && style == kRemoteViewStyleOriginal) { + _scale = 1.0 / _devicePixelRatio; + } + _x = (size.width - displayWidth * _scale) / 2; + _y = (size.height - displayHeight * _scale) / 2; + _imageOverflow.value = _x < 0 || y < 0; notifyListeners(); + parent.target?.inputModel.refreshMousePos(); } updateScrollStyle() async { @@ -625,29 +866,33 @@ class CanvasModel with ChangeNotifier { return parent.target?.ffiModel.display.height ?? defaultHeight; } - double get windowBorderWidth => stateGlobal.windowBorderWidth.value; - double get tabBarHeight => stateGlobal.tabBarHeight; - - Size get size { - final size = MediaQueryData.fromWindow(ui.window).size; - // If minimized, w or h may be negative here. - double w = size.width - windowBorderWidth * 2; - double h = size.height - tabBarHeight - windowBorderWidth * 2; - return Size(w < 0 ? 0 : w, h < 0 ? 0 : h); - } + static double get windowBorderWidth => stateGlobal.windowBorderWidth.value; + static double get tabBarHeight => stateGlobal.tabBarHeight; moveDesktopMouse(double x, double y) { + if (size.width == 0 || size.height == 0) { + return; + } + // On mobile platforms, move the canvas with the cursor. final dw = getDisplayWidth() * _scale; final dh = getDisplayHeight() * _scale; var dxOffset = 0; var dyOffset = 0; - if (dw > size.width) { - dxOffset = (x - dw * (x / size.width) - _x).toInt(); - } - if (dh > size.height) { - dyOffset = (y - dh * (y / size.height) - _y).toInt(); + try { + if (dw > size.width) { + dxOffset = (x - dw * (x / size.width) - _x).toInt(); + } + if (dh > size.height) { + dyOffset = (y - dh * (y / size.height) - _y).toInt(); + } + } catch (e) { + debugPrintStack( + label: + '(x,y) ($x,$y), (_x,_y) ($_x,$_y), _scale $_scale, display size (${getDisplayWidth()},${getDisplayHeight()}), size $size, , $e'); + return; } + _x += dxOffset; _y += dyOffset; if (dxOffset != 0 || dyOffset != 0) { @@ -725,7 +970,7 @@ class CanvasModel with ChangeNotifier { class CursorData { final String peerId; final int id; - final img2.Image? image; + final img2.Image image; double scale; Uint8List? data; final double hotxOrigin; @@ -750,33 +995,40 @@ class CursorData { int _doubleToInt(double v) => (v * 10e6).round().toInt(); - double _checkUpdateScale(double scale, bool shouldScale) { + double _checkUpdateScale(double scale) { double oldScale = this.scale; - if (!shouldScale) { - scale = 1.0; - } else { + if (scale != 1.0) { // Update data if scale changed. - if (Platform.isWindows) { - final tgtWidth = (width * scale).toInt(); - final tgtHeight = (width * scale).toInt(); - if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) { - double sw = kMinCursorSize.toDouble() / width; - double sh = kMinCursorSize.toDouble() / height; - scale = sw < sh ? sh : sw; - } + final tgtWidth = (width * scale).toInt(); + final tgtHeight = (width * scale).toInt(); + if (tgtWidth < kMinCursorSize || tgtHeight < kMinCursorSize) { + double sw = kMinCursorSize.toDouble() / width; + double sh = kMinCursorSize.toDouble() / height; + scale = sw < sh ? sh : sw; } } - if (Platform.isWindows) { - if (_doubleToInt(oldScale) != _doubleToInt(scale)) { + if (_doubleToInt(oldScale) != _doubleToInt(scale)) { + if (Platform.isWindows) { data = img2 .copyResize( - image!, + image, width: (width * scale).toInt(), height: (height * scale).toInt(), interpolation: img2.Interpolation.average, ) .getBytes(format: img2.Format.bgra); + } else { + data = Uint8List.fromList( + img2.encodePng( + img2.copyResize( + image, + width: (width * scale).toInt(), + height: (height * scale).toInt(), + interpolation: img2.Interpolation.average, + ), + ), + ); } } @@ -786,8 +1038,8 @@ class CursorData { return scale; } - String updateGetKey(double scale, bool shouldScale) { - scale = _checkUpdateScale(scale, shouldScale); + String updateGetKey(double scale) { + scale = _checkUpdateScale(scale); return '${peerId}_${id}_${_doubleToInt(width * scale)}_${_doubleToInt(height * scale)}'; } } @@ -845,7 +1097,7 @@ class PredefinedCursor { _cache = CursorData( peerId: '', id: id, - image: _image2?.clone(), + image: _image2!.clone(), scale: scale, data: data, hotxOrigin: @@ -872,9 +1124,10 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; + DateTime? _firstUpdateMouseTime; bool gotMouseControl = true; DateTime _lastPeerMouse = DateTime.now() - .subtract(Duration(milliseconds: 2 * kMouseControlTimeoutMSec)); + .subtract(Duration(milliseconds: 3000 * kMouseControlTimeoutMSec)); String id = ''; WeakReference parent; @@ -893,6 +1146,15 @@ class CursorModel with ChangeNotifier { DateTime.now().difference(_lastPeerMouse).inMilliseconds < kMouseControlTimeoutMSec; + bool isConnIn2Secs() { + if (_firstUpdateMouseTime == null) { + _firstUpdateMouseTime = DateTime.now(); + return true; + } else { + return DateTime.now().difference(_firstUpdateMouseTime!).inSeconds < 2; + } + } + CursorModel(this.parent); Set get cachedKeys => _cacheKeys; @@ -1045,9 +1307,9 @@ class CursorModel with ChangeNotifier { Future _updateCache( Uint8List rgba, ui.Image image, int id, int w, int h) async { Uint8List? data; - img2.Image? imgOrigin; + img2.Image imgOrigin = + img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba); if (Platform.isWindows) { - imgOrigin = img2.Image.fromBytes(w, h, rgba, format: img2.Format.rgba); data = imgOrigin.getBytes(format: img2.Format.bgra); } else { ByteData? imgBytes = @@ -1089,8 +1351,10 @@ class CursorModel with ChangeNotifier { /// Update the cursor position. updateCursorPosition(Map evt, String id) async { - gotMouseControl = false; - _lastPeerMouse = DateTime.now(); + if (!isConnIn2Secs()) { + gotMouseControl = false; + _lastPeerMouse = DateTime.now(); + } _x = double.parse(evt['x']); _y = double.parse(evt['y']); try { @@ -1224,6 +1488,21 @@ class RecordingModel with ChangeNotifier { } } +class ElevationModel with ChangeNotifier { + WeakReference parent; + ElevationModel(this.parent); + bool _running = false; + bool _canElevate = false; + bool get showRequestMenu => _canElevate && !_running; + onPeerInfo(PeerInfo pi) { + _canElevate = pi.platform == kPeerPlatformWindows && pi.sasEnabled == false; + } + + onPortableServiceRunning(Map evt) { + _running = evt['running'] == 'true'; + } +} + enum ConnType { defaultConn, fileTransfer, portForward, rdp } /// Flutter state manager and data communication with the Rust core. @@ -1245,9 +1524,11 @@ class FFI { late final AbModel abModel; // global late final GroupModel groupModel; // global late final UserModel userModel; // global + late final PeerTabModel peerTabModel; // global late final QualityMonitorModel qualityMonitorModel; // session - late final RecordingModel recordingModel; // recording + late final RecordingModel recordingModel; // session late final InputModel inputModel; // session + late final ElevationModel elevationModel; // session FFI() { imageModel = ImageModel(WeakReference(this)); @@ -1258,16 +1539,22 @@ class FFI { chatModel = ChatModel(WeakReference(this)); fileModel = FileModel(WeakReference(this)); userModel = UserModel(WeakReference(this)); + peerTabModel = PeerTabModel(WeakReference(this)); abModel = AbModel(WeakReference(this)); groupModel = GroupModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this)); inputModel = InputModel(WeakReference(this)); + elevationModel = ElevationModel(WeakReference(this)); } /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. void start(String id, - {bool isFileTransfer = false, bool isPortForward = false}) { + {bool isFileTransfer = false, + bool isPortForward = false, + String? switchUuid, + String? password, + bool? forceRelay}) { assert(!(isFileTransfer && isPortForward), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; @@ -1283,28 +1570,57 @@ class FFI { } // ignore: unused_local_variable final addRes = bind.sessionAddSync( - id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward); + id: id, + isFileTransfer: isFileTransfer, + isPortForward: isPortForward, + switchUuid: switchUuid ?? "", + forceRelay: forceRelay ?? false, + password: password ?? "", + ); final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { + final useTextureRender = bind.mainUseTextureRender(); + // Preserved for the rgba data. await for (final message in stream) { - if (message is Event) { + if (message is EventToUI_Event) { + if (message.field0 == "close") { + break; + } try { Map event = json.decode(message.field0); await cb(event); } catch (e) { debugPrint('json.decode fail1(): $e, ${message.field0}'); } - } else if (message is Rgba) { - imageModel.onRgba(message.field0); + } else if (message is EventToUI_Rgba) { + if (useTextureRender) { + if (_waitForImage[id]!) { + _waitForImage[id] = false; + dialogManager.dismissAll(); + for (final cb in imageModel.callbacksOnFirstImage) { + cb(id); + } + await canvasModel.updateViewStyle(); + await canvasModel.updateScrollStyle(); + } + } else { + // Fetch the image buffer from rust codes. + final sz = platformFFI.getRgbaSize(id); + if (sz == null || sz == 0) { + return; + } + final rgba = platformFFI.getRgba(id, sz); + if (rgba != null) { + imageModel.onRgba(rgba); + } + } } } + debugPrint('Exit session event loop'); }(); // every instance will bind a stream this.id = id; - if (isFileTransfer) { - fileModel.initFileFetcher(); - } } /// Login with [password], choose if the client should [remember] it. @@ -1319,14 +1635,14 @@ class FFI { await setCanvasConfig(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } - bind.sessionClose(id: id); - id = ''; imageModel.update(null); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); inputModel.resetModifiers(); + await bind.sessionClose(id: id); debugPrint('model $id closed'); + id = ''; } void setMethodCallHandler(FMethod callback) { @@ -1353,6 +1669,30 @@ class Display { ? kDesktopDefaultDisplayHeight : kMobileDefaultDisplayHeight; } + + @override + bool operator ==(Object other) => + other is Display && + other.runtimeType == runtimeType && + _innerEqual(other); + + bool _innerEqual(Display other) => + other.x == x && + other.y == y && + other.width == width && + other.height == height && + other.cursorEmbedded == cursorEmbedded; +} + +class Resolution { + int width = 0; + int height = 0; + Resolution(this.width, this.height); + + @override + String toString() { + return 'Resolution($width,$height)'; + } } class Features { @@ -1368,6 +1708,10 @@ class PeerInfo { int currentDisplay = 0; List displays = []; Features features = Features(); + List resolutions = []; + Map platform_additions = {}; + + bool get is_wayland => platform_additions['is_wayland'] == true; } const canvasKey = 'canvas'; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index cf2de4219..2b99e1823 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -8,6 +8,8 @@ import 'package:external_path/external_path.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:get/get.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:win32/win32.dart' as win32; @@ -22,8 +24,15 @@ class RgbaFrame extends Struct { } typedef F2 = Pointer Function(Pointer, Pointer); -typedef F3 = void Function(Pointer, Pointer); +typedef F3 = Pointer Function(Pointer); +typedef F4 = Uint64 Function(Pointer); +typedef F4Dart = int Function(Pointer); +typedef F5 = Void Function(Pointer); +typedef F5Dart = void Function(Pointer); typedef HandleEvent = Future Function(Map evt); +// pub fn session_register_texture(id: *const char, ptr: usize) +typedef F6 = Void Function(Pointer, Uint64); +typedef F6Dart = void Function(Pointer, int); /// FFI wrapper around the native Rust core. /// Hides the platform differences. @@ -43,9 +52,15 @@ class PlatformFFI { final _toAndroidChannel = const MethodChannel('mChannel'); RustdeskImpl get ffiBind => _ffiBind; + F3? _session_get_rgba; + F4Dart? _session_get_rgba_size; + F5Dart? _session_next_rgba; + F6Dart? _session_register_texture; static get localeName => Platform.localeName; + static get isMain => instance._appType == kAppTypeMain; + static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; @@ -89,6 +104,43 @@ class PlatformFFI { return res; } + Uint8List? getRgba(String id, int bufSize) { + if (_session_get_rgba == null) return null; + var a = id.toNativeUtf8(); + try { + final buffer = _session_get_rgba!(a); + if (buffer == nullptr) { + return null; + } + final data = buffer.asTypedList(bufSize); + return data; + } finally { + malloc.free(a); + } + } + + int? getRgbaSize(String id) { + if (_session_get_rgba_size == null) return null; + var a = id.toNativeUtf8(); + final bufferSize = _session_get_rgba_size!(a); + malloc.free(a); + return bufferSize; + } + + void nextRgba(String id) { + if (_session_next_rgba == null) return; + final a = id.toNativeUtf8(); + _session_next_rgba!(a); + malloc.free(a); + } + + void registerTexture(String id, int ptr) { + if (_session_register_texture == null) return; + final a = id.toNativeUtf8(); + _session_register_texture!(a, ptr); + malloc.free(a); + } + /// Init the FFI class, loads the native Rust core library. Future init(String appType) async { _appType = appType; @@ -104,6 +156,13 @@ class PlatformFFI { debugPrint('initializing FFI $_appType'); try { _translate = dylib.lookupFunction('translate'); + _session_get_rgba = dylib.lookupFunction("session_get_rgba"); + _session_get_rgba_size = + dylib.lookupFunction("session_get_rgba_size"); + _session_next_rgba = + dylib.lookupFunction("session_next_rgba"); + _session_register_texture = + dylib.lookupFunction("session_register_texture"); try { // SYSTEM user failed _dir = (await getApplicationDocumentsDirectory()).path; @@ -112,8 +171,15 @@ class PlatformFFI { } _ffiBind = RustdeskImpl(dylib); if (Platform.isLinux) { - // start dbus service, no need to await - await _ffiBind.mainStartDbusServer(); + // Start a dbus service, no need to await + _ffiBind.mainStartDbusServer(); + } else if (Platform.isMacOS && isMain) { + Future.wait([ + // Start dbus service. + _ffiBind.mainStartDbusServer(), + // Start local audio pulseaudio server. + _ffiBind.mainStartPa() + ]); } _startListenEvent(_ffiBind); // global event try { @@ -168,6 +234,9 @@ class PlatformFFI { debugPrint( '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir'); } + if (desktopType == DesktopType.cm) { + await _ffiBind.cmStartListenIpcThread(); + } await _ffiBind.mainDeviceId(id: id); await _ffiBind.mainDeviceName(name: name); await _ffiBind.mainSetHomeDir(home: _homeDir); @@ -235,4 +304,8 @@ class PlatformFFI { if (!isAndroid) return Future(() => false); return await _toAndroidChannel.invokeMethod(method, arguments); } + + void syncAndroidServiceAppDirConfigPath() { + invokeMethod(AndroidChannel.kSyncAppDirConfigPath, _dir); + } } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index ad5183ae3..1b71e382e 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -14,6 +14,13 @@ class Peer { String rdpUsername; bool online = false; + String getId() { + if (alias != '') { + return alias; + } + return id; + } + Peer.fromJson(Map json) : id = json['id'] ?? '', username = json['username'] ?? '', diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart new file mode 100644 index 000000000..7c6211682 --- /dev/null +++ b/flutter/lib/models/peer_tab_model.dart @@ -0,0 +1,275 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:scroll_pos/scroll_pos.dart'; + +import '../common.dart'; +import 'model.dart'; + +const int groupTabIndex = 4; +const String defaultGroupTabname = 'Group'; + +class PeerTabModel with ChangeNotifier { + WeakReference parent; + int get currentTab => _currentTab; + int _currentTab = 0; // index in tabNames + List get visibleOrderedTabs => _visibleOrderedTabs; + List _visibleOrderedTabs = List.empty(growable: true); + List get tabOrder => _tabOrder; + List _tabOrder = List.from([0, 1, 2, 3, 4]); // constant length + int get tabHiddenFlag => _tabHiddenFlag; + int _tabHiddenFlag = 0; + bool get showScrollBtn => _showScrollBtn; + bool _showScrollBtn = false; + final List _fullyVisible = List.filled(5, false); + bool get leftFullyVisible => _leftFullyVisible; + bool _leftFullyVisible = false; + bool get rightFullyVisible => _rightFullyVisible; + bool _rightFullyVisible = false; + ScrollPosController sc = ScrollPosController(); + List tabNames = [ + 'Recent Sessions', + 'Favorites', + 'Discovered', + 'Address Book', + defaultGroupTabname, + ]; + + PeerTabModel(this.parent) { + // init tabHiddenFlag + _tabHiddenFlag = int.tryParse( + bind.getLocalFlutterConfig(k: 'hidden-peer-card'), + radix: 2) ?? + 0; + var tabs = _notHiddenTabs(); + // remove dynamic tabs + tabs.remove(groupTabIndex); + // init tabOrder + try { + final conf = bind.getLocalFlutterConfig(k: 'peer-tab-order'); + if (conf.isNotEmpty) { + final json = jsonDecode(conf); + if (json is List) { + final List list = + json.map((e) => int.tryParse(e.toString()) ?? -1).toList(); + if (list.length == _tabOrder.length && + _tabOrder.every((e) => list.contains(e))) { + _tabOrder = list; + } + } + } + } catch (e) { + debugPrintStack(label: '$e'); + } + // init visibleOrderedTabs + var tempList = _tabOrder.toList(); + tempList.removeWhere((e) => !tabs.contains(e)); + _visibleOrderedTabs = tempList; + // init currentTab + _currentTab = + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0; + if (!tabs.contains(_currentTab)) { + if (tabs.isNotEmpty) { + _currentTab = tabs[0]; + } else { + _currentTab = 0; + } + } + sc.itemCount = _visibleOrderedTabs.length; + } + + check_dynamic_tabs() { + var visible = visibleTabs(); + _visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList(); + if (_visibleOrderedTabs.contains(groupTabIndex) && + int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) == + groupTabIndex) { + _currentTab = groupTabIndex; + } + if (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isNotEmpty) { + tabNames[groupTabIndex] = gFFI.userModel.groupName.value; + } else { + tabNames[groupTabIndex] = defaultGroupTabname; + } + sc.itemCount = _visibleOrderedTabs.length; + notifyListeners(); + } + + setCurrentTab(int index) { + if (_currentTab != index) { + _currentTab = index; + notifyListeners(); + } + } + + setTabFullyVisible(int index, bool visible) { + if (index >= 0 && index < _fullyVisible.length) { + if (visible != _fullyVisible[index]) { + _fullyVisible[index] = visible; + bool changed = false; + bool show = _visibleOrderedTabs.any((e) => !_fullyVisible[e]); + if (show != _showScrollBtn) { + _showScrollBtn = show; + changed = true; + } + if (_visibleOrderedTabs.isNotEmpty && _visibleOrderedTabs[0] == index) { + if (_leftFullyVisible != visible) { + _leftFullyVisible = visible; + changed = true; + } + } + if (_visibleOrderedTabs.isNotEmpty && + _visibleOrderedTabs.last == index) { + if (_rightFullyVisible != visible) { + _rightFullyVisible = visible; + changed = true; + } + } + if (changed) { + notifyListeners(); + } + } + } + } + + onReorder(oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex -= 1; + } + var list = _visibleOrderedTabs.toList(); + final int item = list.removeAt(oldIndex); + list.insert(newIndex, item); + _visibleOrderedTabs = list; + + var tmpTabOrder = _visibleOrderedTabs.toList(); + var left = _tabOrder.where((e) => !tmpTabOrder.contains(e)).toList(); + for (var t in left) { + _addTabInOrder(tmpTabOrder, t); + } + _tabOrder = tmpTabOrder; + bind.setLocalFlutterConfig(k: 'peer-tab-order', v: jsonEncode(tmpTabOrder)); + notifyListeners(); + } + + onHideShow(int index, bool show) async { + int bitMask = 1 << index; + if (show) { + _tabHiddenFlag &= ~bitMask; + } else { + _tabHiddenFlag |= bitMask; + } + await bind.setLocalFlutterConfig( + k: 'hidden-peer-card', v: _tabHiddenFlag.toRadixString(2)); + var visible = visibleTabs(); + _visibleOrderedTabs = _tabOrder.where((e) => visible.contains(e)).toList(); + if (_visibleOrderedTabs.isNotEmpty && + !_visibleOrderedTabs.contains(_currentTab)) { + _currentTab = _visibleOrderedTabs[0]; + } + notifyListeners(); + } + + List orderedNotFilteredTabs() { + var list = tabOrder.toList(); + if (_filterGroupCard()) { + list.remove(groupTabIndex); + } + return list; + } + + // return index array of tabNames + List visibleTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i) && !_isTabFilter(i)) { + v.add(i); + } + } + return v; + } + + String translatedTabname(int index) { + if (index >= 0 && index < tabNames.length) { + final name = tabNames[index]; + if (index == groupTabIndex) { + if (name == defaultGroupTabname) { + return translate(name); + } else { + return name; + } + } else { + return translate(name); + } + } + assert(false); + return index.toString(); + } + + bool _isTabHidden(int tabindex) { + return _tabHiddenFlag & (1 << tabindex) != 0; + } + + bool _isTabFilter(int tabIndex) { + if (tabIndex == groupTabIndex) { + return _filterGroupCard(); + } + return false; + } + + // return true if hide group card + bool _filterGroupCard() { + if (gFFI.groupModel.users.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + return true; + } else { + return false; + } + } + + List _notHiddenTabs() { + var v = List.empty(growable: true); + for (int i = 0; i < tabNames.length; i++) { + if (!_isTabHidden(i)) { + v.add(i); + } + } + return v; + } + + // add tabIndex to list + _addTabInOrder(List list, int tabIndex) { + if (!_tabOrder.contains(tabIndex) || list.contains(tabIndex)) { + return; + } + bool sameOrder = true; + int lastIndex = -1; + for (int i = 0; i < list.length; i++) { + var index = _tabOrder.lastIndexOf(list[i]); + if (index > lastIndex) { + lastIndex = index; + continue; + } else { + sameOrder = false; + break; + } + } + if (sameOrder) { + var indexInTabOrder = _tabOrder.indexOf(tabIndex); + var left = List.empty(growable: true); + for (int i = 0; i < indexInTabOrder; i++) { + left.add(_tabOrder[i]); + } + int insertIndex = list.lastIndexWhere((e) => left.contains(e)); + if (insertIndex < 0) { + insertIndex = 0; + } else { + insertIndex += 1; + } + list.insert(insertIndex, tabIndex); + } else { + list.add(tabIndex); + } + } +} diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 338da4ee3..216814cf6 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:get/get.dart'; @@ -28,7 +29,7 @@ class ServerModel with ChangeNotifier { bool _inputOk = false; bool _audioOk = false; bool _fileOk = false; - bool _showElevation = true; + bool _showElevation = false; bool _hideCm = false; int _connectStatus = 0; // Rendezvous Server status String _verificationMethod = ""; @@ -127,10 +128,13 @@ class ServerModel with ChangeNotifier { _connectStatus = status; notifyListeners(); } - final res = await bind.cmCheckClientsLength(length: _clients.length); - if (res != null) { - debugPrint("clients not match!"); - updateClientState(res); + + if (desktopType == DesktopType.cm) { + final res = await bind.cmCheckClientsLength(length: _clients.length); + if (res != null) { + debugPrint("clients not match!"); + updateClientState(res); + } } updatePasswordModel(); @@ -154,7 +158,8 @@ class ServerModel with ChangeNotifier { /// file true by default (if permission on) checkAndroidPermission() async { // audio - if (androidVersion < 30 || !await PermissionManager.check("audio")) { + if (androidVersion < 30 || + !await AndroidPermissionManager.check(kRecordAudio)) { _audioOk = false; bind.mainSetOption(key: "enable-audio", value: "N"); } else { @@ -163,7 +168,7 @@ class ServerModel with ChangeNotifier { } // file - if (!await PermissionManager.check("file")) { + if (!await AndroidPermissionManager.check(kManageExternalStorage)) { _fileOk = false; bind.mainSetOption(key: "enable-file-transfer", value: "N"); } else { @@ -229,10 +234,13 @@ class ServerModel with ChangeNotifier { } toggleAudio() async { - if (!_audioOk && !await PermissionManager.check("audio")) { - final res = await PermissionManager.request("audio"); + if (clients.isNotEmpty) { + await showClientsMayNotBeChangedAlert(parent.target); + } + if (!_audioOk && !await AndroidPermissionManager.check(kRecordAudio)) { + final res = await AndroidPermissionManager.request(kRecordAudio); if (!res) { - // TODO handle fail + showToast(translate('Failed')); return; } } @@ -243,10 +251,15 @@ class ServerModel with ChangeNotifier { } toggleFile() async { - if (!_fileOk && !await PermissionManager.check("file")) { - final res = await PermissionManager.request("file"); + if (clients.isNotEmpty) { + await showClientsMayNotBeChangedAlert(parent.target); + } + if (!_fileOk && + !await AndroidPermissionManager.check(kManageExternalStorage)) { + final res = + await AndroidPermissionManager.request(kManageExternalStorage); if (!res) { - // TODO handle fail + showToast(translate('Failed')); return; } } @@ -256,11 +269,17 @@ class ServerModel with ChangeNotifier { notifyListeners(); } - toggleInput() { + toggleInput() async { + if (clients.isNotEmpty) { + await showClientsMayNotBeChangedAlert(parent.target); + } if (_inputOk) { parent.target?.invokeMethod("stop_input"); + bind.mainSetOption(key: "enable-keyboard", value: 'N'); } else { if (parent.target != null) { + /// the result of toggle-on depends on user actions in the settings page. + /// handle result, see [ServerModel.changeStatue] showInputWarnAlert(parent.target!); } } @@ -282,7 +301,7 @@ class ServerModel with ChangeNotifier { content: Text(translate("android_stop_service_tip")), actions: [ TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + TextButton(onPressed: submit, child: Text(translate("OK"))), ], onSubmit: submit, onCancel: close, @@ -304,8 +323,8 @@ class ServerModel with ChangeNotifier { ]), content: Text(translate("android_service_will_start_tip")), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton(onPressed: submit, child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -323,10 +342,10 @@ class ServerModel with ChangeNotifier { notifyListeners(); parent.target?.ffiModel.updateEventListener(""); await parent.target?.invokeMethod("init_service"); + // ugly is here, because for desktop, below is useless await bind.mainStartService(); updateClientState(); - if (!Platform.isLinux) { - // current linux is not supported + if (Platform.isAndroid) { Wakelock.enable(); } } @@ -344,10 +363,6 @@ class ServerModel with ChangeNotifier { } } - Future initInput() async { - await parent.target?.invokeMethod("init_input"); - } - Future setPermanentPassword(String newPW) async { await bind.mainSetPermanentPassword(password: newPW); await Future.delayed(Duration(milliseconds: 500)); @@ -456,7 +471,8 @@ class ServerModel with ChangeNotifier { Future.delayed(Duration.zero, () async { if (!hideCm) window_on_top(null); }); - if (client.authorized) { + // Only do the hidden task when on Desktop. + if (client.authorized && isDesktop) { cmHiddenTimer = Timer(const Duration(seconds: 3), () { if (!hideCm) windowManager.minimize(); cmHiddenTimer = null; @@ -501,8 +517,8 @@ class ServerModel with ChangeNotifier { ], ), actions: [ - TextButton(onPressed: cancel, child: Text(translate("Dismiss"))), - ElevatedButton(onPressed: submit, child: Text(translate("Accept"))), + dialogButton("Dismiss", onPressed: cancel, isOutline: true), + dialogButton("Accept", onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -560,10 +576,9 @@ class ServerModel with ChangeNotifier { } } - closeAll() { - for (var client in _clients) { - bind.cmCloseConnection(connId: client.id); - } + Future closeAll() async { + await Future.wait( + _clients.map((client) => bind.cmCloseConnection(connId: client.id))); _clients.clear(); tabController.state.value.tabs.clear(); } @@ -579,6 +594,26 @@ class ServerModel with ChangeNotifier { notifyListeners(); } } + + void updateVoiceCallState(Map evt) { + try { + final client = Client.fromJson(jsonDecode(evt["client"])); + final index = _clients.indexWhere((element) => element.id == client.id); + if (index != -1) { + _clients[index].inVoiceCall = client.inVoiceCall; + _clients[index].incomingVoiceCall = client.incomingVoiceCall; + if (client.incomingVoiceCall) { + // Has incoming phone call, let's set the window on top. + Future.delayed(Duration.zero, () { + window_on_top(null); + }); + } + notifyListeners(); + } + } catch (e) { + debugPrint("updateVoiceCallState failed: $e"); + } + } } enum ClientType { @@ -601,6 +636,9 @@ class Client { bool restart = false; bool recording = false; bool disconnected = false; + bool fromSwitch = false; + bool inVoiceCall = false; + bool incomingVoiceCall = false; RxBool hasUnreadChatMessage = false.obs; @@ -621,6 +659,9 @@ class Client { restart = json['restart']; recording = json['recording']; disconnected = json['disconnected']; + fromSwitch = json['from_switch']; + inVoiceCall = json['in_voice_call']; + incomingVoiceCall = json['incoming_voice_call']; } Map toJson() { @@ -638,6 +679,7 @@ class Client { data['restart'] = restart; data['recording'] = recording; data['disconnected'] = disconnected; + data['from_switch'] = fromSwitch; return data; } @@ -659,7 +701,7 @@ String getLoginDialogTag(int id) { showInputWarnAlert(FFI ffi) { ffi.dialogManager.show((setState, close) { submit() { - ffi.serverModel.initInput(); + AndroidPermissionManager.startAction(kActionAccessibilitySettings); close(); } @@ -674,12 +716,30 @@ showInputWarnAlert(FFI ffi) { ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: submit, child: Text(translate("Open System Setting"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Open System Setting", onPressed: submit), ], onSubmit: submit, onCancel: close, ); }); } + +Future showClientsMayNotBeChangedAlert(FFI? ffi) async { + await ffi?.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Permissions")), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(translate("android_permission_may_not_change_tip")), + ], + ), + actions: [ + dialogButton("OK", onPressed: close), + ], + onSubmit: close, + onCancel: close, + ); + }); +} diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 53f1a19b1..187b1ffc5 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -9,25 +9,42 @@ import '../consts.dart'; class StateGlobal { int _windowId = -1; bool _fullscreen = false; + bool _maximize = false; + bool grabKeyboard = false; final RxBool _showTabBar = true.obs; + final RxBool _showResizeEdge = true.obs; final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); final RxBool showRemoteMenuBar = false.obs; + final RxInt displaysCount = 0.obs; int get windowId => _windowId; bool get fullscreen => _fullscreen; + bool get maximize => _maximize; double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight; RxBool get showTabBar => _showTabBar; RxDouble get resizeEdgeSize => _resizeEdgeSize; RxDouble get windowBorderWidth => _windowBorderWidth; setWindowId(int id) => _windowId = id; + setMaximize(bool v) { + if (_maximize != v && !_fullscreen) { + _maximize = v; + _resizeEdgeSize.value = _maximize ? kMaximizeEdgeSize : kWindowEdgeSize; + } + } setFullscreen(bool v) { if (_fullscreen != v) { _fullscreen = v; _showTabBar.value = !_fullscreen; _resizeEdgeSize.value = - fullscreen ? kFullScreenEdgeSize : kWindowEdgeSize; + fullscreen + ? kFullScreenEdgeSize + : _maximize + ? kMaximizeEdgeSize + : kWindowEdgeSize; + print( + "fullscreen: ${fullscreen}, resizeEdgeSize: ${_resizeEdgeSize.value}"); _windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth; WindowController.fromWindowId(windowId) .setFullscreen(_fullscreen) diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index e5d2c9e15..7f40b3333 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; import 'package:get/get.dart'; import 'package:http/http.dart' as http; @@ -45,7 +46,9 @@ class UserModel { if (error != null) { throw error; } - await _parseUserInfo(data); + + final user = UserPayload.fromJson(data); + await _parseAndUpdateUser(user); } catch (e) { print('Failed to refreshCurrentUser: $e'); } finally { @@ -55,19 +58,17 @@ class UserModel { Future reset() async { await bind.mainSetLocalOption(key: 'access_token', value: ''); - await bind.mainSetLocalOption(key: 'user_info', value: ''); await gFFI.abModel.reset(); await gFFI.groupModel.reset(); userName.value = ''; groupName.value = ''; - statePeerTab.check(); + gFFI.peerTabModel.check_dynamic_tabs(); } - Future _parseUserInfo(dynamic userinfo) async { - bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(userinfo)); - userName.value = userinfo['name'] ?? ''; - groupName.value = userinfo['grp'] ?? ''; - isAdmin.value = userinfo['is_admin'] == true; + Future _parseAndUpdateUser(UserPayload user) async { + userName.value = user.name; + groupName.value = user.grp; + isAdmin.value = user.isAdmin; } Future _updateOtherModels() async { @@ -79,13 +80,15 @@ class UserModel { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); try { final url = await bind.mainGetApiServer(); + final authHeaders = getHttpHeaders(); + authHeaders['Content-Type'] = "application/json"; await http .post(Uri.parse('$url/api/logout'), - body: { + body: jsonEncode({ 'id': await bind.mainGetMyId(), 'uuid': await bind.mainGetUuid(), - }, - headers: await getHttpHeaders()) + }), + headers: authHeaders) .timeout(Duration(seconds: 2)); } catch (e) { print("request /api/logout failed: err=$e"); @@ -95,26 +98,37 @@ class UserModel { } } - Future> login(String userName, String pass) async { + /// throw [RequestException] + Future login(LoginRequest loginRequest) async { final url = await bind.mainGetApiServer(); + final resp = await http.post(Uri.parse('$url/api/login'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(loginRequest.toJson())); + + final Map body; try { - final resp = await http.post(Uri.parse('$url/api/login'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'username': userName, - 'password': pass, - 'id': await bind.mainGetMyId(), - 'uuid': await bind.mainGetUuid() - })); - final body = jsonDecode(resp.body); - bind.mainSetLocalOption( - key: 'access_token', value: body['access_token'] ?? ''); - await _parseUserInfo(body['user']); - return body; - } catch (err) { - return {'error': '$err'}; - } finally { - await _updateOtherModels(); + body = jsonDecode(resp.body); + } catch (e) { + print("jsonDecode resp body failed: ${e.toString()}"); + rethrow; } + + if (resp.statusCode != 200) { + throw RequestException(resp.statusCode, body['error'] ?? ''); + } + + final LoginResponse loginResponse; + try { + loginResponse = LoginResponse.fromJson(body); + } catch (e) { + print("jsonDecode LoginResponse failed: ${e.toString()}"); + rethrow; + } + + if (loginResponse.user != null) { + await _parseAndUpdateUser(loginResponse.user!); + } + + return loginResponse; } } diff --git a/flutter/lib/utils/event_loop.dart b/flutter/lib/utils/event_loop.dart new file mode 100644 index 000000000..a982cf9e2 --- /dev/null +++ b/flutter/lib/utils/event_loop.dart @@ -0,0 +1,79 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +typedef EventCallback = Future Function(Data data); + +abstract class BaseEvent { + EventType type; + Data data; + + /// Constructor. + BaseEvent(this.type, this.data); + + /// Consume this event. + @visibleForTesting + Future consume() async { + final cb = findCallback(type); + if (cb == null) { + return null; + } else { + return cb(data); + } + } + + EventCallback? findCallback(EventType type); +} + +abstract class BaseEventLoop { + final List> _evts = []; + Timer? _timer; + + List> get evts => _evts; + + Future onReady() async { + // Poll every 100ms. + _timer = Timer.periodic(Duration(milliseconds: 100), _handleTimer); + } + + /// An Event is about to be consumed. + Future onPreConsume(BaseEvent evt) async {} + /// An Event was consumed. + Future onPostConsume(BaseEvent evt) async {} + /// Events are all handled and cleared. + Future onEventsClear() async {} + /// Events start to consume. + Future onEventsStartConsuming() async {} + + Future _handleTimer(Timer timer) async { + if (_evts.isEmpty) { + return; + } + timer.cancel(); + _timer = null; + // Handle the logic. + await onEventsStartConsuming(); + while (_evts.isNotEmpty) { + final evt = _evts.first; + _evts.remove(evt); + await onPreConsume(evt); + await evt.consume(); + await onPostConsume(evt); + } + await onEventsClear(); + // Now events are all processed. + _timer = Timer.periodic(Duration(milliseconds: 100), _handleTimer); + } + + Future close() async { + _timer?.cancel(); + } + + void pushEvent(BaseEvent evt) { + _evts.add(evt); + } + + void clear() { + _evts.clear(); + } +} diff --git a/flutter/lib/utils/image.dart b/flutter/lib/utils/image.dart index 1f0d5b0cd..a153dbc63 100644 --- a/flutter/lib/utils/image.dart +++ b/flutter/lib/utils/image.dart @@ -1,6 +1,8 @@ import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:flutter/widgets.dart'; + Future decodeImageFromPixels( Uint8List pixels, int width, @@ -9,6 +11,7 @@ Future decodeImageFromPixels( int? rowBytes, int? targetWidth, int? targetHeight, + VoidCallback? onPixelsCopied, bool allowUpscaling = true, }) async { if (targetWidth != null) { @@ -20,6 +23,7 @@ Future decodeImageFromPixels( final ui.ImmutableBuffer buffer = await ui.ImmutableBuffer.fromUint8List(pixels); + onPixelsCopied?.call(); final ui.ImageDescriptor descriptor = ui.ImageDescriptor.raw( buffer, width: width, @@ -47,3 +51,40 @@ Future decodeImageFromPixels( descriptor.dispose(); return frameInfo.image; } + +class ImagePainter extends CustomPainter { + ImagePainter({ + required this.image, + required this.x, + required this.y, + required this.scale, + }); + + ui.Image? image; + double x; + double y; + double scale; + + @override + void paint(Canvas canvas, Size size) { + if (image == null) return; + if (x.isNaN || y.isNaN) return; + canvas.scale(scale, scale); + // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 + // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html + var paint = Paint(); + if ((scale - 1.0).abs() > 0.001) { + paint.filterQuality = FilterQuality.medium; + if (scale > 10.00000) { + paint.filterQuality = FilterQuality.high; + } + } + canvas.drawImage( + image!, Offset(x.toInt().toDouble(), y.toInt().toDouble()), paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return oldDelegate != this; + } +} diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index de6750a3f..1aec57680 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,6 +1,8 @@ import 'dart:convert'; +import 'dart:io'; import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; @@ -34,14 +36,27 @@ class RustDeskMultiWindowManager { static final instance = RustDeskMultiWindowManager._(); final List _activeWindows = List.empty(growable: true); - final List _windowActiveCallbacks = List.empty(growable: true); + final List _windowActiveCallbacks = List.empty(growable: true); int? _remoteDesktopWindowId; int? _fileTransferWindowId; int? _portForwardWindowId; - Future newRemoteDesktop(String remoteId) async { - final msg = - jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remoteId}); + Future newRemoteDesktop( + String remoteId, { + String? password, + String? switch_uuid, + bool? forceRelay, + }) async { + var params = { + "type": WindowType.RemoteDesktop.index, + "id": remoteId, + "password": password, + "forceRelay": forceRelay + }; + if (switch_uuid != null) { + params['switch_uuid'] = switch_uuid; + } + final msg = jsonEncode(params); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -57,8 +72,11 @@ class RustDeskMultiWindowManager { remoteDesktopController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - remote desktop") - ..show(); + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.RemoteDesktop)); + if (Platform.isMacOS) { + Future.microtask(() => remoteDesktopController.show()); + } registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; } else { @@ -66,9 +84,12 @@ class RustDeskMultiWindowManager { } } - Future newFileTransfer(String remoteId) async { - final msg = - jsonEncode({"type": WindowType.FileTransfer.index, "id": remoteId}); + Future newFileTransfer(String remoteId, {bool? forceRelay}) async { + var msg = jsonEncode({ + "type": WindowType.FileTransfer.index, + "id": remoteId, + "forceRelay": forceRelay, + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -83,8 +104,11 @@ class RustDeskMultiWindowManager { fileTransferController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - file transfer") - ..show(); + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.FileTransfer)); + if (Platform.isMacOS) { + Future.microtask(() => fileTransferController.show()); + } registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; } else { @@ -92,9 +116,14 @@ class RustDeskMultiWindowManager { } } - Future newPortForward(String remoteId, bool isRDP) async { - final msg = jsonEncode( - {"type": WindowType.PortForward.index, "id": remoteId, "isRDP": isRDP}); + Future newPortForward(String remoteId, bool isRDP, + {bool? forceRelay}) async { + final msg = jsonEncode({ + "type": WindowType.PortForward.index, + "id": remoteId, + "isRDP": isRDP, + "forceRelay": forceRelay, + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -109,8 +138,11 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - port forward") - ..show(); + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.PortForward)); + if (Platform.isMacOS) { + Future.microtask(() => portForwardController.show()); + } registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; } else { @@ -142,6 +174,24 @@ class RustDeskMultiWindowManager { return null; } + void clearWindowType(WindowType type) { + switch (type) { + case WindowType.Main: + return; + case WindowType.RemoteDesktop: + _remoteDesktopWindowId = null; + break; + case WindowType.FileTransfer: + _fileTransferWindowId = null; + break; + case WindowType.PortForward: + _portForwardWindowId = null; + break; + case WindowType.Unknown: + break; + } + } + void setMethodHandler( Future Function(MethodCall call, int fromWindowId)? handler) { DesktopMultiWindow.setMethodHandler(handler); @@ -168,8 +218,11 @@ class RustDeskMultiWindowManager { } await WindowController.fromWindowId(wId).setPreventClose(false); await WindowController.fromWindowId(wId).close(); - } on Error { + } catch (e) { + debugPrint("$e"); return; + } finally { + clearWindowType(type); } } } @@ -191,41 +244,41 @@ class RustDeskMultiWindowManager { return _activeWindows; } - void _notifyActiveWindow() { + Future _notifyActiveWindow() async { for (final callback in _windowActiveCallbacks) { - callback.call(); + await callback.call(); } } - void registerActiveWindow(int windowId) { + Future registerActiveWindow(int windowId) async { if (_activeWindows.contains(windowId)) { // ignore } else { _activeWindows.add(windowId); } - _notifyActiveWindow(); + await _notifyActiveWindow(); } /// Remove active window which has [`windowId`] - /// + /// /// [Availability] /// This function should only be called from main window. /// For other windows, please post a unregister(hide) event to main window handler: /// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});` - void unregisterActiveWindow(int windowId) { + Future unregisterActiveWindow(int windowId) async { if (!_activeWindows.contains(windowId)) { // ignore } else { _activeWindows.remove(windowId); } - _notifyActiveWindow(); + await _notifyActiveWindow(); } - void registerActiveWindowListener(VoidCallback callback) { + void registerActiveWindowListener(AsyncCallback callback) { _windowActiveCallbacks.add(callback); } - void unregisterActiveWindowListener(VoidCallback callback) { + void unregisterActiveWindowListener(AsyncCallback callback) { _windowActiveCallbacks.remove(callback); } } diff --git a/flutter/lib/utils/platform_channel.dart b/flutter/lib/utils/platform_channel.dart new file mode 100644 index 000000000..7b60ef63c --- /dev/null +++ b/flutter/lib/utils/platform_channel.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/main.dart'; + +enum SystemWindowTheme { light, dark } + +/// The platform channel for RustDesk. +class RdPlatformChannel { + RdPlatformChannel._(); + + static final RdPlatformChannel _windowUtil = RdPlatformChannel._(); + + static RdPlatformChannel get instance => _windowUtil; + + final MethodChannel _osxMethodChannel = + MethodChannel("org.rustdesk.rustdesk/macos"); + final MethodChannel _winMethodChannel = + MethodChannel("org.rustdesk.rustdesk/windows"); + final MethodChannel _linuxMethodChannel = + MethodChannel("org.rustdesk.rustdesk/linux"); + + /// Change the theme of the system window + Future changeSystemWindowTheme(SystemWindowTheme theme) { + assert(Platform.isMacOS); + if (kDebugMode) { + print( + "[Window ${kWindowId ?? 'Main'}] change system window theme to ${theme.name}"); + } + return _osxMethodChannel + .invokeMethod("setWindowTheme", {"themeName": theme.name}); + } + + /// Terminate .app manually. + Future terminate() { + assert(Platform.isMacOS); + return _osxMethodChannel.invokeMethod("terminate"); + } +} diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt index c03d4c576..a9fd84088 100644 --- a/flutter/linux/CMakeLists.txt +++ b/flutter/linux/CMakeLists.txt @@ -9,7 +9,7 @@ set(BINARY_NAME "rustdesk") # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "com.carriez.flutter_hbb") -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# Explicitly opt into modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) diff --git a/flutter/linux/flutter/CMakeLists.txt b/flutter/linux/flutter/CMakeLists.txt index d5bd01648..52af0069a 100644 --- a/flutter/linux/flutter/CMakeLists.txt +++ b/flutter/linux/flutter/CMakeLists.txt @@ -70,7 +70,7 @@ target_link_libraries(flutter INTERFACE add_dependencies(flutter flutter_assemble) # === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, +# _phony_ is a nonexistent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. add_custom_command( diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc index 215c6f0ee..21e25fa28 100644 --- a/flutter/linux/my_application.cc +++ b/flutter/linux/my_application.cc @@ -23,7 +23,15 @@ static void my_application_activate(GApplication* application) { GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // we have custom window frame gtk_window_set_decorated(window, FALSE); - + // try setting icon for rustdesk, which uses the system cache + GtkIconTheme* theme = gtk_icon_theme_get_default(); + gint icons[4] = {256, 128, 64, 32}; + for (int i = 0; i < 4; i++) { + GdkPixbuf* icon = gtk_icon_theme_load_icon(theme, "rustdesk", icons[i], GTK_ICON_LOOKUP_NO_SVG, NULL); + if (icon != nullptr) { + gtk_window_set_icon(window, icon); + } + } // Use a header bar when running in GNOME as this is the common style used // by applications and is the setup most users will be using (e.g. Ubuntu // desktop). diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index 8d41945c8..16dc0d352 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -13,13 +13,16 @@ PODS: - FMDB/standard (2.7.5) - package_info_plus_macos (0.0.1): - FlutterMacOS - - path_provider_macos (0.0.1): + - path_provider_foundation (0.0.1): + - Flutter - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) + - texture_rgba_renderer (0.0.1): + - FlutterMacOS - uni_links_desktop (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): @@ -38,9 +41,10 @@ DEPENDENCIES: - flutter_custom_cursor (from `Flutter/ephemeral/.symlinks/plugins/flutter_custom_cursor/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - texture_rgba_renderer (from `Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos`) - uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) @@ -64,12 +68,14 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral package_info_plus_macos: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos - path_provider_macos: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + texture_rgba_renderer: + :path: Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos uni_links_desktop: :path: Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos url_launcher_macos: @@ -86,14 +92,15 @@ SPEC CHECKSUMS: desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 - FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c - path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 - url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + url_launcher_macos: c04e4fa86382d4f94f6b38f14625708be3ae52e2 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index a5a476285..c73e666c7 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -60,7 +60,7 @@ 295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* rustdesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = rustdesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* RustDesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RustDesk.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -119,7 +119,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* rustdesk.app */, + 33CC10ED2044A3C60003C045 /* RustDesk.app */, ); name = Products; sourceTree = ""; @@ -200,7 +200,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* rustdesk.app */; + productReference = 33CC10ED2044A3C60003C045 /* RustDesk.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -215,7 +215,7 @@ TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; + LastSwiftMigration = 1420; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { @@ -263,6 +263,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -403,6 +404,7 @@ CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -412,7 +414,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -428,8 +430,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -442,6 +447,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; }; name = Profile; @@ -481,7 +487,7 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -538,6 +544,7 @@ CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -550,6 +557,12 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; ONLY_ACTIVE_ARCH = YES; + OTHER_LDFLAGS = ( + "-sectcreate", + __CGPreLoginApp, + __cgpreloginapp, + /dev/null, + ); SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -563,8 +576,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -577,6 +592,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -590,8 +606,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = ""; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -602,8 +621,15 @@ ../../target/release, ); MACOSX_DEPLOYMENT_TARGET = 10.14; + OTHER_LDFLAGS = ( + "-sectcreate", + __CGPreLoginApp, + __cgpreloginapp, + /dev/null, + ); PRODUCT_BUNDLE_IDENTIFIER = com.carriez.rustdesk; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; SWIFT_VERSION = 5.0; }; diff --git a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 898fbe4e7..9c428a004 100644 --- a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 156e0c79b..3498decd3 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -3,8 +3,22 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { + var launched = false; override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { dummy_method_to_enforce_bundling() - return true + // https://github.com/leanflutter/window_manager/issues/214 + return false } + + override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { + if (launched) { + handle_applicationShouldOpenUntitledFile(); + } + return true + } + + override func applicationDidFinishLaunching(_ aNotification: Notification) { + launched = true; + NSApplication.shared.activate(ignoringOtherApps: true); + } } diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 7b4d860d6..682280dc5 100644 --- a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images": [ - { - "filename": "app_icon_16.png", - "idiom": "mac", - "scale": "1x", - "size": "16x16" + "info": { + "author": "icons_launcher", + "version": 1 }, - { - "filename": "app_icon_32.png", - "idiom": "mac", - "scale": "2x", - "size": "16x16" - }, - { - "filename": "app_icon_32.png", - "idiom": "mac", - "scale": "1x", - "size": "32x32" - }, - { - "filename": "app_icon_64.png", - "idiom": "mac", - "scale": "2x", - "size": "32x32" - }, - { - "filename": "app_icon_128.png", - "idiom": "mac", - "scale": "1x", - "size": "128x128" - }, - { - "filename": "app_icon_256.png", - "idiom": "mac", - "scale": "2x", - "size": "128x128" - }, - { - "filename": "app_icon_256.png", - "idiom": "mac", - "scale": "1x", - "size": "256x256" - }, - { - "filename": "app_icon_512.png", - "idiom": "mac", - "scale": "2x", - "size": "256x256" - }, - { - "filename": "app_icon_512.png", - "idiom": "mac", - "scale": "1x", - "size": "512x512" - }, - { - "filename": "app_icon_1024.png", - "idiom": "mac", - "scale": "2x", - "size": "512x512" - } - ], - "info": { - "author": "icons_launcher", - "version": 1 - } + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] } \ No newline at end of file diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index f51492c53..fc39cb2ff 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 97555e4a2..d08d9bb62 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 9ea846547..3bd2b7ede 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index c695fd3a3..88f2eee49 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index a99d7db5a..18151e82b 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 5ad5a6bcd..f8d7befb3 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 645580310..d2bd35cd1 100644 Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/flutter/macos/Runner/Configs/AppInfo.xcconfig b/flutter/macos/Runner/Configs/AppInfo.xcconfig index 389ae0a70..66dbee50c 100644 --- a/flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -5,7 +5,7 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = rustdesk +PRODUCT_NAME = RustDesk // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb diff --git a/flutter/macos/Runner/DebugProfile.entitlements b/flutter/macos/Runner/DebugProfile.entitlements index 9f56413f3..b52c39df4 100644 --- a/flutter/macos/Runner/DebugProfile.entitlements +++ b/flutter/macos/Runner/DebugProfile.entitlements @@ -6,6 +6,8 @@ com.apple.security.cs.allow-jit + com.apple.security.device.audio-input + com.apple.security.network.server diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist index d1077e0e4..96616e8c4 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -23,8 +23,10 @@ CFBundleTypeRole Editor - CFBundleURLName + CFBundleURLIconFile + CFBundleURLName + com.carriez.rustdesk CFBundleURLSchemes rustdesk @@ -35,13 +37,15 @@ $(FLUTTER_BUILD_NUMBER) LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) + LSUIElement + 1 NSHumanReadableCopyright $(PRODUCT_COPYRIGHT) NSMainNibFile MainMenu + NSMicrophoneUsageDescription + Record the sound from microphone for the purpose of the remote desktop. NSPrincipalClass - NSApplication - LSUIElement - 1 + NSApplication diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 1d16763ee..e9043da71 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -1,4 +1,5 @@ import Cocoa +import AVFoundation import FlutterMacOS import desktop_multi_window // import bitsdojo_window_macos @@ -7,7 +8,7 @@ import desktop_drop import device_info_plus_macos import flutter_custom_cursor import package_info_plus_macos -import path_provider_macos +import path_provider_foundation import screen_retriever import sqflite // import tray_manager @@ -16,6 +17,7 @@ import url_launcher_macos import wakelock_macos import window_manager import window_size +import texture_rgba_renderer class MainFlutterWindow: NSWindow { override func awakeFromNib() { @@ -27,12 +29,16 @@ class MainFlutterWindow: NSWindow { let windowFrame = self.frame self.contentViewController = flutterViewController self.setFrame(windowFrame, display: true) + // register self method handler + let registrar = flutterViewController.registrar(forPlugin: "RustDeskPlugin") + setMethodHandler(registrar: registrar) RegisterGeneratedPlugins(registry: flutterViewController) FlutterMultiWindowPlugin.setOnWindowCreatedCallback { controller in // Register the plugin which you want access from other isolate. // DesktopLifecyclePlugin.register(with: controller.registrar(forPlugin: "DesktopLifecyclePlugin")) + self.setMethodHandler(registrar: controller.registrar(forPlugin: "RustDeskPlugin")) DesktopDropPlugin.register(with: controller.registrar(forPlugin: "DesktopDropPlugin")) DeviceInfoPlusMacosPlugin.register(with: controller.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FlutterCustomCursorPlugin.register(with: controller.registrar(forPlugin: "FlutterCustomCursorPlugin")) @@ -44,12 +50,60 @@ class MainFlutterWindow: NSWindow { UrlLauncherPlugin.register(with: controller.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockMacosPlugin")) WindowSizePlugin.register(with: controller.registrar(forPlugin: "WindowSizePlugin")) + TextureRgbaRendererPlugin.register(with: controller.registrar(forPlugin: "TextureRgbaRendererPlugin")) } super.awakeFromNib() } -// override func bitsdojo_window_configure() -> UInt { -// return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP -// } + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { + super.order(place, relativeTo: otherWin) + hiddenWindowAtLaunch() + } + + /// Override window theme. + public func setWindowInterfaceMode(window: NSWindow, themeName: String) { + window.appearance = NSAppearance(named: themeName == "light" ? .aqua : .darkAqua) + } + + public func setMethodHandler(registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "org.rustdesk.rustdesk/macos", binaryMessenger: registrar.messenger) + channel.setMethodCallHandler({ + (call, result) -> Void in + switch call.method { + case "setWindowTheme": + let arg = call.arguments as! [String: Any] + let themeName = arg["themeName"] as? String + guard let window = registrar.view?.window else { + result(nil) + return + } + self.setWindowInterfaceMode(window: window,themeName: themeName ?? "light") + result(nil) + break; + case "terminate": + NSApplication.shared.terminate(self) + result(nil) + case "canRecordAudio": + switch AVCaptureDevice.authorizationStatus(for: .audio) { + case .authorized: + result(1) + break + case .notDetermined: + result(0) + break + default: + result(-1) + break + } + case "requestRecordAudio": + AVCaptureDevice.requestAccess(for: .audio, completionHandler: { granted in + result(granted) + }) + break + default: + result(FlutterMethodNotImplemented) + } + }) + } } diff --git a/flutter/macos/Runner/Release.entitlements b/flutter/macos/Runner/Release.entitlements index 08ba3a3fa..7f588d928 100644 --- a/flutter/macos/Runner/Release.entitlements +++ b/flutter/macos/Runner/Release.entitlements @@ -4,6 +4,10 @@ com.apple.security.app-sandbox + com.apple.security.cs.allow-jit + + com.apple.security.device.audio-input + com.apple.security.network.client diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj index e334f0ac5..6c58fef3d 100644 --- a/flutter/macos/rustdesk.xcodeproj/project.pbxproj +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -84,7 +84,7 @@ "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; ONLY_ACTIVE_ARCH = YES; - PRODUCT_NAME = rustdesk; + PRODUCT_NAME = RustDesk; SDKROOT = macosx; SUPPORTS_MACCATALYST = YES; }; @@ -105,9 +105,15 @@ "CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios; "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; - PRODUCT_NAME = rustdesk; + PRODUCT_NAME = RustDesk; SDKROOT = macosx; SUPPORTS_MACCATALYST = YES; + OTHER_LDFLAGS = ( + "-sectcreate", + __CGPreLoginApp, + __cgpreloginapp, + /dev/null, + ); }; name = Release; }; diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index bcbad530c..6bd19d01a 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,267 +5,328 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201" + url: "https://pub.dev" source: hosted - version: "50.0.0" + version: "52.0.0" after_layout: dependency: transitive description: name: after_layout - url: "https://pub.dartlang.org" + sha256: "95a1cb2ca1464f44f14769329fbf15987d20ab6c88f8fc5d359bd362be625f29" + url: "https://pub.dev" source: hosted version: "1.2.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4 + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.0" animations: dependency: transitive description: name: animations - url: "https://pub.dartlang.org" + sha256: fe8a6bdca435f718bb1dc8a11661b2c22504c6da40ef934cee8327ed77934164 + url: "https://pub.dev" source: hosted version: "2.0.7" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.3.6" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" auto_size_text: dependency: "direct main" description: name: auto_size_text - url: "https://pub.dartlang.org" + sha256: "3f5261cd3fb5f2a9ab4e2fc3fba84fd9fcaac8821f20a1d4e71f557521b22599" + url: "https://pub.dev" source: hosted version: "3.0.0" back_button_interceptor: dependency: "direct main" description: name: back_button_interceptor - url: "https://pub.dartlang.org" + sha256: e47660f2178a4392eb72001f9594d3fdcb5efde93e59d2819d61fda499e781c8 + url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" bot_toast: dependency: "direct main" description: name: bot_toast - url: "https://pub.dartlang.org" + sha256: "19306147033316a7873c5d261b874fca3f341c05e4e1c12be56153ad11187edd" + url: "https://pub.dev" source: hosted version: "4.0.3" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" + source: hosted + version: "2.1.0" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf" + url: "https://pub.dev" source: hosted version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab" + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.1.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted - version: "7.2.4" + version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "169565c8ad06adb760c3645bf71f00bff161b00002cace266cad42c5d22a7725" + url: "https://pub.dev" source: hosted - version: "8.4.1" + version: "8.4.3" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://pub.dartlang.org" + sha256: fd3d0dc1d451f9a252b32d95d3f0c3c487bc41a75eba2e6097cb0b9c71491b15 + url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.3" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://pub.dartlang.org" + sha256: bb2b8403b4ccdc60ef5f25c70dead1f3d32d24b9d6117cfc087f496b178594a7 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://pub.dartlang.org" + sha256: b8eb814ebfcb4dea049680f8c1ffb2df399e4d03bf7a352c775e26fa06e02fa0 + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + url: "https://pub.dev" source: hosted version: "1.2.1" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" + source: hosted + version: "0.3.5" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.4.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" + colorize: + dependency: transitive + description: + name: colorize + sha256: "584746cd6ba1cba0633b6720f494fe6f9601c4170f0666c1579d2aa2a61071ba" + url: "https://pub.dev" + source: hosted + version: "3.0.0" contextmenu: dependency: "direct main" description: name: contextmenu - url: "https://pub.dartlang.org" + sha256: e0c7d60e2fc9f316f5b03f5fe2c0f977d65125345d1a1f77eea02be612e32d0c + url: "https://pub.dev" source: hosted version: "3.0.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.1" cross_file: dependency: transitive description: name: cross_file - url: "https://pub.dartlang.org" + sha256: f71079978789bc2fe78d79227f1f8cfe195b31bbd8db2399b0d15a4b96fb843b + url: "https://pub.dev" source: hosted version: "0.3.3+2" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4" + url: "https://pub.dev" source: hosted version: "2.2.4" dash_chat_2: dependency: "direct main" description: name: dash_chat_2 - url: "https://pub.dartlang.org" + sha256: "7ffdeb023fb2c9e194e2147ef8e967d36e4481493178051ceb36d98c62396ddd" + url: "https://pub.dev" source: hosted version: "0.0.15" debounce_throttle: dependency: "direct main" description: name: debounce_throttle - url: "https://pub.dartlang.org" + sha256: c95cf47afda975fc507794a52040a16756fb2f31ad3027d4e691c41862ff5692 + url: "https://pub.dev" source: hosted version: "2.0.0" desktop_drop: dependency: "direct main" description: name: desktop_drop - url: "https://pub.dartlang.org" + sha256: "0cd056191b701a2b5ba040f2306349e461fafdaa5df4569b2228cdf87b58eced" + url: "https://pub.dev" source: hosted version: "0.3.3" desktop_multi_window: dependency: "direct main" description: path: "." - ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" - resolved-ref: "82f9eab81cb2c7bfb938def7a1b399a6279bbc75" + ref: "6cd00db1d68443aeeb13dc89f6a090a0ad5dbb3e" + resolved-ref: "6cd00db1d68443aeeb13dc89f6a090a0ad5dbb3e" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -273,91 +334,120 @@ packages: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" + sha256: b809c4ed5f7fcdb325ccc70b80ad934677dc4e2aa414bf46859a42bfdfafcbb6 + url: "https://pub.dev" source: hosted version: "4.1.3" device_info_plus_linux: dependency: transitive description: name: device_info_plus_linux - url: "https://pub.dartlang.org" + sha256: "77a8b3c4af06bc46507f89304d9f49dfc64b4ae004b994532ed23b34adeae4b3" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_macos: dependency: transitive description: name: device_info_plus_macos - url: "https://pub.dartlang.org" + sha256: "37961762fbd46d3620c7b69ca606671014db55fc1b7a11e696fd90ed2e8fe03d" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: "83fdba24fcf6846d3b10f10dfdc8b6c6d7ada5f8ed21d62ea2909c2dfa043773" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_web: dependency: transitive description: name: device_info_plus_web - url: "https://pub.dartlang.org" + sha256: "5890f6094df108181c7a29720bc23d0fd6159f17d82787fac093d1fefcaf6325" + url: "https://pub.dev" source: hosted version: "3.0.0" device_info_plus_windows: dependency: transitive description: name: device_info_plus_windows - url: "https://pub.dartlang.org" + sha256: "23a2874af0e23ee6e3a2a0ebcecec3a9da13241f2cb93a93a44c8764df123dd7" + url: "https://pub.dev" source: hosted version: "4.1.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://pub.dartlang.org" + sha256: f3b291b335b7f7c7b721a6f42aeb6209fdfb055ea87980bff68c551b250795ea + url: "https://pub.dev" source: hosted version: "0.0.2" + dropdown_button2: + dependency: "direct main" + description: + name: dropdown_button2 + sha256: "4458d81bfd24207f3d58f66f78097064e02f810f94cf1bc80bf20fe7685ebc80" + url: "https://pub.dev" + source: hosted + version: "2.0.0" event_bus: dependency: transitive description: name: event_bus - url: "https://pub.dartlang.org" + sha256: "44baa799834f4c803921873e7446a2add0f3efa45e101a054b1f0ab9b95f8edc" + url: "https://pub.dev" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + sha256: "2095c626fbbefe70d5a4afc9b1137172a68ee2c276e51c3c1283394485bea8f4" + url: "https://pub.dev" source: hosted version: "1.0.3" ffi: dependency: "direct main" description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" + ffigen: + dependency: "direct dev" + description: + name: ffigen + sha256: "42bbfddebacef09c9a4eb2d9ef4049fa6a39edb8622b72ca69200cb6f1e3a6c0" + url: "https://pub.dev" + source: hosted + version: "7.2.4" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.dartlang.org" + sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 + url: "https://pub.dev" source: hosted - version: "5.2.3" + version: "5.2.5" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "04be3e934c52e082558cc9ee21f42f5c1cd7a1262f4c63cd0357c08d5bba81ec" + url: "https://pub.dev" source: hosted version: "1.0.1" flutter: @@ -369,42 +459,105 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://pub.dartlang.org" + sha256: "05001537bd3fac7644fa6558b09ec8c0a3f2eba78c0765f88912882b1331a5c6" + url: "https://pub.dev" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://pub.dartlang.org" + sha256: "1531680034def621878562ad763079933dabe9f9f5d5add5a094190edc33259b" + url: "https://pub.dev" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://pub.dartlang.org" + sha256: "32cd900555219333326a2d0653aaaf8671264c29befa65bbd9856d204a4c9fb3" + url: "https://pub.dev" source: hosted version: "3.3.0" flutter_custom_cursor: dependency: "direct main" description: name: flutter_custom_cursor - url: "https://pub.dartlang.org" + sha256: "3850a32ac6de351ccc5e4286b6d94ff70c10abecd44479ea6c5aaea17264285d" + url: "https://pub.dev" source: hosted - version: "0.0.2" + version: "0.0.4" flutter_improved_scrolling: dependency: "direct main" description: - name: flutter_improved_scrolling - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: "62f09545149f320616467c306c8c5f71714a18e6" + resolved-ref: "62f09545149f320616467c306c8c5f71714a18e6" + url: "https://github.com/Kingtous/flutter_improved_scrolling" + source: git version: "0.0.3" + flutter_keyboard_visibility: + dependency: "direct main" + description: + name: flutter_keyboard_visibility + sha256: "86b71bbaffa38e885f5c21b1182408b9be6951fd125432cf6652c636254cef2d" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + url: "https://pub.dev" + source: hosted + version: "0.11.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted version: "2.0.1" flutter_localizations: @@ -416,32 +569,34 @@ packages: dependency: transitive description: name: flutter_parsed_text - url: "https://pub.dartlang.org" + sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2" + url: "https://pub.dev" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" + url: "https://pub.dev" source: hosted version: "2.0.7" flutter_rust_bridge: dependency: "direct main" description: - path: frb_dart - ref: master - resolved-ref: e5adce55eea0b74d3680e66a2c5252edf17b07e1 - url: "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" - source: git - version: "1.32.0" + name: flutter_rust_bridge + sha256: "5aea0f3980dcd314f1890ef0d2392263817899cc15e543734b5d4dbe66b761eb" + url: "https://pub.dev" + source: hosted + version: "1.62.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: "6ff9fa12892ae074092de2fa6a9938fb21dbabfdaa2ff57dc697ff912fc8d4b2" + url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.6" flutter_web_plugins: dependency: transitive description: flutter @@ -451,401 +606,490 @@ packages: dependency: "direct dev" description: name: freezed - url: "https://pub.dartlang.org" + sha256: e819441678f1679b719008ff2ff0ef045d66eed9f9ec81166ca0d9b02a187454 + url: "https://pub.dev" source: hosted version: "2.3.2" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - url: "https://pub.dartlang.org" + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + url: "https://pub.dev" source: hosted version: "2.2.0" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.dev" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" icons_launcher: dependency: "direct dev" description: name: icons_launcher - url: "https://pub.dartlang.org" + sha256: c8e3ae1263822feafaec8a3c666ec84c2143470e1612f5481f1c875024c5f37e + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.6" image: dependency: "direct main" description: name: image - url: "https://pub.dartlang.org" + sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.3.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://pub.dartlang.org" + sha256: f98d76672d309c8b7030c323b3394669e122d52b307d2bbd8d06bd70f5b2aabe + url: "https://pub.dev" source: hosted - version: "0.8.6" + version: "0.8.6+1" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://pub.dartlang.org" + sha256: b1cbfec0f5aef427a18eb573f5445af8c9c568626bf3388553e40c263d3f7368 + url: "https://pub.dev" source: hosted - version: "0.8.5+3" + version: "0.8.5+5" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://pub.dartlang.org" + sha256: "7d319fb74955ca46d9bf7011497860e3923bb67feebcf068f489311065863899" + url: "https://pub.dev" source: hosted version: "2.1.10" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://pub.dartlang.org" + sha256: "39c013200046d14c58b71dc4fa3d00e425fc9c699d589136cd3ca018727c0493" + url: "https://pub.dev" source: hosted - version: "0.8.6+1" + version: "0.8.6+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://pub.dartlang.org" + sha256: "7cef2f28f4f2fef99180f636c3d446b4ccbafd6ba0fad2adc9a80c4040f656b8" + url: "https://pub.dev" source: hosted version: "2.6.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + url: "https://pub.dev" source: hosted version: "0.17.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.5" json_annotation: dependency: transitive description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.8.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: c94db23593b89766cda57aab9ac311e3616cf87c6fa4e9749df032f66f30dcb8 + url: "https://pub.dev" source: hosted - version: "0.12.12" + version: "0.12.14" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.5" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + url: "https://pub.dev" source: hosted version: "1.8.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://pub.dartlang.org" + sha256: "107f3ed1330006a3bea63615e81cf637433f5135a52466c7caa0e7152bca9143" + url: "https://pub.dev" source: hosted version: "1.0.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" + sha256: f62d7253edc197fe3c88d7c2ddab82d68f555e778d55390ccc3537eca8e8d637 + url: "https://pub.dev" source: hosted version: "1.4.3+1" package_info_plus_linux: dependency: transitive description: name: package_info_plus_linux - url: "https://pub.dartlang.org" + sha256: "04b575f44233d30edbb80a94e57cad9107aada334fc02aabb42b6becd13c43fc" + url: "https://pub.dev" source: hosted version: "1.0.5" package_info_plus_macos: dependency: transitive description: name: package_info_plus_macos - url: "https://pub.dartlang.org" + sha256: a2ad8b4acf4cd479d4a0afa5a74ea3f5b1c7563b77e52cc32b3ee6956d5482a6 + url: "https://pub.dev" source: hosted version: "1.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: f7a0c8f1e7e981bc65f8b64137a53fd3c195b18d429fba960babc59a5a1c7ae8 + url: "https://pub.dev" source: hosted version: "1.0.2" package_info_plus_web: dependency: transitive description: name: package_info_plus_web - url: "https://pub.dartlang.org" + sha256: f0829327eb534789e0a16ccac8936a80beed4e2401c4d3a74f3f39094a822d3b + url: "https://pub.dev" source: hosted version: "1.0.6" package_info_plus_windows: dependency: transitive description: name: package_info_plus_windows - url: "https://pub.dartlang.org" + sha256: "79524f11c42dd9078b96d797b3cf79c0a2883a50c4920dc43da8562c115089bc" + url: "https://pub.dev" source: hosted version: "2.1.0" + password_strength: + dependency: "direct main" + description: + name: password_strength + sha256: "0e51e3d864e37873a1347e658147f88b66e141ee36c58e19828dc5637961e1ce" + url: "https://pub.dev" + source: hosted + version: "0.2.0" path: dependency: "direct main" description: name: path - url: "https://pub.dartlang.org" + sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + url: "https://pub.dev" source: hosted version: "1.8.2" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.dartlang.org" + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: dcea5feb97d8abf90cab9e9030b497fb7c3cbf26b7a1fe9e3ef7dcb0a1ddec95 + url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.0.12" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: a776c088d671b27f6e3aa8881d64b87b3e80201c64e8869b811325de7a76c15e + url: "https://pub.dev" source: hosted - version: "2.0.20" - path_provider_ios: + version: "2.0.22" + path_provider_foundation: dependency: transitive description: - name: path_provider_ios - url: "https://pub.dartlang.org" + name: path_provider_foundation + sha256: "62a68e7e1c6c459f9289859e2fae58290c981ce21d1697faf54910fe1faa4c74" + url: "https://pub.dev" source: hosted - version: "2.0.11" + version: "2.1.1" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: ab0987bf95bc591da42dffb38c77398fc43309f0b9b894dcc5d6f40c4b26c379 + url: "https://pub.dev" source: hosted version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: f0abc8ebd7253741f05488b4813d936b4d07c6bae3e86148a09e342ee4b08e76 + url: "https://pub.dev" source: hosted version: "2.0.5" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c + url: "https://pub.dev" source: hosted version: "2.1.3" pedantic: dependency: transitive description: name: pedantic - url: "https://pub.dartlang.org" + sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602" + url: "https://pub.dev" source: hosted version: "1.11.1" + percent_indicator: + dependency: "direct main" + description: + name: percent_indicator + sha256: cec41f67181fbd5322aa68b355621d1a4eea827426b8eeb613f6cbe195ff7b4a + url: "https://pub.dev" + source: hosted + version: "4.2.2" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a + url: "https://pub.dev" source: hosted version: "2.1.3" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + url: "https://pub.dev" + source: hosted + version: "3.6.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.0.5" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: "75f6614d6dde2dc68948dffbaa4fe5dae32cd700eb9fb763fe11dfb45a3c4d0a" + url: "https://pub.dev" source: hosted version: "1.2.1" + puppeteer: + dependency: transitive + description: + name: puppeteer + sha256: "4e235aaf9a338a45c9eb1ee38956e0ba369867bf144d7a27fdaf245409b2b87b" + url: "https://pub.dev" + source: hosted + version: "2.21.0" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://pub.dartlang.org" + sha256: f23b68d893505a424f0bd2e324ebea71ed88465d572d26bb8d2e78a4749591fd + url: "https://pub.dev" source: hosted version: "1.0.1" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" rxdart: dependency: transitive description: name: rxdart - url: "https://pub.dartlang.org" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" source: hosted - version: "0.27.5" + version: "0.27.7" screen_retriever: dependency: transitive description: @@ -859,35 +1103,48 @@ packages: dependency: "direct main" description: name: scroll_pos - url: "https://pub.dartlang.org" + sha256: cfca311b6b8d51538ff90e206fbe6ce3b36e7125ea6da4a40eb626c7f9f083b1 + url: "https://pub.dev" source: hosted version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://pub.dartlang.org" + sha256: d9838037cb554b24b4218b2d07666fbada3478882edefae375ee892b6c820ef3 + url: "https://pub.dev" source: hosted version: "2.0.2" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + url: "https://pub.dev" + source: hosted + version: "1.1.1" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" simple_observable: dependency: transitive description: name: simple_observable - url: "https://pub.dartlang.org" + sha256: b392795c48f8b5f301b4c8f73e15f56e38fe70f42278c649d8325e859a783301 + url: "https://pub.dev" source: hosted version: "2.0.0" sky_engine: @@ -899,319 +1156,371 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d" + url: "https://pub.dev" source: hosted - version: "1.2.5" + version: "1.2.6" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted version: "1.9.1" sqflite: dependency: transitive description: name: sqflite - url: "https://pub.dartlang.org" + sha256: "78324387dc81df14f78df06019175a86a2ee0437624166c382e145d0a7fd9a4f" + url: "https://pub.dev" source: hosted - version: "2.0.3+1" + version: "2.2.4+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://pub.dartlang.org" + sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f + url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2+2" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + url: "https://pub.dev" source: hosted - version: "3.0.0+3" + version: "3.0.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted version: "1.2.1" + texture_rgba_renderer: + dependency: "direct main" + description: + name: texture_rgba_renderer + sha256: "52bc9f217b7b07a760ee837d5a17329ad1f78ae8ed1e3fa612c6f1bed3c77f79" + url: "https://pub.dev" + source: hosted + version: "0.0.13" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://pub.dartlang.org" + sha256: "3814548f25ee11f88d3b1905e2e7c8e47e4a406752f553ed287f6d86a2dcf91d" + url: "https://pub.dev" source: hosted version: "1.4.0" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + sha256: "0ea99cd2f9352b2586583ab2ce6489d1f95a5f6de6fb9492faaf97ae2060f0aa" + url: "https://pub.dev" source: hosted version: "2.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" uni_links: dependency: "direct main" description: name: uni_links - url: "https://pub.dartlang.org" + sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" + url: "https://pub.dev" source: hosted version: "0.5.1" uni_links_desktop: dependency: "direct main" description: name: uni_links_desktop - url: "https://pub.dartlang.org" + sha256: "205484c01890259b56d9271bcf299adf9889e881616c976f13061e29e94bb9f0" + url: "https://pub.dev" source: hosted version: "0.1.4" uni_links_platform_interface: dependency: transitive description: name: uni_links_platform_interface - url: "https://pub.dartlang.org" + sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" + url: "https://pub.dev" source: hosted version: "1.0.0" uni_links_web: dependency: transitive description: name: uni_links_web - url: "https://pub.dartlang.org" + sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" + url: "https://pub.dev" source: hosted version: "0.1.0" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "79f78ddad839ee3aae3ec7c01eb4575faf0d5c860f8e5223bc9f9c17f7f03cef" + url: "https://pub.dev" source: hosted version: "2.0.4" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "698fa0b4392effdc73e9e184403b627362eb5fbf904483ac9defbb1c2191d809" + url: "https://pub.dev" source: hosted - version: "6.1.6" + version: "6.1.8" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "3e2f6dfd2c7d9cd123296cab8ef66cfc2c1a13f5845f42c7a0f365690a8a7dd1" + url: "https://pub.dev" source: hosted - version: "6.0.19" + version: "6.0.23" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: bb328b24d3bccc20bdf1024a0990ac4f869d57663660de9c936fb8c043edefe3 + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.0.18" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "318c42cba924e18180c029be69caf0a1a710191b9ec49bb42b5998fdcccee3cc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "41988b55570df53b3dd2a7fc90c76756a963de6a8c5f8e113330cb35992e2094" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "4eae912628763eb48fc214522e58e942fd16ce195407dbf45638239523c759a6" + url: "https://pub.dev" source: hosted version: "2.1.1" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "44d79408ce9f07052095ef1f9a693c258d6373dc3944249374e30eff7219ccb0" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.14" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: b6217370f8eb1fd85c8890c539f5a639a01ab209a36db82c921ebeacefc7a615 + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" uuid: dependency: transitive description: name: uuid - url: "https://pub.dartlang.org" + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" video_player: dependency: transitive description: name: video_player - url: "https://pub.dartlang.org" + sha256: "59f7f31c919c59cbedd37c617317045f5f650dc0eeb568b0b0de9a36472bdb28" + url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.5.1" video_player_android: dependency: transitive description: name: video_player_android - url: "https://pub.dartlang.org" + sha256: "984388511230bac63feb53b2911a70e829fe0976b6b2213f5c579c4e0a882db3" + url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.3.10" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://pub.dartlang.org" + sha256: d9f7a46d6a77680adb03ec05a381025d6e890ebe636637c6c3014cc3926b97e9 + url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.8" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://pub.dartlang.org" + sha256: "42bb75de5e9b79e1f20f1d95f688fac0f95beac4d89c6eb2cd421724d4432dae" + url: "https://pub.dev" source: hosted - version: "5.1.4" + version: "6.0.1" video_player_web: dependency: transitive description: name: video_player_web - url: "https://pub.dartlang.org" + sha256: b649b07b8f8f553bee4a97a0a53d0fe78a70b115eafaf0105b612b32b05ddb99 + url: "https://pub.dev" source: hosted - version: "2.0.12" + version: "2.0.13" visibility_detector: dependency: "direct main" description: name: visibility_detector - url: "https://pub.dartlang.org" + sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + url: "https://pub.dev" source: hosted version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://pub.dartlang.org" + sha256: "769ecf42eb2d07128407b50cb93d7c10bd2ee48f0276ef0119db1d25cc2f87db" + url: "https://pub.dev" source: hosted version: "0.6.2" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://pub.dartlang.org" + sha256: "047c6be2f88cb6b76d02553bca5a3a3b95323b15d30867eca53a19a0a319d4cd" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://pub.dartlang.org" + sha256: "1f4aeb81fb592b863da83d2d0f7b8196067451e4df91046c26b54a403f9de621" + url: "https://pub.dev" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://pub.dartlang.org" + sha256: "1b256b811ee3f0834888efddfe03da8d18d0819317f20f6193e2922b41a501b5" + url: "https://pub.dev" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://pub.dartlang.org" + sha256: "857f77b3fe6ae82dd045455baa626bc4b93cb9bb6c86bf3f27c182167c3a5567" + url: "https://pub.dev" source: hosted version: "0.2.1" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" win32: dependency: "direct main" description: name: win32 - url: "https://pub.dartlang.org" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.3" win32_registry: dependency: transitive description: name: win32_registry - url: "https://pub.dartlang.org" + sha256: "66e78552f17501aced68fe77425b13156998f1bd3d58f1cd8cd0af2dbe4520e3" + url: "https://pub.dev" source: hosted version: "1.0.2" window_manager: dependency: "direct main" description: path: "." - ref: "32b24c66151b72bba033ef8b954486aa9351d97b" - resolved-ref: "32b24c66151b72bba033ef8b954486aa9351d97b" + ref: "94ff0eeb7e20e93f50022b2090f0b6ffb725c7df" + resolved-ref: "94ff0eeb7e20e93f50022b2090f0b6ffb725c7df" url: "https://github.com/Kingtous/rustdesk_window_manager" source: git - version: "0.2.7" + version: "0.3.1" window_size: dependency: "direct main" description: @@ -1225,30 +1534,42 @@ packages: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" source: hosted - version: "0.2.0+2" + version: "0.2.0+3" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "4240d1b19841b8af5786121e4e357735cc2a8ffb19176bff5769d73c34e2a8a5" + url: "https://pub.dev" + source: hosted + version: "2.0.3" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://pub.dartlang.org" + sha256: "1913c33844c68b62573741134ef5f987f1e15e331c95ac7dc327afbb9896e9ec" + url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" sdks: - dart: ">=2.17.1 <3.0.0" - flutter: ">=3.0.0" + dart: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index a87727f7b..abe5c0f38 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -19,154 +19,154 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 1.2.0 environment: - sdk: ">=2.17.0" + sdk: ">=2.17.0" dependencies: - flutter: - sdk: flutter - flutter_localizations: - sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.3 - ffi: ^2.0.1 - path_provider: ^2.0.2 - external_path: ^1.0.1 - provider: ^6.0.3 - tuple: ^2.0.0 - wakelock: ^0.6.2 - device_info_plus: ^4.1.2 - #firebase_analytics: ^9.1.5 - package_info_plus: ^1.4.2 - url_launcher: ^6.0.9 - toggle_switch: ^1.4.0 - dash_chat_2: ^0.0.14 - draggable_float_widget: ^0.0.2 - settings_ui: ^2.0.2 - flutter_breadcrumb: ^1.0.1 - http: ^0.13.4 - qr_code_scanner: ^1.0.0 - zxing2: ^0.1.0 - image_picker: ^0.8.5 - image: ^3.1.3 - back_button_interceptor: ^6.0.1 - flutter_rust_bridge: - git: - url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge - ref: master - path: frb_dart - window_manager: - git: - url: https://github.com/Kingtous/rustdesk_window_manager - ref: 32b24c66151b72bba033ef8b954486aa9351d97b - desktop_multi_window: - git: - url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: 82f9eab81cb2c7bfb938def7a1b399a6279bbc75 - freezed_annotation: ^2.0.3 - flutter_custom_cursor: ^0.0.2 - window_size: - git: - url: https://github.com/google/flutter-desktop-embedding.git - path: plugins/window_size - ref: a738913c8ce2c9f47515382d40827e794a334274 - get: ^4.6.5 - visibility_detector: ^0.3.3 - contextmenu: ^3.0.0 - desktop_drop: ^0.3.3 - scroll_pos: ^0.3.0 - debounce_throttle: ^2.0.0 - file_picker: ^5.1.0 - flutter_svg: ^1.1.5 - flutter_improved_scrolling: ^0.0.3 - # currently, we use flutter 3.0.5 for windows build, latest for other builds. - # - # for flutter 3.0.5, please use official version(just comment code below). - # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). - # git: - # url: https://github.com/Kingtous/flutter_improved_scrolling - # ref: 62f09545149f320616467c306c8c5f71714a18e6 - uni_links: ^0.5.1 - uni_links_desktop: ^0.1.4 - path: ^1.8.1 - auto_size_text: ^3.0.0 - bot_toast: ^4.0.3 - win32: any + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.3 + ffi: ^2.0.1 + path_provider: ^2.0.12 + external_path: ^1.0.1 + provider: ^6.0.3 + tuple: ^2.0.0 + wakelock: ^0.6.2 + device_info_plus: ^4.1.2 + #firebase_analytics: ^9.1.5 + package_info_plus: ^1.4.2 + url_launcher: ^6.0.9 + toggle_switch: ^1.4.0 + dash_chat_2: ^0.0.14 + draggable_float_widget: ^0.0.2 + settings_ui: ^2.0.2 + flutter_breadcrumb: ^1.0.1 + http: ^0.13.4 + qr_code_scanner: ^1.0.0 + zxing2: ^0.1.0 + image_picker: ^0.8.5 + image: ^3.1.3 + back_button_interceptor: ^6.0.1 + flutter_rust_bridge: ^1.61.1 + window_manager: + git: + url: https://github.com/Kingtous/rustdesk_window_manager + ref: 94ff0eeb7e20e93f50022b2090f0b6ffb725c7df + desktop_multi_window: + git: + url: https://github.com/Kingtous/rustdesk_desktop_multi_window + ref: 6cd00db1d68443aeeb13dc89f6a090a0ad5dbb3e + freezed_annotation: ^2.0.3 + flutter_custom_cursor: ^0.0.4 + window_size: + git: + url: https://github.com/google/flutter-desktop-embedding.git + path: plugins/window_size + ref: a738913c8ce2c9f47515382d40827e794a334274 + get: ^4.6.5 + visibility_detector: ^0.3.3 + contextmenu: ^3.0.0 + desktop_drop: ^0.3.3 + scroll_pos: ^0.3.0 + debounce_throttle: ^2.0.0 + file_picker: ^5.1.0 + flutter_svg: ^1.1.5 + flutter_improved_scrolling: + # currently, we use flutter 3.7.0+. + # + # for flutter 3.0.5, please use official version(just comment code below). + # if build rustdesk by flutter >=3.3, please use our custom pub below (uncomment code below). + git: + url: https://github.com/Kingtous/flutter_improved_scrolling + ref: 62f09545149f320616467c306c8c5f71714a18e6 + uni_links: ^0.5.1 + uni_links_desktop: ^0.1.4 + path: ^1.8.1 + auto_size_text: ^3.0.0 + bot_toast: ^4.0.3 + win32: any + password_strength: ^0.2.0 + flutter_launcher_icons: ^0.11.0 + flutter_keyboard_visibility: ^5.4.0 + texture_rgba_renderer: ^0.0.14 + percent_indicator: ^4.2.2 + dropdown_button2: ^2.0.0 dev_dependencies: - icons_launcher: ^2.0.4 - #flutter_test: - #sdk: flutter - build_runner: ^2.1.11 - freezed: ^2.0.3 - flutter_lints: ^2.0.0 + icons_launcher: ^2.0.4 + #flutter_test: + #sdk: flutter + build_runner: ^2.1.11 + freezed: ^2.0.3 + flutter_lints: ^2.0.0 + ffigen: ^7.2.4 -# rerun: flutter pub run flutter_launcher_icons:main -icons_launcher: - image_path: "../res/icon.png" - platforms: - android: - enable: true - ios: - enable: true - windows: - enable: true - macos: - enable: true - linux: - enable: true +# rerun: flutter pub run flutter_launcher_icons +flutter_icons: + image_path: "../res/icon.png" + remove_alpha_ios: true + android: true + ios: true + windows: + generate: true + macos: + image_path: "../res/mac-icon.png" + generate: true + linux: true + web: + generate: true # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true - # To add assets to your application, add an assets section, like this: - assets: - - assets/ + # To add assets to your application, add an assets section, like this: + assets: + - assets/ - fonts: - - family: GestureIcons - fonts: - - asset: assets/gestures.ttf - - family: Tabbar - fonts: - - asset: assets/tabbar.ttf - - family: PeerSearchbar - fonts: - - asset: assets/peer_searchbar.ttf + fonts: + - family: GestureIcons + fonts: + - asset: assets/gestures.ttf + - family: Tabbar + fonts: + - asset: assets/tabbar.ttf + - family: PeerSearchbar + fonts: + - asset: assets/peer_searchbar.ttf - + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/flutter/run.sh b/flutter/run.sh index f1066306a..78fd1e80c 100755 --- a/flutter/run.sh +++ b/flutter/run.sh @@ -4,5 +4,5 @@ dart pub global activate ffigen --version 5.0.1 flutter pub get # call `flutter clean` if cargo build fails # export LLVM_HOME=/Library/Developer/CommandLineTools/usr/ -cargo build --features flutter +cargo build --features "flutter,flutter_texture_render" flutter run $@ diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 592a28fcf..2c037c7b0 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -16,7 +16,7 @@ final testClients = [ Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) ]; -/// -t lib/cm_main.dart to test cm +/// flutter run -d {platform} -t lib/cm_test.dart to test cm void main(List args) async { isTest = true; WidgetsFlutterBinding.ensureInitialized(); diff --git a/flutter/web/icons/Icon-192.png b/flutter/web/icons/Icon-192.png index 5d4566850..e8c754f4a 100644 Binary files a/flutter/web/icons/Icon-192.png and b/flutter/web/icons/Icon-192.png differ diff --git a/flutter/web/icons/Icon-512.png b/flutter/web/icons/Icon-512.png index 2b1abc3f2..2f8929e26 100644 Binary files a/flutter/web/icons/Icon-512.png and b/flutter/web/icons/Icon-512.png differ diff --git a/flutter/web/icons/Icon-maskable-192.png b/flutter/web/icons/Icon-maskable-192.png index 30147e96e..e8c754f4a 100644 Binary files a/flutter/web/icons/Icon-maskable-192.png and b/flutter/web/icons/Icon-maskable-192.png differ diff --git a/flutter/web/icons/Icon-maskable-512.png b/flutter/web/icons/Icon-maskable-512.png index e84ca5bc7..2f8929e26 100644 Binary files a/flutter/web/icons/Icon-maskable-512.png and b/flutter/web/icons/Icon-maskable-512.png differ diff --git a/flutter/web/js/src/connection.ts b/flutter/web/js/src/connection.ts index 2846d9078..b0c479c90 100644 --- a/flutter/web/js/src/connection.ts +++ b/flutter/web/js/src/connection.ts @@ -82,10 +82,10 @@ export default class Connection { this._ws = ws; this._id = id; console.log( - new Date() + ": Conntecting to rendezvoous server: " + uri + ", for " + id + new Date() + ": Connecting to rendezvous server: " + uri + ", for " + id ); await ws.open(); - console.log(new Date() + ": Connected to rendezvoous server"); + console.log(new Date() + ": Connected to rendezvous server"); const conn_type = rendezvous.ConnType.DEFAULT_CONN; const nat_type = rendezvous.NatType.SYMMETRIC; const punch_hole_request = rendezvous.PunchHoleRequest.fromPartial({ diff --git a/flutter/web/manifest.json b/flutter/web/manifest.json index 9723be242..77d831d4f 100644 --- a/flutter/web/manifest.json +++ b/flutter/web/manifest.json @@ -32,4 +32,4 @@ "purpose": "maskable" } ] -} +} \ No newline at end of file diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt index 5cf603360..926941b84 100644 --- a/flutter/windows/CMakeLists.txt +++ b/flutter/windows/CMakeLists.txt @@ -6,7 +6,7 @@ project(rustdesk LANGUAGES CXX) # the on-disk name of your application. set(BINARY_NAME "rustdesk") -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# Explicitly opt into modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(SET CMP0063 NEW) diff --git a/flutter/windows/flutter/CMakeLists.txt b/flutter/windows/flutter/CMakeLists.txt index 930d2071a..b5655b2fa 100644 --- a/flutter/windows/flutter/CMakeLists.txt +++ b/flutter/windows/flutter/CMakeLists.txt @@ -79,7 +79,7 @@ target_include_directories(flutter_wrapper_app PUBLIC add_dependencies(flutter_wrapper_app flutter_assemble) # === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, +# _phony_ is a nonexistent file to force this command to run every time, # since currently there's no way to get a full input/output list from the # flutter tool. set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") diff --git a/flutter/windows/runner/flutter_window.cpp b/flutter/windows/runner/flutter_window.cpp index b43b9095e..2f1f36f73 100644 --- a/flutter/windows/runner/flutter_window.cpp +++ b/flutter/windows/runner/flutter_window.cpp @@ -2,6 +2,9 @@ #include +#include +#include + #include "flutter/generated_plugin_registrant.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) @@ -25,6 +28,13 @@ bool FlutterWindow::OnCreate() { return false; } RegisterPlugins(flutter_controller_->engine()); + DesktopMultiWindowSetWindowCreatedCallback([](void *controller) { + auto *flutter_view_controller = + reinterpret_cast(controller); + auto *registry = flutter_view_controller->engine(); + TextureRgbaRendererPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TextureRgbaRendererPlugin")); + }); SetChildContent(flutter_controller_->view()->GetNativeWindow()); return true; } diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp index 9b75aa086..f1ea6e579 100644 --- a/flutter/windows/runner/main.cpp +++ b/flutter/windows/runner/main.cpp @@ -1,20 +1,21 @@ #include #include -#include #include +#include +#include + +#include #include #include "flutter_window.h" #include "utils.h" -// #include - -#include typedef char** (*FUNC_RUSTDESK_CORE_MAIN)(int*); typedef void (*FUNC_RUSTDESK_FREE_ARGS)( char**, int); const char* uniLinksPrefix = "rustdesk://"; +/// Note: `--server`, `--service` are already handled in [core_main.rs]. +const std::vector parameters_white_list = {"--install", "--cm"}; -// auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { @@ -40,6 +41,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, } std::vector command_line_arguments = GetCommandLineArguments(); + // Remove possible trailing whitespace from command line arguments + for (auto& argument : command_line_arguments) { + argument.erase(argument.find_last_not_of(" \n\r\t")); + } int args_len = 0; char** c_args = rustdesk_core_main(&args_len); @@ -51,19 +56,33 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, std::vector rust_args(c_args, c_args + args_len); free_c_args(c_args, args_len); - // uni links dispatch - // only do uni links when dispatch a rustdesk links - auto prefix = std::string(uniLinksPrefix); - if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, prefix.size(), prefix.c_str()) == 0) { - HWND hwnd = ::FindWindow(_T("FLUTTER_RUNNER_WIN32_WINDOW"), _T("RustDesk")); - if (hwnd != NULL) { - DispatchToUniLinksDesktop(hwnd); - - ::ShowWindow(hwnd, SW_NORMAL); - ::SetForegroundWindow(hwnd); + // Uri links dispatch + HWND hwnd = ::FindWindow(_T("FLUTTER_RUNNER_WIN32_WINDOW"), _T("RustDesk")); + if (hwnd != NULL) { + // Allow multiple flutter instances when being executed by parameters + // contained in whitelists. + bool allow_multiple_instances = false; + for (auto& whitelist_param : parameters_white_list) { + allow_multiple_instances = + allow_multiple_instances || + std::find(command_line_arguments.begin(), + command_line_arguments.end(), + whitelist_param) != command_line_arguments.end(); + } + if (!allow_multiple_instances) { + if (!command_line_arguments.empty()) { + // Dispatch command line arguments + DispatchToUniLinksDesktop(hwnd); + } else { + // Not called with arguments, or just open the app shortcut on desktop. + // So we just show the main window instead. + ::ShowWindow(hwnd, SW_NORMAL); + ::SetForegroundWindow(hwnd); + } return EXIT_FAILURE; } } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) @@ -77,10 +96,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, flutter::DartProject project(L"data"); // connection manager hide icon from taskbar - bool showOnTaskBar = true; + bool is_cm_page = false; auto cmParam = std::string("--cm"); if (!command_line_arguments.empty() && command_line_arguments.front().compare(0, cmParam.size(), cmParam.c_str()) == 0) { - showOnTaskBar = false; + is_cm_page = true; } command_line_arguments.insert(command_line_arguments.end(), rust_args.begin(), rust_args.end()); project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); @@ -88,9 +107,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(800, 600); - if (!window.CreateAndShow(L"RustDesk", origin, size, showOnTaskBar)) - { - return EXIT_FAILURE; + if (!window.CreateAndShow( + is_cm_page ? L"RustDesk - Connection Manager" : L"RustDesk", origin, + size, !is_cm_page)) { + return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/flutter/windows/runner/resources/app_icon.ico b/flutter/windows/runner/resources/app_icon.ico index 9b52c497e..bdf42cfdb 100644 Binary files a/flutter/windows/runner/resources/app_icon.ico and b/flutter/windows/runner/resources/app_icon.ico differ diff --git a/flutter/windows/runner/win32_window.cpp b/flutter/windows/runner/win32_window.cpp index 2ff6d686c..c15819df0 100644 --- a/flutter/windows/runner/win32_window.cpp +++ b/flutter/windows/runner/win32_window.cpp @@ -43,7 +43,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); diff --git a/flutter/windows/runner/win32_window.h b/flutter/windows/runner/win32_window.h index 77e52ff01..94a7bcd56 100644 --- a/flutter/windows/runner/win32_window.h +++ b/flutter/windows/runner/win32_window.h @@ -31,7 +31,7 @@ class Win32Window { // Creates and shows a win32 window with |title| and position and size using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function + // consistent size to will treat the width height passed into this function // as logical pixels and scale to appropriate for the default monitor. Returns // true if the window was created successfully. bool CreateAndShow(const std::wstring& title, @@ -77,7 +77,7 @@ class Win32Window { // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index b992e39e3..e7a533d69 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -21,7 +21,7 @@ pub use context_send::*; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] -pub enum ClipbaordFile { +pub enum ClipboardFile { MonitorReady, FormatList { format_list: Vec<(i32, String)>, @@ -61,8 +61,8 @@ struct ConnEnabled { struct MsgChannel { peer_id: String, conn_id: i32, - sender: UnboundedSender, - receiver: Arc>>, + sender: UnboundedSender, + receiver: Arc>>, } #[derive(PartialEq)] @@ -89,7 +89,7 @@ pub fn get_client_conn_id(peer_id: &str) -> Option { pub fn get_rx_cliprdr_client( peer_id: &str, -) -> (i32, Arc>>) { +) -> (i32, Arc>>) { let mut lock = VEC_MSG_CHANNEL.write().unwrap(); match lock.iter().find(|x| x.peer_id == peer_id.to_owned()) { Some(msg_channel) => (msg_channel.conn_id, msg_channel.receiver.clone()), @@ -110,7 +110,7 @@ pub fn get_rx_cliprdr_client( } } -pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc>> { +pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc>> { let mut lock = VEC_MSG_CHANNEL.write().unwrap(); match lock.iter().find(|x| x.conn_id == conn_id) { Some(msg_channel) => msg_channel.receiver.clone(), @@ -131,7 +131,7 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc, conn_id: i32) -> pub fn server_clip_file( context: &mut Box, conn_id: i32, - msg: ClipbaordFile, + msg: ClipboardFile, ) -> u32 { match msg { - ClipbaordFile::MonitorReady => { + ClipboardFile::MonitorReady => { log::debug!("server_monitor_ready called"); let ret = server_monitor_ready(context, conn_id); log::debug!("server_monitor_ready called, return {}", ret); ret } - ClipbaordFile::FormatList { format_list } => { + ClipboardFile::FormatList { format_list } => { log::debug!("server_format_list called"); let ret = server_format_list(context, conn_id, format_list); log::debug!("server_format_list called, return {}", ret); ret } - ClipbaordFile::FormatListResponse { msg_flags } => { + ClipboardFile::FormatListResponse { msg_flags } => { log::debug!("format_list_response called"); let ret = server_format_list_response(context, conn_id, msg_flags); log::debug!("server_format_list_response called, return {}", ret); ret } - ClipbaordFile::FormatDataRequest { + ClipboardFile::FormatDataRequest { requested_format_id, } => { log::debug!("format_data_request called"); @@ -186,7 +186,7 @@ pub fn server_clip_file( log::debug!("server_format_data_request called, return {}", ret); ret } - ClipbaordFile::FormatDataResponse { + ClipboardFile::FormatDataResponse { msg_flags, format_data, } => { @@ -195,7 +195,7 @@ pub fn server_clip_file( log::debug!("server_format_data_response called, return {}", ret); ret } - ClipbaordFile::FileContentsRequest { + ClipboardFile::FileContentsRequest { stream_id, list_index, dw_flags, @@ -221,7 +221,7 @@ pub fn server_clip_file( log::debug!("server_file_contents_request called, return {}", ret); ret } - ClipbaordFile::FileContentsResponse { + ClipboardFile::FileContentsResponse { msg_flags, stream_id, requested_data, @@ -492,7 +492,7 @@ extern "C" fn client_format_list( } conn_id = (*clip_format_list).connID as i32; } - let data = ClipbaordFile::FormatList { format_list }; + let data = ClipboardFile::FormatList { format_list }; // no need to handle result here if conn_id == 0 { VEC_MSG_CHANNEL @@ -519,7 +519,7 @@ extern "C" fn client_format_list_response( conn_id = (*format_list_response).connID as i32; msg_flags = (*format_list_response).msgFlags as i32; } - let data = ClipbaordFile::FormatListResponse { msg_flags }; + let data = ClipboardFile::FormatListResponse { msg_flags }; send_data(conn_id, data); 0 @@ -537,7 +537,7 @@ extern "C" fn client_format_data_request( conn_id = (*format_data_request).connID as i32; requested_format_id = (*format_data_request).requestedFormatId as i32; } - let data = ClipbaordFile::FormatDataRequest { + let data = ClipboardFile::FormatDataRequest { requested_format_id, }; // no need to handle result here @@ -568,7 +568,7 @@ extern "C" fn client_format_data_response( .to_vec(); } } - let data = ClipbaordFile::FormatDataResponse { + let data = ClipboardFile::FormatDataResponse { msg_flags, format_data, }; @@ -614,7 +614,7 @@ extern "C" fn client_file_contents_request( clip_data_id = (*file_contents_request).clipDataId as i32; } - let data = ClipbaordFile::FileContentsRequest { + let data = ClipboardFile::FileContentsRequest { stream_id, list_index, dw_flags, @@ -653,7 +653,7 @@ extern "C" fn client_file_contents_response( .to_vec(); } } - let data = ClipbaordFile::FileContentsResponse { + let data = ClipboardFile::FileContentsResponse { msg_flags, stream_id, requested_data, diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index 00ef7254e..a66150c40 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -795,11 +795,11 @@ static HRESULT STDMETHODCALLTYPE CliprdrDataObject_QueryGetData(IDataObject *Thi } static HRESULT STDMETHODCALLTYPE CliprdrDataObject_GetCanonicalFormatEtc(IDataObject *This, - FORMATETC *pformatectIn, + FORMATETC *pformatetcIn, FORMATETC *pformatetcOut) { (void)This; - (void)pformatectIn; + (void)pformatetcIn; if (!pformatetcOut) return E_INVALIDARG; diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index cc4173a97..fc4db9a63 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -37,8 +37,5 @@ core-graphics = "0.22" objc = "0.2" unicode-segmentation = "1.6" -[target.'cfg(target_os = "linux")'.dependencies] -libc = "0.2" - [build-dependencies] pkg-config = "0.3" diff --git a/libs/enigo/appveyor.yml b/libs/enigo/appveyor.yml index af3142ad9..5ad7bc249 100644 --- a/libs/enigo/appveyor.yml +++ b/libs/enigo/appveyor.yml @@ -1,9 +1,9 @@ -# Appveyor configuration template for Rust using rustup for Rust installation +# AppVeyor configuration template for Rust using rustup for Rust installation # https://github.com/starkat99/appveyor-rust ## Operating System (VM environment) ## -# Rust needs at least Visual Studio 2013 Appveyor OS for MSVC targets. +# Rust needs at least Visual Studio 2013 AppVeyor OS for MSVC targets. os: Visual Studio 2015 ## Build Matrix ## @@ -83,7 +83,7 @@ environment: ### Allowed failures ### -# See Appveyor documentation for specific details. In short, place any channel or targets you wish +# See AppVeyor documentation for specific details. In short, place any channel or targets you wish # to allow build failures on (usually nightly at least is a wise choice). This will prevent a build # or test failure in the matching channels/targets from failing the entire build. matrix: @@ -95,7 +95,7 @@ matrix: ## Install Script ## -# This is the most important part of the Appveyor configuration. This installs the version of Rust +# This is the most important part of the AppVeyor configuration. This installs the version of Rust # specified by the 'channel' and 'target' environment variables from the build matrix. This uses # rustup to install Rust. # @@ -110,7 +110,7 @@ install: ## Build Script ## -# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents +# 'cargo test' takes care of building for us, so disable AppVeyor's build stage. This prevents # the "directory does not contain a project or solution file" error. build: false diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index caa08bd55..509bbf97c 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -19,7 +19,7 @@ //! or any other "special" key on the Linux, macOS and Windows operating system. //! //! Possible use cases could be for testing user interfaces on different -//! plattforms, +//! platforms, //! building remote control applications or just automating tasks for user //! interfaces unaccessible by a public API or scripting language. //! @@ -104,6 +104,10 @@ pub enum MouseButton { Middle, /// Right mouse button Right, + /// Back mouse button + Back, + /// Forward mouse button + Forward, /// Scroll up button ScrollUp, @@ -202,7 +206,7 @@ pub trait MouseControllable { /// Click a mouse button /// - /// it's esentially just a consecutive invokation of + /// it's essentially just a consecutive invocation of /// [mouse_down](trait.MouseControllable.html#tymethod.mouse_down) followed /// by a [mouse_up](trait.MouseControllable.html#tymethod.mouse_up). Just /// for @@ -464,7 +468,7 @@ pub trait KeyboardControllable { /// Emits keystrokes such that the given string is inputted. /// /// You can use many unicode here like: ❤️. This works - /// regadless of the current keyboardlayout. + /// regardless of the current keyboardlayout. /// /// # Example /// diff --git a/libs/enigo/src/linux/nix_impl.rs b/libs/enigo/src/linux/nix_impl.rs index 47e6d53c0..e7dc69ab1 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -1,5 +1,5 @@ use super::xdo::EnigoXdo; -use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; +use crate::{Key, KeyboardControllable, MouseButton, MouseControllable, ResultType}; use std::io::Read; use tfc::{traits::*, Context as TFC_Context, Key as TFC_Key}; @@ -13,7 +13,7 @@ pub struct Enigo { is_x11: bool, tfc: Option, custom_keyboard: Option, - cutsom_mouse: Option, + custom_mouse: Option, } impl Enigo { @@ -21,7 +21,7 @@ impl Enigo { pub fn delay(&self) -> u64 { self.xdo.delay() } - /// Set delay of xdo implemetation. + /// Set delay of xdo implementation. pub fn set_delay(&mut self, delay: u64) { self.xdo.set_delay(delay) } @@ -31,7 +31,7 @@ impl Enigo { } /// Set custom mouse. pub fn set_custom_mouse(&mut self, custom_mouse: CustomMouce) { - self.cutsom_mouse = Some(custom_mouse) + self.custom_mouse = Some(custom_mouse) } /// Get custom keyboard. pub fn get_custom_keyboard(&mut self) -> &mut Option { @@ -39,7 +39,38 @@ impl Enigo { } /// Get custom mouse. pub fn get_custom_mouse(&mut self) -> &mut Option { - &mut self.cutsom_mouse + &mut self.custom_mouse + } + + /// Clear remapped keycodes + pub fn tfc_clear_remapped(&mut self) { + if let Some(tfc) = &mut self.tfc { + tfc.recover_remapped_keycodes(); + } + } + + fn tfc_key_click(&mut self, key: Key) -> ResultType { + if let Some(tfc) = &mut self.tfc { + let res = match key { + Key::Layout(chr) => tfc.unicode_char(chr), + key => { + let tfc_key: TFC_Key = match convert_to_tfc_key(key) { + Some(key) => key, + None => { + return Err(format!("Failed to convert {:?} to TFC_Key", key).into()); + } + }; + tfc.key_click(tfc_key) + } + }; + if res.is_err() { + Err(format!("Failed to click {:?} by tfc", key).into()) + } else { + Ok(()) + } + } else { + Err("Not Found TFC".into()) + } } fn tfc_key_down_or_up(&mut self, key: Key, down: bool, up: bool) -> bool { @@ -99,7 +130,7 @@ impl Default for Enigo { None }, custom_keyboard: None, - cutsom_mouse: None, + custom_mouse: None, xdo: EnigoXdo::default(), } } @@ -118,7 +149,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_move_to(x, y); } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_move_to(x, y) } } @@ -127,7 +158,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_move_relative(x, y); } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_move_relative(x, y) } } @@ -136,7 +167,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_down(button) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_down(button) } else { Ok(()) @@ -147,7 +178,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_up(button) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_up(button) } } @@ -156,7 +187,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_click(button) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_click(button) } } @@ -165,7 +196,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_scroll_x(length) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_scroll_x(length) } } @@ -174,7 +205,7 @@ impl MouseControllable for Enigo { if self.is_x11 { self.xdo.mouse_scroll_y(length) } else { - if let Some(mouse) = &mut self.cutsom_mouse { + if let Some(mouse) = &mut self.custom_mouse { mouse.mouse_scroll_y(length) } } @@ -183,6 +214,7 @@ impl MouseControllable for Enigo { fn get_led_state(key: Key) -> bool { let led_file = match key { + // FIXME: the file may be /sys/class/leds/input2 or input5 ... Key::CapsLock => "/sys/class/leds/input1::capslock/brightness", Key::NumLock => "/sys/class/leds/input1::numlock/brightness", _ => { @@ -222,6 +254,7 @@ impl KeyboardControllable for Enigo { } } + /// Warning: Get 6^ in French. fn key_sequence(&mut self, sequence: &str) { if self.is_x11 { self.xdo.key_sequence(sequence) @@ -261,8 +294,10 @@ impl KeyboardControllable for Enigo { } } fn key_click(&mut self, key: Key) { - self.key_down(key).ok(); - self.key_up(key); + if self.tfc_key_click(key).is_err() { + self.key_down(key).ok(); + self.key_up(key); + } } } @@ -334,3 +369,10 @@ fn convert_to_tfc_key(key: Key) -> Option { }; Some(key) } + +#[test] +fn test_key_seq() { + // Get 6^ in French. + let mut en = Enigo::new(); + en.key_sequence("^^"); +} diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index 204420adc..f0f7d49af 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -1,8 +1,6 @@ -use libc; - use crate::{Key, KeyboardControllable, MouseButton, MouseControllable}; -use self::libc::{c_char, c_int, c_void, useconds_t}; +use hbb_common::libc::{c_char, c_int, c_void, useconds_t}; use std::{borrow::Cow, ffi::CString, ptr}; const CURRENT_WINDOW: c_int = 0; @@ -57,6 +55,8 @@ fn mousebutton(button: MouseButton) -> c_int { MouseButton::ScrollDown => 5, MouseButton::ScrollLeft => 6, MouseButton::ScrollRight => 7, + MouseButton::Back => 8, + MouseButton::Forward => 9, } } diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index 68457a4a2..55f350895 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -226,7 +226,10 @@ impl MouseControllable for Enigo { MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseDown), MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseDown), MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseDown), - _ => unimplemented!(), + _ => { + log::info!("Unsupported button {:?}", button); + return Ok(()); + }, }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { @@ -249,7 +252,10 @@ impl MouseControllable for Enigo { MouseButton::Left => (CGMouseButton::Left, CGEventType::LeftMouseUp), MouseButton::Middle => (CGMouseButton::Center, CGEventType::OtherMouseUp), MouseButton::Right => (CGMouseButton::Right, CGEventType::RightMouseUp), - _ => unimplemented!(), + _ => { + log::info!("Unsupported button {:?}", button); + return; + }, }; let dest = CGPoint::new(current_x as f64, current_y as f64); if let Some(src) = self.event_source.as_ref() { diff --git a/libs/enigo/src/win/win_impl.rs b/libs/enigo/src/win/win_impl.rs index 4a4fd7fc4..115cb9789 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -39,7 +39,7 @@ fn mouse_event(flags: u32, data: u32, dx: i32, dy: i32) -> DWORD { unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } } -fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { +fn keybd_event(mut flags: u32, vk: u16, scan: u16) -> DWORD { let mut scan = scan; unsafe { // https://github.com/rustdesk/rustdesk/issues/366 @@ -52,21 +52,33 @@ fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { scan = MapVirtualKeyExW(vk as _, 0, LAYOUT) as _; } } - let mut input: INPUT = unsafe { std::mem::MaybeUninit::zeroed().assume_init() }; - input.type_ = INPUT_KEYBOARD; + + if flags & KEYEVENTF_UNICODE == 0 { + if scan >> 8 == 0xE0 || scan >> 8 == 0xE1 { + flags |= winapi::um::winuser::KEYEVENTF_EXTENDEDKEY; + } + } + let mut union: INPUT_u = unsafe { std::mem::zeroed() }; unsafe { - let dst_ptr = (&mut input.u as *mut _) as *mut u8; - let k = KEYBDINPUT { + *union.ki_mut() = KEYBDINPUT { wVk: vk, wScan: scan, dwFlags: flags, time: 0, dwExtraInfo: ENIGO_INPUT_EXTRA_VALUE, }; - let src_ptr = (&k as *const _) as *const u8; - std::ptr::copy_nonoverlapping(src_ptr, dst_ptr, size_of::()); } - unsafe { SendInput(1, &mut input as LPINPUT, size_of::() as c_int) } + let mut inputs = [INPUT { + type_: INPUT_KEYBOARD, + u: union, + }; 1]; + unsafe { + SendInput( + inputs.len() as UINT, + inputs.as_mut_ptr(), + size_of::() as c_int, + ) + } } fn get_error() -> String { @@ -134,9 +146,18 @@ impl MouseControllable for Enigo { MouseButton::Left => MOUSEEVENTF_LEFTDOWN, MouseButton::Middle => MOUSEEVENTF_MIDDLEDOWN, MouseButton::Right => MOUSEEVENTF_RIGHTDOWN, - _ => unimplemented!(), + MouseButton::Back => MOUSEEVENTF_XDOWN, + MouseButton::Forward => MOUSEEVENTF_XDOWN, + _ => { + log::info!("Unsupported button {:?}", button); + return Ok(()); + } + }, + match button { + MouseButton::Back => XBUTTON1 as _, + MouseButton::Forward => XBUTTON2 as _, + _ => 0, }, - 0, 0, 0, ); @@ -155,9 +176,18 @@ impl MouseControllable for Enigo { MouseButton::Left => MOUSEEVENTF_LEFTUP, MouseButton::Middle => MOUSEEVENTF_MIDDLEUP, MouseButton::Right => MOUSEEVENTF_RIGHTUP, - _ => unimplemented!(), + MouseButton::Back => MOUSEEVENTF_XUP, + MouseButton::Forward => MOUSEEVENTF_XUP, + _ => { + log::info!("Unsupported button {:?}", button); + return; + } + }, + match button { + MouseButton::Back => XBUTTON1 as _, + MouseButton::Forward => XBUTTON2 as _, + _ => 0, }, - 0, 0, 0, ); diff --git a/libs/hbb_common/.gitignore b/libs/hbb_common/.gitignore index b1cf151e3..693699042 100644 --- a/libs/hbb_common/.gitignore +++ b/libs/hbb_common/.gitignore @@ -1,4 +1,3 @@ /target **/*.rs.bk Cargo.lock -src/protos/ diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml index 59f0896cc..ed9ec73be 100644 --- a/libs/hbb_common/Cargo.toml +++ b/libs/hbb_common/Cargo.toml @@ -7,16 +7,17 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +flexi_logger = { version = "0.25", features = ["async"] } protobuf = { version = "3.1", features = ["with-bytes"] } tokio = { version = "1.20", features = ["full"] } tokio-util = { version = "0.7", features = ["full"] } futures = "0.3" bytes = { version = "1.2", features = ["serde"] } log = "0.4" -env_logger = "0.9" +env_logger = "0.10" socket2 = { version = "0.3", features = ["reuseport"] } zstd = "0.9" -quinn = {version = "0.8", optional = true } +quinn = {version = "0.9", optional = true } anyhow = "1.0" futures-util = "0.3" directories-next = "2.0" @@ -31,10 +32,13 @@ sodiumoxide = "0.2" regex = "1.4" tokio-socks = { git = "https://github.com/open-trade/tokio-socks" } chrono = "0.4" +backtrace = "0.3" +libc = "0.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" machine-uid = "0.2" +sysinfo = "0.28" [features] quic = [] @@ -46,6 +50,9 @@ protobuf-codegen = { version = "3.1" } [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["winuser"] } +[target.'cfg(target_os = "macos")'.dependencies] +osascript = "0.3.0" + [dev-dependencies] -toml = "0.5" +toml = "0.7" serde_json = "1.0" diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs index 225ec34c7..5ebc3a287 100644 --- a/libs/hbb_common/build.rs +++ b/libs/hbb_common/build.rs @@ -1,14 +1,14 @@ fn main() { - std::fs::create_dir_all("src/protos").unwrap(); + let out_dir = format!("{}/protos", std::env::var("OUT_DIR").unwrap()); + + std::fs::create_dir_all(&out_dir).unwrap(); + protobuf_codegen::Codegen::new() .pure() - .out_dir("src/protos") - .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) + .out_dir(out_dir) + .inputs(["protos/rendezvous.proto", "protos/message.proto"]) .include("protos") - .customize( - protobuf_codegen::Customize::default() - .tokio_bytes(true) - ) + .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) .run() .expect("Codegen failed."); } diff --git a/libs/hbb_common/examples/config.rs b/libs/hbb_common/examples/config.rs new file mode 100644 index 000000000..95169df8e --- /dev/null +++ b/libs/hbb_common/examples/config.rs @@ -0,0 +1,5 @@ +extern crate hbb_common; + +fn main() { + println!("{:?}", hbb_common::config::PeerConfig::load("455058072")); +} diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs new file mode 100644 index 000000000..0be788428 --- /dev/null +++ b/libs/hbb_common/examples/system_message.rs @@ -0,0 +1,20 @@ +extern crate hbb_common; +#[cfg(target_os = "linux")] +use hbb_common::platform::linux; +#[cfg(target_os = "macos")] +use hbb_common::platform::macos; + +fn main() { + #[cfg(target_os = "linux")] + let res = linux::system_message("test title", "test message", true); + #[cfg(target_os = "macos")] + let res = macos::alert( + "System Preferences".to_owned(), + "warning".to_owned(), + "test title".to_owned(), + "test message".to_owned(), + ["Ok".to_owned()].to_vec(), + ); + #[cfg(any(target_os = "linux", target_os = "macos"))] + println!("result {:?}", &res); +} diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 650e42104..6295c160b 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -1,595 +1,653 @@ -syntax = "proto3"; -package hbb; - -message EncodedVideoFrame { - bytes data = 1; - bool key = 2; - int64 pts = 3; -} - -message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } - -message RGB { bool compress = 1; } - -// planes data send directly in binary for better use arraybuffer on web -message YUV { - bool compress = 1; - int32 stride = 2; -} - -message VideoFrame { - oneof union { - EncodedVideoFrames vp9s = 6; - RGB rgb = 7; - YUV yuv = 8; - EncodedVideoFrames h264s = 10; - EncodedVideoFrames h265s = 11; - } - int64 timestamp = 9; -} - -message IdPk { - string id = 1; - bytes pk = 2; -} - -message DisplayInfo { - sint32 x = 1; - sint32 y = 2; - int32 width = 3; - int32 height = 4; - string name = 5; - bool online = 6; - bool cursor_embedded = 7; -} - -message PortForward { - string host = 1; - int32 port = 2; -} - -message FileTransfer { - string dir = 1; - bool show_hidden = 2; -} - -message LoginRequest { - string username = 1; - bytes password = 2; - string my_id = 4; - string my_name = 5; - OptionMessage option = 6; - oneof union { - FileTransfer file_transfer = 7; - PortForward port_forward = 8; - } - bool video_ack_required = 9; - uint64 session_id = 10; - string version = 11; -} - -message ChatMessage { string text = 1; } - -message Features { - bool privacy_mode = 1; -} - -message SupportedEncoding { - bool h264 = 1; - bool h265 = 2; -} - -message PeerInfo { - string username = 1; - string hostname = 2; - string platform = 3; - repeated DisplayInfo displays = 4; - int32 current_display = 5; - bool sas_enabled = 6; - string version = 7; - int32 conn_id = 8; - Features features = 9; - SupportedEncoding encoding = 10; -} - -message LoginResponse { - oneof union { - string error = 1; - PeerInfo peer_info = 2; - } -} - -message MouseEvent { - int32 mask = 1; - sint32 x = 2; - sint32 y = 3; - repeated ControlKey modifiers = 4; -} - -enum KeyboardMode{ - Legacy = 0; - Map = 1; - Translate = 2; - Auto = 3; -} - -enum ControlKey { - Unknown = 0; - Alt = 1; - Backspace = 2; - CapsLock = 3; - Control = 4; - Delete = 5; - DownArrow = 6; - End = 7; - Escape = 8; - F1 = 9; - F10 = 10; - F11 = 11; - F12 = 12; - F2 = 13; - F3 = 14; - F4 = 15; - F5 = 16; - F6 = 17; - F7 = 18; - F8 = 19; - F9 = 20; - Home = 21; - LeftArrow = 22; - /// meta key (also known as "windows"; "super"; and "command") - Meta = 23; - /// option key on macOS (alt key on Linux and Windows) - Option = 24; // deprecated, use Alt instead - PageDown = 25; - PageUp = 26; - Return = 27; - RightArrow = 28; - Shift = 29; - Space = 30; - Tab = 31; - UpArrow = 32; - Numpad0 = 33; - Numpad1 = 34; - Numpad2 = 35; - Numpad3 = 36; - Numpad4 = 37; - Numpad5 = 38; - Numpad6 = 39; - Numpad7 = 40; - Numpad8 = 41; - Numpad9 = 42; - Cancel = 43; - Clear = 44; - Menu = 45; // deprecated, use Alt instead - Pause = 46; - Kana = 47; - Hangul = 48; - Junja = 49; - Final = 50; - Hanja = 51; - Kanji = 52; - Convert = 53; - Select = 54; - Print = 55; - Execute = 56; - Snapshot = 57; - Insert = 58; - Help = 59; - Sleep = 60; - Separator = 61; - Scroll = 62; - NumLock = 63; - RWin = 64; - Apps = 65; - Multiply = 66; - Add = 67; - Subtract = 68; - Decimal = 69; - Divide = 70; - Equals = 71; - NumpadEnter = 72; - RShift = 73; - RControl = 74; - RAlt = 75; - CtrlAltDel = 100; - LockScreen = 101; -} - -message KeyEvent { - bool down = 1; - bool press = 2; - oneof union { - ControlKey control_key = 3; - uint32 chr = 4; - uint32 unicode = 5; - string seq = 6; - } - repeated ControlKey modifiers = 8; - KeyboardMode mode = 9; -} - -message CursorData { - uint64 id = 1; - sint32 hotx = 2; - sint32 hoty = 3; - int32 width = 4; - int32 height = 5; - bytes colors = 6; -} - -message CursorPosition { - sint32 x = 1; - sint32 y = 2; -} - -message Hash { - string salt = 1; - string challenge = 2; -} - -message Clipboard { - bool compress = 1; - bytes content = 2; -} - -enum FileType { - Dir = 0; - DirLink = 2; - DirDrive = 3; - File = 4; - FileLink = 5; -} - -message FileEntry { - FileType entry_type = 1; - string name = 2; - bool is_hidden = 3; - uint64 size = 4; - uint64 modified_time = 5; -} - -message FileDirectory { - int32 id = 1; - string path = 2; - repeated FileEntry entries = 3; -} - -message ReadDir { - string path = 1; - bool include_hidden = 2; -} - -message ReadAllFiles { - int32 id = 1; - string path = 2; - bool include_hidden = 3; -} - -message FileAction { - oneof union { - ReadDir read_dir = 1; - FileTransferSendRequest send = 2; - FileTransferReceiveRequest receive = 3; - FileDirCreate create = 4; - FileRemoveDir remove_dir = 5; - FileRemoveFile remove_file = 6; - ReadAllFiles all_files = 7; - FileTransferCancel cancel = 8; - FileTransferSendConfirmRequest send_confirm = 9; - } -} - -message FileTransferCancel { int32 id = 1; } - -message FileResponse { - oneof union { - FileDirectory dir = 1; - FileTransferBlock block = 2; - FileTransferError error = 3; - FileTransferDone done = 4; - FileTransferDigest digest = 5; - } -} - -message FileTransferDigest { - int32 id = 1; - sint32 file_num = 2; - uint64 last_modified = 3; - uint64 file_size = 4; - bool is_upload = 5; -} - -message FileTransferBlock { - int32 id = 1; - sint32 file_num = 2; - bytes data = 3; - bool compressed = 4; - uint32 blk_id = 5; -} - -message FileTransferError { - int32 id = 1; - string error = 2; - sint32 file_num = 3; -} - -message FileTransferSendRequest { - int32 id = 1; - string path = 2; - bool include_hidden = 3; - int32 file_num = 4; -} - -message FileTransferSendConfirmRequest { - int32 id = 1; - sint32 file_num = 2; - oneof union { - bool skip = 3; - uint32 offset_blk = 4; - } -} - -message FileTransferDone { - int32 id = 1; - sint32 file_num = 2; -} - -message FileTransferReceiveRequest { - int32 id = 1; - string path = 2; // path written to - repeated FileEntry files = 3; - int32 file_num = 4; -} - -message FileRemoveDir { - int32 id = 1; - string path = 2; - bool recursive = 3; -} - -message FileRemoveFile { - int32 id = 1; - string path = 2; - sint32 file_num = 3; -} - -message FileDirCreate { - int32 id = 1; - string path = 2; -} - -// main logic from freeRDP -message CliprdrMonitorReady { -} - -message CliprdrFormat { - int32 id = 2; - string format = 3; -} - -message CliprdrServerFormatList { - repeated CliprdrFormat formats = 2; -} - -message CliprdrServerFormatListResponse { - int32 msg_flags = 2; -} - -message CliprdrServerFormatDataRequest { - int32 requested_format_id = 2; -} - -message CliprdrServerFormatDataResponse { - int32 msg_flags = 2; - bytes format_data = 3; -} - -message CliprdrFileContentsRequest { - int32 stream_id = 2; - int32 list_index = 3; - int32 dw_flags = 4; - int32 n_position_low = 5; - int32 n_position_high = 6; - int32 cb_requested = 7; - bool have_clip_data_id = 8; - int32 clip_data_id = 9; -} - -message CliprdrFileContentsResponse { - int32 msg_flags = 3; - int32 stream_id = 4; - bytes requested_data = 5; -} - -message Cliprdr { - oneof union { - CliprdrMonitorReady ready = 1; - CliprdrServerFormatList format_list = 2; - CliprdrServerFormatListResponse format_list_response = 3; - CliprdrServerFormatDataRequest format_data_request = 4; - CliprdrServerFormatDataResponse format_data_response = 5; - CliprdrFileContentsRequest file_contents_request = 6; - CliprdrFileContentsResponse file_contents_response = 7; - } -} - -message SwitchDisplay { - int32 display = 1; - sint32 x = 2; - sint32 y = 3; - int32 width = 4; - int32 height = 5; - bool cursor_embedded = 6; -} - -message PermissionInfo { - enum Permission { - Keyboard = 0; - Clipboard = 2; - Audio = 3; - File = 4; - Restart = 5; - Recording = 6; - } - - Permission permission = 1; - bool enabled = 2; -} - -enum ImageQuality { - NotSet = 0; - Low = 2; - Balanced = 3; - Best = 4; -} - -message VideoCodecState { - enum PerferCodec { - Auto = 0; - VPX = 1; - H264 = 2; - H265 = 3; - } - - int32 score_vpx = 1; - int32 score_h264 = 2; - int32 score_h265 = 3; - PerferCodec perfer = 4; -} - -message OptionMessage { - enum BoolOption { - NotSet = 0; - No = 1; - Yes = 2; - } - ImageQuality image_quality = 1; - BoolOption lock_after_session_end = 2; - BoolOption show_remote_cursor = 3; - BoolOption privacy_mode = 4; - BoolOption block_input = 5; - int32 custom_image_quality = 6; - BoolOption disable_audio = 7; - BoolOption disable_clipboard = 8; - BoolOption enable_file_transfer = 9; - VideoCodecState video_codec_state = 10; - int32 custom_fps = 11; -} - -message TestDelay { - int64 time = 1; - bool from_client = 2; - uint32 last_delay = 3; - uint32 target_bitrate = 4; -} - -message PublicKey { - bytes asymmetric_value = 1; - bytes symmetric_value = 2; -} - -message SignedId { bytes id = 1; } - -message AudioFormat { - uint32 sample_rate = 1; - uint32 channels = 2; -} - -message AudioFrame { - bytes data = 1; - int64 timestamp = 2; -} - -// Notify peer to show message box. -message MessageBox { - // Message type. Refer to flutter/lib/commom.dart/msgBox(). - string msgtype = 1; - string title = 2; - // English - string text = 3; - // If not empty, msgbox provides a button to following the link. - // The link here can't be directly http url. - // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). - string link = 4; -} - -message BackNotification { - // no need to consider block input by someone else - enum BlockInputState { - BlkStateUnknown = 0; - BlkOnSucceeded = 2; - BlkOnFailed = 3; - BlkOffSucceeded = 4; - BlkOffFailed = 5; - } - enum PrivacyModeState { - PrvStateUnknown = 0; - // Privacy mode on by someone else - PrvOnByOther = 2; - // Privacy mode is not supported on the remote side - PrvNotSupported = 3; - // Privacy mode on by self - PrvOnSucceeded = 4; - // Privacy mode on by self, but denied - PrvOnFailedDenied = 5; - // Some plugins are not found - PrvOnFailedPlugin = 6; - // Privacy mode on by self, but failed - PrvOnFailed = 7; - // Privacy mode off by self - PrvOffSucceeded = 8; - // Ctrl + P - PrvOffByPeer = 9; - // Privacy mode off by self, but failed - PrvOffFailed = 10; - PrvOffUnknown = 11; - } - - oneof union { - PrivacyModeState privacy_mode_state = 1; - BlockInputState block_input_state = 2; - } -} - -message Misc { - oneof union { - ChatMessage chat_message = 4; - SwitchDisplay switch_display = 5; - PermissionInfo permission_info = 6; - OptionMessage option = 7; - AudioFormat audio_format = 8; - string close_reason = 9; - bool refresh_video = 10; - bool video_received = 12; - BackNotification back_notification = 13; - bool restart_remote_device = 14; - bool uac = 15; - bool foreground_window_elevated = 16; - bool stop_service = 17; - } -} - -message Message { - oneof union { - SignedId signed_id = 3; - PublicKey public_key = 4; - TestDelay test_delay = 5; - VideoFrame video_frame = 6; - LoginRequest login_request = 7; - LoginResponse login_response = 8; - Hash hash = 9; - MouseEvent mouse_event = 10; - AudioFrame audio_frame = 11; - CursorData cursor_data = 12; - CursorPosition cursor_position = 13; - uint64 cursor_id = 14; - KeyEvent key_event = 15; - Clipboard clipboard = 16; - FileAction file_action = 17; - FileResponse file_response = 18; - Misc misc = 19; - Cliprdr cliprdr = 20; - MessageBox message_box = 21; - } -} +syntax = "proto3"; +package hbb; + +message EncodedVideoFrame { + bytes data = 1; + bool key = 2; + int64 pts = 3; +} + +message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } + +message RGB { bool compress = 1; } + +// planes data send directly in binary for better use arraybuffer on web +message YUV { + bool compress = 1; + int32 stride = 2; +} + +message VideoFrame { + oneof union { + EncodedVideoFrames vp9s = 6; + RGB rgb = 7; + YUV yuv = 8; + EncodedVideoFrames h264s = 10; + EncodedVideoFrames h265s = 11; + } +} + +message IdPk { + string id = 1; + bytes pk = 2; +} + +message DisplayInfo { + sint32 x = 1; + sint32 y = 2; + int32 width = 3; + int32 height = 4; + string name = 5; + bool online = 6; + bool cursor_embedded = 7; +} + +message PortForward { + string host = 1; + int32 port = 2; +} + +message FileTransfer { + string dir = 1; + bool show_hidden = 2; +} + +message LoginRequest { + string username = 1; + bytes password = 2; + string my_id = 4; + string my_name = 5; + OptionMessage option = 6; + oneof union { + FileTransfer file_transfer = 7; + PortForward port_forward = 8; + } + bool video_ack_required = 9; + uint64 session_id = 10; + string version = 11; +} + +message ChatMessage { string text = 1; } + +message Features { + bool privacy_mode = 1; +} + +message SupportedEncoding { + bool h264 = 1; + bool h265 = 2; +} + +message PeerInfo { + string username = 1; + string hostname = 2; + string platform = 3; + repeated DisplayInfo displays = 4; + int32 current_display = 5; + bool sas_enabled = 6; + string version = 7; + int32 conn_id = 8; + Features features = 9; + SupportedEncoding encoding = 10; + SupportedResolutions resolutions = 11; + // Use JSON's key-value format which is friendly for peer to handle. + string platform_additions = 12; +} + +message LoginResponse { + oneof union { + string error = 1; + PeerInfo peer_info = 2; + } +} + +message MouseEvent { + int32 mask = 1; + sint32 x = 2; + sint32 y = 3; + repeated ControlKey modifiers = 4; +} + +enum KeyboardMode{ + Legacy = 0; + Map = 1; + Translate = 2; + Auto = 3; +} + +enum ControlKey { + Unknown = 0; + Alt = 1; + Backspace = 2; + CapsLock = 3; + Control = 4; + Delete = 5; + DownArrow = 6; + End = 7; + Escape = 8; + F1 = 9; + F10 = 10; + F11 = 11; + F12 = 12; + F2 = 13; + F3 = 14; + F4 = 15; + F5 = 16; + F6 = 17; + F7 = 18; + F8 = 19; + F9 = 20; + Home = 21; + LeftArrow = 22; + /// meta key (also known as "windows"; "super"; and "command") + Meta = 23; + /// option key on macOS (alt key on Linux and Windows) + Option = 24; // deprecated, use Alt instead + PageDown = 25; + PageUp = 26; + Return = 27; + RightArrow = 28; + Shift = 29; + Space = 30; + Tab = 31; + UpArrow = 32; + Numpad0 = 33; + Numpad1 = 34; + Numpad2 = 35; + Numpad3 = 36; + Numpad4 = 37; + Numpad5 = 38; + Numpad6 = 39; + Numpad7 = 40; + Numpad8 = 41; + Numpad9 = 42; + Cancel = 43; + Clear = 44; + Menu = 45; // deprecated, use Alt instead + Pause = 46; + Kana = 47; + Hangul = 48; + Junja = 49; + Final = 50; + Hanja = 51; + Kanji = 52; + Convert = 53; + Select = 54; + Print = 55; + Execute = 56; + Snapshot = 57; + Insert = 58; + Help = 59; + Sleep = 60; + Separator = 61; + Scroll = 62; + NumLock = 63; + RWin = 64; + Apps = 65; + Multiply = 66; + Add = 67; + Subtract = 68; + Decimal = 69; + Divide = 70; + Equals = 71; + NumpadEnter = 72; + RShift = 73; + RControl = 74; + RAlt = 75; + CtrlAltDel = 100; + LockScreen = 101; +} + +message KeyEvent { + bool down = 1; + bool press = 2; + oneof union { + ControlKey control_key = 3; + // high word, sym key code. win: virtual-key code, linux: keysym ?, macos: + // low word, position key code. win: scancode, linux: key code, macos: key code + uint32 chr = 4; + uint32 unicode = 5; + string seq = 6; + } + repeated ControlKey modifiers = 8; + KeyboardMode mode = 9; +} + +message CursorData { + uint64 id = 1; + sint32 hotx = 2; + sint32 hoty = 3; + int32 width = 4; + int32 height = 5; + bytes colors = 6; +} + +message CursorPosition { + sint32 x = 1; + sint32 y = 2; +} + +message Hash { + string salt = 1; + string challenge = 2; +} + +message Clipboard { + bool compress = 1; + bytes content = 2; +} + +enum FileType { + Dir = 0; + DirLink = 2; + DirDrive = 3; + File = 4; + FileLink = 5; +} + +message FileEntry { + FileType entry_type = 1; + string name = 2; + bool is_hidden = 3; + uint64 size = 4; + uint64 modified_time = 5; +} + +message FileDirectory { + int32 id = 1; + string path = 2; + repeated FileEntry entries = 3; +} + +message ReadDir { + string path = 1; + bool include_hidden = 2; +} + +message ReadAllFiles { + int32 id = 1; + string path = 2; + bool include_hidden = 3; +} + +message FileAction { + oneof union { + ReadDir read_dir = 1; + FileTransferSendRequest send = 2; + FileTransferReceiveRequest receive = 3; + FileDirCreate create = 4; + FileRemoveDir remove_dir = 5; + FileRemoveFile remove_file = 6; + ReadAllFiles all_files = 7; + FileTransferCancel cancel = 8; + FileTransferSendConfirmRequest send_confirm = 9; + } +} + +message FileTransferCancel { int32 id = 1; } + +message FileResponse { + oneof union { + FileDirectory dir = 1; + FileTransferBlock block = 2; + FileTransferError error = 3; + FileTransferDone done = 4; + FileTransferDigest digest = 5; + } +} + +message FileTransferDigest { + int32 id = 1; + sint32 file_num = 2; + uint64 last_modified = 3; + uint64 file_size = 4; + bool is_upload = 5; + bool is_identical = 6; +} + +message FileTransferBlock { + int32 id = 1; + sint32 file_num = 2; + bytes data = 3; + bool compressed = 4; + uint32 blk_id = 5; +} + +message FileTransferError { + int32 id = 1; + string error = 2; + sint32 file_num = 3; +} + +message FileTransferSendRequest { + int32 id = 1; + string path = 2; + bool include_hidden = 3; + int32 file_num = 4; +} + +message FileTransferSendConfirmRequest { + int32 id = 1; + sint32 file_num = 2; + oneof union { + bool skip = 3; + uint32 offset_blk = 4; + } +} + +message FileTransferDone { + int32 id = 1; + sint32 file_num = 2; +} + +message FileTransferReceiveRequest { + int32 id = 1; + string path = 2; // path written to + repeated FileEntry files = 3; + int32 file_num = 4; +} + +message FileRemoveDir { + int32 id = 1; + string path = 2; + bool recursive = 3; +} + +message FileRemoveFile { + int32 id = 1; + string path = 2; + sint32 file_num = 3; +} + +message FileDirCreate { + int32 id = 1; + string path = 2; +} + +// main logic from freeRDP +message CliprdrMonitorReady { +} + +message CliprdrFormat { + int32 id = 2; + string format = 3; +} + +message CliprdrServerFormatList { + repeated CliprdrFormat formats = 2; +} + +message CliprdrServerFormatListResponse { + int32 msg_flags = 2; +} + +message CliprdrServerFormatDataRequest { + int32 requested_format_id = 2; +} + +message CliprdrServerFormatDataResponse { + int32 msg_flags = 2; + bytes format_data = 3; +} + +message CliprdrFileContentsRequest { + int32 stream_id = 2; + int32 list_index = 3; + int32 dw_flags = 4; + int32 n_position_low = 5; + int32 n_position_high = 6; + int32 cb_requested = 7; + bool have_clip_data_id = 8; + int32 clip_data_id = 9; +} + +message CliprdrFileContentsResponse { + int32 msg_flags = 3; + int32 stream_id = 4; + bytes requested_data = 5; +} + +message Cliprdr { + oneof union { + CliprdrMonitorReady ready = 1; + CliprdrServerFormatList format_list = 2; + CliprdrServerFormatListResponse format_list_response = 3; + CliprdrServerFormatDataRequest format_data_request = 4; + CliprdrServerFormatDataResponse format_data_response = 5; + CliprdrFileContentsRequest file_contents_request = 6; + CliprdrFileContentsResponse file_contents_response = 7; + } +} + +message Resolution { + int32 width = 1; + int32 height = 2; +} + +message SupportedResolutions { repeated Resolution resolutions = 1; } + +message SwitchDisplay { + int32 display = 1; + sint32 x = 2; + sint32 y = 3; + int32 width = 4; + int32 height = 5; + bool cursor_embedded = 6; + SupportedResolutions resolutions = 7; +} + +message PermissionInfo { + enum Permission { + Keyboard = 0; + Clipboard = 2; + Audio = 3; + File = 4; + Restart = 5; + Recording = 6; + } + + Permission permission = 1; + bool enabled = 2; +} + +enum ImageQuality { + NotSet = 0; + Low = 2; + Balanced = 3; + Best = 4; +} + +message VideoCodecState { + enum PreferCodec { + Auto = 0; + VPX = 1; + H264 = 2; + H265 = 3; + } + + int32 score_vpx = 1; + int32 score_h264 = 2; + int32 score_h265 = 3; + PreferCodec prefer = 4; +} + +message OptionMessage { + enum BoolOption { + NotSet = 0; + No = 1; + Yes = 2; + } + ImageQuality image_quality = 1; + BoolOption lock_after_session_end = 2; + BoolOption show_remote_cursor = 3; + BoolOption privacy_mode = 4; + BoolOption block_input = 5; + int32 custom_image_quality = 6; + BoolOption disable_audio = 7; + BoolOption disable_clipboard = 8; + BoolOption enable_file_transfer = 9; + VideoCodecState video_codec_state = 10; + int32 custom_fps = 11; + BoolOption disable_keyboard = 12; +} + +message TestDelay { + int64 time = 1; + bool from_client = 2; + uint32 last_delay = 3; + uint32 target_bitrate = 4; +} + +message PublicKey { + bytes asymmetric_value = 1; + bytes symmetric_value = 2; +} + +message SignedId { bytes id = 1; } + +message AudioFormat { + uint32 sample_rate = 1; + uint32 channels = 2; +} + +message AudioFrame { + bytes data = 1; +} + +// Notify peer to show message box. +message MessageBox { + // Message type. Refer to flutter/lib/common.dart/msgBox(). + string msgtype = 1; + string title = 2; + // English + string text = 3; + // If not empty, msgbox provides a button to following the link. + // The link here can't be directly http url. + // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). + string link = 4; +} + +message BackNotification { + // no need to consider block input by someone else + enum BlockInputState { + BlkStateUnknown = 0; + BlkOnSucceeded = 2; + BlkOnFailed = 3; + BlkOffSucceeded = 4; + BlkOffFailed = 5; + } + enum PrivacyModeState { + PrvStateUnknown = 0; + // Privacy mode on by someone else + PrvOnByOther = 2; + // Privacy mode is not supported on the remote side + PrvNotSupported = 3; + // Privacy mode on by self + PrvOnSucceeded = 4; + // Privacy mode on by self, but denied + PrvOnFailedDenied = 5; + // Some plugins are not found + PrvOnFailedPlugin = 6; + // Privacy mode on by self, but failed + PrvOnFailed = 7; + // Privacy mode off by self + PrvOffSucceeded = 8; + // Ctrl + P + PrvOffByPeer = 9; + // Privacy mode off by self, but failed + PrvOffFailed = 10; + PrvOffUnknown = 11; + } + + oneof union { + PrivacyModeState privacy_mode_state = 1; + BlockInputState block_input_state = 2; + } +} + +message ElevationRequestWithLogon { + string username = 1; + string password = 2; +} + +message ElevationRequest { + oneof union { + bool direct = 1; + ElevationRequestWithLogon logon = 2; + } +} + +message SwitchSidesRequest { + bytes uuid = 1; +} + +message SwitchSidesResponse { + bytes uuid = 1; + LoginRequest lr = 2; +} + +message SwitchBack {} + +message Misc { + oneof union { + ChatMessage chat_message = 4; + SwitchDisplay switch_display = 5; + PermissionInfo permission_info = 6; + OptionMessage option = 7; + AudioFormat audio_format = 8; + string close_reason = 9; + bool refresh_video = 10; + bool video_received = 12; + BackNotification back_notification = 13; + bool restart_remote_device = 14; + bool uac = 15; + bool foreground_window_elevated = 16; + bool stop_service = 17; + ElevationRequest elevation_request = 18; + string elevation_response = 19; + bool portable_service_running = 20; + SwitchSidesRequest switch_sides_request = 21; + SwitchBack switch_back = 22; + Resolution change_resolution = 24; + } +} + +message VoiceCallRequest { + int64 req_timestamp = 1; + // Indicates whether the request is a connect action or a disconnect action. + bool is_connect = 2; +} + +message VoiceCallResponse { + bool accepted = 1; + int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp]. + int64 ack_timestamp = 3; +} + +message Message { + oneof union { + SignedId signed_id = 3; + PublicKey public_key = 4; + TestDelay test_delay = 5; + VideoFrame video_frame = 6; + LoginRequest login_request = 7; + LoginResponse login_response = 8; + Hash hash = 9; + MouseEvent mouse_event = 10; + AudioFrame audio_frame = 11; + CursorData cursor_data = 12; + CursorPosition cursor_position = 13; + uint64 cursor_id = 14; + KeyEvent key_event = 15; + Clipboard clipboard = 16; + FileAction file_action = 17; + FileResponse file_response = 18; + Misc misc = 19; + Cliprdr cliprdr = 20; + MessageBox message_box = 21; + SwitchSidesResponse switch_sides_response = 22; + VoiceCallRequest voice_call_request = 23; + VoiceCallResponse voice_call_response = 24; + PeerInfo peer_info = 25; + } +} diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto index 1ac60f3f3..d86226290 100644 --- a/libs/hbb_common/protos/rendezvous.proto +++ b/libs/hbb_common/protos/rendezvous.proto @@ -27,6 +27,7 @@ message PunchHole { bytes socket_addr = 1; string relay_server = 2; NatType nat_type = 3; + string request_region = 4; } message TestNatRequest { @@ -51,6 +52,7 @@ message PunchHoleSent { string relay_server = 3; NatType nat_type = 4; string version = 5; + string request_region = 6; } message RegisterPk { @@ -105,6 +107,7 @@ message RequestRelay { string licence_key = 6; ConnType conn_type = 7; string token = 8; + string request_region = 9; } message RelayResponse { @@ -117,6 +120,7 @@ message RelayResponse { } string refuse_reason = 6; string version = 7; + string request_region = 8; } message SoftwareUpdate { string url = 1; } @@ -128,6 +132,7 @@ message SoftwareUpdate { string url = 1; } message FetchLocalAddr { bytes socket_addr = 1; string relay_server = 2; + string request_region = 3; } message LocalAddr { @@ -136,6 +141,7 @@ message LocalAddr { string relay_server = 3; string id = 4; string version = 5; + string request_region = 6; } message PeerDiscovery { diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs index e029f1cc0..bfc798715 100644 --- a/libs/hbb_common/src/bytes_codec.rs +++ b/libs/hbb_common/src/bytes_codec.rs @@ -15,6 +15,12 @@ enum DecodeState { Data(usize), } +impl Default for BytesCodec { + fn default() -> Self { + Self::new() + } +} + impl BytesCodec { pub fn new() -> Self { Self { @@ -56,7 +62,7 @@ impl BytesCodec { } src.advance(head_len); src.reserve(n); - return Ok(Some(n)); + Ok(Some(n)) } fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result> { @@ -137,32 +143,32 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3F, 1); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); let buf_saved = buf.clone(); assert_eq!(buf.len(), 0x3F + 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F); assert_eq!(res[0], 1); } else { - assert!(false); + panic!(); } let mut codec2 = BytesCodec::new(); let mut buf2 = BytesMut::new(); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[0..1]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[1..]); if let Ok(Some(res)) = codec2.decode(&mut buf2) { assert_eq!(res.len(), 0x3F); assert_eq!(res[0], 1); } else { - assert!(false); + panic!(); } } @@ -171,21 +177,21 @@ mod tests { let mut codec = BytesCodec::new(); let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); - assert!(!codec.encode("".into(), &mut buf).is_err()); + assert!(codec.encode("".into(), &mut buf).is_ok()); assert_eq!(buf.len(), 1); bytes.resize(0x3F + 1, 2); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3F + 2 + 2); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0); } else { - assert!(false); + panic!(); } if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F + 1); assert_eq!(res[0], 2); } else { - assert!(false); + panic!(); } } @@ -195,13 +201,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3F - 1, 3); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3F + 1 - 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3F - 1); assert_eq!(res[0], 3); } else { - assert!(false); + panic!(); } } #[test] @@ -210,13 +216,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFF, 4); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3FFF + 2); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFF); assert_eq!(res[0], 4); } else { - assert!(false); + panic!(); } } @@ -226,13 +232,13 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFFFF, 5); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); assert_eq!(buf.len(), 0x3FFFFF + 3); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFFFF); assert_eq!(res[0], 5); } else { - assert!(false); + panic!(); } } @@ -242,33 +248,33 @@ mod tests { let mut buf = BytesMut::new(); let mut bytes: Vec = Vec::new(); bytes.resize(0x3FFFFF + 1, 6); - assert!(!codec.encode(bytes.into(), &mut buf).is_err()); + assert!(codec.encode(bytes.into(), &mut buf).is_ok()); let buf_saved = buf.clone(); assert_eq!(buf.len(), 0x3FFFFF + 4 + 1); if let Ok(Some(res)) = codec.decode(&mut buf) { assert_eq!(res.len(), 0x3FFFFF + 1); assert_eq!(res[0], 6); } else { - assert!(false); + panic!(); } let mut codec2 = BytesCodec::new(); let mut buf2 = BytesMut::new(); buf2.extend(&buf_saved[0..1]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[1..6]); if let Ok(None) = codec2.decode(&mut buf2) { } else { - assert!(false); + panic!(); } buf2.extend(&buf_saved[6..]); if let Ok(Some(res)) = codec2.decode(&mut buf2) { assert_eq!(res.len(), 0x3FFFFF + 1); assert_eq!(res[0], 6); } else { - assert!(false); + panic!(); } } } diff --git a/libs/hbb_common/src/compress.rs b/libs/hbb_common/src/compress.rs index a969ccf86..e7668a949 100644 --- a/libs/hbb_common/src/compress.rs +++ b/libs/hbb_common/src/compress.rs @@ -32,12 +32,7 @@ pub fn decompress(data: &[u8]) -> Vec { const MAX: usize = 1024 * 1024 * 64; const MIN: usize = 1024 * 1024; let mut n = 30 * data.len(); - if n > MAX { - n = MAX; - } - if n < MIN { - n = MIN; - } + n = n.clamp(MIN, MAX); match d.decompress(data, n) { Ok(res) => out = res, Err(err) => { diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 1d427a2e9..0624af672 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -4,7 +4,7 @@ use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::{Path, PathBuf}, sync::{Arc, Mutex, RwLock}, - time::SystemTime, + time::{Duration, Instant, SystemTime}, }; use anyhow::Result; @@ -29,30 +29,30 @@ pub const READ_TIMEOUT: u64 = 30_000; pub const REG_INTERVAL: i64 = 12_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; -const PASSWORD_ENC_VERSION: &'static str = "00"; -// 128x128 -#[cfg(target_os = "macos")] // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding -pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAyVBMVEUAAAAAcf8Acf8Acf8Acv8Acf8Acf8Acf8Acf8AcP8Acf8Ab/8AcP8Acf////8AaP/z+f/o8v/k7v/5/v/T5f8AYP/u9v/X6f+hx/+Kuv95pP8Aef/B1/+TwP9xoP8BdP/g6P+Irv9ZmP8Bgf/E3f98q/9sn/+01f+Es/9nm/9Jif8hhv8off/M4P+syP+avP86iP/c7f+xy/9yqf9Om/9hk/9Rjv+60P99tv9fpf88lv8yjf8Tgf8deP+kvP8BiP8NeP8hkP80gP8oj2VLAAAADXRSTlMA7o7qLvnaxZ1FOxYPjH9HWgAABHJJREFUeNrtm+tW4jAQgBfwuu7MtIUWsOUiCCioIIgLiqvr+z/UHq/LJKVkmwTcc/r9E2nzlU4mSTP9lpGRkZGR8VX5cZjfL+yCEXYL+/nDH//U/Pd8DgyTy39Xbv7oIAcWyB0cqbW/sweW2NtRaj8H1sgpGOwUIAH7Bkd7YJW9dXFwAJY5WNP/cmCZQnJvzIN18on5LwfWySXlxEPYAIcad8D6PdiHDbCfIFCADVBIENiFDbCbIACKPPXrZ+cP8E6/0znvP4EymgIEravIRcTxu8HxNSJ60a8W0AYECKrlAN+YwAthCd9wm1Ug6wKzIn5SgRduXfwkqDasCjx0XFzi9PV6zwNcIuhcWBOg+ikySq8C9UD4dEKWBCoOcspvAuLHTo9sCDQiFPHotRM48j8G5gVur1FdAN2uaYEuiz7xFsgEJ2RUoMUakXuBTHHoGxQYOBhHjeUBAefEnMAowFhaLBOKuOemBBbxLRQrH2PBCgMvNCPQGMeevTb9zLrPxz2Mo+QbEaijzPUcOOHMQZkKGRAIPem39+bypREMPTkQW/oCfk866zAkiIFG4yIKRE/aAnfiSd0WrORY6pFdXQEqi9mvAQm0RIOSnoCcZ8vJoz3diCnjRk+g8VP4/fuQDJ2Lxr6WwG0gXs9aTpDzW0vgDBlVUpixR8gYk44AD8FrUKHr8JQJGgIDnoDqoALxmWPQSi9AVVzm8gKUuEPGr/QCvptwJkbSYT/TC4S8C96DGjTj86aHtAI0x2WaBIq0eSYYpRa4EsdWVVwWu9O0Aj6f6dyBMnwEraeOgSYu0wZlauzA47QCbT7DgAQSE+hZWoEBF/BBmWOewNMK3BsSqKUW4MGcWqCSVmDkbvkXGKQOwg6PAUO9oL3xXhA20yaiCjuwYygRVQlUOTWTCf2SuNJTxeFjgaHByGuAIvd8ItdPLTDhS7IuqEE1YSKVOgbayLhSFQhMzYh8hwfBs1r7c505YVIQYEdNoKwxK06MJiyrpUFHiF0NAfCQUVHoiRclIXJIR6C2fqG37pBHvcWpgwzvAtYwkR5UGV2e42UISdBJETl3mg8ouo54Rcnti1/vaT+iuUQBt500Cgo4U10BeHSkk57FB0JjWkKRMWgLUA0lLodtImAQdaMiiri3+gIAPZQoutHNsgKF1aaDMhMyIdBf8Th+Bh8MTjGWCpl5Wv43tDmnF+IUVMrcZgRoiAxhtrloYizNkZaAnF5leglbNhj0wYCAbCDvGb0mP4nib7O7ZlcYQ2m1gPtIZgVgGNNMeaVAaWR+57TrqgtUnm3sHQ+kYeE6fufUubG1ez50FXbPnWgBlgSABmN3TTcsRl2yWkHRrwbiunvk/W2+Mg1hPZplPDeXRbZzStFH15s1QIVd3UImP5z/bHpeeQLvRJ7XLFUffQIlCvqlXETQbgN9/rlYABGosv+Vi9m2Xs639YLGrZd0br+odetlvdsvbN56abfd4vbCzv9Q3v/ygoOV21A4OPpfXvH4Ai+5ZGRkZGRkbJA/t/I0QMzoMiEAAAAASUVORK5CYII= -"; -#[cfg(not(target_os = "macos"))] // 128x128 no padding -pub const ICON: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAA7VBMVEUAAAAAcf8Acf8Acf8Adf8Acf8Acf8AcP8Acv8AcP8Acf8Acf8Acf8Acv8Acf8Acf8Ab/8AcP8Acf8Acf8Acf/////7/f8Dc/8TfP/1+f/n8v9Hmf/u9v+Uw//Q5f9hp/8Yfv8Qev8Ld/+52P+z1f+s0f81j/8wjP8Hdf/3+/8mh/8fg//x9//h7//H4P9xsP9rrf9oq/8rif/r9P/D3v+92/+Duv9bpP/d7f/U5/9NnP8/lP8jhP/L4v/B3P+OwP9+t/95tf9Rn/8bgf/Z6v+Zx/90sv9lqf85kf+hy/9UoP+Wxf+kzP+dyP+Lvv/H4q8IAAAAFHRSTlMA+u6bB6x5XR4V0+S4i4k5N+a81W8MiAQAAAVcSURBVHjazdvpWtpAGIbhgEutdW3fL2GHsMsiq4KI+66t5384XahF/GbizJAy3j/1Ah5CJhNCxpm1vbryLRrBfxKJrq+sbjtSa5u7WIDdzTVH5PNSBAsSWfrsMJ+iWKDoJ2fW8hIWbGl55vW/YuE2XhUsb8CCr9OCJVix9G//gyWf/o6/KCyJfrbwAfAPYS0CayK/j4mbsGjrV8AXWLTrONuwasdZhVWrzgqsWnG+wap1Jwqrok4EVkUcmKhdVvBaOVnzYEY/oJpMD4mo6ONF/ZSIUsX2FZjQA7xRqUET+y/v2W/Sy59u62DCDMgdJmhqgIk7eqWQBBNWwPhmj147w8QTzTjKVsGEEBBLuzSrhIkivTF8DD/Aa6forQNMHBD/VyXkgHGfuBN5ALln1TADOnESyGCiT8L/1kILqD6Q0BEm9kkofhdSwNUJiV1jQvZ/SnthBNSaJJGZbgGJUnX+gEqCZPpsJ2T2Y/MGVBrE8eOAvCA/X8A4QXLnmEhTgIPqPAG5IQU4fhmkFOT7HAFenwIU8Jd/TUEODQIUtu1eOj/dUD9cknOTpgEDkup3YrOfVStDUomcWcBVisTiNxVw3TPpgCl4RgFFybZ/9iHmn8uS2yYBA8m7qUEu9oOEejH9gHxC+PazCHbcFM8K+gGHJNAs4z2xgnAkVHQDcnG1IzvnCSfvom7AM3EZ9voah4+KXoAvGFJHMSgqEfegF3BBTKoOVfkMMXFfJ8AT7MuXUDeOE9PWCUiKBpKOlmAP1gngH2LChw7vhJgr9YD8Hnt0BxrE27CtHnDJR4AHTX1+KFAP4Ef0LHTxN9HwlAMSbAjmoavKZ8ayakDXYAhwN3wzqgZk2UPvwRjshmeqATeCT09f3mWnEqoBGf4NxAB/moRqADuOtmDiid6KqQVcsQeOYOKW3uqqBRwL5nITj/yrlFpAVrDpTJT5llQLaLMHwshY7UDgvD+VujDC96WWWsBtSAE5FnChFnAeUkDMdAvw88EqTNT5SYXpTlgPaRQM1AIGorkolNnoUS1gJHigCX48SaoF3Asuspg4Mz0U8+FTgIkCG01V09kwBQP8xG5ofD5AXeirkPEJSUlwSVIfP5ykVQNaggvz+k7prTvVgDKF8BnUXP4kqgEe/257E8Ig7EE1gA8g2stBTz7FLxqrB3SIeYaeQ2IG6gE5l2+Cmt5MGOfP4KsGiH8DOYWOoujnDY2ALHF3810goZFOQDVBTFx9Uj7eI6bp6QTgnLjeGGq6KeJuoRUQixN3pDYWyz1Rva8XIL5UPFQZCsmG3gV7R+dieS+Jd3iHLglce7oBuCOhp3zwHLxPQpfQDvBOSKjZqUIml3ZJ6AD6AajFSZJwewWR8ZPsEY26SQDaJOMeZP23w6bTJ6kBjAJQILm9hzqm7otu4G+nhgGxIQUlPLKzL7GhbxqAboMCuN2XXd+lAL0ajAMwclV+FD6jAPEy5ghAlhfwX2FODX445gHKxyN++fs64PUHmDMAbbYN2DlKk2QaScwdgMs4SZxMv4OJJSoIIQBl2Qtk3gk4qiOUANRPJQHB+0A6j5AC4J27QQEZ4eZPAsYBXFk0N/YD7iUrxRBqALxOTzoMC3x8lCFlfkMjuz8iLfk6fzQCQgjg8q3ZEd8RzUVuKelBh96Nzcc3qelL1V+2zfRv1xc56Ino3tpdPT7cd//MspfTrD/7R6p4W4O2qLMObfnyIHvvYcrPtkZjDybW7d/eb32Bg/UlHnYXuXz5CMt8rC90sr7Uy/5iN+vL/ewveLS/5NNKwcbyR1r2a3/h8wdY+v3L2tZC5oUvW2uO1M7qyvp/Xv6/48z4CTxjJEfyjEaMAAAAAElFTkSuQmCC -"; +const PASSWORD_ENC_VERSION: &str = "00"; + #[cfg(target_os = "macos")] lazy_static::lazy_static! { pub static ref ORG: Arc> = Arc::new(RwLock::new("com.carriez".to_owned())); } type Size = (i32, i32, i32, i32); +type KeyPair = (Vec, Vec); lazy_static::lazy_static! { static ref CONFIG: Arc> = Arc::new(RwLock::new(Config::load())); static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc> = Arc::new(RwLock::new(LocalConfig::load())); + pub static ref CONFIG_OIDC: Arc> = Arc::new(RwLock::new(ConfigOidc::load())); pub static ref ONLINE: Arc>> = Default::default(); - pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Default::default(); + pub static ref PROD_RENDEZVOUS_SERVER: Arc> = Arc::new(RwLock::new(match option_env!("RENDEZVOUS_SERVER") { + Some(key) if !key.is_empty() => key, + _ => "", + }.to_owned())); pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); - static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); + static ref KEY_PAIR: Arc>> = Default::default(); static ref HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); + static ref USER_DEFAULT_CONFIG: Arc> = Arc::new(RwLock::new((UserDefaultConfig::load(), Instant::now()))); } lazy_static::lazy_static! { @@ -72,17 +72,22 @@ lazy_static::lazy_static! { ]); } -const CHARS: &'static [char] = &[ +const CHARS: &[char] = &[ '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ +pub const RENDEZVOUS_SERVERS: &[&str] = &[ "rs-ny.rustdesk.com", "rs-sg.rustdesk.com", "rs-cn.rustdesk.com", ]; -pub const RS_PUB_KEY: &'static str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; + +pub const RS_PUB_KEY: &str = match option_env!("RS_PUB_KEY") { + Some(key) if !key.is_empty() => key, + _ => "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=", +}; + pub const RENDEZVOUS_PORT: i32 = 21116; pub const RELAY_PORT: i32 = 21117; @@ -106,6 +111,26 @@ macro_rules! serde_field_string { }; } +macro_rules! serde_field_bool { + ($struct_name: ident, $field_name: literal, $func: ident, $default: literal) => { + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] + pub struct $struct_name { + #[serde(default = $default, rename = $field_name)] + pub v: bool, + } + impl Default for $struct_name { + fn default() -> Self { + Self { v: Self::$func() } + } + } + impl $struct_name { + pub fn $func() -> bool { + UserDefaultConfig::read().get($field_name) == "Y" + } + } + }; +} + #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum NetworkType { Direct, @@ -123,7 +148,7 @@ pub struct Config { #[serde(default)] salt: String, #[serde(default)] - key_pair: (Vec, Vec), // sk, pk + key_pair: KeyPair, // sk, pk #[serde(default)] key_confirmed: bool, #[serde(default)] @@ -183,32 +208,39 @@ pub struct PeerConfig { deserialize_with = "PeerConfig::deserialize_image_quality" )] pub image_quality: String, - #[serde(default)] + #[serde( + default = "PeerConfig::default_custom_image_quality", + deserialize_with = "PeerConfig::deserialize_custom_image_quality" + )] pub custom_image_quality: Vec, - #[serde(default)] - pub show_remote_cursor: bool, - #[serde(default)] - pub lock_after_session_end: bool, - #[serde(default)] - pub privacy_mode: bool, + #[serde(flatten)] + pub show_remote_cursor: ShowRemoteCursor, + #[serde(flatten)] + pub lock_after_session_end: LockAfterSessionEnd, + #[serde(flatten)] + pub privacy_mode: PrivacyMode, + #[serde(flatten)] + pub allow_swap_key: AllowSwapKey, #[serde(default)] pub port_forwards: Vec<(i32, String, i32)>, #[serde(default)] pub direct_failures: i32, - #[serde(default)] - pub disable_audio: bool, - #[serde(default)] - pub disable_clipboard: bool, - #[serde(default)] - pub enable_file_transfer: bool, - #[serde(default)] - pub show_quality_monitor: bool, + #[serde(flatten)] + pub disable_audio: DisableAudio, + #[serde(flatten)] + pub disable_clipboard: DisableClipboard, + #[serde(flatten)] + pub enable_file_transfer: EnableFileTransfer, + #[serde(flatten)] + pub show_quality_monitor: ShowQualityMonitor, #[serde(default)] pub keyboard_mode: String, + #[serde(flatten)] + pub view_only: ViewOnly, // The other scalar value must before this #[serde(default, deserialize_with = "PeerConfig::deserialize_options")] - pub options: HashMap, + pub options: HashMap, // not use delete to represent default values // Various data for flutter ui #[serde(default)] pub ui_flutter: HashMap, @@ -228,6 +260,35 @@ pub struct PeerInfoSerde { pub platform: String, } +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct ConfigOidc { + #[serde(default)] + pub max_auth_count: usize, + #[serde(default)] + pub callback_url: String, + #[serde(default)] + pub providers: HashMap, +} + +#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] +pub struct ConfigOidcProvider { + // seconds. 0 means never expires + #[serde(default)] + pub refresh_token_expires_in: u32, + #[serde(default)] + pub client_id: String, + #[serde(default)] + pub client_secret: String, + #[serde(default)] + pub issuer: Option, + #[serde(default)] + pub authorization_endpoint: Option, + #[serde(default)] + pub token_endpoint: Option, + #[serde(default)] + pub userinfo_endpoint: Option, +} + #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] pub struct TransferSerde { #[serde(default)] @@ -256,7 +317,7 @@ fn patch(path: PathBuf) -> PathBuf { .trim() .to_owned(); if user != "root" { - return format!("/home/{}", user).into(); + return format!("/home/{user}").into(); } } } @@ -311,7 +372,7 @@ impl Config2 { pub fn load_path( file: PathBuf, ) -> T { - let cfg = match confy::load_path(&file) { + let cfg = match confy::load_path(file) { Ok(config) => config, Err(err) => { log::error!("Failed to load config: {}", err); @@ -358,20 +419,16 @@ impl Config { config.id = id; id_valid = true; store |= store2; - } else { - if crate::get_modified_time(&Self::file_("")) - .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation - .unwrap_or(crate::get_exe_time()) - < crate::get_exe_time() - { - if !config.id.is_empty() - && config.enc_id.is_empty() - && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 - { - id_valid = true; - store = true; - } - } + } else if crate::get_modified_time(&Self::file_("")) + .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation + .unwrap_or_else(crate::get_exe_time) + < crate::get_exe_time() + && !config.id.is_empty() + && config.enc_id.is_empty() + && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 + { + id_valid = true; + store = true; } if !id_valid { for _ in 0..3 { @@ -436,18 +493,18 @@ impl Config { #[cfg(not(any(target_os = "android", target_os = "ios")))] { #[cfg(not(target_os = "macos"))] - let org = ""; + let org = "".to_owned(); #[cfg(target_os = "macos")] let org = ORG.read().unwrap().clone(); // /var/root for root if let Some(project) = - directories_next::ProjectDirs::from("", &org, &*APP_NAME.read().unwrap()) + directories_next::ProjectDirs::from("", &org, &APP_NAME.read().unwrap()) { let mut path = patch(project.config_dir().to_path_buf()); path.push(p); return path; } - return "".into(); + "".into() } } @@ -497,7 +554,7 @@ impl Config { let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into(); fs::create_dir(&path).ok(); fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); - path.push(format!("ipc{}", postfix)); + path.push(format!("ipc{postfix}")); path.to_str().unwrap_or("").to_owned() } } @@ -531,10 +588,10 @@ impl Config { rendezvous_server = Self::get_rendezvous_servers() .drain(..) .next() - .unwrap_or("".to_owned()); + .unwrap_or_default(); } - if !rendezvous_server.contains(":") { - rendezvous_server = format!("{}:{}", rendezvous_server, RENDEZVOUS_PORT); + if !rendezvous_server.contains(':') { + rendezvous_server = format!("{rendezvous_server}:{RENDEZVOUS_PORT}"); } rendezvous_server } @@ -551,8 +608,8 @@ impl Config { let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL; if serial_obsolute { let ss: Vec = Self::get_option("rendezvous-servers") - .split(",") - .filter(|x| x.contains(".")) + .split(',') + .filter(|x| x.contains('.')) .map(|x| x.to_owned()) .collect(); if !ss.is_empty() { @@ -572,7 +629,7 @@ impl Config { let mut delay = i64::MAX; for (tmp_host, tmp_delay) in ONLINE.lock().unwrap().iter() { if tmp_delay > &0 && tmp_delay < &delay { - delay = tmp_delay.clone(); + delay = *tmp_delay; host = tmp_host.to_string(); } } @@ -639,7 +696,7 @@ impl Config { for x in &ma.bytes()[2..] { id = (id << 8) | (*x as u32); } - id = id & 0x1FFFFFFF; + id &= 0x1FFFFFFF; Some(id.to_string()) } else { None @@ -671,11 +728,7 @@ impl Config { } pub fn get_host_key_confirmed(host: &str) -> bool { - if let Some(true) = CONFIG.read().unwrap().keys_confirmed.get(host) { - true - } else { - false - } + matches!(CONFIG.read().unwrap().keys_confirmed.get(host), Some(true)) } pub fn set_host_key_confirmed(host: &str, v: bool) { @@ -687,7 +740,7 @@ impl Config { config.store(); } - pub fn get_key_pair() -> (Vec, Vec) { + pub fn get_key_pair() -> KeyPair { // lock here to make sure no gen_keypair more than once // no use of CONFIG directly here to ensure no recursive calling in Config::load because of password dec which calling this function let mut lock = KEY_PAIR.lock().unwrap(); @@ -706,7 +759,7 @@ impl Config { }); } *lock = Some(config.key_pair.clone()); - return config.key_pair; + config.key_pair } pub fn get_id() -> String { @@ -841,7 +894,7 @@ impl Config { let ext = path.extension(); if let Some(ext) = ext { let ext = format!("{}.toml", ext.to_string_lossy()); - path.with_extension(&ext) + path.with_extension(ext) } else { path.with_extension("toml") } @@ -853,7 +906,7 @@ const PEERS: &str = "peers"; impl PeerConfig { pub fn load(id: &str) -> PeerConfig { let _lock = CONFIG.read().unwrap(); - match confy::load_path(&Self::path(id)) { + match confy::load_path(Self::path(id)) { Ok(config) => { let mut config: PeerConfig = config; let mut store = false; @@ -861,16 +914,16 @@ impl PeerConfig { decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); config.password = password; store = store || store2; - config.options.get_mut("rdp_password").map(|v| { + if let Some(v) = config.options.get_mut("rdp_password") { let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); *v = password; store = store || store2; - }); - config.options.get_mut("os-password").map(|v| { + } + if let Some(v) = config.options.get_mut("os-password") { let (password, _, store2) = decrypt_str_or_original(v, PASSWORD_ENC_VERSION); *v = password; store = store || store2; - }); + } if store { config.store(id); } @@ -887,34 +940,29 @@ impl PeerConfig { let _lock = CONFIG.read().unwrap(); let mut config = self.clone(); config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); - config - .options - .get_mut("rdp_password") - .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)); - config - .options - .get_mut("os-password") - .map(|v| *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)); + if let Some(v) = config.options.get_mut("rdp_password") { + *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) + } + if let Some(v) = config.options.get_mut("os-password") { + *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION) + }; if let Err(err) = store_path(Self::path(id), config) { log::error!("Failed to store config: {}", err); } } pub fn remove(id: &str) { - fs::remove_file(&Self::path(id)).ok(); + fs::remove_file(Self::path(id)).ok(); } fn path(id: &str) -> PathBuf { - let id_encoded: String; - //If the id contains invalid chars, encode it let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*").unwrap(); - if forbidden_paths.is_match(id) { - id_encoded = - "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str(); + let id_encoded = if forbidden_paths.is_match(id) { + "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str() } else { - id_encoded = id.to_string(); - } + id.to_string() + }; let path: PathBuf = [PEERS, id_encoded.as_str()].iter().collect(); Config::with_extension(Config::path(path)) } @@ -932,26 +980,24 @@ impl PeerConfig { && p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml") }) .map(|p| { - let t = crate::get_modified_time(&p); + let t = crate::get_modified_time(p); let id = p .file_stem() .map(|p| p.to_str().unwrap_or("")) .unwrap_or("") .to_owned(); - let id_decoded_string: String; - if id.starts_with("base64_") && id.len() != 7 { + let id_decoded_string = if id.starts_with("base64_") && id.len() != 7 { let id_decoded = base64::decode(&id[7..], base64::Variant::Original) - .unwrap_or(Vec::new()); - id_decoded_string = - String::from_utf8_lossy(&id_decoded).as_ref().to_owned(); + .unwrap_or_default(); + String::from_utf8_lossy(&id_decoded).as_ref().to_owned() } else { - id_decoded_string = id; - } + id + }; let c = PeerConfig::load(&id_decoded_string); if c.info.platform.is_empty() { - fs::remove_file(&p).ok(); + fs::remove_file(p).ok(); } (id_decoded_string, t, c) }) @@ -967,31 +1013,117 @@ impl PeerConfig { serde_field_string!( default_view_style, deserialize_view_style, - "original".to_owned() + UserDefaultConfig::read().get("view_style") ); serde_field_string!( default_scroll_style, deserialize_scroll_style, - "scrollauto".to_owned() + UserDefaultConfig::read().get("scroll_style") ); serde_field_string!( default_image_quality, deserialize_image_quality, - "balanced".to_owned() + UserDefaultConfig::read().get("image_quality") ); + fn default_custom_image_quality() -> Vec { + let f: f64 = UserDefaultConfig::read() + .get("custom_image_quality") + .parse() + .unwrap_or(50.0); + vec![f as _] + } + + fn deserialize_custom_image_quality<'de, D>(deserializer: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + let v: Vec = de::Deserialize::deserialize(deserializer)?; + if v.len() == 1 && v[0] >= 10 && v[0] <= 100 { + Ok(v) + } else { + Ok(Self::default_custom_image_quality()) + } + } + fn deserialize_options<'de, D>(deserializer: D) -> Result, D::Error> where D: de::Deserializer<'de>, { let mut mp: HashMap = de::Deserialize::deserialize(deserializer)?; - if !mp.contains_key("codec-preference") { - mp.insert("codec-preference".to_owned(), "auto".to_owned()); + let mut key = "codec-preference"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); + } + key = "custom-fps"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); + } + key = "zoom-cursor"; + if !mp.contains_key(key) { + mp.insert(key.to_owned(), UserDefaultConfig::read().get(key)); } Ok(mp) } } +serde_field_bool!( + ShowRemoteCursor, + "show_remote_cursor", + default_show_remote_cursor, + "ShowRemoteCursor::default_show_remote_cursor" +); +serde_field_bool!( + ShowQualityMonitor, + "show_quality_monitor", + default_show_quality_monitor, + "ShowQualityMonitor::default_show_quality_monitor" +); +serde_field_bool!( + DisableAudio, + "disable_audio", + default_disable_audio, + "DisableAudio::default_disable_audio" +); +serde_field_bool!( + EnableFileTransfer, + "enable_file_transfer", + default_enable_file_transfer, + "EnableFileTransfer::default_enable_file_transfer" +); +serde_field_bool!( + DisableClipboard, + "disable_clipboard", + default_disable_clipboard, + "DisableClipboard::default_disable_clipboard" +); +serde_field_bool!( + LockAfterSessionEnd, + "lock_after_session_end", + default_lock_after_session_end, + "LockAfterSessionEnd::default_lock_after_session_end" +); +serde_field_bool!( + PrivacyMode, + "privacy_mode", + default_privacy_mode, + "PrivacyMode::default_privacy_mode" +); + +serde_field_bool!( + AllowSwapKey, + "allow_swap_key", + default_allow_swap_key, + "AllowSwapKey::default_allow_swap_key" +); + +serde_field_bool!( + ViewOnly, + "view_only", + default_view_only, + "ViewOnly::default_view_only" +); + #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct LocalConfig { #[serde(default)] @@ -1141,7 +1273,7 @@ pub struct LanPeers { impl LanPeers { pub fn load() -> LanPeers { let _lock = CONFIG.read().unwrap(); - match confy::load_path(&Config::file_("_lan_peers")) { + match confy::load_path(Config::file_("_lan_peers")) { Ok(peers) => peers, Err(err) => { log::error!("Failed to load lan peers: {}", err); @@ -1150,9 +1282,9 @@ impl LanPeers { } } - pub fn store(peers: &Vec) { + pub fn store(peers: &[DiscoveryPeer]) { let f = LanPeers { - peers: peers.clone(), + peers: peers.to_owned(), }; if let Err(err) = store_path(Config::file_("_lan_peers"), f) { log::error!("Failed to store lan peers: {}", err); @@ -1198,6 +1330,105 @@ impl HwCodecConfig { } } +#[derive(Debug, Default, Serialize, Deserialize, Clone)] +pub struct UserDefaultConfig { + #[serde(default)] + options: HashMap, +} + +impl UserDefaultConfig { + pub fn read() -> UserDefaultConfig { + let mut cfg = USER_DEFAULT_CONFIG.write().unwrap(); + if cfg.1.elapsed() > Duration::from_secs(1) { + *cfg = (Self::load(), Instant::now()); + } + cfg.0.clone() + } + + pub fn load() -> UserDefaultConfig { + Config::load_::("_default") + } + + #[inline] + fn store(&self) { + Config::store_(self, "_default"); + } + + pub fn get(&self, key: &str) -> String { + match key { + "view_style" => self.get_string(key, "original", vec!["adaptive"]), + "scroll_style" => self.get_string(key, "scrollauto", vec!["scrollbar"]), + "image_quality" => self.get_string(key, "balanced", vec!["best", "low", "custom"]), + "codec-preference" => self.get_string(key, "auto", vec!["vp9", "h264", "h265"]), + "custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 100.0), + "custom-fps" => self.get_double_string(key, 30.0, 10.0, 120.0), + _ => self + .options + .get(key) + .map(|v| v.to_string()) + .unwrap_or_default(), + } + } + + pub fn set(&mut self, key: String, value: String) { + self.options.insert(key, value); + self.store(); + } + + #[inline] + fn get_string(&self, key: &str, default: &str, others: Vec<&str>) -> String { + match self.options.get(key) { + Some(option) => { + if others.contains(&option.as_str()) { + option.to_owned() + } else { + default.to_owned() + } + } + None => default.to_owned(), + } + } + + #[inline] + fn get_double_string(&self, key: &str, default: f64, min: f64, max: f64) -> String { + match self.options.get(key) { + Some(option) => { + let v: f64 = option.parse().unwrap_or(default); + if v >= min && v <= max { + v.to_string() + } else { + default.to_string() + } + } + None => default.to_string(), + } + } +} + +impl ConfigOidc { + fn suffix() -> &'static str { + "_oidc" + } + + fn load() -> Self { + Config::load_::(Self::suffix())._load_env() + } + + fn _load_env(mut self) -> Self { + use std::env; + for (k, mut v) in &mut self.providers { + if let Ok(client_id) = env::var(format!("OIDC-{}-CLIENT-ID", k.to_uppercase())) { + v.client_id = client_id; + } + if let Ok(client_secret) = env::var(format!("OIDC-{}-CLIENT-SECRET", k.to_uppercase())) + { + v.client_secret = client_secret; + } + } + self + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index fec8b8670..41160f49d 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -13,13 +13,13 @@ use crate::{ config::{Config, COMPRESS_LEVEL}, }; -pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType { +pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType { let mut dir = FileDirectory { - path: get_string(&path), + path: get_string(path), ..Default::default() }; #[cfg(windows)] - if "/" == &get_string(&path) { + if "/" == &get_string(path) { let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() }; for i in 0..32 { if drives & (1 << i) != 0 { @@ -36,74 +36,70 @@ pub fn read_dir(path: &PathBuf, include_hidden: bool) -> ResultType String { +pub fn get_file_name(p: &Path) -> String { p.file_name() .map(|p| p.to_str().unwrap_or("")) .unwrap_or("") @@ -111,7 +107,7 @@ pub fn get_file_name(p: &PathBuf) -> String { } #[inline] -pub fn get_string(path: &PathBuf) -> String { +pub fn get_string(path: &Path) -> String { path.to_str().unwrap_or("").to_owned() } @@ -127,14 +123,14 @@ pub fn get_home_as_string() -> String { fn read_dir_recursive( path: &PathBuf, - prefix: &PathBuf, + prefix: &Path, include_hidden: bool, ) -> ResultType> { let mut files = Vec::new(); if path.is_dir() { // to-do: symbol link handling, cp the link rather than the content // to-do: file mode, for unix - let fd = read_dir(&path, include_hidden)?; + let fd = read_dir(path, include_hidden)?; for entry in fd.entries.iter() { match entry.entry_type.enum_value() { Ok(FileType::File) => { @@ -158,7 +154,7 @@ fn read_dir_recursive( } Ok(files) } else if path.is_file() { - let (size, modified_time) = if let Ok(meta) = std::fs::metadata(&path) { + let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) { ( meta.len(), meta.modified() @@ -167,7 +163,7 @@ fn read_dir_recursive( .map(|x| x.as_secs()) .unwrap_or(0) }) - .unwrap_or(0) as u64, + .unwrap_or(0), ) } else { (0, 0) @@ -249,7 +245,7 @@ pub struct RemoveJobMeta { #[inline] fn get_ext(name: &str) -> &str { - if let Some(i) = name.rfind(".") { + if let Some(i) = name.rfind('.') { return &name[i + 1..]; } "" @@ -270,6 +266,7 @@ fn is_compressed_file(name: &str) -> bool { } impl TransferJob { + #[allow(clippy::too_many_arguments)] pub fn new_write( id: i32, remote: String, @@ -281,7 +278,7 @@ impl TransferJob { enable_overwrite_detection: bool, ) -> Self { log::info!("new write {}", path); - let total_size = files.iter().map(|x| x.size as u64).sum(); + let total_size = files.iter().map(|x| x.size).sum(); Self { id, remote, @@ -307,7 +304,7 @@ impl TransferJob { ) -> ResultType { log::info!("new read {}", path); let files = get_recursive_files(&path, show_hidden)?; - let total_size = files.iter().map(|x| x.size as u64).sum(); + let total_size = files.iter().map(|x| x.size).sum(); Ok(Self { id, remote, @@ -363,7 +360,7 @@ impl TransferJob { let entry = &self.files[file_num]; let path = self.join(&entry.name); let download_path = format!("{}.download", get_string(&path)); - std::fs::rename(&download_path, &path).ok(); + std::fs::rename(download_path, &path).ok(); filetime::set_file_mtime( &path, filetime::FileTime::from_unix_time(entry.modified_time as _, 0), @@ -378,7 +375,7 @@ impl TransferJob { let entry = &self.files[file_num]; let path = self.join(&entry.name); let download_path = format!("{}.download", get_string(&path)); - std::fs::remove_file(&download_path).ok(); + std::fs::remove_file(download_path).ok(); } } @@ -433,7 +430,7 @@ impl TransferJob { } let name = &self.files[file_num].name; if self.file.is_none() { - match File::open(self.join(&name)).await { + match File::open(self.join(name)).await { Ok(file) => { self.file = Some(file); self.file_confirmed = false; @@ -447,20 +444,15 @@ impl TransferJob { } } } - if self.enable_overwrite_detection { - if !self.file_confirmed() { - if !self.file_is_waiting() { - self.send_current_digest(stream).await?; - self.set_file_is_waiting(true); - } - return Ok(None); + if self.enable_overwrite_detection && !self.file_confirmed() { + if !self.file_is_waiting() { + self.send_current_digest(stream).await?; + self.set_file_is_waiting(true); } + return Ok(None); } const BUF_SIZE: usize = 128 * 1024; - let mut buf: Vec = Vec::with_capacity(BUF_SIZE); - unsafe { - buf.set_len(BUF_SIZE); - } + let mut buf: Vec = vec![0; BUF_SIZE]; let mut compressed = false; let mut offset: usize = 0; loop { @@ -582,10 +574,7 @@ impl TransferJob { #[inline] pub fn job_completed(&self) -> bool { // has no error, Condition 2 - if !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) { - return true; - } - return false; + !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) } /// Get job error message, useful for getting status when job had finished @@ -660,7 +649,7 @@ pub fn new_dir(id: i32, path: String, files: Vec) -> Message { resp.set_dir(FileDirectory { id, path, - entries: files.into(), + entries: files, ..Default::default() }); let mut msg_out = Message::new(); @@ -692,7 +681,7 @@ pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec) action.set_receive(FileTransferReceiveRequest { id, path, - files: files.into(), + files, file_num, ..Default::default() }); @@ -736,8 +725,8 @@ pub fn remove_job(id: i32, jobs: &mut Vec) { } #[inline] -pub fn get_job(id: i32, jobs: &mut Vec) -> Option<&mut TransferJob> { - jobs.iter_mut().filter(|x| x.id() == id).next() +pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> { + jobs.iter_mut().find(|x| x.id() == id) } pub async fn handle_read_jobs( @@ -789,7 +778,7 @@ pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> { remove_all_empty_dir(&path.join(&entry.name)).ok(); } Ok(FileType::DirLink) | Ok(FileType::FileLink) => { - std::fs::remove_file(&path.join(&entry.name)).ok(); + std::fs::remove_file(path.join(&entry.name)).ok(); } _ => {} } @@ -813,7 +802,7 @@ pub fn create_dir(dir: &str) -> ResultType<()> { #[inline] pub fn transform_windows_path(entries: &mut Vec) { for entry in entries { - entry.name = entry.name.replace("\\", "/"); + entry.name = entry.name.replace('\\', "/"); } } @@ -834,14 +823,19 @@ pub fn is_write_need_confirmation( let modified_time = metadata.modified()?; let remote_mt = Duration::from_secs(digest.last_modified); let local_mt = modified_time.duration_since(UNIX_EPOCH)?; + // [Note] + // We decide to give the decision whether to override the existing file to users, + // which obey the behavior of the file manager in our system. + let mut is_identical = false; if remote_mt == local_mt && digest.file_size == metadata.len() { - return Ok(DigestCheckResult::IsSame); + is_identical = true; } Ok(DigestCheckResult::NeedConfirm(FileTransferDigest { id: digest.id, file_num: digest.file_num, last_modified: local_mt.as_secs(), file_size: metadata.len(), + is_identical, ..Default::default() })) } else { diff --git a/libs/hbb_common/src/keyboard.rs b/libs/hbb_common/src/keyboard.rs new file mode 100644 index 000000000..10979f520 --- /dev/null +++ b/libs/hbb_common/src/keyboard.rs @@ -0,0 +1,39 @@ +use std::{fmt, slice::Iter, str::FromStr}; + +use crate::protos::message::KeyboardMode; + +impl fmt::Display for KeyboardMode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + KeyboardMode::Legacy => write!(f, "legacy"), + KeyboardMode::Map => write!(f, "map"), + KeyboardMode::Translate => write!(f, "translate"), + KeyboardMode::Auto => write!(f, "auto"), + } + } +} + +impl FromStr for KeyboardMode { + type Err = (); + fn from_str(s: &str) -> Result { + match s { + "legacy" => Ok(KeyboardMode::Legacy), + "map" => Ok(KeyboardMode::Map), + "translate" => Ok(KeyboardMode::Translate), + "auto" => Ok(KeyboardMode::Auto), + _ => Err(()), + } + } +} + +impl KeyboardMode { + pub fn iter() -> Iter<'static, KeyboardMode> { + static KEYBOARD_MODES: [KeyboardMode; 4] = [ + KeyboardMode::Legacy, + KeyboardMode::Map, + KeyboardMode::Translate, + KeyboardMode::Auto, + ]; + KEYBOARD_MODES.iter() + } +} diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs index d1893564c..799093d24 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -40,6 +40,10 @@ pub use tokio_socks::TargetAddr; pub mod password_security; pub use chrono; pub use directories_next; +pub use libc; +pub mod keyboard; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub use sysinfo; #[cfg(feature = "quic")] pub type Stream = quic::Connection; @@ -95,8 +99,24 @@ pub type ResultType = anyhow::Result; pub struct AddrMangle(); +#[inline] +pub fn try_into_v4(addr: SocketAddr) -> SocketAddr { + match addr { + SocketAddr::V6(v6) if !addr.ip().is_loopback() => { + if let Some(v4) = v6.ip().to_ipv4() { + SocketAddr::new(IpAddr::V4(v4), addr.port()) + } else { + addr + } + } + _ => addr, + } +} + impl AddrMangle { pub fn encode(addr: SocketAddr) -> Vec { + // not work with [:1]: + let addr = try_into_v4(addr); match addr { SocketAddr::V4(addr_v4) => { let tm = (SystemTime::now() @@ -128,22 +148,20 @@ impl AddrMangle { } pub fn decode(bytes: &[u8]) -> SocketAddr { + use std::convert::TryInto; + if bytes.len() > 16 { if bytes.len() != 18 { return Config::get_any_listen_addr(false); } - #[allow(invalid_value)] - let mut tmp: [u8; 2] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; - tmp.copy_from_slice(&bytes[16..]); + let tmp: [u8; 2] = bytes[16..].try_into().unwrap(); let port = u16::from_le_bytes(tmp); - #[allow(invalid_value)] - let mut tmp: [u8; 16] = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; - tmp.copy_from_slice(&bytes[..16]); + let tmp: [u8; 16] = bytes[..16].try_into().unwrap(); let ip = std::net::Ipv6Addr::from(tmp); return SocketAddr::new(IpAddr::V6(ip), port); } let mut padded = [0u8; 16]; - padded[..bytes.len()].copy_from_slice(&bytes); + padded[..bytes.len()].copy_from_slice(bytes); let number = u128::from_le_bytes(padded); let tm = (number >> 17) & (u32::max_value() as u128); let ip = (((number >> 49) - tm) as u32).to_le_bytes(); @@ -157,21 +175,9 @@ impl AddrMangle { pub fn get_version_from_url(url: &str) -> String { let n = url.chars().count(); - let a = url - .chars() - .rev() - .enumerate() - .filter(|(_, x)| x == &'-') - .next() - .map(|(i, _)| i); + let a = url.chars().rev().position(|x| x == '-'); if let Some(a) = a { - let b = url - .chars() - .rev() - .enumerate() - .filter(|(_, x)| x == &'.') - .next() - .map(|(i, _)| i); + let b = url.chars().rev().position(|x| x == '.'); if let Some(b) = b { if a > b { if url @@ -194,22 +200,23 @@ pub fn get_version_from_url(url: &str) -> String { } pub fn gen_version() { + println!("cargo:rerun-if-changed=Cargo.toml"); use std::io::prelude::*; let mut file = File::create("./src/version.rs").unwrap(); - for line in read_lines("Cargo.toml").unwrap() { - if let Ok(line) = line { - let ab: Vec<&str> = line.split("=").map(|x| x.trim()).collect(); - if ab.len() == 2 && ab[0] == "version" { - file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) - .ok(); - break; - } + for line in read_lines("Cargo.toml").unwrap().flatten() { + let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect(); + if ab.len() == 2 && ab[0] == "version" { + file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) + .ok(); + break; } } // generate build date let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); - file.write_all(format!("pub const BUILD_DATE: &str = \"{}\";", build_date).as_bytes()) - .ok(); + file.write_all( + format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(), + ) + .ok(); file.sync_all().ok(); } @@ -229,20 +236,20 @@ pub fn is_valid_custom_id(id: &str) -> bool { pub fn get_version_number(v: &str) -> i64 { let mut n = 0; - for x in v.split(".") { + for x in v.split('.') { n = n * 1000 + x.parse::().unwrap_or(0); } n } pub fn get_modified_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(&path) + std::fs::metadata(path) .map(|m| m.modified().unwrap_or(UNIX_EPOCH)) .unwrap_or(UNIX_EPOCH) } pub fn get_created_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(&path) + std::fs::metadata(path) .map(|m| m.created().unwrap_or(UNIX_EPOCH)) .unwrap_or(UNIX_EPOCH) } @@ -275,9 +282,79 @@ pub fn get_time() -> i64 { .unwrap_or(0) as _ } +#[inline] +pub fn is_ipv4_str(id: &str) -> bool { + regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") + .unwrap() + .is_match(id) +} + +#[inline] +pub fn is_ipv6_str(id: &str) -> bool { + regex::Regex::new(r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$") + .unwrap() + .is_match(id) +} + +#[inline] +pub fn is_ip_str(id: &str) -> bool { + is_ipv4_str(id) || is_ipv6_str(id) +} + +#[inline] +pub fn is_domain_port_str(id: &str) -> bool { + // modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname. + // according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700, + // there is no digits in TLD, and length is 2~63. + regex::Regex::new( + r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$", + ) + .unwrap() + .is_match(id) +} + +pub fn init_log(_is_async: bool, _name: &str) -> Option { + #[cfg(debug_assertions)] + { + use env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + None + } + #[cfg(not(debug_assertions))] + { + // https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write + // though async logger more efficient, but it also causes more problems, disable it for now + let mut logger_holder: Option = None; + let mut path = config::Config::log_path(); + if !_name.is_empty() { + path.push(_name); + } + use flexi_logger::*; + if let Ok(x) = Logger::try_with_env_or_str("debug") { + logger_holder = x + .log_to_file(FileSpec::default().directory(path)) + .write_mode(if _is_async { + WriteMode::Async + } else { + WriteMode::Direct + }) + .format(opt_format) + .rotate( + Criterion::Age(Age::Day), + Naming::Timestamps, + Cleanup::KeepLogFiles(6), + ) + .start() + .ok(); + } + logger_holder + } +} + #[cfg(test)] -mod tests { +mod test { use super::*; + #[test] fn test_mangle() { let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); @@ -285,7 +362,7 @@ mod tests { let addr = "[2001:db8::1]:8080".parse::().unwrap(); assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); - + let addr = "[2001:db8:ff::1111]:80".parse::().unwrap(); assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); } @@ -299,11 +376,57 @@ mod tests { "failed" ); } -} -#[inline] -pub fn is_ipv4_str(id: &str) -> bool { - regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") - .unwrap() - .is_match(id) -} \ No newline at end of file + #[test] + fn test_ipv6() { + assert!(is_ipv6_str("1:2:3")); + assert!(is_ipv6_str("[ab:2:3]:12")); + assert!(is_ipv6_str("[ABEF:2a:3]:12")); + assert!(!is_ipv6_str("[ABEG:2a:3]:12")); + assert!(!is_ipv6_str("1[ab:2:3]:12")); + assert!(!is_ipv6_str("1.1.1.1")); + assert!(is_ip_str("1.1.1.1")); + assert!(!is_ipv6_str("1:2:")); + assert!(is_ipv6_str("1:2::0")); + assert!(is_ipv6_str("[1:2::0]:1")); + assert!(!is_ipv6_str("[1:2::0]:")); + assert!(!is_ipv6_str("1:2::0]:1")); + } + + #[test] + fn test_hostname_port() { + assert!(!is_domain_port_str("a:12")); + assert!(!is_domain_port_str("a.b.c:12")); + assert!(is_domain_port_str("test.com:12")); + assert!(is_domain_port_str("test-UPPER.com:12")); + assert!(is_domain_port_str("some-other.domain.com:12")); + assert!(!is_domain_port_str("under_score:12")); + assert!(!is_domain_port_str("a@bc:12")); + assert!(!is_domain_port_str("1.1.1.1:12")); + assert!(!is_domain_port_str("1.2.3:12")); + assert!(!is_domain_port_str("1.2.3.45:12")); + assert!(!is_domain_port_str("a.b.c:123456")); + assert!(!is_domain_port_str("---:12")); + assert!(!is_domain_port_str(".:12")); + // todo: should we also check for these edge cases? + // out-of-range port + assert!(is_domain_port_str("test.com:0")); + assert!(is_domain_port_str("test.com:98989")); + } + + #[test] + fn test_mangle2() { + let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap(); + let addr_v4 = "127.0.0.1:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4); + assert_eq!( + AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())), + addr_v4 + ); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4); + let addr_v6 = "[ef::fe]:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); + let addr_v6 = "[::1]:8080".parse().unwrap(); + assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); + } +} diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs index 602906990..ddfe28baa 100644 --- a/libs/hbb_common/src/password_security.rs +++ b/libs/hbb_common/src/password_security.rs @@ -104,7 +104,7 @@ pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, if s.len() > VERSION_LEN { let version = &s[..VERSION_LEN]; if version == "00" { - if let Ok(v) = decrypt(&s[VERSION_LEN..].as_bytes()) { + if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) { return ( String::from_utf8_lossy(&v).to_string(), true, @@ -149,7 +149,7 @@ pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, boo } fn encrypt(v: &[u8]) -> Result { - if v.len() > 0 { + if !v.is_empty() { symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) } else { Err(()) @@ -157,7 +157,7 @@ fn encrypt(v: &[u8]) -> Result { } fn decrypt(v: &[u8]) -> Result, ()> { - if v.len() > 0 { + if !v.is_empty() { base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false)) } else { Err(()) @@ -192,51 +192,51 @@ mod test { let data = "Hello World"; let encrypted = encrypt_str_or_original(data, version); let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); - println!("data: {}", data); - println!("encrypted: {}", encrypted); - println!("decrypted: {}", decrypted); + println!("data: {data}"); + println!("encrypted: {encrypted}"); + println!("decrypted: {decrypted}"); assert_eq!(data, decrypted); assert_eq!(version, &encrypted[..2]); - assert_eq!(succ, true); - assert_eq!(store, false); + assert!(succ); + assert!(!store); let (_, _, store) = decrypt_str_or_original(&encrypted, "99"); - assert_eq!(store, true); - assert_eq!(decrypt_str_or_original(&decrypted, version).1, false); + assert!(store); + assert!(!decrypt_str_or_original(&decrypted, version).1); assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted); println!("test vec"); let data: Vec = vec![1, 2, 3, 4, 5, 6]; let encrypted = encrypt_vec_or_original(&data, version); let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); - println!("data: {:?}", data); - println!("encrypted: {:?}", encrypted); - println!("decrypted: {:?}", decrypted); + println!("data: {data:?}"); + println!("encrypted: {encrypted:?}"); + println!("decrypted: {decrypted:?}"); assert_eq!(data, decrypted); assert_eq!(version.as_bytes(), &encrypted[..2]); - assert_eq!(store, false); - assert_eq!(succ, true); + assert!(!store); + assert!(succ); let (_, _, store) = decrypt_vec_or_original(&encrypted, "99"); - assert_eq!(store, true); - assert_eq!(decrypt_vec_or_original(&decrypted, version).1, false); + assert!(store); + assert!(!decrypt_vec_or_original(&decrypted, version).1); assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted); println!("test original"); let data = version.to_string() + "Hello World"; let (decrypted, succ, store) = decrypt_str_or_original(&data, version); assert_eq!(data, decrypted); - assert_eq!(store, true); - assert_eq!(succ, false); + assert!(store); + assert!(!succ); let verbytes = version.as_bytes(); - let data: Vec = vec![verbytes[0] as u8, verbytes[1] as u8, 1, 2, 3, 4, 5, 6]; + let data: Vec = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6]; let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); assert_eq!(data, decrypted); - assert_eq!(store, true); - assert_eq!(succ, false); + assert!(store); + assert!(!succ); let (_, succ, store) = decrypt_str_or_original("", version); - assert_eq!(store, false); - assert_eq!(succ, false); - let (_, succ, store) = decrypt_vec_or_original(&vec![], version); - assert_eq!(store, false); - assert_eq!(succ, false); + assert!(!store); + assert!(!succ); + let (_, succ, store) = decrypt_vec_or_original(&[], version); + assert!(!store); + assert!(!succ); } } diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 4c6375dd7..f6133415a 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,15 +1,16 @@ use crate::ResultType; +use std::{collections::HashMap, process::Command}; lazy_static::lazy_static! { - pub static ref DISTRO: Disto = Disto::new(); + pub static ref DISTRO: Distro = Distro::new(); } -pub struct Disto { +pub struct Distro { pub name: String, pub version_id: String, } -impl Disto { +impl Distro { fn new() -> Self { let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release".to_owned()) .unwrap_or_default() @@ -32,7 +33,7 @@ pub fn get_display_server() -> String { // loginctl has not given the expected output. try something else. if let Ok(sid) = std::env::var("XDG_SESSION_ID") { // could also execute "cat /proc/self/sessionid" - session = sid.to_owned(); + session = sid; } if session.is_empty() { session = run_cmds("cat /proc/self/sessionid".to_owned()).unwrap_or_default(); @@ -60,10 +61,10 @@ fn get_display_server_of_session(session: &str) -> String { .replace("TTY=", "") .trim_end() .into(); - if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{}.\\\\+Xorg\"", tty)) + if let Ok(xorg_results) = run_cmds(format!("ps -e | grep \"{tty}.\\\\+Xorg\"")) // And check if Xorg is running on that tty { - if xorg_results.trim_end().to_string() != "" { + if xorg_results.trim_end() != "" { // If it is, manually return "x11", otherwise return tty return "x11".to_owned(); } @@ -74,21 +75,23 @@ fn get_display_server_of_session(session: &str) -> String { } else { "".to_owned() }; - if display_server.is_empty() { + if display_server.is_empty() || display_server == "tty" { // loginctl has not given the expected output. try something else. if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { display_server = sestype; } } - // If the session is not a tty, then just return the type as usual - display_server + if display_server == "" { + display_server = "x11".to_owned(); + } + display_server.to_lowercase() } pub fn get_values_of_seat0(indices: Vec) -> Vec { if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains("seat0") { - if let Some(sid) = line.split_whitespace().nth(0) { + if let Some(sid) = line.split_whitespace().next() { if is_active(sid) { return indices .into_iter() @@ -103,7 +106,7 @@ pub fn get_values_of_seat0(indices: Vec) -> Vec { // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 if let Ok(output) = run_loginctl(None) { for line in String::from_utf8_lossy(&output.stdout).lines() { - if let Some(sid) = line.split_whitespace().nth(0) { + if let Some(sid) = line.split_whitespace().next() { let d = get_display_server_of_session(sid); if is_active(sid) && d != "tty" { return indices @@ -155,3 +158,42 @@ fn run_loginctl(args: Option>) -> std::io::Result ResultType<()> { + let cmds: HashMap<&str, Vec<&str>> = HashMap::from([ + ("notify-send", [title, msg].to_vec()), + ( + "zenity", + [ + "--info", + "--timeout", + if forever { "0" } else { "3" }, + "--title", + title, + "--text", + msg, + ] + .to_vec(), + ), + ("kdialog", ["--title", title, "--msgbox", msg].to_vec()), + ( + "xmessage", + [ + "-center", + "-timeout", + if forever { "0" } else { "3" }, + title, + msg, + ] + .to_vec(), + ), + ]); + for (k, v) in cmds { + if Command::new(k).args(v).spawn().is_ok() { + return Ok(()); + } + } + crate::bail!("failed to post system message"); +} diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs new file mode 100644 index 000000000..dd83a8738 --- /dev/null +++ b/libs/hbb_common/src/platform/macos.rs @@ -0,0 +1,55 @@ +use crate::ResultType; +use osascript; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Serialize)] +struct AlertParams { + title: String, + message: String, + alert_type: String, + buttons: Vec, +} + +#[derive(Deserialize)] +struct AlertResult { + #[serde(rename = "buttonReturned")] + button: String, +} + +/// Firstly run the specified app, then alert a dialog. Return the clicked button value. +/// +/// # Arguments +/// +/// * `app` - The app to execute the script. +/// * `alert_type` - Alert type. . informational, warning, critical +/// * `title` - The alert title. +/// * `message` - The alert message. +/// * `buttons` - The buttons to show. +pub fn alert( + app: String, + alert_type: String, + title: String, + message: String, + buttons: Vec, +) -> ResultType { + let script = osascript::JavaScript::new(&format!( + " + var App = Application('{}'); + App.includeStandardAdditions = true; + return App.displayAlert($params.title, {{ + message: $params.message, + 'as': $params.alert_type, + buttons: $params.buttons, + }}); + ", + app + )); + + let result: AlertResult = script.execute_with_params(AlertParams { + title, + message, + alert_type, + buttons, + })?; + Ok(result.button) +} diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs index 8daba257f..c37c8aaf5 100644 --- a/libs/hbb_common/src/platform/mod.rs +++ b/libs/hbb_common/src/platform/mod.rs @@ -1,2 +1,62 @@ #[cfg(target_os = "linux")] pub mod linux; + +#[cfg(target_os = "macos")] +pub mod macos; + +use crate::{config::Config, log}; +use std::process::exit; + +static mut GLOBAL_CALLBACK: Option> = None; + +extern "C" fn breakdown_signal_handler(sig: i32) { + let mut stack = vec![]; + backtrace::trace(|frame| { + backtrace::resolve_frame(frame, |symbol| { + if let Some(name) = symbol.name() { + stack.push(name.to_string()); + } + }); + true // keep going to the next frame + }); + let mut info = String::default(); + if stack.iter().any(|s| { + s.contains(&"nouveau_pushbuf_kick") + || s.to_lowercase().contains("nvidia") + || s.contains("gdk_window_end_draw_frame") + }) { + Config::set_option("allow-always-software-render".to_string(), "Y".to_string()); + info = "Always use software rendering will be set.".to_string(); + log::info!("{}", info); + } + log::error!( + "Got signal {} and exit. stack:\n{}", + sig, + stack.join("\n").to_string() + ); + if !info.is_empty() { + #[cfg(target_os = "linux")] + linux::system_message( + "RustDesk", + &format!("Got signal {} and exit.{}", sig, info), + true, + ) + .ok(); + } + unsafe { + if let Some(callback) = &GLOBAL_CALLBACK { + callback() + } + } + exit(0); +} + +pub fn register_breakdown_handler(callback: T) +where + T: Fn() + 'static, +{ + unsafe { + GLOBAL_CALLBACK = Some(Box::new(callback)); + libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); + } +} diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs new file mode 100644 index 000000000..57d9b68fe --- /dev/null +++ b/libs/hbb_common/src/protos/mod.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index b7cb13754..2d9b5a984 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -9,11 +9,48 @@ use std::net::SocketAddr; use tokio::net::ToSocketAddrs; use tokio_socks::{IntoTargetAddr, TargetAddr}; -pub fn test_if_valid_server(host: &str) -> String { - let mut host = host.to_owned(); - if !host.contains(":") { - host = format!("{}:{}", host, 0); +#[inline] +pub fn check_port(host: T, port: i32) -> String { + let host = host.to_string(); + if crate::is_ipv6_str(&host) { + if host.starts_with('[') { + return host; + } + return format!("[{host}]:{port}"); } + if !host.contains(':') { + return format!("{host}:{port}"); + } + host +} + +#[inline] +pub fn increase_port(host: T, offset: i32) -> String { + let host = host.to_string(); + if crate::is_ipv6_str(&host) { + if host.starts_with('[') { + let tmp: Vec<&str> = host.split("]:").collect(); + if tmp.len() == 2 { + let port: i32 = tmp[1].parse().unwrap_or(0); + if port > 0 { + return format!("{}]:{}", tmp[0], port + offset); + } + } + } + } else if host.contains(':') { + let tmp: Vec<&str> = host.split(':').collect(); + if tmp.len() == 2 { + let port: i32 = tmp[1].parse().unwrap_or(0); + if port > 0 { + return format!("{}:{}", tmp[0], port + offset); + } + } + } + host +} + +pub fn test_if_valid_server(host: &str) -> String { + let host = check_port(host, 0); use std::net::ToSocketAddrs; match Config::get_network_type() { @@ -34,7 +71,7 @@ pub trait IsResolvedSocketAddr { impl IsResolvedSocketAddr for SocketAddr { fn resolve(&self) -> Option<&SocketAddr> { - Some(&self) + Some(self) } } @@ -83,12 +120,12 @@ pub async fn connect_tcp_local< if let Some(target) = target.resolve() { if let Some(local) = local { if local.is_ipv6() && target.is_ipv4() { - let target = query_nip_io(&target).await?; - return Ok(FramedStream::new(target, Some(local), ms_timeout).await?); + let target = query_nip_io(target).await?; + return FramedStream::new(target, Some(local), ms_timeout).await; } } } - Ok(FramedStream::new(target, local, ms_timeout).await?) + FramedStream::new(target, local, ms_timeout).await } #[inline] @@ -103,16 +140,15 @@ pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port())) .await? - .filter(|x| x.is_ipv6()) - .next() + .find(|x| x.is_ipv6()) .context("Failed to get ipv6 from nip.io") } #[inline] pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { if !ipv4 && crate::is_ipv4_str(&addr) { - if let Some(ip) = addr.split(":").next() { - return addr.replace(ip, &format!("{}.nip.io", ip)); + if let Some(ip) = addr.split(':').next() { + return addr.replace(ip, &format!("{ip}.nip.io")); } } addr @@ -127,7 +163,7 @@ async fn test_target(target: &str) -> ResultType { tokio::net::lookup_host(target) .await? .next() - .context(format!("Failed to look up host for {}", target)) + .context(format!("Failed to look up host for {target}")) } #[inline] @@ -216,4 +252,30 @@ mod tests { } assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()).await.is_err()); } + + #[test] + fn test_test_if_valid_server() { + assert!(!test_if_valid_server("a").is_empty()); + // on Linux, "1" is resolved to "0.0.0.1" + assert!(test_if_valid_server("1.1.1.1").is_empty()); + assert!(test_if_valid_server("1.1.1.1:1").is_empty()); + } + + #[test] + fn test_check_port() { + assert_eq!(check_port("[1:2]:12", 32), "[1:2]:12"); + assert_eq!(check_port("1:2", 32), "[1:2]:32"); + assert_eq!(check_port("z1:2", 32), "z1:2"); + assert_eq!(check_port("1.1.1.1", 32), "1.1.1.1:32"); + assert_eq!(check_port("1.1.1.1:32", 32), "1.1.1.1:32"); + assert_eq!(check_port("test.com:32", 0), "test.com:32"); + assert_eq!(increase_port("[1:2]:12", 1), "[1:2]:13"); + assert_eq!(increase_port("1.2.2.4:12", 1), "1.2.2.4:13"); + assert_eq!(increase_port("1.2.2.4", 1), "1.2.2.4"); + assert_eq!(increase_port("test.com", 1), "test.com"); + assert_eq!(increase_port("test.com:13", 4), "test.com:17"); + assert_eq!(increase_port("1:13", 4), "1:13"); + assert_eq!(increase_port("22:1:13", 4), "22:1:13"); + assert_eq!(increase_port("z1:2", 1), "z1:3"); + } } diff --git a/libs/hbb_common/src/tcp.rs b/libs/hbb_common/src/tcp.rs index f46d836da..f574e8309 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -1,11 +1,12 @@ use crate::{bail, bytes_codec::BytesCodec, ResultType}; +use anyhow::Context as AnyhowCtx; use bytes::{BufMut, Bytes, BytesMut}; use futures::{SinkExt, StreamExt}; use protobuf::Message; use sodiumoxide::crypto::secretbox::{self, Key, Nonce}; use std::{ io::{self, Error, ErrorKind}, - net::SocketAddr, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, ops::{Deref, DerefMut}, pin::Pin, task::{Context, Poll}, @@ -99,7 +100,7 @@ impl FramedStream { } } } - bail!(format!("Failed to connect to {}", remote_addr)); + bail!(format!("Failed to connect to {remote_addr}")); } pub async fn connect<'a, 't, P, T>( @@ -209,7 +210,7 @@ impl FramedStream { if let Some(Ok(bytes)) = res.as_mut() { key.2 += 1; let nonce = Self::get_nonce(key.2); - match secretbox::open(&bytes, &nonce, &key.0) { + match secretbox::open(bytes, &nonce, &key.0) { Ok(res) => { bytes.clear(); bytes.put_slice(&res); @@ -245,19 +246,52 @@ impl FramedStream { const DEFAULT_BACKLOG: u32 = 128; -#[allow(clippy::never_loop)] pub async fn new_listener(addr: T, reuse: bool) -> ResultType { if !reuse { Ok(TcpListener::bind(addr).await?) } else { - for addr in lookup_host(&addr).await? { - let socket = new_socket(addr, true)?; - return Ok(socket.listen(DEFAULT_BACKLOG)?); - } - bail!("could not resolve to any address"); + let addr = lookup_host(&addr) + .await? + .next() + .context("could not resolve to any address")?; + new_socket(addr, true)? + .listen(DEFAULT_BACKLOG) + .map_err(anyhow::Error::msg) } } +pub async fn listen_any(port: u16) -> ResultType { + if let Ok(mut socket) = TcpSocket::new_v6() { + #[cfg(unix)] + { + use std::os::unix::io::{FromRawFd, IntoRawFd}; + let raw_fd = socket.into_raw_fd(); + let sock2 = unsafe { socket2::Socket::from_raw_fd(raw_fd) }; + sock2.set_only_v6(false).ok(); + socket = unsafe { TcpSocket::from_raw_fd(sock2.into_raw_fd()) }; + } + #[cfg(windows)] + { + use std::os::windows::prelude::{FromRawSocket, IntoRawSocket}; + let raw_socket = socket.into_raw_socket(); + let sock2 = unsafe { socket2::Socket::from_raw_socket(raw_socket) }; + sock2.set_only_v6(false).ok(); + socket = unsafe { TcpSocket::from_raw_socket(sock2.into_raw_socket()) }; + } + if socket + .bind(SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), port)) + .is_ok() + { + if let Ok(l) = socket.listen(DEFAULT_BACKLOG) { + return Ok(l); + } + } + } + let s = TcpSocket::new_v4()?; + s.bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port))?; + Ok(s.listen(DEFAULT_BACKLOG)?) +} + impl Unpin for DynTcpStream {} impl AsyncRead for DynTcpStream { diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs index 38121a4e1..bb0d071a2 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -1,11 +1,11 @@ -use crate::{bail, ResultType}; -use anyhow::anyhow; +use crate::ResultType; +use anyhow::{anyhow, Context}; use bytes::{Bytes, BytesMut}; use futures::{SinkExt, StreamExt}; use protobuf::Message; use socket2::{Domain, Socket, Type}; use std::net::SocketAddr; -use tokio::net::{ToSocketAddrs, UdpSocket}; +use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket}; use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs}; use tokio_util::{codec::BytesCodec, udp::UdpFramed}; @@ -37,39 +37,31 @@ fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result 0 { + socket.set_only_v6(false).ok(); + } socket.bind(&addr.into())?; Ok(socket) } impl FramedSocket { pub async fn new(addr: T) -> ResultType { - let socket = UdpSocket::bind(addr).await?; - Ok(Self::Direct(UdpFramed::new(socket, BytesCodec::new()))) + Self::new_reuse(addr, false, 0).await } - #[allow(clippy::never_loop)] - pub async fn new_reuse(addr: T) -> ResultType { - for addr in addr.to_socket_addrs()? { - let socket = new_socket(addr, true, 0)?.into_udp_socket(); - return Ok(Self::Direct(UdpFramed::new( - UdpSocket::from_std(socket)?, - BytesCodec::new(), - ))); - } - bail!("could not resolve to any address"); - } - - pub async fn new_with_buf_size( + pub async fn new_reuse( addr: T, + reuse: bool, buf_size: usize, ) -> ResultType { - for addr in addr.to_socket_addrs()? { - return Ok(Self::Direct(UdpFramed::new( - UdpSocket::from_std(new_socket(addr, false, buf_size)?.into_udp_socket())?, - BytesCodec::new(), - ))); - } - bail!("could not resolve to any address"); + let addr = lookup_host(&addr) + .await? + .next() + .context("could not resolve to any address")?; + Ok(Self::Direct(UdpFramed::new( + UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?, + BytesCodec::new(), + ))) } pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>( @@ -104,11 +96,12 @@ impl FramedSocket { ) -> ResultType<()> { let addr = addr.into_target_addr()?.to_owned(); let send_data = Bytes::from(msg.write_to_bytes()?); - let _ = match self { - Self::Direct(f) => match addr { - TargetAddr::Ip(addr) => f.send((send_data, addr)).await?, - _ => {} - }, + match self { + Self::Direct(f) => { + if let TargetAddr::Ip(addr) = addr { + f.send((send_data, addr)).await? + } + } Self::ProxySocks(f) => f.send((send_data, addr)).await?, }; Ok(()) @@ -123,11 +116,12 @@ impl FramedSocket { ) -> ResultType<()> { let addr = addr.into_target_addr()?.to_owned(); - let _ = match self { - Self::Direct(f) => match addr { - TargetAddr::Ip(addr) => f.send((Bytes::from(msg), addr)).await?, - _ => {} - }, + match self { + Self::Direct(f) => { + if let TargetAddr::Ip(addr) = addr { + f.send((Bytes::from(msg), addr)).await? + } + } Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?, }; Ok(()) @@ -165,12 +159,12 @@ impl FramedSocket { } } - pub fn is_ipv4(&self) -> bool { + pub fn local_addr(&self) -> Option { if let FramedSocket::Direct(x) = self { if let Ok(v) = x.get_ref().local_addr() { - return v.is_ipv4(); + return Some(v); } } - true + None } } diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index 2d0b1bf7e..0a6cd8ef9 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -74,7 +74,7 @@ impl BinaryReader { assert!(BIN_DATA.len() > IDENTIFIER_LENGTH, "bin data invalid!"); let mut iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]); if iden != "rustdesk" { - panic!("bin file is not vaild!"); + panic!("bin file is not valid!"); } base += IDENTIFIER_LENGTH; loop { diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index e2eb43177..99e15aeef 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -16,7 +16,6 @@ mediacodec = ["ndk"] [dependencies] block = "0.1" cfg-if = "1.0" -libc = "0.2" num_cpus = "1.13" lazy_static = "1.4" hbb_common = { path = "../hbb_common" } @@ -43,7 +42,7 @@ quest = "0.3" [build-dependencies] target_build_utils = "0.3" -bindgen = "0.59" +bindgen = "0.64" [target.'cfg(target_os = "linux")'.dependencies] dbus = { version = "0.9", optional = true } diff --git a/libs/scrap/examples/benchmark.rs b/libs/scrap/examples/benchmark.rs new file mode 100644 index 000000000..003830f95 --- /dev/null +++ b/libs/scrap/examples/benchmark.rs @@ -0,0 +1,292 @@ +use docopt::Docopt; +use hbb_common::env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV}; +use scrap::{ + codec::{EncoderApi, EncoderCfg}, + Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, + VpxVideoCodecId, STRIDE_ALIGN, +}; +use std::{io::Write, time::Instant}; + +// cargo run --package scrap --example benchmark --release --features hwcodec + +const USAGE: &'static str = " +Codec benchmark. + +Usage: + benchmark [--count=COUNT] [--bitrate=KBS] [--hw-pixfmt=PIXFMT] + benchmark (-h | --help) + +Options: + -h --help Show this screen. + --count=COUNT Capture frame count [default: 100]. + --bitrate=KBS Video bitrate in kilobits per second [default: 5000]. + --hw-pixfmt=PIXFMT Hardware codec pixfmt. [default: i420] + Valid values: i420, nv12. +"; + +#[derive(Debug, serde::Deserialize)] +struct Args { + flag_count: usize, + flag_bitrate: usize, + flag_hw_pixfmt: Pixfmt, +} + +#[derive(Debug, serde::Deserialize)] +enum Pixfmt { + I420, + NV12, +} + +fn main() { + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.deserialize()) + .unwrap_or_else(|e| e.exit()); + let bitrate_k = args.flag_bitrate; + let yuv_count = args.flag_count; + let (yuvs, width, height) = capture_yuv(yuv_count); + println!( + "benchmark {}x{} bitrate:{}k hw_pixfmt:{:?}", + width, height, bitrate_k, args.flag_hw_pixfmt + ); + test_vp9(&yuvs, width, height, bitrate_k, yuv_count); + #[cfg(feature = "hwcodec")] + { + use hwcodec::AVPixelFormat; + let hw_pixfmt = match args.flag_hw_pixfmt { + Pixfmt::I420 => AVPixelFormat::AV_PIX_FMT_YUV420P, + Pixfmt::NV12 => AVPixelFormat::AV_PIX_FMT_NV12, + }; + let yuvs = hw::vp9_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt); + hw::test(&yuvs, width, height, bitrate_k, yuv_count, hw_pixfmt); + } +} + +fn capture_yuv(yuv_count: usize) -> (Vec>, usize, usize) { + let mut index = 0; + let mut displays = Display::all().unwrap(); + for i in 0..displays.len() { + if displays[i].is_primary() { + index = i; + break; + } + } + let d = displays.remove(index); + let mut c = Capturer::new(d, true).unwrap(); + let mut v = vec![]; + loop { + if let Ok(frame) = c.frame(std::time::Duration::from_millis(30)) { + v.push(frame.0.to_vec()); + print!("\rcapture {}/{}", v.len(), yuv_count); + std::io::stdout().flush().ok(); + if v.len() == yuv_count { + println!(); + return (v, c.width(), c.height()); + } + } + } +} + +fn test_vp9(yuvs: &Vec>, width: usize, height: usize, bitrate_k: usize, yuv_count: usize) { + let config = EncoderCfg::VPX(VpxEncoderConfig { + width: width as _, + height: height as _, + timebase: [1, 1000], + bitrate: bitrate_k as _, + codec: VpxVideoCodecId::VP9, + num_threads: (num_cpus::get() / 2) as _, + }); + let mut encoder = VpxEncoder::new(config).unwrap(); + let start = Instant::now(); + for yuv in yuvs { + let _ = encoder + .encode(start.elapsed().as_millis() as _, yuv, STRIDE_ALIGN) + .unwrap(); + let _ = encoder.flush().unwrap(); + } + println!("vp9 encode: {:?}", start.elapsed() / yuv_count as _); + + // prepare data separately + let mut vp9s = vec![]; + let start = Instant::now(); + for yuv in yuvs { + for ref frame in encoder + .encode(start.elapsed().as_millis() as _, yuv, STRIDE_ALIGN) + .unwrap() + { + vp9s.push(frame.data.to_vec()); + } + for ref frame in encoder.flush().unwrap() { + vp9s.push(frame.data.to_vec()); + } + } + assert_eq!(vp9s.len(), yuv_count); + + let mut decoder = VpxDecoder::new(VpxDecoderConfig { + codec: VpxVideoCodecId::VP9, + num_threads: (num_cpus::get() / 2) as _, + }) + .unwrap(); + let start = Instant::now(); + for vp9 in vp9s { + let _ = decoder.decode(&vp9); + let _ = decoder.flush(); + } + println!("vp9 decode: {:?}", start.elapsed() / yuv_count as _); +} + +#[cfg(feature = "hwcodec")] +mod hw { + use super::*; + use hwcodec::{ + decode::{DecodeContext, Decoder}, + encode::{EncodeContext, Encoder}, + ffmpeg::{ffmpeg_linesize_offset_length, CodecInfo, CodecInfos}, + AVPixelFormat, + Quality::*, + RateControl::*, + }; + use scrap::{ + convert::{ + hw::{hw_bgra_to_i420, hw_bgra_to_nv12}, + i420_to_bgra, + }, + HW_STRIDE_ALIGN, + }; + + pub fn test( + yuvs: &Vec>, + width: usize, + height: usize, + bitrate_k: usize, + yuv_count: usize, + pixfmt: AVPixelFormat, + ) { + let ctx = EncodeContext { + name: String::from(""), + width: width as _, + height: height as _, + pixfmt, + align: 0, + bitrate: (bitrate_k * 1000) as _, + timebase: [1, 30], + gop: 60, + quality: Quality_Default, + rc: RC_DEFAULT, + }; + + let encoders = Encoder::available_encoders(ctx.clone()); + println!("hw encoders: {}", encoders.len()); + let best = CodecInfo::score(encoders.clone()); + for info in encoders { + test_encoder(info.clone(), ctx.clone(), yuvs, is_best(&best, &info)); + } + + let (h264s, h265s) = prepare_h26x(best, ctx.clone(), yuvs); + assert!(h264s.is_empty() || h264s.len() == yuv_count); + assert!(h265s.is_empty() || h265s.len() == yuv_count); + let decoders = Decoder::available_decoders(); + println!("hw decoders: {}", decoders.len()); + let best = CodecInfo::score(decoders.clone()); + for info in decoders { + let h26xs = if info.name.contains("h264") { + &h264s + } else { + &h265s + }; + if h26xs.len() == yuvs.len() { + test_decoder(info.clone(), h26xs, is_best(&best, &info)); + } + } + } + + fn test_encoder(info: CodecInfo, ctx: EncodeContext, yuvs: &Vec>, best: bool) { + let mut ctx = ctx; + ctx.name = info.name; + let mut encoder = Encoder::new(ctx.clone()).unwrap(); + let start = Instant::now(); + for yuv in yuvs { + let _ = encoder.encode(yuv).unwrap(); + } + println!( + "{}{}: {:?}", + if best { "*" } else { "" }, + ctx.name, + start.elapsed() / yuvs.len() as _ + ); + } + + fn test_decoder(info: CodecInfo, h26xs: &Vec>, best: bool) { + let ctx = DecodeContext { + name: info.name, + device_type: info.hwdevice, + }; + + let mut decoder = Decoder::new(ctx.clone()).unwrap(); + let start = Instant::now(); + let mut cnt = 0; + for h26x in h26xs { + let _ = decoder.decode(h26x).unwrap(); + cnt += 1; + } + let device = format!("{:?}", ctx.device_type).to_lowercase(); + let device = device.split("_").last().unwrap(); + println!( + "{}{} {}: {:?}", + if best { "*" } else { "" }, + ctx.name, + device, + start.elapsed() / cnt + ); + } + + fn prepare_h26x( + best: CodecInfos, + ctx: EncodeContext, + yuvs: &Vec>, + ) -> (Vec>, Vec>) { + let f = |info: Option| { + let mut h26xs = vec![]; + if let Some(info) = info { + let mut ctx = ctx.clone(); + ctx.name = info.name; + let mut encoder = Encoder::new(ctx).unwrap(); + for yuv in yuvs { + let h26x = encoder.encode(yuv).unwrap(); + for frame in h26x { + h26xs.push(frame.data.to_vec()); + } + } + } + h26xs + }; + (f(best.h264), f(best.h265)) + } + + fn is_best(best: &CodecInfos, info: &CodecInfo) -> bool { + Some(info.clone()) == best.h264 || Some(info.clone()) == best.h265 + } + + pub fn vp9_yuv_to_hw_yuv( + yuvs: Vec>, + width: usize, + height: usize, + pixfmt: AVPixelFormat, + ) -> Vec> { + let yuvs = yuvs; + let mut bgra = vec![]; + let mut v = vec![]; + let (linesize, offset, length) = + ffmpeg_linesize_offset_length(pixfmt, width, height, HW_STRIDE_ALIGN).unwrap(); + for mut yuv in yuvs { + i420_to_bgra(width, height, &yuv, &mut bgra); + if pixfmt == AVPixelFormat::AV_PIX_FMT_YUV420P { + hw_bgra_to_i420(width, height, &linesize, &offset, length, &bgra, &mut yuv); + } else { + hw_bgra_to_nv12(width, height, &linesize, &offset, length, &bgra, &mut yuv); + } + v.push(yuv); + } + v + } +} diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 9535e9f3a..9e4b6fce4 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -11,7 +11,7 @@ use crate::hwcodec::*; use crate::mediacodec::{ MediaCodecDecoder, MediaCodecDecoders, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT, }; -use crate::vpxcodec::*; +use crate::{vpxcodec::*, ImageFormat}; use hbb_common::{ anyhow::anyhow, @@ -23,7 +23,7 @@ use hbb_common::{ use hbb_common::{ config::{Config2, PeerConfig}, lazy_static, - message_proto::video_codec_state::PerferCodec, + message_proto::video_codec_state::PreferCodec, }; #[cfg(feature = "hwcodec")] @@ -149,29 +149,29 @@ impl Encoder { && states.iter().all(|(_, s)| s.score_h265 > 0); // Preference first - let mut preference = PerferCodec::Auto; + let mut preference = PreferCodec::Auto; let preferences: Vec<_> = states .iter() .filter(|(_, s)| { - s.perfer == PerferCodec::VPX.into() - || s.perfer == PerferCodec::H264.into() && enabled_h264 - || s.perfer == PerferCodec::H265.into() && enabled_h265 + s.prefer == PreferCodec::VPX.into() + || s.prefer == PreferCodec::H264.into() && enabled_h264 + || s.prefer == PreferCodec::H265.into() && enabled_h265 }) - .map(|(_, s)| s.perfer) + .map(|(_, s)| s.prefer) .collect(); if preferences.len() > 0 && preferences.iter().all(|&p| p == preferences[0]) { - preference = preferences[0].enum_value_or(PerferCodec::Auto); + preference = preferences[0].enum_value_or(PreferCodec::Auto); } match preference { - PerferCodec::VPX => *name.lock().unwrap() = None, - PerferCodec::H264 => { + PreferCodec::VPX => *name.lock().unwrap() = None, + PreferCodec::H264 => { *name.lock().unwrap() = best.h264.map_or(None, |c| Some(c.name)) } - PerferCodec::H265 => { + PreferCodec::H265 => { *name.lock().unwrap() = best.h265.map_or(None, |c| Some(c.name)) } - PerferCodec::Auto => { + PreferCodec::Auto => { // score encoder let mut score_vpx = SCORE_VPX; let mut score_h264 = best.h264.as_ref().map_or(0, |c| c.score); @@ -252,7 +252,7 @@ impl Decoder { score_vpx: SCORE_VPX, score_h264: best.h264.map_or(0, |c| c.score), score_h265: best.h265.map_or(0, |c| c.score), - perfer: Self::codec_preference(_id).into(), + prefer: Self::codec_preference(_id).into(), ..Default::default() }; } @@ -272,7 +272,7 @@ impl Decoder { score_vpx: SCORE_VPX, score_h264, score_h265, - perfer: Self::codec_preference(_id).into(), + prefer: Self::codec_preference(_id).into(), ..Default::default() }; } @@ -306,16 +306,17 @@ impl Decoder { pub fn handle_video_frame( &mut self, frame: &video_frame::Union, + fmt: (ImageFormat, usize), rgb: &mut Vec, ) -> ResultType { match frame { video_frame::Union::Vp9s(vp9s) => { - Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, rgb) + Decoder::handle_vp9s_video_frame(&mut self.vpx, vp9s, fmt, rgb) } #[cfg(feature = "hwcodec")] video_frame::Union::H264s(h264s) => { if let Some(decoder) = &mut self.hw.h264 { - Decoder::handle_hw_video_frame(decoder, h264s, rgb, &mut self.i420) + Decoder::handle_hw_video_frame(decoder, h264s, fmt, rgb, &mut self.i420) } else { Err(anyhow!("don't support h264!")) } @@ -323,7 +324,7 @@ impl Decoder { #[cfg(feature = "hwcodec")] video_frame::Union::H265s(h265s) => { if let Some(decoder) = &mut self.hw.h265 { - Decoder::handle_hw_video_frame(decoder, h265s, rgb, &mut self.i420) + Decoder::handle_hw_video_frame(decoder, h265s, fmt, rgb, &mut self.i420) } else { Err(anyhow!("don't support h265!")) } @@ -331,7 +332,7 @@ impl Decoder { #[cfg(feature = "mediacodec")] video_frame::Union::H264s(h264s) => { if let Some(decoder) = &mut self.media_codec.h264 { - Decoder::handle_mediacodec_video_frame(decoder, h264s, rgb) + Decoder::handle_mediacodec_video_frame(decoder, h264s, fmt, rgb) } else { Err(anyhow!("don't support h264!")) } @@ -339,7 +340,7 @@ impl Decoder { #[cfg(feature = "mediacodec")] video_frame::Union::H265s(h265s) => { if let Some(decoder) = &mut self.media_codec.h265 { - Decoder::handle_mediacodec_video_frame(decoder, h265s, rgb) + Decoder::handle_mediacodec_video_frame(decoder, h265s, fmt, rgb) } else { Err(anyhow!("don't support h265!")) } @@ -351,6 +352,7 @@ impl Decoder { fn handle_vp9s_video_frame( decoder: &mut VpxDecoder, vp9s: &EncodedVideoFrames, + fmt: (ImageFormat, usize), rgb: &mut Vec, ) -> ResultType { let mut last_frame = Image::new(); @@ -367,7 +369,7 @@ impl Decoder { if last_frame.is_null() { Ok(false) } else { - last_frame.rgb(1, true, rgb); + last_frame.to(fmt.0, fmt.1, rgb); Ok(true) } } @@ -376,14 +378,15 @@ impl Decoder { fn handle_hw_video_frame( decoder: &mut HwDecoder, frames: &EncodedVideoFrames, - rgb: &mut Vec, + fmt: (ImageFormat, usize), + raw: &mut Vec, i420: &mut Vec, ) -> ResultType { let mut ret = false; for h264 in frames.frames.iter() { for image in decoder.decode(&h264.data)? { // TODO: just process the last frame - if image.bgra(rgb, i420).is_ok() { + if image.to_fmt(fmt, raw, i420).is_ok() { ret = true; } } @@ -395,29 +398,30 @@ impl Decoder { fn handle_mediacodec_video_frame( decoder: &mut MediaCodecDecoder, frames: &EncodedVideoFrames, - rgb: &mut Vec, + fmt: (ImageFormat, usize), + raw: &mut Vec, ) -> ResultType { let mut ret = false; for h264 in frames.frames.iter() { - return decoder.decode(&h264.data, rgb); + return decoder.decode(&h264.data, fmt, raw); } return Ok(false); } #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] - fn codec_preference(id: &str) -> PerferCodec { + fn codec_preference(id: &str) -> PreferCodec { let codec = PeerConfig::load(id) .options .get("codec-preference") .map_or("".to_owned(), |c| c.to_owned()); if codec == "vp9" { - PerferCodec::VPX + PreferCodec::VPX } else if codec == "h264" { - PerferCodec::H264 + PreferCodec::H264 } else if codec == "h265" { - PerferCodec::H265 + PreferCodec::H265 } else { - PerferCodec::Auto + PreferCodec::Auto } } } diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs index 2b0223a0a..65e3a16ad 100644 --- a/libs/scrap/src/common/convert.rs +++ b/libs/scrap/src/common/convert.rs @@ -103,6 +103,19 @@ extern "C" { height: c_int, ) -> c_int; + pub fn I420ToABGR( + src_y: *const u8, + src_stride_y: c_int, + src_u: *const u8, + src_stride_u: c_int, + src_v: *const u8, + src_stride_v: c_int, + dst_rgba: *mut u8, + dst_stride_rgba: c_int, + width: c_int, + height: c_int, + ) -> c_int; + pub fn NV12ToARGB( src_y: *const u8, src_stride_y: c_int, @@ -113,6 +126,17 @@ extern "C" { width: c_int, height: c_int, ) -> c_int; + + pub fn NV12ToABGR( + src_y: *const u8, + src_stride_y: c_int, + src_uv: *const u8, + src_stride_uv: c_int, + dst_rgba: *mut u8, + dst_stride_rgba: c_int, + width: c_int, + height: c_int, + ) -> c_int; } // https://github.com/webmproject/libvpx/blob/master/vpx/src/vpx_image.c @@ -166,6 +190,29 @@ pub fn i420_to_rgb(width: usize, height: usize, src: &[u8], dst: &mut Vec) { }; } +pub fn i420_to_bgra(width: usize, height: usize, src: &[u8], dst: &mut Vec) { + let (_, _, src_stride_y, src_stride_uv, u, v) = + get_vpx_i420_stride(width, height, super::STRIDE_ALIGN); + let src_y = src.as_ptr(); + let src_u = src[u..].as_ptr(); + let src_v = src[v..].as_ptr(); + dst.resize(width * height * 4, 0); + unsafe { + super::I420ToARGB( + src_y, + src_stride_y as _, + src_u, + src_stride_uv as _, + src_v, + src_stride_uv as _, + dst.as_mut_ptr(), + (width * 3) as _, + width as _, + height as _, + ); + }; +} + pub fn bgra_to_i420(width: usize, height: usize, src: &[u8], dst: &mut Vec) { let (_, h, dst_stride_y, dst_stride_uv, u, v) = get_vpx_i420_stride(width, height, super::STRIDE_ALIGN); @@ -245,6 +292,7 @@ pub unsafe fn nv12_to_i420( #[cfg(feature = "hwcodec")] pub mod hw { + use crate::ImageFormat; use hbb_common::{anyhow::anyhow, ResultType}; #[cfg(target_os = "windows")] use hwcodec::{ffmpeg::ffmpeg_linesize_offset_length, AVPixelFormat}; @@ -315,7 +363,8 @@ pub mod hw { } #[cfg(target_os = "windows")] - pub fn hw_nv12_to_bgra( + pub fn hw_nv12_to( + fmt: ImageFormat, width: usize, height: usize, src_y: &[u8], @@ -355,18 +404,39 @@ pub mod hw { width as _, height as _, ); - super::I420ToARGB( - i420_offset_y, - i420_stride_y, - i420_offset_u, - i420_stride_u, - i420_offset_v, - i420_stride_v, - dst.as_mut_ptr(), - (width * 4) as _, - width as _, - height as _, - ); + match fmt { + ImageFormat::ARGB => { + super::I420ToARGB( + i420_offset_y, + i420_stride_y, + i420_offset_u, + i420_stride_u, + i420_offset_v, + i420_stride_v, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + ImageFormat::ABGR => { + super::I420ToABGR( + i420_offset_y, + i420_stride_y, + i420_offset_u, + i420_stride_u, + i420_offset_v, + i420_stride_v, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + _ => { + return Err(anyhow!("unsupported image format")); + } + } return Ok(()); }; } @@ -374,7 +444,8 @@ pub mod hw { } #[cfg(not(target_os = "windows"))] - pub fn hw_nv12_to_bgra( + pub fn hw_nv12_to( + fmt: ImageFormat, width: usize, height: usize, src_y: &[u8], @@ -387,23 +458,44 @@ pub mod hw { ) -> ResultType<()> { dst.resize(width * height * 4, 0); unsafe { - match super::NV12ToARGB( - src_y.as_ptr(), - src_stride_y as _, - src_uv.as_ptr(), - src_stride_uv as _, - dst.as_mut_ptr(), - (width * 4) as _, - width as _, - height as _, - ) { - 0 => Ok(()), - _ => Err(anyhow!("NV12ToARGB failed")), + match fmt { + ImageFormat::ARGB => { + match super::NV12ToARGB( + src_y.as_ptr(), + src_stride_y as _, + src_uv.as_ptr(), + src_stride_uv as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ) { + 0 => Ok(()), + _ => Err(anyhow!("NV12ToARGB failed")), + } + } + ImageFormat::ABGR => { + match super::NV12ToABGR( + src_y.as_ptr(), + src_stride_y as _, + src_uv.as_ptr(), + src_stride_uv as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ) { + 0 => Ok(()), + _ => Err(anyhow!("NV12ToABGR failed")), + } + } + _ => Err(anyhow!("unsupported image format")), } } } - pub fn hw_i420_to_bgra( + pub fn hw_i420_to( + fmt: ImageFormat, width: usize, height: usize, src_y: &[u8], @@ -419,18 +511,37 @@ pub mod hw { let src_v = src_v.as_ptr(); dst.resize(width * height * 4, 0); unsafe { - super::I420ToARGB( - src_y, - src_stride_y as _, - src_u, - src_stride_u as _, - src_v, - src_stride_v as _, - dst.as_mut_ptr(), - (width * 4) as _, - width as _, - height as _, - ); + match fmt { + ImageFormat::ARGB => { + super::I420ToARGB( + src_y, + src_stride_y as _, + src_u, + src_stride_u as _, + src_v, + src_stride_v as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + ImageFormat::ABGR => { + super::I420ToABGR( + src_y, + src_stride_y as _, + src_u, + src_stride_u as _, + src_v, + src_stride_v as _, + dst.as_mut_ptr(), + (width * 4) as _, + width as _, + height as _, + ); + } + _ => {} + } }; } } diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index c77da3f8f..fa1c00976 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -1,6 +1,6 @@ use crate::{ codec::{EncoderApi, EncoderCfg}, - hw, HW_STRIDE_ALIGN, + hw, ImageFormat, HW_STRIDE_ALIGN, }; use hbb_common::{ anyhow::{anyhow, Context}, @@ -16,7 +16,7 @@ use hwcodec::{ ffmpeg::{CodecInfo, CodecInfos, DataFormat}, AVPixelFormat, Quality::{self, *}, - RateContorl::{self, *}, + RateControl::{self, *}, }; use std::sync::{Arc, Mutex}; @@ -31,7 +31,7 @@ const DEFAULT_PIXFMT: AVPixelFormat = AVPixelFormat::AV_PIX_FMT_YUV420P; pub const DEFAULT_TIME_BASE: [i32; 2] = [1, 30]; const DEFAULT_GOP: i32 = 60; const DEFAULT_HW_QUALITY: Quality = Quality_Default; -const DEFAULT_RC: RateContorl = RC_DEFAULT; +const DEFAULT_RC: RateControl = RC_DEFAULT; pub struct HwEncoder { encoder: Encoder, @@ -107,7 +107,6 @@ impl EncoderApi for HwEncoder { DataFormat::H264 => vf.set_h264s(frames), DataFormat::H265 => vf.set_h265s(frames), } - vf.timestamp = get_time(); msg_out.set_video_frame(vf); Ok(msg_out) } else { @@ -236,22 +235,30 @@ pub struct HwDecoderImage<'a> { } impl HwDecoderImage<'_> { - pub fn bgra(&self, bgra: &mut Vec, i420: &mut Vec) -> ResultType<()> { + // take dst_stride into account when you convert + pub fn to_fmt( + &self, + (fmt, dst_stride): (ImageFormat, usize), + fmt_data: &mut Vec, + i420: &mut Vec, + ) -> ResultType<()> { let frame = self.frame; match frame.pixfmt { - AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_nv12_to_bgra( + AVPixelFormat::AV_PIX_FMT_NV12 => hw::hw_nv12_to( + fmt, frame.width as _, frame.height as _, &frame.data[0], &frame.data[1], frame.linesize[0] as _, frame.linesize[1] as _, - bgra, + fmt_data, i420, HW_STRIDE_ALIGN, ), AVPixelFormat::AV_PIX_FMT_YUV420P => { - hw::hw_i420_to_bgra( + hw::hw_i420_to( + fmt, frame.width as _, frame.height as _, &frame.data[0], @@ -260,12 +267,20 @@ impl HwDecoderImage<'_> { frame.linesize[0] as _, frame.linesize[1] as _, frame.linesize[2] as _, - bgra, + fmt_data, ); return Ok(()); } } } + + pub fn bgra(&self, bgra: &mut Vec, i420: &mut Vec) -> ResultType<()> { + self.to_fmt((ImageFormat::ARGB, 1), bgra, i420) + } + + pub fn rgba(&self, rgba: &mut Vec, i420: &mut Vec) -> ResultType<()> { + self.to_fmt((ImageFormat::ABGR, 1), rgba, i420) + } } fn get_config(k: &str) -> ResultType { @@ -293,8 +308,8 @@ pub fn check_config() { quality: DEFAULT_HW_QUALITY, rc: DEFAULT_RC, }; - let encoders = CodecInfo::score(Encoder::avaliable_encoders(ctx)); - let decoders = CodecInfo::score(Decoder::avaliable_decoders()); + let encoders = CodecInfo::score(Encoder::available_encoders(ctx)); + let decoders = CodecInfo::score(Decoder::available_decoders()); if let Ok(old_encoders) = get_config(CFG_KEY_ENCODER) { if let Ok(old_decoders) = get_config(CFG_KEY_DECODER) { @@ -317,16 +332,30 @@ pub fn check_config() { } pub fn check_config_process(force_reset: bool) { - if force_reset { - HwCodecConfig::remove(); - } - if let Ok(exe) = std::env::current_exe() { - std::thread::spawn(move || { - std::process::Command::new(exe) - .arg("--check-hwcodec-config") - .status() - .ok(); - HwCodecConfig::refresh(); - }); - }; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; + + std::thread::spawn(move || { + if force_reset { + HwCodecConfig::remove(); + } + if let Ok(exe) = std::env::current_exe() { + if let Some(file_name) = exe.file_name().to_owned() { + let s = System::new_all(); + let arg = "--check-hwcodec-config"; + for process in s.processes_by_name(&file_name.to_string_lossy().to_string()) { + if process.cmd().iter().any(|cmd| cmd.contains(arg)) { + log::warn!("already have process {}", arg); + return; + } + } + if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { + let second = 3; + std::thread::sleep(std::time::Duration::from_secs(second)); + // kill: Different platforms have different results + child.kill().ok(); + HwCodecConfig::refresh(); + } + } + }; + }); } diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index 406baecb5..a0272d867 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -1,5 +1,4 @@ -use hbb_common::anyhow::Error; -use hbb_common::{bail, ResultType}; +use hbb_common::{anyhow::Error, bail, log, ResultType}; use ndk::media::media_codec::{MediaCodec, MediaCodecDirection, MediaFormat}; use std::ops::Deref; use std::{ @@ -8,9 +7,10 @@ use std::{ time::Duration, }; +use crate::ImageFormat; use crate::{ codec::{EncoderApi, EncoderCfg}, - I420ToARGB, + I420ToABGR, I420ToARGB, }; /// MediaCodec mime type name @@ -50,7 +50,13 @@ impl MediaCodecDecoder { MediaCodecDecoders { h264, h265 } } - pub fn decode(&mut self, data: &[u8], rgb: &mut Vec) -> ResultType { + // take dst_stride into account please + pub fn decode( + &mut self, + data: &[u8], + (fmt, dst_stride): (ImageFormat, usize), + raw: &mut Vec, + ) -> ResultType { match self.dequeue_input_buffer(Duration::from_millis(10))? { Some(mut input_buffer) => { let mut buf = input_buffer.buffer_mut(); @@ -83,23 +89,44 @@ impl MediaCodecDecoder { let bps = 4; let u = buf.len() * 2 / 3; let v = buf.len() * 5 / 6; - rgb.resize(h * w * bps, 0); + raw.resize(h * w * bps, 0); let y_ptr = buf.as_ptr(); let u_ptr = buf[u..].as_ptr(); let v_ptr = buf[v..].as_ptr(); unsafe { - I420ToARGB( - y_ptr, - stride, - u_ptr, - stride / 2, - v_ptr, - stride / 2, - rgb.as_mut_ptr(), - (w * bps) as _, - w as _, - h as _, - ); + match fmt { + ImageFormat::ARGB => { + I420ToARGB( + y_ptr, + stride, + u_ptr, + stride / 2, + v_ptr, + stride / 2, + raw.as_mut_ptr(), + (w * bps) as _, + w as _, + h as _, + ); + } + ImageFormat::ARGB => { + I420ToABGR( + y_ptr, + stride, + u_ptr, + stride / 2, + v_ptr, + stride / 2, + raw.as_mut_ptr(), + (w * bps) as _, + w as _, + h as _, + ); + } + _ => { + bail!("Unsupported image format"); + } + } } self.release_output_buffer(output_buffer, false)?; Ok(true) diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 1e78656cf..9e15cf084 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -30,7 +30,7 @@ cfg_if! { } pub mod codec; -mod convert; +pub mod convert; #[cfg(feature = "hwcodec")] pub mod hwcodec; #[cfg(feature = "mediacodec")] @@ -43,6 +43,13 @@ pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer pub mod record; mod vpx; +#[derive(Copy, Clone)] +pub enum ImageFormat { + Raw, + ABGR, + ARGB, +} + #[inline] pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { // does this really help? @@ -74,9 +81,9 @@ pub fn is_x11() -> bool { #[inline] pub fn is_cursor_embedded() -> bool { if is_x11() { - x11::is_cursor_embedded + x11::IS_CURSOR_EMBEDDED } else { - wayland::is_cursor_embedded + wayland::is_cursor_embedded() } } diff --git a/libs/scrap/src/common/quartz.rs b/libs/scrap/src/common/quartz.rs index a02d55ebb..dc59edac1 100644 --- a/libs/scrap/src/common/quartz.rs +++ b/libs/scrap/src/common/quartz.rs @@ -83,7 +83,7 @@ impl crate::TraitCapturer for Capturer { } } -pub struct Frame<'a>(quartz::Frame, PhantomData<&'a [u8]>); +pub struct Frame<'a>(pub quartz::Frame, PhantomData<&'a [u8]>); impl<'a> ops::Deref for Frame<'a> { type Target = [u8]; diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index 5164886a1..3df9c0461 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -4,10 +4,10 @@ use hbb_common::anyhow::{anyhow, Context}; use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame}; -use hbb_common::{get_time, ResultType}; +use hbb_common::ResultType; -use crate::codec::EncoderApi; use crate::STRIDE_ALIGN; +use crate::{codec::EncoderApi, ImageFormat}; use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; use hbb_common::bytes::Bytes; @@ -287,7 +287,6 @@ impl VpxEncoder { frames: vp9s.into(), ..Default::default() }); - vf.timestamp = get_time(); msg_out.set_video_frame(vf); msg_out } @@ -417,7 +416,7 @@ impl VpxDecoder { Ok(Self { ctx }) } - pub fn decode2rgb(&mut self, data: &[u8], rgba: bool) -> Result> { + pub fn decode2rgb(&mut self, data: &[u8], fmt: ImageFormat) -> Result> { let mut img = Image::new(); for frame in self.decode(data)? { drop(img); @@ -431,7 +430,7 @@ impl VpxDecoder { Ok(Vec::new()) } else { let mut out = Default::default(); - img.rgb(1, rgba, &mut out); + img.to(fmt, 1, &mut out); Ok(out) } } @@ -539,40 +538,62 @@ impl Image { self.inner().stride[iplane] } - pub fn rgb(&self, stride_align: usize, rgba: bool, dst: &mut Vec) { + pub fn to(&self, fmt: ImageFormat, stride: usize, dst: &mut Vec) { let h = self.height(); - let mut w = self.width(); - let bps = if rgba { 4 } else { 3 }; - w = (w + stride_align - 1) & !(stride_align - 1); - dst.resize(h * w * bps, 0); + let w = self.width(); + let bytes_per_pixel = match fmt { + ImageFormat::Raw => 3, + ImageFormat::ARGB | ImageFormat::ABGR => 4, + }; + // https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L128 + // https://github.com/lemenkov/libyuv/blob/6900494d90ae095d44405cd4cc3f346971fa69c9/source/convert_argb.cc#L129 + let bytes_per_row = (w * bytes_per_pixel + stride - 1) & !(stride - 1); + dst.resize(h * bytes_per_row, 0); let img = self.inner(); unsafe { - if rgba { - super::I420ToARGB( - img.planes[0], - img.stride[0], - img.planes[1], - img.stride[1], - img.planes[2], - img.stride[2], - dst.as_mut_ptr(), - (w * bps) as _, - self.width() as _, - self.height() as _, - ); - } else { - super::I420ToRAW( - img.planes[0], - img.stride[0], - img.planes[1], - img.stride[1], - img.planes[2], - img.stride[2], - dst.as_mut_ptr(), - (w * bps) as _, - self.width() as _, - self.height() as _, - ); + match fmt { + ImageFormat::Raw => { + super::I420ToRAW( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + bytes_per_row as _, + self.width() as _, + self.height() as _, + ); + } + ImageFormat::ARGB => { + super::I420ToARGB( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + bytes_per_row as _, + self.width() as _, + self.height() as _, + ); + } + ImageFormat::ABGR => { + super::I420ToABGR( + img.planes[0], + img.stride[0], + img.planes[1], + img.stride[1], + img.planes[2], + img.stride[2], + dst.as_mut_ptr(), + bytes_per_row as _, + self.width() as _, + self.height() as _, + ); + } } } } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 99752fa7f..2d3abeeaf 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,12 +4,34 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); -pub const is_cursor_embedded: bool = true; +static mut IS_CURSOR_EMBEDDED: Option = None; lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); } +pub fn is_cursor_embedded() -> bool { + unsafe { + if IS_CURSOR_EMBEDDED.is_none() { + init_cursor_embedded(); + } + IS_CURSOR_EMBEDDED.unwrap_or(false) + } +} + +unsafe fn init_cursor_embedded() { + use crate::common::wayland::pipewire::get_available_cursor_modes; + match get_available_cursor_modes() { + Ok(_modes) => { + // IS_CURSOR_EMBEDDED = Some((_modes & 0x02) > 0); + IS_CURSOR_EMBEDDED = Some(false) + } + Err(..) => { + IS_CURSOR_EMBEDDED = Some(false); + } + } +} + pub fn set_map_err(f: fn(err: String) -> io::Error) { *MAP_ERR.write().unwrap() = Some(f); } @@ -74,7 +96,7 @@ impl Display { } pub fn all() -> io::Result> { - Ok(pipewire::get_capturables(true) + Ok(pipewire::get_capturables(is_cursor_embedded()) .map_err(map_err)? .drain(..) .map(|x| Display(x)) diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index 138ab5000..514608e4a 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -1,9 +1,9 @@ -use crate::{x11, common::TraitCapturer}; +use crate::{common::TraitCapturer, x11}; use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); -pub const is_cursor_embedded: bool = false; +pub const IS_CURSOR_EMBEDDED: bool = false; impl Capturer { pub fn new(display: Display, yuv: bool) -> io::Result { @@ -29,7 +29,7 @@ impl TraitCapturer for Capturer { } } -pub struct Frame<'a>(pub(crate) &'a [u8]); +pub struct Frame<'a>(pub &'a [u8]); impl<'a> ops::Deref for Frame<'a> { type Target = [u8]; @@ -90,6 +90,6 @@ impl Display { } pub fn name(&self) -> String { - "".to_owned() + self.0.name() } } diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs index 0de86055e..62e90c08b 100644 --- a/libs/scrap/src/dxgi/mag.rs +++ b/libs/scrap/src/dxgi/mag.rs @@ -127,15 +127,6 @@ impl MagInterface { }; s.init_succeeded = false; unsafe { - if GetSystemMetrics(SM_CMONITORS) != 1 { - // Do not try to use the magnifier in multi-screen setup (where the API - // crashes sometimes). - return Err(Error::new( - ErrorKind::Other, - "Magnifier capturer cannot work on multi-screen system.", - )); - } - // load lib let lib_file_name = "Magnification.dll"; let lib_file_name_c = CString::new(lib_file_name).unwrap(); @@ -282,10 +273,10 @@ impl CapturerMag { let y = GetSystemMetrics(SM_YVIRTUALSCREEN); let w = GetSystemMetrics(SM_CXVIRTUALSCREEN); let h = GetSystemMetrics(SM_CYVIRTUALSCREEN); - if !(origin.0 == x as i32 - && origin.1 == y as i32 - && width == w as usize - && height == h as usize) + if !(origin.0 >= x as i32 + && origin.1 >= y as i32 + && width <= w as usize + && height <= h as usize) { return Err(Error::new( ErrorKind::Other, @@ -518,10 +509,10 @@ impl CapturerMag { let y = GetSystemMetrics(SM_YVIRTUALSCREEN); let w = GetSystemMetrics(SM_CXVIRTUALSCREEN); let h = GetSystemMetrics(SM_CYVIRTUALSCREEN); - if !(self.rect.left == x as i32 - && self.rect.top == y as i32 - && self.rect.right == (x + w) as i32 - && self.rect.bottom == (y + h) as i32) + if !(self.rect.left >= x as i32 + && self.rect.top >= y as i32 + && self.rect.right <= (x + w) as i32 + && self.rect.bottom <= (y + h) as i32) { return Err(Error::new( ErrorKind::Other, @@ -545,8 +536,8 @@ impl CapturerMag { HWND_TOP, self.rect.left, self.rect.top, - self.rect.right, - self.rect.bottom, + self.rect.right - self.rect.left, + self.rect.bottom - self.rect.top, 0, ) { @@ -556,8 +547,8 @@ impl CapturerMag { "Failed SetWindowPos (x, y, w , h) - ({}, {}, {}, {}), error {}", self.rect.left, self.rect.top, - self.rect.right, - self.rect.bottom, + self.rect.right - self.rect.left, + self.rect.bottom - self.rect.top, GetLastError() ), )); diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 5829686b5..4a0a53402 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -58,6 +58,7 @@ impl Capturer { let mut device = ptr::null_mut(); let mut context = ptr::null_mut(); let mut duplication = ptr::null_mut(); + #[allow(invalid_value)] let mut desc = unsafe { mem::MaybeUninit::uninit().assume_init() }; let mut gdi_capturer = None; @@ -176,6 +177,7 @@ impl Capturer { unsafe fn load_frame(&mut self, timeout: UINT) -> io::Result<(*const u8, i32)> { let mut frame = ptr::null_mut(); + #[allow(invalid_value)] let mut info = mem::MaybeUninit::uninit().assume_init(); wrap_hresult((*self.duplication.0).AcquireNextFrame(timeout, &mut info, &mut frame))?; @@ -185,6 +187,7 @@ impl Capturer { return Err(std::io::ErrorKind::WouldBlock.into()); } + #[allow(invalid_value)] let mut rect = mem::MaybeUninit::uninit().assume_init(); if self.fastlane { wrap_hresult((*self.duplication.0).MapDesktopSurface(&mut rect))?; @@ -204,6 +207,7 @@ impl Capturer { ); let texture = ComPtr(texture); + #[allow(invalid_value)] let mut texture_desc = mem::MaybeUninit::uninit().assume_init(); (*texture.0).GetDesc(&mut texture_desc); @@ -262,7 +266,7 @@ impl Capturer { _ => { return Err(io::Error::new( io::ErrorKind::Other, - "Unknown roration".to_string(), + "Unknown rotation".to_string(), )); } }; @@ -362,6 +366,7 @@ impl Displays { let mut all = Vec::new(); let mut i: DWORD = 0; loop { + #[allow(invalid_value)] let mut d: DISPLAY_DEVICEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; d.cb = std::mem::size_of::() as _; let ok = unsafe { EnumDisplayDevicesW(std::ptr::null(), i, &mut d as _, 0) }; @@ -382,6 +387,7 @@ impl Displays { gdi: true, }; disp.desc.DeviceName = d.DeviceName; + #[allow(invalid_value)] let mut m: DEVMODEW = unsafe { std::mem::MaybeUninit::uninit().assume_init() }; m.dmSize = std::mem::size_of::() as _; m.dmDriverExtra = 0; @@ -441,6 +447,7 @@ impl Displays { // We get the display's details. let desc = unsafe { + #[allow(invalid_value)] let mut desc = mem::MaybeUninit::uninit().assume_init(); (*output.0).GetDesc(&mut desc); desc diff --git a/libs/scrap/src/lib.rs b/libs/scrap/src/lib.rs index 504f0a4b3..77070d1a2 100644 --- a/libs/scrap/src/lib.rs +++ b/libs/scrap/src/lib.rs @@ -2,7 +2,7 @@ extern crate block; #[macro_use] extern crate cfg_if; -pub extern crate libc; +pub use hbb_common::libc; #[cfg(dxgi)] extern crate winapi; diff --git a/libs/scrap/src/quartz/capturer.rs b/libs/scrap/src/quartz/capturer.rs index 5be55ea22..cf442c2b4 100644 --- a/libs/scrap/src/quartz/capturer.rs +++ b/libs/scrap/src/quartz/capturer.rs @@ -1,7 +1,7 @@ use std::ptr; use block::{Block, ConcreteBlock}; -use libc::c_void; +use hbb_common::libc::c_void; use std::sync::{Arc, Mutex}; use super::config::Config; diff --git a/libs/scrap/src/quartz/config.rs b/libs/scrap/src/quartz/config.rs index 11a6d5fc0..d5f992f0b 100644 --- a/libs/scrap/src/quartz/config.rs +++ b/libs/scrap/src/quartz/config.rs @@ -1,6 +1,6 @@ use std::ptr; -use libc::c_void; +use hbb_common::libc::c_void; use super::ffi::*; diff --git a/libs/scrap/src/quartz/ffi.rs b/libs/scrap/src/quartz/ffi.rs index ca39c0a61..6b8c6e0e1 100644 --- a/libs/scrap/src/quartz/ffi.rs +++ b/libs/scrap/src/quartz/ffi.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use block::RcBlock; -use libc::c_void; +use hbb_common::libc::c_void; pub type CGDisplayStreamRef = *mut c_void; pub type CFDictionaryRef = *mut c_void; diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index c1c84f98e..5583dfef5 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -386,21 +386,22 @@ fn streams_from_response(response: OrgFreedesktopPortalRequestResponse) -> Vec

    >(), - ) - }) - .next(); - if let Some(v) = v { - if v.len() == 2 { - info.position.0 = v[0] as _; - info.position.1 = v[1] as _; + if let Some(pos) = attributes.get("position") { + let v = pos + .as_iter()? + .filter_map(|v| { + Some( + v.as_iter()? + .map(|x| x.as_i64().unwrap_or(0)) + .collect::>(), + ) + }) + .next(); + if let Some(v) = v { + if v.len() == 2 { + info.position.0 = v[0] as _; + info.position.1 = v[1] as _; + } } } Some(info) @@ -415,6 +416,18 @@ static mut INIT: bool = false; const RESTORE_TOKEN: &str = "restore_token"; const RESTORE_TOKEN_CONF_KEY: &str = "wayland-restore-token"; +const PORTAL_CURSOR_MODE_HIDDEN: u32 = 1; +#[allow(dead_code)] +const PORTAL_CURSOR_MODE_EMBEDDED: u32 = 2; +#[allow(dead_code)] +const PORTAL_CURSOR_MODE_METADATA: u32 = 4; + +pub fn get_available_cursor_modes() -> Result { + let conn = SyncConnection::new_session()?; + let portal = get_portal(&conn); + portal.available_cursor_modes() +} + // mostly inspired by https://gitlab.gnome.org/snippets/19 fn request_screen_cast( capture_cursor: bool, @@ -473,7 +486,17 @@ fn request_screen_cast( args.insert("multiple".into(), Variant(Box::new(true))); args.insert("types".into(), Variant(Box::new(1u32))); //| 2u32))); - let cursor_mode = if capture_cursor { 2u32 } else { 1u32 }; + let mut cursor_mode = 0u32; + let mut available_cursor_modes = 0u32; + if let Ok(modes) = portal.available_cursor_modes() { + available_cursor_modes = modes; + } + // if capture_cursor { + // cursor_mode = PORTAL_CURSOR_MODE_METADATA & available_cursor_modes; + // } + if cursor_mode == 0 { + cursor_mode = PORTAL_CURSOR_MODE_HIDDEN & available_cursor_modes; + } let plasma = std::env::var("DESKTOP_SESSION").map_or(false, |s| s.contains("plasma")); if plasma && capture_cursor { // Warn the user if capturing the cursor is tried on kde as this can crash @@ -483,7 +506,9 @@ fn request_screen_cast( desktop, see https://bugs.kde.org/show_bug.cgi?id=435042 for details! \ You have been warned."); } - args.insert("cursor_mode".into(), Variant(Box::new(cursor_mode))); + if cursor_mode > 0 { + args.insert("cursor_mode".into(), Variant(Box::new(cursor_mode))); + } let session: dbus::Path = r .results .get("session_handle") diff --git a/libs/scrap/src/x11/capturer.rs b/libs/scrap/src/x11/capturer.rs index 0dcfcfdab..6486af55c 100644 --- a/libs/scrap/src/x11/capturer.rs +++ b/libs/scrap/src/x11/capturer.rs @@ -1,6 +1,6 @@ use std::{io, ptr, slice}; -use libc; +use hbb_common::libc; use super::ffi::*; use super::Display; diff --git a/libs/scrap/src/x11/display.rs b/libs/scrap/src/x11/display.rs index 0c5ba5035..a33903caa 100644 --- a/libs/scrap/src/x11/display.rs +++ b/libs/scrap/src/x11/display.rs @@ -9,6 +9,7 @@ pub struct Display { default: bool, rect: Rect, root: xcb_window_t, + name: String, } #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] @@ -25,12 +26,14 @@ impl Display { default: bool, rect: Rect, root: xcb_window_t, + name: String, ) -> Display { Display { server, default, rect, root, + name, } } @@ -52,4 +55,8 @@ impl Display { pub fn root(&self) -> xcb_window_t { self.root } + + pub fn name(&self) -> String { + self.name.clone() + } } diff --git a/libs/scrap/src/x11/ffi.rs b/libs/scrap/src/x11/ffi.rs index 5df5c46a8..b34fed416 100644 --- a/libs/scrap/src/x11/ffi.rs +++ b/libs/scrap/src/x11/ffi.rs @@ -1,6 +1,6 @@ #![allow(non_camel_case_types)] -use libc::c_void; +use hbb_common::libc::c_void; #[link(name = "xcb")] #[link(name = "xcb-shm")] @@ -65,6 +65,21 @@ extern "C" { ) -> xcb_randr_monitor_info_iterator_t; pub fn xcb_randr_monitor_info_next(i: *mut xcb_randr_monitor_info_iterator_t); + + pub fn xcb_get_atom_name( + c: *mut xcb_connection_t, + atom: xcb_atom_t, + ) -> xcb_get_atom_name_cookie_t; + + pub fn xcb_get_atom_name_reply( + c: *mut xcb_connection_t, + cookie: xcb_get_atom_name_cookie_t, + e: *mut *mut xcb_generic_error_t, + ) -> *const xcb_get_atom_name_reply_t; + + pub fn xcb_get_atom_name_name(reply: *const xcb_get_atom_name_request_t) -> *const u8; + + pub fn xcb_get_atom_name_name_length(reply: *const xcb_get_atom_name_reply_t) -> i32; } pub const XCB_IMAGE_FORMAT_Z_PIXMAP: u8 = 2; @@ -78,6 +93,9 @@ pub type xcb_timestamp_t = u32; pub type xcb_colormap_t = u32; pub type xcb_shm_seg_t = u32; pub type xcb_drawable_t = u32; +pub type xcb_get_atom_name_cookie_t = u32; +pub type xcb_get_atom_name_reply_t = u32; +pub type xcb_get_atom_name_request_t = xcb_get_atom_name_reply_t; #[repr(C)] pub struct xcb_setup_t { diff --git a/libs/scrap/src/x11/iter.rs b/libs/scrap/src/x11/iter.rs index cb3310be9..28609376b 100644 --- a/libs/scrap/src/x11/iter.rs +++ b/libs/scrap/src/x11/iter.rs @@ -1,7 +1,8 @@ +use std::ffi::CString; use std::ptr; use std::rc::Rc; -use libc; +use hbb_common::libc; use super::ffi::*; use super::{Display, Rect, Server}; @@ -64,6 +65,7 @@ impl Iterator for DisplayIter { if inner.rem != 0 { unsafe { let data = &*inner.data; + let name = get_atom_name(self.server.raw(), data.name); let display = Display::new( self.server.clone(), @@ -75,6 +77,7 @@ impl Iterator for DisplayIter { h: data.height, }, root, + name, ); xcb_randr_monitor_info_next(inner); @@ -91,3 +94,30 @@ impl Iterator for DisplayIter { } } } + +fn get_atom_name(conn: *mut xcb_connection_t, atom: xcb_atom_t) -> String { + let empty = "".to_owned(); + if atom == 0 { + return empty; + } + unsafe { + let mut e: xcb_generic_error_t = std::mem::zeroed(); + let reply = xcb_get_atom_name_reply( + conn, + xcb_get_atom_name(conn, atom), + &mut ((&mut e) as *mut xcb_generic_error_t) as _, + ); + if reply == std::ptr::null() { + return empty; + } + let length = xcb_get_atom_name_name_length(reply); + let name = xcb_get_atom_name_name(reply); + let mut v = vec![0u8; length as _]; + std::ptr::copy_nonoverlapping(name as _, v.as_mut_ptr(), length as _); + libc::free(reply as *mut _); + if let Ok(s) = CString::new(v) { + return s.to_string_lossy().to_string(); + } + empty + } +} diff --git a/libs/virtual_display/dylib/src/win10/IddController.h b/libs/virtual_display/dylib/src/win10/IddController.h index 767d64798..909f17423 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.h +++ b/libs/virtual_display/dylib/src/win10/IddController.h @@ -134,7 +134,7 @@ const char* GetLastMsg(); * * @param b [in] TRUE to enable printing message. * - * @remark For now, no need to read evironment variable to check if should print. + * @remark For now, no need to read environment variable to check if should print. * */ VOID SetPrintErrMsg(BOOL b); diff --git a/res/128x128.png b/res/128x128.png deleted file mode 100644 index 26cbf702c..000000000 Binary files a/res/128x128.png and /dev/null differ diff --git a/res/128x128.png b/res/128x128.png new file mode 120000 index 000000000..f69b60eb2 --- /dev/null +++ b/res/128x128.png @@ -0,0 +1 @@ +../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png \ No newline at end of file diff --git a/res/128x128@2x.png b/res/128x128@2x.png index d6f8d20fa..9bccf65bb 100644 Binary files a/res/128x128@2x.png and b/res/128x128@2x.png differ diff --git a/res/32x32.png b/res/32x32.png deleted file mode 100644 index 33dc80537..000000000 Binary files a/res/32x32.png and /dev/null differ diff --git a/res/32x32.png b/res/32x32.png new file mode 120000 index 000000000..7c1136a73 --- /dev/null +++ b/res/32x32.png @@ -0,0 +1 @@ +../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png \ No newline at end of file diff --git a/res/64x64.png b/res/64x64.png new file mode 120000 index 000000000..45d1cf759 --- /dev/null +++ b/res/64x64.png @@ -0,0 +1 @@ +../flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png \ No newline at end of file diff --git a/res/com.rustdesk.RustDesk.policy b/res/com.rustdesk.RustDesk.policy index a77223120..55f13629b 100644 --- a/res/com.rustdesk.RustDesk.policy +++ b/res/com.rustdesk.RustDesk.policy @@ -11,6 +11,7 @@ Authentication is required to change RustDesk options 要更改RustDesk选项, 需要您先通过身份验证 要變更RustDesk選項, 需要您先通過身份驗證 + Authentifizierung zum Ändern der RustDesk-Optionen /usr/share/rustdesk/files/polkit true diff --git a/res/design.svg b/res/design.svg new file mode 100644 index 000000000..62e568242 --- /dev/null +++ b/res/design.svg @@ -0,0 +1,374 @@ + +rustdesk diff --git a/res/icon-margin.png b/res/icon-margin.png deleted file mode 100644 index 6449ec490..000000000 Binary files a/res/icon-margin.png and /dev/null differ diff --git a/res/icon.ico b/res/icon.ico deleted file mode 100644 index 41c02bb4e..000000000 Binary files a/res/icon.ico and /dev/null differ diff --git a/res/icon.ico b/res/icon.ico new file mode 120000 index 000000000..75324b38c --- /dev/null +++ b/res/icon.ico @@ -0,0 +1 @@ +../flutter/windows/runner/resources/app_icon.ico \ No newline at end of file diff --git a/res/icon.png b/res/icon.png index 823967c49..2575d80e7 100644 Binary files a/res/icon.png and b/res/icon.png differ diff --git a/res/lang.py b/res/lang.py index 37bbfb3b1..aa5f99f83 100644 --- a/res/lang.py +++ b/res/lang.py @@ -36,16 +36,16 @@ def main(): def expand(): for fn in glob.glob('./src/lang/*'): lang = os.path.basename(fn)[:-3] - if lang in ['en','cn']: continue + if lang in ['en','template']: continue print(lang) dict = get_lang(lang) fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') - for line in open('./src/lang/cn.rs', encoding='utf8'): + for line in open('./src/lang/template.rs', encoding='utf8'): line_strip = line.strip() if line_strip.startswith('("'): k, v = line_split(line_strip) if k in dict: - # embrased with " to avoid empty v + # embraced with " to avoid empty v line = line.replace('"%s"'%v, '"%s"'%dict[k]) else: line = line.replace(v, "") diff --git a/res/logo-header.svg b/res/logo-header.svg index 40c19c43c..9712636bf 100644 --- a/res/logo-header.svg +++ b/res/logo-header.svg @@ -1 +1 @@ -RUSTDESKYour remote desktop \ No newline at end of file + \ No newline at end of file diff --git a/res/logo.svg b/res/logo.svg index 0001d0762..965218c95 100644 --- a/res/logo.svg +++ b/res/logo.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/res/mac-icon.png b/res/mac-icon.png new file mode 100644 index 000000000..fc39cb2ff Binary files /dev/null and b/res/mac-icon.png differ diff --git a/res/mac-tray-dark-x2.png b/res/mac-tray-dark-x2.png new file mode 100644 index 000000000..595b850ae Binary files /dev/null and b/res/mac-tray-dark-x2.png differ diff --git a/res/mac-tray-dark.png b/res/mac-tray-dark.png deleted file mode 100644 index ba8ed8c12..000000000 Binary files a/res/mac-tray-dark.png and /dev/null differ diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png new file mode 100644 index 000000000..2e2711888 Binary files /dev/null and b/res/mac-tray-light-x2.png differ diff --git a/res/mac-tray-light.png b/res/mac-tray-light.png deleted file mode 100644 index ad8bfa396..000000000 Binary files a/res/mac-tray-light.png and /dev/null differ diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec new file mode 100644 index 000000000..77c28a94e --- /dev/null +++ b/res/rpm-flutter-suse.spec @@ -0,0 +1,87 @@ +Name: rustdesk +Version: 1.2.0 +Release: 0 +Summary: RPM package +License: GPL-3.0 +Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils curl libXtst6 libappindicator-gtk3 libvdpau1 libva2 +Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) + +%description +The best open-source remote desktop client software, written in Rust. + +%prep +# we have no source, so nothing here + +%build +# we have no source, so nothing here + +# %global __python %{__python3} + +%install + +mkdir -p "%{buildroot}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/lib/rustdesk" +mkdir -p "%{buildroot}/usr/bin" +install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files" +install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/rustdesk/files/rustdesk.png" + +%files +/usr/lib/rustdesk/* +/usr/share/rustdesk/files/rustdesk.service +/usr/share/rustdesk/files/rustdesk.png +/usr/share/rustdesk/files/rustdesk.desktop +/usr/share/rustdesk/files/rustdesk-link.desktop + +%changelog +# let's skip this for now + +# https://www.cnblogs.com/xingmuxin/p/8990255.html +%pre +# can do something for centos7 +case "$1" in + 1) + # for install + ;; + 2) + # for upgrade + systemctl stop rustdesk || true + ;; +esac + +%post +cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service +cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/ +cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/ +ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk +systemctl daemon-reload +systemctl enable rustdesk +systemctl start rustdesk +update-desktop-database + +%preun +case "$1" in + 0) + # for uninstall + systemctl stop rustdesk || true + systemctl disable rustdesk || true + rm /etc/systemd/system/rustdesk.service || true + ;; + 1) + # for upgrade + ;; +esac + +%postun +case "$1" in + 0) + # for uninstall + rm /usr/share/applications/rustdesk.desktop || true + rm /usr/share/applications/rustdesk-link.desktop || true + rm /usr/bin/rustdesk || true + update-desktop-database + ;; + 1) + # for upgrade + ;; +esac diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 73bb993aa..6124cbb70 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -4,7 +4,7 @@ Release: 0 Summary: RPM package License: GPL-3.0 Requires: gtk3 libxcb libxdo libXfixes alsa-lib curl libappindicator-gtk3 libvdpau libva -Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit) +Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) %description The best open-source remote desktop client software, written in Rust. diff --git a/res/rustdesk.desktop b/res/rustdesk.desktop index c9cf1f254..f31a16dec 100644 --- a/res/rustdesk.desktop +++ b/res/rustdesk.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Version=1.2.0 +Version=1.5 Name=RustDesk GenericName=Remote Desktop Comment=Remote Desktop @@ -16,4 +16,4 @@ X-Desktop-File-Install-Version=0.23 [Desktop Action new-window] Name=Open a New Window - +Exec=rustdesk %u diff --git a/res/setup.nsi b/res/setup.nsi index 5410e0ff5..635851d0a 100644 --- a/res/setup.nsi +++ b/res/setup.nsi @@ -56,8 +56,74 @@ InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" #################################################################### # Language -!insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "English" ; The first language is the default language +!insertmacro MUI_LANGUAGE "French" +!insertmacro MUI_LANGUAGE "German" +!insertmacro MUI_LANGUAGE "Spanish" +!insertmacro MUI_LANGUAGE "SpanishInternational" !insertmacro MUI_LANGUAGE "SimpChinese" +!insertmacro MUI_LANGUAGE "TradChinese" +!insertmacro MUI_LANGUAGE "Japanese" +!insertmacro MUI_LANGUAGE "Korean" +!insertmacro MUI_LANGUAGE "Italian" +!insertmacro MUI_LANGUAGE "Dutch" +!insertmacro MUI_LANGUAGE "Danish" +!insertmacro MUI_LANGUAGE "Swedish" +!insertmacro MUI_LANGUAGE "Norwegian" +!insertmacro MUI_LANGUAGE "NorwegianNynorsk" +!insertmacro MUI_LANGUAGE "Finnish" +!insertmacro MUI_LANGUAGE "Greek" +!insertmacro MUI_LANGUAGE "Russian" +!insertmacro MUI_LANGUAGE "Portuguese" +!insertmacro MUI_LANGUAGE "PortugueseBR" +!insertmacro MUI_LANGUAGE "Polish" +!insertmacro MUI_LANGUAGE "Ukrainian" +!insertmacro MUI_LANGUAGE "Czech" +!insertmacro MUI_LANGUAGE "Slovak" +!insertmacro MUI_LANGUAGE "Croatian" +!insertmacro MUI_LANGUAGE "Bulgarian" +!insertmacro MUI_LANGUAGE "Hungarian" +!insertmacro MUI_LANGUAGE "Thai" +!insertmacro MUI_LANGUAGE "Romanian" +!insertmacro MUI_LANGUAGE "Latvian" +!insertmacro MUI_LANGUAGE "Macedonian" +!insertmacro MUI_LANGUAGE "Estonian" +!insertmacro MUI_LANGUAGE "Turkish" +!insertmacro MUI_LANGUAGE "Lithuanian" +!insertmacro MUI_LANGUAGE "Slovenian" +!insertmacro MUI_LANGUAGE "Serbian" +!insertmacro MUI_LANGUAGE "SerbianLatin" +!insertmacro MUI_LANGUAGE "Arabic" +!insertmacro MUI_LANGUAGE "Farsi" +!insertmacro MUI_LANGUAGE "Hebrew" +!insertmacro MUI_LANGUAGE "Indonesian" +!insertmacro MUI_LANGUAGE "Mongolian" +!insertmacro MUI_LANGUAGE "Luxembourgish" +!insertmacro MUI_LANGUAGE "Albanian" +!insertmacro MUI_LANGUAGE "Breton" +!insertmacro MUI_LANGUAGE "Belarusian" +!insertmacro MUI_LANGUAGE "Icelandic" +!insertmacro MUI_LANGUAGE "Malay" +!insertmacro MUI_LANGUAGE "Bosnian" +!insertmacro MUI_LANGUAGE "Kurdish" +!insertmacro MUI_LANGUAGE "Irish" +!insertmacro MUI_LANGUAGE "Uzbek" +!insertmacro MUI_LANGUAGE "Galician" +!insertmacro MUI_LANGUAGE "Afrikaans" +!insertmacro MUI_LANGUAGE "Catalan" +!insertmacro MUI_LANGUAGE "Esperanto" +!insertmacro MUI_LANGUAGE "Asturian" +!insertmacro MUI_LANGUAGE "Basque" +!insertmacro MUI_LANGUAGE "Pashto" +!insertmacro MUI_LANGUAGE "ScotsGaelic" +!insertmacro MUI_LANGUAGE "Georgian" +!insertmacro MUI_LANGUAGE "Vietnamese" +!insertmacro MUI_LANGUAGE "Welsh" +!insertmacro MUI_LANGUAGE "Armenian" +!insertmacro MUI_LANGUAGE "Corsican" +!insertmacro MUI_LANGUAGE "Tatar" +!insertmacro MUI_LANGUAGE "Hindi" + #################################################################### # Sections diff --git a/res/tray-icon.ico b/res/tray-icon.ico index fd2e61628..df8bdaccb 100644 Binary files a/res/tray-icon.ico and b/res/tray-icon.ico differ diff --git a/src/cli.rs b/src/cli.rs index 57d63d397..40ab21188 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -36,7 +36,7 @@ impl Session { .lc .write() .unwrap() - .initialize(id.to_owned(), ConnType::PORT_FORWARD); + .initialize(id.to_owned(), ConnType::PORT_FORWARD, None); session } } @@ -72,6 +72,10 @@ impl Interface for Session { } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { + log::info!( + "password={}", + hbb_common::password_security::temporary_password() + ); handle_hash(self.lc.clone(), &pass, hash, self, peer).await; } diff --git a/src/client.rs b/src/client.rs index fe9d9dac0..607b96635 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,13 @@ +use std::{ + collections::HashMap, + net::SocketAddr, + ops::Deref, + str::FromStr, + sync::{mpsc, Arc, Mutex, RwLock}, +}; + pub use async_trait::async_trait; +use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "linux")))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, @@ -6,15 +15,11 @@ use cpal::{ }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; -use std::{ - collections::HashMap, - net::SocketAddr, - ops::{Deref, Not}, - sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, -}; use uuid::Uuid; pub use file_trait::FileManager; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::tokio::sync::mpsc::UnboundedSender; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -23,7 +28,7 @@ use hbb_common::{ Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT, }, - log, + get_version_number, log, message_proto::{option_message::BoolOption, *}, protobuf::Message as _, rand, @@ -34,12 +39,22 @@ use hbb_common::{ tokio::time::Duration, AddrMangle, ResultType, Stream, }; -pub use helper::LatencyController; pub use helper::*; use scrap::{ codec::{Decoder, DecoderCfg}, record::{Recorder, RecorderContext}, - VpxDecoderConfig, VpxVideoCodecId, + ImageFormat, VpxDecoderConfig, VpxVideoCodecId, +}; + +use crate::{ + common::{self, is_keyboard_mode_supported}, + server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, +}; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::{ + common::{check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, + ui_session_interface::SessionPermissionConfig, }; pub use super::lang::*; @@ -47,16 +62,19 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; -use crate::server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}; -pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); -pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); -pub static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); + pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); /// Client of the remote desktop. pub struct Client; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +struct TextClipboardState { + is_required: bool, + running: bool, +} + #[cfg(not(any(target_os = "android", target_os = "linux")))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); @@ -65,6 +83,8 @@ lazy_static::lazy_static! { #[cfg(not(any(target_os = "android", target_os = "ios")))] lazy_static::lazy_static! { static ref ENIGO: Arc> = Arc::new(Mutex::new(enigo::Enigo::new())); + static ref OLD_CLIPBOARD_TEXT: Arc> = Default::default(); + static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -80,7 +100,7 @@ pub fn get_key_state(key: enigo::Key) -> bool { cfg_if::cfg_if! { if #[cfg(target_os = "android")] { -use libc::{c_float, c_int, c_void}; +use hbb_common::libc::{c_float, c_int, c_void}; type Oboe = *mut c_void; extern "C" { fn create_oboe_player(channels: c_int, sample_rate: c_int) -> Oboe; @@ -167,7 +187,7 @@ impl Client { interface: impl Interface, ) -> ResultType<(Stream, bool)> { // to-do: remember the port for each peer, so that we can retry easier - if hbb_common::is_ipv4_str(peer) { + if hbb_common::is_ip_str(peer) { return Ok(( socket_client::connect_tcp( crate::check_port(peer, RELAY_PORT + 1), @@ -177,6 +197,13 @@ impl Client { true, )); } + // Allow connect to {domain}:{port} + if hbb_common::is_domain_port_str(peer) { + return Ok(( + socket_client::connect_tcp(peer, RENDEZVOUS_TIMEOUT).await?, + true, + )); + } let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await; let mut socket = socket_client::connect_tcp(&*rendezvous_server, RENDEZVOUS_TIMEOUT).await; debug_assert!(!servers.contains(&rendezvous_server)); @@ -376,7 +403,8 @@ impl Client { log::info!("peer address: {}, timeout: {}", peer, connect_timeout); let start = std::time::Instant::now(); // NOTICE: Socks5 is be used event in intranet. Which may be not a good way. - let mut conn = socket_client::connect_tcp_local(peer, Some(local_addr), connect_timeout).await; + let mut conn = + socket_client::connect_tcp_local(peer, Some(local_addr), connect_timeout).await; let mut direct = !conn.is_err(); if interface.is_force_relay() || conn.is_err() { if !relay_server.is_empty() { @@ -582,6 +610,86 @@ impl Client { conn.send(&msg_out).await?; Ok(conn) } + + #[inline] + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn set_is_text_clipboard_required(b: bool) { + TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_stop_clipboard(_self_id: &str) { + #[cfg(feature = "flutter")] + if crate::flutter::other_sessions_running(_self_id) { + return; + } + TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn try_start_clipboard(_conf_tx: Option<(SessionPermissionConfig, UnboundedSender)>) { + let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + if clipboard_lock.running { + return; + } + + match ClipboardContext::new() { + Ok(mut ctx) => { + clipboard_lock.running = true; + // ignore clipboard update before service start + check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)); + std::thread::spawn(move || { + log::info!("Start text clipboard loop"); + loop { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + break; + } + + if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { + continue; + } + + if let Some(msg) = check_clipboard(&mut ctx, Some(&OLD_CLIPBOARD_TEXT)) { + #[cfg(feature = "flutter")] + crate::flutter::send_text_clipboard_msg(msg); + #[cfg(not(feature = "flutter"))] + if let Some((cfg, tx)) = &_conf_tx { + if cfg.is_text_clipboard_required() { + let _ = tx.send(Data::Message(msg)); + } + } + } + } + log::info!("Stop text clipboard loop"); + }); + } + Err(err) => { + log::error!("Failed to start clipboard service of client: {}", err); + } + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn get_current_text_clipboard_msg() -> Option { + let txt = &*OLD_CLIPBOARD_TEXT.lock().unwrap(); + if txt.is_empty() { + None + } else { + Some(crate::create_clipboard_msg(txt.clone())) + } + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl TextClipboardState { + fn new() -> Self { + Self { + is_required: true, + running: false, + } + } } /// Audio handler for the [`Client`]. @@ -598,18 +706,9 @@ pub struct AudioHandler { #[cfg(not(any(target_os = "android", target_os = "linux")))] audio_stream: Option>, channels: u16, - latency_controller: Arc>, } impl AudioHandler { - /// Create a new audio handler. - pub fn new(latency_controller: Arc>) -> Self { - AudioHandler { - latency_controller, - ..Default::default() - } - } - /// Start the audio playback. #[cfg(target_os = "linux")] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { @@ -692,25 +791,15 @@ impl AudioHandler { } /// Handle audio frame and play it. + #[inline] pub fn handle_frame(&mut self, frame: AudioFrame) { - if frame.timestamp != 0 { - if self - .latency_controller - .lock() - .unwrap() - .check_audio(frame.timestamp) - .not() - { - return; - } - } - #[cfg(not(any(target_os = "android", target_os = "linux")))] if self.audio_stream.is_none() { return; } #[cfg(target_os = "linux")] if self.simple.is_none() { + log::debug!("PulseAudio simple binding does not exists"); return; } #[cfg(target_os = "android")] @@ -798,7 +887,6 @@ impl AudioHandler { /// Video handler for the [`Client`]. pub struct VideoHandler { decoder: Decoder, - latency_controller: Arc>, pub rgb: Vec, recorder: Arc>>, record: bool, @@ -806,7 +894,7 @@ pub struct VideoHandler { impl VideoHandler { /// Create a new video handler. - pub fn new(latency_controller: Arc>) -> Self { + pub fn new() -> Self { VideoHandler { decoder: Decoder::new(DecoderCfg { vpx: VpxDecoderConfig { @@ -814,7 +902,6 @@ impl VideoHandler { num_threads: (num_cpus::get() / 2) as _, }, }), - latency_controller, rgb: Default::default(), recorder: Default::default(), record: false, @@ -822,17 +909,15 @@ impl VideoHandler { } /// Handle a new video frame. + #[inline] pub fn handle_frame(&mut self, vf: VideoFrame) -> ResultType { - if vf.timestamp != 0 { - // Update the lantency controller with the latest timestamp. - self.latency_controller - .lock() - .unwrap() - .update_video(vf.timestamp); - } match &vf.union { Some(frame) => { - let res = self.decoder.handle_video_frame(frame, &mut self.rgb); + let res = self.decoder.handle_video_frame( + frame, + (ImageFormat::ARGB, crate::DST_STRIDE_RGBA), + &mut self.rgb, + ); if self.record { self.recorder .lock() @@ -874,6 +959,7 @@ impl VideoHandler { } else { self.recorder = Default::default(); } + self.record = start; } } @@ -897,6 +983,9 @@ pub struct LoginConfigHandler { pub force_relay: bool, pub direct: Option, pub received: bool, + switch_uuid: Option, + pub success_time: Option, + pub direct_error_counter: usize, } impl Deref for LoginConfigHandler { @@ -924,7 +1013,13 @@ impl LoginConfigHandler { /// /// * `id` - id of peer /// * `conn_type` - Connection type enum. - pub fn initialize(&mut self, id: String, conn_type: ConnType) { + pub fn initialize( + &mut self, + id: String, + conn_type: ConnType, + switch_uuid: Option, + force_relay: bool, + ) { self.id = id; self.conn_type = conn_type; let config = self.load_config(); @@ -933,15 +1028,18 @@ impl LoginConfigHandler { self.session_id = rand::random(); self.supported_encoding = None; self.restarting_remote_device = false; - self.force_relay = !self.get_option("force-always-relay").is_empty(); + self.force_relay = !self.get_option("force-always-relay").is_empty() || force_relay; self.direct = None; self.received = false; + self.switch_uuid = switch_uuid; + self.success_time = None; + self.direct_error_counter = 0; } /// Check if the client should auto login. /// Return password if the client should auto login, otherwise return empty string. pub fn should_auto_login(&self) -> String { - let l = self.lock_after_session_end; + let l = self.lock_after_session_end.v; let a = !self.get_option("auto-login").is_empty(); let p = self.get_option("os-password"); if !p.is_empty() && l && a { @@ -1048,32 +1146,32 @@ impl LoginConfigHandler { let mut option = OptionMessage::default(); let mut config = self.load_config(); if name == "show-remote-cursor" { - config.show_remote_cursor = !config.show_remote_cursor; - option.show_remote_cursor = (if config.show_remote_cursor { + config.show_remote_cursor.v = !config.show_remote_cursor.v; + option.show_remote_cursor = (if config.show_remote_cursor.v { BoolOption::Yes } else { BoolOption::No }) .into(); } else if name == "disable-audio" { - config.disable_audio = !config.disable_audio; - option.disable_audio = (if config.disable_audio { + config.disable_audio.v = !config.disable_audio.v; + option.disable_audio = (if config.disable_audio.v { BoolOption::Yes } else { BoolOption::No }) .into(); } else if name == "disable-clipboard" { - config.disable_clipboard = !config.disable_clipboard; - option.disable_clipboard = (if config.disable_clipboard { + config.disable_clipboard.v = !config.disable_clipboard.v; + option.disable_clipboard = (if config.disable_clipboard.v { BoolOption::Yes } else { BoolOption::No }) .into(); } else if name == "lock-after-session-end" { - config.lock_after_session_end = !config.lock_after_session_end; - option.lock_after_session_end = (if config.lock_after_session_end { + config.lock_after_session_end.v = !config.lock_after_session_end.v; + option.lock_after_session_end = (if config.lock_after_session_end.v { BoolOption::Yes } else { BoolOption::No @@ -1081,15 +1179,15 @@ impl LoginConfigHandler { .into(); } else if name == "privacy-mode" { // try toggle privacy mode - option.privacy_mode = (if config.privacy_mode { + option.privacy_mode = (if config.privacy_mode.v { BoolOption::No } else { BoolOption::Yes }) .into(); } else if name == "enable-file-transfer" { - config.enable_file_transfer = !config.enable_file_transfer; - option.enable_file_transfer = (if config.enable_file_transfer { + config.enable_file_transfer.v = !config.enable_file_transfer.v; + option.enable_file_transfer = (if config.enable_file_transfer.v { BoolOption::Yes } else { BoolOption::No @@ -1100,11 +1198,40 @@ impl LoginConfigHandler { } else if name == "unblock-input" { option.block_input = BoolOption::No.into(); } else if name == "show-quality-monitor" { - config.show_quality_monitor = !config.show_quality_monitor; + config.show_quality_monitor.v = !config.show_quality_monitor.v; + } else if name == "allow_swap_key" { + config.allow_swap_key.v = !config.allow_swap_key.v; + } else if name == "view-only" { + config.view_only.v = !config.view_only.v; + let f = |b: bool| { + if b { + BoolOption::Yes.into() + } else { + BoolOption::No.into() + } + }; + if config.view_only.v { + option.disable_keyboard = f(true); + option.disable_clipboard = f(true); + option.show_remote_cursor = f(true); + } else { + option.disable_keyboard = f(false); + option.disable_clipboard = f(self.get_toggle_option("disable-clipboard")); + option.show_remote_cursor = f(self.get_toggle_option("show-remote-cursor")); + } } else { - let v = self.options.get(&name).is_some(); - if v { - self.config.options.remove(&name); + let is_set = self + .options + .get(&name) + .map(|o| !o.is_empty()) + .unwrap_or(false); + if is_set { + if name == "zoom-cursor" { + self.config.options.insert(name, "".to_owned()); + } else { + // Notice: When PeerConfig loads, the default value is taken when the option key does not exist. + self.config.options.remove(&name); + } } else { self.config.options.insert(name, "Y".to_owned()); } @@ -1158,7 +1285,12 @@ impl LoginConfigHandler { if let Some(custom_fps) = self.options.get("custom-fps") { msg.custom_fps = custom_fps.parse().unwrap_or(30); } - if self.get_toggle_option("show-remote-cursor") { + let view_only = self.get_toggle_option("view-only"); + if view_only { + msg.disable_keyboard = BoolOption::Yes.into(); + n += 1; + } + if view_only || self.get_toggle_option("show-remote-cursor") { msg.show_remote_cursor = BoolOption::Yes.into(); n += 1; } @@ -1174,7 +1306,7 @@ impl LoginConfigHandler { msg.enable_file_transfer = BoolOption::Yes.into(); n += 1; } - if self.get_toggle_option("disable-clipboard") { + if view_only || self.get_toggle_option("disable-clipboard") { msg.disable_clipboard = BoolOption::Yes.into(); n += 1; } @@ -1237,19 +1369,23 @@ impl LoginConfigHandler { /// * `name` - The name of the toggle option. pub fn get_toggle_option(&self, name: &str) -> bool { if name == "show-remote-cursor" { - self.config.show_remote_cursor + self.config.show_remote_cursor.v } else if name == "lock-after-session-end" { - self.config.lock_after_session_end + self.config.lock_after_session_end.v } else if name == "privacy-mode" { - self.config.privacy_mode + self.config.privacy_mode.v } else if name == "enable-file-transfer" { - self.config.enable_file_transfer + self.config.enable_file_transfer.v } else if name == "disable-audio" { - self.config.disable_audio + self.config.disable_audio.v } else if name == "disable-clipboard" { - self.config.disable_clipboard + self.config.disable_clipboard.v } else if name == "show-quality-monitor" { - self.config.show_quality_monitor + self.config.show_quality_monitor.v + } else if name == "allow_swap_key" { + self.config.allow_swap_key.v + } else if name == "view-only" { + self.config.view_only.v } else { !self.get_option(name).is_empty() } @@ -1391,12 +1527,18 @@ impl LoginConfigHandler { log::debug!("remove password of {}", self.id); } } - if config.keyboard_mode == "" { - if hbb_common::get_version_number(&pi.version) < hbb_common::get_version_number("1.2.0") - { - config.keyboard_mode = "legacy".to_string(); + if config.keyboard_mode.is_empty() { + if is_keyboard_mode_supported(&KeyboardMode::Map, get_version_number(&pi.version)) { + config.keyboard_mode = KeyboardMode::Map.to_string(); } else { - config.keyboard_mode = "map".to_string(); + config.keyboard_mode = KeyboardMode::Legacy.to_string(); + } + } else { + let keyboard_modes = + common::get_supported_keyboard_modes(get_version_number(&pi.version)); + let current_mode = &KeyboardMode::from_str(&config.keyboard_mode).unwrap_or_default(); + if !keyboard_modes.contains(current_mode) { + config.keyboard_mode = KeyboardMode::Legacy.to_string(); } } self.conn_id = pi.conn_id; @@ -1515,23 +1657,19 @@ pub type MediaSender = mpsc::Sender; /// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where - F: 'static + FnMut(&[u8]) + Send, + F: 'static + FnMut(&mut Vec) + Send, { let (video_sender, video_receiver) = mpsc::channel::(); - let (audio_sender, audio_receiver) = mpsc::channel::(); let mut video_callback = video_callback; - let latency_controller = LatencyController::new(); - let latency_controller_cl = latency_controller.clone(); - std::thread::spawn(move || { - let mut video_handler = VideoHandler::new(latency_controller); + let mut video_handler = VideoHandler::new(); loop { if let Ok(data) = video_receiver.recv() { match data { MediaData::VideoFrame(vf) => { if let Ok(true) = video_handler.handle_frame(vf) { - video_callback(&video_handler.rgb); + video_callback(&mut video_handler.rgb); } } MediaData::Reset => { @@ -1548,8 +1686,16 @@ where } log::info!("Video decoder loop exits"); }); + let audio_sender = start_audio_thread(); + return (video_sender, audio_sender); +} + +/// Start an audio thread +/// Return a audio [`MediaSender`] +pub fn start_audio_thread() -> MediaSender { + let (audio_sender, audio_receiver) = mpsc::channel::(); std::thread::spawn(move || { - let mut audio_handler = AudioHandler::new(latency_controller_cl); + let mut audio_handler = AudioHandler::default(); loop { if let Ok(data) = audio_receiver.recv() { match data { @@ -1557,6 +1703,7 @@ where audio_handler.handle_frame(af); } MediaData::AudioFormat(f) => { + log::debug!("recved audio format, sample rate={}", f.sample_rate); audio_handler.handle_format(f); } _ => {} @@ -1567,7 +1714,7 @@ where } log::info!("Audio decoder loop exits"); }); - return (video_sender, audio_sender); + audio_sender } /// Handle latency test. @@ -1657,11 +1804,12 @@ pub fn send_mouse( if check_scroll_on_mac(mask, x, y) { mouse_event.modifiers.push(ControlKey::Scroll.into()); } + interface.swap_modifier_mouse(&mut mouse_event); msg_out.set_mouse_event(mouse_event); interface.send(Data::Message(msg_out)); } -/// Avtivate OS by sending mouse movement. +/// Activate OS by sending mouse movement. /// /// # Arguments /// @@ -1689,7 +1837,7 @@ fn activate_os(interface: &impl Interface) { /// # Arguments /// /// * `p` - The password. -/// * `avtivate` - Whether to activate OS. +/// * `activate` - Whether to activate OS. /// * `interface` - The interface for sending data. pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { std::thread::spawn(move || { @@ -1702,7 +1850,7 @@ pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { /// # Arguments /// /// * `p` - The password. -/// * `avtivate` - Whether to activate OS. +/// * `activate` - Whether to activate OS. /// * `interface` - The interface for sending data. fn _input_os_password(p: String, activate: bool, interface: impl Interface) { if activate { @@ -1766,6 +1914,14 @@ pub async fn handle_hash( interface: &impl Interface, peer: &mut Stream, ) { + lc.write().unwrap().hash = hash.clone(); + let uuid = lc.read().unwrap().switch_uuid.clone(); + if let Some(uuid) = uuid { + if let Ok(uuid) = uuid::Uuid::from_str(&uuid) { + send_switch_login_request(lc.clone(), peer, uuid).await; + return; + } + } let mut password = lc.read().unwrap().password.clone(); if password.is_empty() { if !password_preset.is_empty() { @@ -1830,6 +1986,26 @@ pub async fn handle_login_from_ui( send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; } +async fn send_switch_login_request( + lc: Arc>, + peer: &mut Stream, + uuid: Uuid, +) { + let mut msg_out = Message::new(); + msg_out.set_switch_sides_response(SwitchSidesResponse { + uuid: Bytes::from(uuid.as_bytes().to_vec()), + lr: hbb_common::protobuf::MessageField::some( + lc.read() + .unwrap() + .create_login_msg(vec![]) + .login_request() + .to_owned(), + ), + ..Default::default() + }); + allow_err!(peer.send(&msg_out).await); +} + /// Interface for client to send data and commands. #[async_trait] pub trait Interface: Send + Clone + 'static + Sized { @@ -1847,11 +2023,15 @@ pub trait Interface: Send + Clone + 'static + Sized { fn get_login_config_handler(&self) -> Arc>; fn set_force_relay(&self, direct: bool, received: bool) { - self.get_login_config_handler().write().unwrap().set_force_relay(direct, received); + self.get_login_config_handler() + .write() + .unwrap() + .set_force_relay(direct, received); } fn is_force_relay(&self) -> bool { self.get_login_config_handler().read().unwrap().force_relay } + fn swap_modifier_mouse(&self, _msg: &mut hbb_common::protos::message::MouseEvent) {} } /// Data used by the client interface. @@ -1876,6 +2056,10 @@ pub enum Data { AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), RecordScreen(bool, i32, i32, String), + ElevateDirect, + ElevateWithLogon(String, String), + NewVoiceCall, + CloseVoiceCall, } /// Keycode for key events. @@ -2028,8 +2212,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b && !text.to_lowercase().contains("resolve") && !text.to_lowercase().contains("mismatch") && !text.to_lowercase().contains("manually") - && !text.to_lowercase().contains("not allowed") - && !text.to_lowercase().contains("reset by the peer"))) + && !text.to_lowercase().contains("not allowed"))) } #[inline] @@ -2045,7 +2228,7 @@ fn get_pk(pk: &[u8]) -> Option<[u8; 32]> { #[inline] fn get_rs_pk(str_base64: &str) -> Option { - if let Ok(pk) = base64::decode(str_base64) { + if let Ok(pk) = crate::decode64(str_base64) { get_pk(&pk).map(|x| sign::PublicKey(x)) } else { None diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 2ecfca837..49e3f2358 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -7,7 +7,7 @@ pub trait FileManager: Interface { fs::get_home_as_string() } - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { match fs::read_dir(&fs::get_path(&path), include_hidden) { Err(_) => sciter::Value::null(), @@ -20,7 +20,7 @@ pub trait FileManager: Interface { } } - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { diff --git a/src/client/helper.rs b/src/client/helper.rs index d38fbf223..a9696a8e8 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -1,68 +1,8 @@ -use std::{ - sync::{Arc, Mutex}, - time::Instant, -}; - use hbb_common::{ - log, - message_proto::{video_frame, VideoFrame}, + get_time, + message_proto::{video_frame, Message, VideoFrame, VoiceCallRequest, VoiceCallResponse}, }; -const MAX_LATENCY: i64 = 500; -const MIN_LATENCY: i64 = 100; - -/// Latency controller for syncing audio with the video stream. -/// Only sync the audio to video, not the other way around. -#[derive(Debug)] -pub struct LatencyController { - last_video_remote_ts: i64, // generated on remote deivce - update_time: Instant, - allow_audio: bool, -} - -impl Default for LatencyController { - fn default() -> Self { - Self { - last_video_remote_ts: Default::default(), - update_time: Instant::now(), - allow_audio: Default::default(), - } - } -} - -impl LatencyController { - /// Create a new latency controller. - pub fn new() -> Arc> { - Arc::new(Mutex::new(LatencyController::default())) - } - - /// Update the latency controller with the latest video timestamp. - pub fn update_video(&mut self, timestamp: i64) { - self.last_video_remote_ts = timestamp; - self.update_time = Instant::now(); - } - - /// Check if the audio should be played based on the current latency. - pub fn check_audio(&mut self, timestamp: i64) -> bool { - // Compute audio latency. - let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; - let latency = expected - timestamp; - // Set MAX and MIN, avoid fixing too frequently. - if self.allow_audio { - if latency.abs() > MAX_LATENCY { - log::debug!("LATENCY > {}ms cut off, latency:{}", MAX_LATENCY, latency); - self.allow_audio = false; - } - } else { - if latency.abs() < MIN_LATENCY { - log::debug!("LATENCY < {}ms resume, latency:{}", MIN_LATENCY, latency); - self.allow_audio = true; - } - } - self.allow_audio - } -} - #[derive(PartialEq, Debug, Clone)] pub enum CodecFormat { VP9, @@ -101,3 +41,24 @@ pub struct QualityStatus { pub target_bitrate: Option, pub codec_format: Option, } + +#[inline] +pub fn new_voice_call_request(is_connect: bool) -> Message { + let mut req = VoiceCallRequest::new(); + req.is_connect = is_connect; + req.req_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_request(req); + msg +} + +#[inline] +pub fn new_voice_call_response(request_timestamp: i64, accepted: bool) -> Message { + let mut resp = VoiceCallResponse::new(); + resp.accepted = accepted; + resp.req_timestamp = request_timestamp; + resp.ack_timestamp = get_time(); + let mut msg = Message::new(); + msg.set_voice_call_response(resp); + msg +} diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 1f81dfa55..4a020d197 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,17 +1,10 @@ -use crate::client::{ - Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, SEC30, - SERVER_CLIPBOARD_ENABLED, SERVER_FILE_TRANSFER_ENABLED, SERVER_KEYBOARD_ENABLED, -}; -use crate::common; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::common::{check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}; +use std::collections::HashMap; +use std::num::NonZeroI64; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; #[cfg(windows)] use clipboard::{cliprdr::CliprdrClientContext, ContextSend}; - -use crate::ui_session_interface::{InvokeUiSession, Session}; -use crate::{client::Data, client::Interface}; - use hbb_common::config::{PeerConfig, TransferSerde}; use hbb_common::fs::{ can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, @@ -20,6 +13,7 @@ use hbb_common::fs::{ use hbb_common::message_proto::permission_info::Permission; use hbb_common::protobuf::Message as _; use hbb_common::rendezvous_proto::ConnType; +use hbb_common::tokio::sync::mpsc::error::TryRecvError; #[cfg(windows)] use hbb_common::tokio::sync::Mutex as TokioMutex; use hbb_common::tokio::{ @@ -27,12 +21,19 @@ use hbb_common::tokio::{ sync::mpsc, time::{self, Duration, Instant, Interval}, }; -use hbb_common::{allow_err, message_proto::*, sleep}; +use hbb_common::{allow_err, get_time, message_proto::*, sleep}; use hbb_common::{fs, log, Stream}; -use std::collections::HashMap; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex}; +use crate::client::{ + new_voice_call_request, Client, CodecFormat, MediaData, MediaSender, QualityStatus, MILLI1, + SEC30, +}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::update_clipboard; +use crate::common::{get_default_sound_input, set_sound_input}; +use crate::ui_session_interface::{InvokeUiSession, Session}; +use crate::{audio_service, common, ConnInner, CLIENT_SERVER}; +use crate::{client::Data, client::Interface}; pub struct Remote { handler: Session, @@ -40,6 +41,9 @@ pub struct Remote { audio_sender: MediaSender, receiver: mpsc::UnboundedReceiver, sender: mpsc::UnboundedSender, + // Stop sending local audio to remote client. + stop_voice_call_sender: Option>, + voice_call_request_timestamp: Option, old_clipboard: Arc>, read_jobs: Vec, write_jobs: Vec, @@ -52,6 +56,7 @@ pub struct Remote { data_count: Arc, frame_count: Arc, video_format: CodecFormat, + elevation_requested: bool, } impl Remote { @@ -81,11 +86,13 @@ impl Remote { data_count: Arc::new(AtomicUsize::new(0)), frame_count, video_format: CodecFormat::Unknown, + stop_voice_call_sender: None, + voice_call_request_timestamp: None, + elevation_requested: false, } } pub async fn io_loop(&mut self, key: &str, token: &str) { - let stop_clipboard = self.start_clipboard(); let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { @@ -93,6 +100,7 @@ impl Remote { } else { ConnType::default() }; + match Client::start( &self.handler.id, key, @@ -103,9 +111,6 @@ impl Remote { .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.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready self.handler.set_connection_info(direct, false); @@ -140,7 +145,15 @@ impl Remote { Err(err) => { log::error!("Connection closed: {}", err); self.handler.set_force_relay(direct, received); - self.handler.msgbox("error", "Connection Error", &err.to_string(), ""); + let msgtype = "error"; + let title = "Connection Error"; + let text = err.to_string(); + let show_relay_hint = self.handler.show_relay_hint(last_recv_time, msgtype, title, &text); + if show_relay_hint{ + self.handler.msgbox("relay-hint", title, &text, ""); + } else { + self.handler.msgbox(msgtype, title, &text, ""); + } break; } Ok(ref bytes) => { @@ -212,18 +225,18 @@ impl Remote { } } log::debug!("Exit io_loop of id={}", self.handler.id); + // Stop client audio server. + if let Some(s) = self.stop_voice_call_sender.take() { + s.send(()).ok(); + } } 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); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Client::try_stop_clipboard(&self.handler.id); } fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option) { @@ -253,43 +266,76 @@ impl Remote { } } - fn start_clipboard(&mut self) -> Option> { + fn stop_voice_call(&mut self) { + let voice_call_sender = std::mem::replace(&mut self.stop_voice_call_sender, None); + if let Some(stopper) = voice_call_sender { + let _ = stopper.send(()); + } + } + + // Start a voice call recorder, records audio and send to remote + fn start_voice_call(&mut self) -> Option> { if self.handler.is_file_transfer() || self.handler.is_port_forward() { return None; } + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } + // Create a channel to receive error or closed message 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(); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - 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; + let (tx_audio_data, mut rx_audio_data) = hbb_common::tokio::sync::mpsc::unbounded_channel(); + // Create a stand-alone inner, add subscribe to audio service + let conn_id = CLIENT_SERVER.write().unwrap().get_new_id(); + let client_conn_inner = ConnInner::new(conn_id.clone(), Some(tx_audio_data), None); + // now we subscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner.clone(), + true, + ); + let tx_audio = self.sender.clone(); + std::thread::spawn(move || { + loop { + // check if client is closed + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit voice call audio service of client"); + // unsubscribe + CLIENT_SERVER.write().unwrap().subscribe( + audio_service::NAME, + client_conn_inner, + false, + ); + break; + } + _ => {} + } + match rx_audio_data.try_recv() { + Ok((_instant, msg)) => match &msg.union { + Some(message::Union::AudioFrame(frame)) => { + let mut msg = Message::new(); + msg.set_audio_frame(frame.clone()); + tx_audio.send(Data::Message(msg)).ok(); + } + Some(message::Union::Misc(misc)) => { + let mut msg = Message::new(); + msg.set_misc(misc.clone()); + tx_audio.send(Data::Message(msg)).ok(); } _ => {} + }, + Err(err) => { + if err == TryRecvError::Empty { + // ignore + } else { + log::debug!("Failed to record local audio channel: {}", err); + } } - 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) } @@ -632,6 +678,47 @@ impl Remote { .video_sender .send(MediaData::RecordScreen(start, w, h, id)); } + Data::ElevateDirect => { + let mut request = ElevationRequest::new(); + request.set_direct(true); + let mut misc = Misc::new(); + misc.set_elevation_request(request); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + self.elevation_requested = true; + } + Data::ElevateWithLogon(username, password) => { + let mut request = ElevationRequest::new(); + request.set_logon(ElevationRequestWithLogon { + username, + password, + ..Default::default() + }); + let mut misc = Misc::new(); + misc.set_elevation_request(request); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + self.elevation_requested = true; + } + Data::NewVoiceCall => { + let msg = new_voice_call_request(true); + // Save the voice call request timestamp for the further validation. + self.voice_call_request_timestamp = Some( + NonZeroI64::new(msg.voice_call_request().req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + allow_err!(peer.send(&msg).await); + self.handler.on_voice_call_waiting(); + } + Data::CloseVoiceCall => { + self.stop_voice_call(); + let msg = new_voice_call_request(false); + self.handler + .on_voice_call_closed("Closed manually by the peer"); + allow_err!(peer.send(&msg).await); + } _ => {} } true @@ -728,11 +815,11 @@ impl Remote { self.handler.adapt_size(); 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(); + let incoming_format = CodecFormat::from(&vf); + if self.video_format != incoming_format { + self.video_format = incoming_format.clone(); self.handler.update_quality_status(QualityStatus { - codec_format: Some(incomming_format), + codec_format: Some(incoming_format), ..Default::default() }) }; @@ -752,22 +839,31 @@ impl Remote { 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.handler.is_port_forward()) { + let sender = self.sender.clone(); + let permission_config = self.handler.get_permission_config(); + + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Client::try_start_clipboard(None); + #[cfg(not(feature = "flutter"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Client::try_start_clipboard(Some(( + permission_config.clone(), + sender.clone(), + ))); + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + tokio::spawn(async move { + // due to clipboard service interval time + sleep(common::CLIPBOARD_INTERVAL as f32 / 1_000.).await; + if permission_config.is_text_clipboard_required() { + if let Some(msg_out) = Client::get_current_text_clipboard_msg() + { + sender.send(Data::Message(msg_out)).ok(); + } + } + }); } if self.handler.is_file_transfer() { @@ -786,7 +882,7 @@ impl Remote { self.handler.set_cursor_position(cp); } Some(message::Union::Clipboard(cb)) => { - if !self.handler.lc.read().unwrap().disable_clipboard { + if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(not(any(target_os = "android", target_os = "ios")))] update_clipboard(cb, Some(&self.old_clipboard)); #[cfg(any(target_os = "android", target_os = "ios"))] @@ -856,6 +952,7 @@ impl Remote { digest.file_num, read_path, true, + digest.is_identical, ); } } @@ -899,6 +996,7 @@ impl Remote { digest.file_num, write_path, false, + digest.is_identical, ); } } @@ -915,7 +1013,7 @@ impl Remote { } }, Err(err) => { - println!("error recving digest: {}", err); + println!("error receiving digest: {}", err); } } } @@ -957,40 +1055,54 @@ impl Remote { } 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); + // https://github.com/rustdesk/rustdesk/issues/3703#issuecomment-1474734754 + match p.permission.enum_value() { + Ok(Permission::Keyboard) => { + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::flutter::update_text_clipboard_required(); + *self.handler.server_keyboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("keyboard", p.enabled); } - Permission::Clipboard => { - SERVER_CLIPBOARD_ENABLED.store(p.enabled, Ordering::SeqCst); + Ok(Permission::Clipboard) => { + #[cfg(feature = "flutter")] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::flutter::update_text_clipboard_required(); + *self.handler.server_clipboard_enabled.write().unwrap() = p.enabled; self.handler.set_permission("clipboard", p.enabled); } - Permission::Audio => { + Ok(Permission::Audio) => { self.handler.set_permission("audio", p.enabled); } - Permission::File => { - SERVER_FILE_TRANSFER_ENABLED.store(p.enabled, Ordering::SeqCst); + Ok(Permission::File) => { + *self.handler.server_file_transfer_enabled.write().unwrap() = + p.enabled; if !p.enabled && self.handler.is_file_transfer() { return true; } self.check_clipboard_file_context(); self.handler.set_permission("file", p.enabled); } - Permission::Restart => { + Ok(Permission::Restart) => { self.handler.set_permission("restart", p.enabled); } - Permission::Recording => { + Ok(Permission::Recording) => { self.handler.set_permission("recording", p.enabled); } + _ => {} } } Some(misc::Union::SwitchDisplay(s)) => { self.handler.ui_handler.switch_display(&s); self.video_sender.send(MediaData::Reset).ok(); if s.width > 0 && s.height > 0 { - self.handler - .set_display(s.x, s.y, s.width, s.height, s.cursor_embedded); + self.handler.set_display( + s.x, + s.y, + s.width, + s.height, + s.cursor_embedded, + ); } } Some(misc::Union::CloseReason(c)) => { @@ -1003,40 +1115,99 @@ impl Remote { } } Some(misc::Union::Uac(uac)) => { - let msgtype = "custom-uac-nocancel"; - let title = "Prompt"; - let text = "Please wait for confirmation of UAC..."; - let link = ""; - if uac { - self.handler.msgbox(msgtype, title, text, link); - } else { - self.handler - .cancel_msgbox( - &format!("{}-{}-{}-{}", msgtype, title, text, link,), + #[cfg(feature = "flutter")] + { + if uac { + self.handler.msgbox( + "on-uac", + "Prompt", + "Please wait for confirmation of UAC...", + "", ); + } else { + self.handler.cancel_msgbox("on-uac"); + self.handler.cancel_msgbox("wait-uac"); + self.handler.cancel_msgbox("elevation-error"); + } + } + #[cfg(not(feature = "flutter"))] + { + let msgtype = "custom-uac-nocancel"; + let title = "Prompt"; + let text = "Please wait for confirmation of UAC..."; + let link = ""; + if uac { + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler.cancel_msgbox(&format!( + "{}-{}-{}-{}", + msgtype, title, text, link, + )); + } } } Some(misc::Union::ForegroundWindowElevated(elevated)) => { - let msgtype = "custom-elevated-foreground-nocancel"; - let title = "Prompt"; - let text = "elevated_foreground_window_tip"; - let link = ""; - if elevated { - self.handler.msgbox(msgtype, title, text, link); + #[cfg(feature = "flutter")] + { + if elevated { + self.handler.msgbox( + "on-foreground-elevated", + "Prompt", + "elevated_foreground_window_tip", + "", + ); + } else { + self.handler.cancel_msgbox("on-foreground-elevated"); + self.handler.cancel_msgbox("wait-uac"); + self.handler.cancel_msgbox("elevation-error"); + } + } + #[cfg(not(feature = "flutter"))] + { + let msgtype = "custom-elevated-foreground-nocancel"; + let title = "Prompt"; + let text = "elevated_foreground_window_tip"; + let link = ""; + if elevated { + self.handler.msgbox(msgtype, title, text, link); + } else { + self.handler.cancel_msgbox(&format!( + "{}-{}-{}-{}", + msgtype, title, text, link, + )); + } + } + } + Some(misc::Union::ElevationResponse(err)) => { + if err.is_empty() { + self.handler.msgbox("wait-uac", "", "", ""); } else { self.handler - .cancel_msgbox( - &format!("{}-{}-{}-{}", msgtype, title, text, link,), - ); + .msgbox("elevation-error", "Elevation Error", &err, ""); } } + Some(misc::Union::PortableServiceRunning(b)) => { + self.handler.portable_service_running(b); + if self.elevation_requested && b { + self.handler.msgbox( + "custom-nocancel-success", + "Successful", + "Elevate successfully", + "", + ); + } + } + Some(misc::Union::SwitchBack(_)) => { + #[cfg(feature = "flutter")] + self.handler.switch_back(&self.handler.id); + } _ => {} }, 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 { + if !self.handler.lc.read().unwrap().disable_audio.v { self.audio_sender.send(MediaData::AudioFrame(frame)).ok(); } } @@ -1061,6 +1232,40 @@ impl Remote { self.handler .msgbox(&msgbox.msgtype, &msgbox.title, &msgbox.text, &link); } + Some(message::Union::VoiceCallRequest(request)) => { + if request.is_connect { + // TODO: maybe we will do a voice call from the peer in the future. + } else { + log::debug!("The remote has requested to close the voice call"); + if let Some(sender) = self.stop_voice_call_sender.take() { + allow_err!(sender.send(())); + self.handler.on_voice_call_closed(""); + } + } + } + Some(message::Union::VoiceCallResponse(response)) => { + let ts = std::mem::replace(&mut self.voice_call_request_timestamp, None); + if let Some(ts) = ts { + if response.req_timestamp != ts.get() { + log::debug!("Possible encountering a voice call attack."); + } else { + if response.accepted { + // The peer accepted the voice call. + self.handler.on_voice_call_started(); + self.stop_voice_call_sender = self.start_voice_call(); + } else { + // The peer refused the voice call. + self.handler.on_voice_call_closed(""); + } + } + } + } + Some(message::Union::PeerInfo(pi)) => match pi.conn_id { + crate::SYNC_PEER_INFO_DISPLAYS => { + self.handler.set_displays(&pi.displays); + } + _ => {} + }, _ => {} } } @@ -1119,7 +1324,7 @@ impl Remote { #[inline(always)] fn update_privacy_mode(&mut self, on: bool) { let mut config = self.handler.load_config(); - config.privacy_mode = on; + config.privacy_mode.v = on; self.handler.save_config(config); self.handler.update_privacy_mode(); @@ -1192,15 +1397,15 @@ impl Remote { fn check_clipboard_file_context(&self) { #[cfg(windows)] { - let enabled = SERVER_FILE_TRANSFER_ENABLED.load(Ordering::SeqCst) - && self.handler.lc.read().unwrap().enable_file_transfer; + let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() + && self.handler.lc.read().unwrap().enable_file_transfer.v; ContextSend::enable(enabled); } } #[cfg(windows)] fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) { - if !self.handler.lc.read().unwrap().disable_clipboard { + if !self.handler.lc.read().unwrap().disable_clipboard.v { #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { if self.client_conn_id diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index e6f40e215..f0fe41b8d 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -1,9 +1,9 @@ -use clipboard::ClipbaordFile; +use clipboard::ClipboardFile; use hbb_common::message_proto::*; -pub fn clip_2_msg(clip: ClipbaordFile) -> Message { +pub fn clip_2_msg(clip: ClipboardFile) -> Message { match clip { - ClipbaordFile::MonitorReady => Message { + ClipboardFile::MonitorReady => Message { union: Some(message::Union::Cliprdr(Cliprdr { union: Some(cliprdr::Union::Ready(CliprdrMonitorReady { ..Default::default() @@ -12,7 +12,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FormatList { format_list } => { + ClipboardFile::FormatList { format_list } => { let mut formats: Vec = Vec::new(); for v in format_list.iter() { formats.push(CliprdrFormat { @@ -32,7 +32,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { ..Default::default() } } - ClipbaordFile::FormatListResponse { msg_flags } => Message { + ClipboardFile::FormatListResponse { msg_flags } => Message { union: Some(message::Union::Cliprdr(Cliprdr { union: Some(cliprdr::Union::FormatListResponse( CliprdrServerFormatListResponse { @@ -44,7 +44,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FormatDataRequest { + ClipboardFile::FormatDataRequest { requested_format_id, } => Message { union: Some(message::Union::Cliprdr(Cliprdr { @@ -58,7 +58,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FormatDataResponse { + ClipboardFile::FormatDataResponse { msg_flags, format_data, } => Message { @@ -74,7 +74,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FileContentsRequest { + ClipboardFile::FileContentsRequest { stream_id, list_index, dw_flags, @@ -102,7 +102,7 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { })), ..Default::default() }, - ClipbaordFile::FileContentsResponse { + ClipboardFile::FileContentsResponse { msg_flags, stream_id, requested_data, @@ -123,28 +123,28 @@ pub fn clip_2_msg(clip: ClipbaordFile) -> Message { } } -pub fn msg_2_clip(msg: Cliprdr) -> Option { +pub fn msg_2_clip(msg: Cliprdr) -> Option { match msg.union { - Some(cliprdr::Union::Ready(_)) => Some(ClipbaordFile::MonitorReady), + Some(cliprdr::Union::Ready(_)) => Some(ClipboardFile::MonitorReady), Some(cliprdr::Union::FormatList(data)) => { let mut format_list: Vec<(i32, String)> = Vec::new(); for v in data.formats.iter() { format_list.push((v.id, v.format.clone())); } - Some(ClipbaordFile::FormatList { format_list }) + Some(ClipboardFile::FormatList { format_list }) } - Some(cliprdr::Union::FormatListResponse(data)) => Some(ClipbaordFile::FormatListResponse { + Some(cliprdr::Union::FormatListResponse(data)) => Some(ClipboardFile::FormatListResponse { msg_flags: data.msg_flags, }), - Some(cliprdr::Union::FormatDataRequest(data)) => Some(ClipbaordFile::FormatDataRequest { + Some(cliprdr::Union::FormatDataRequest(data)) => Some(ClipboardFile::FormatDataRequest { requested_format_id: data.requested_format_id, }), - Some(cliprdr::Union::FormatDataResponse(data)) => Some(ClipbaordFile::FormatDataResponse { + Some(cliprdr::Union::FormatDataResponse(data)) => Some(ClipboardFile::FormatDataResponse { msg_flags: data.msg_flags, format_data: data.format_data.into(), }), Some(cliprdr::Union::FileContentsRequest(data)) => { - Some(ClipbaordFile::FileContentsRequest { + Some(ClipboardFile::FileContentsRequest { stream_id: data.stream_id, list_index: data.list_index, dw_flags: data.dw_flags, @@ -156,7 +156,7 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option { }) } Some(cliprdr::Union::FileContentsResponse(data)) => { - Some(ClipbaordFile::FileContentsResponse { + Some(ClipboardFile::FileContentsResponse { msg_flags: data.msg_flags, stream_id: data.stream_id, requested_data: data.requested_data.into(), diff --git a/src/common.rs b/src/common.rs index c28bbc3fc..7bdfd9012 100644 --- a/src/common.rs +++ b/src/common.rs @@ -18,7 +18,6 @@ pub use arboard::Clipboard as ClipboardContext; use hbb_common::compress::decompress; use hbb_common::{ allow_err, - anyhow::bail, compress::compress as compress_func, config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, get_version_number, log, @@ -31,11 +30,22 @@ use hbb_common::{ // #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; +use crate::ui_interface::{get_option, set_option}; + pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future; pub const CLIPBOARD_NAME: &'static str = "clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; +pub const SYNC_PEER_INFO_DISPLAYS: i32 = 1; + +#[cfg(all(target_os = "macos", feature = "flutter_texture_render"))] +// https://developer.apple.com/forums/thread/712709 +// Memory alignment should be multiple of 64. +pub const DST_STRIDE_RGBA: usize = 64; +#[cfg(not(all(target_os = "macos", feature = "flutter_texture_render")))] +pub const DST_STRIDE_RGBA: usize = 1; + // the executable name of the portable version pub const PORTABLE_APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; @@ -49,11 +59,16 @@ lazy_static::lazy_static! { pub static ref DEVICE_NAME: Arc> = Default::default(); } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + static ref ARBOARD_MTX: Arc> = Arc::new(Mutex::new(())); +} + pub fn global_init() -> bool { #[cfg(target_os = "linux")] { - if !scrap::is_x11() { - crate::server::wayland::set_wayland_scrap_map_err(); + if !*IS_X11 { + crate::server::wayland::init(); } } true @@ -93,7 +108,11 @@ pub fn check_clipboard( ) -> Option { let side = if old.is_none() { "host" } else { "client" }; let old = if let Some(old) = old { old } else { &CONTENT }; - if let Ok(content) = ctx.get_text() { + let content = { + let _lock = ARBOARD_MTX.lock().unwrap(); + ctx.get_text() + }; + if let Ok(content) = content { if content.len() < 2_000_000 && !content.is_empty() { let changed = content != *old.lock().unwrap(); if changed { @@ -106,6 +125,54 @@ pub fn check_clipboard( None } +/// Set sound input device. +pub fn set_sound_input(device: String) { + let prior_device = get_option("audio-input".to_owned()); + if prior_device != device { + log::info!("switch to audio input device {}", device); + std::thread::spawn(move || { + set_option("audio-input".to_owned(), device); + }); + } else { + log::info!("audio input is already set to {}", device); + } +} + +/// Get system's default sound input device name. +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_default_sound_input() -> Option { + #[cfg(not(target_os = "linux"))] + { + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + let dev = host.default_input_device(); + return if let Some(dev) = dev { + match dev.name() { + Ok(name) => Some(name), + Err(_) => None, + } + } else { + None + }; + } + #[cfg(target_os = "linux")] + { + let input = crate::platform::linux::get_default_pa_source(); + return if let Some(input) = input { + Some(input.1) + } else { + None + }; + } +} + +#[inline] +#[cfg(any(target_os = "android", target_os = "ios"))] +pub fn get_default_sound_input() -> Option { + None +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) { let content = if clipboard.compress { @@ -123,6 +190,7 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) let side = if old.is_none() { "host" } else { "client" }; let old = if let Some(old) = old { old } else { &CONTENT }; *old.lock().unwrap() = content.clone(); + let _lock = ARBOARD_MTX.lock().unwrap(); allow_err!(ctx.set_text(content)); log::debug!("{} updated on {}", CLIPBOARD_NAME, side); } @@ -293,15 +361,7 @@ async fn test_nat_type_() -> ResultType { let start = std::time::Instant::now(); let (rendezvous_server, _, _) = get_rendezvous_server(1_000).await; let server1 = rendezvous_server; - let tmp: Vec<&str> = server1.split(":").collect(); - if tmp.len() != 2 { - bail!("Invalid server address: {}", server1); - } - let port: u16 = tmp[1].parse()?; - if port == 0 { - bail!("Invalid server address: {}", server1); - } - let server2 = format!("{}:{}", tmp[0], port - 1); + let server2 = crate::increase_port(&server1, -1); let mut msg_out = RendezvousMessage::new(); let serial = Config::get_serial(); msg_out.set_test_nat_request(TestNatRequest { @@ -364,13 +424,7 @@ pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec, boo let (mut a, mut b) = get_rendezvous_server_(ms_timeout).await; let mut b: Vec = b .drain(..) - .map(|x| { - if !x.contains(":") { - format!("{}:{}", x, config::RENDEZVOUS_PORT) - } else { - x - } - }) + .map(|x| socket_client::check_port(x, config::RENDEZVOUS_PORT)) .collect(); let c = if b.contains(&a) { b = b.drain(..).filter(|x| x != &a).collect(); @@ -466,6 +520,7 @@ pub fn run_me>(args: Vec) -> std::io::Result String { // fix bug of whoami #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -474,13 +529,22 @@ pub fn username() -> String { return DEVICE_NAME.lock().unwrap().clone(); } +#[inline] +pub fn hostname() -> String { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return whoami::hostname(); + #[cfg(any(target_os = "android", target_os = "ios"))] + return DEVICE_NAME.lock().unwrap().clone(); +} + #[inline] pub fn check_port(host: T, port: i32) -> String { - let host = host.to_string(); - if !host.contains(":") { - return format!("{}:{}", host, port); - } - return host; + hbb_common::socket_client::check_port(host, port) +} + +#[inline] +pub fn increase_port(host: T, offset: i32) -> String { + hbb_common::socket_client::increase_port(host, offset) } pub const POSTFIX_SERVICE: &'static str = "_service"; @@ -543,11 +607,6 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { Ok(()) } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn get_icon() -> String { - hbb_common::config::ICON.to_owned() -} - pub fn get_app_name() -> String { hbb_common::config::APP_NAME.read().unwrap().clone() } @@ -591,18 +650,13 @@ pub fn get_api_server(api: String, custom: String) -> String { return lic.api.clone(); } } - let s = get_custom_rendezvous_server(custom); - if !s.is_empty() { - if s.contains(':') { - let tmp: Vec<&str> = s.split(":").collect(); - if tmp.len() == 2 { - let port: u16 = tmp[1].parse().unwrap_or(0); - if port > 2 { - return format!("http://{}:{}", tmp[0], port - 2); - } - } - } else { + let s0 = get_custom_rendezvous_server(custom); + if !s0.is_empty() { + let s = crate::increase_port(&s0, -2); + if s == s0 { return format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); + } else { + return format!("http://{}", s); } } "https://admin.rustdesk.com".to_owned() @@ -657,7 +711,7 @@ pub async fn post_request(url: String, body: String, header: &str) -> ResultType if !res.is_empty() { return Ok(res); } - bail!(String::from_utf8_lossy(&output.stderr).to_string()); + hbb_common::bail!(String::from_utf8_lossy(&output.stderr).to_string()); } } @@ -677,15 +731,31 @@ pub fn make_privacy_mode_msg(state: back_notification::PrivacyModeState) -> Mess msg_out } +pub fn is_keyboard_mode_supported(keyboard_mode: &KeyboardMode, version_number: i64) -> bool { + match keyboard_mode { + KeyboardMode::Legacy => true, + KeyboardMode::Map => version_number >= hbb_common::get_version_number("1.2.0"), + KeyboardMode::Translate => version_number >= hbb_common::get_version_number("1.2.0"), + KeyboardMode::Auto => version_number >= hbb_common::get_version_number("1.2.0"), + } +} + +pub fn get_supported_keyboard_modes(version: i64) -> Vec { + KeyboardMode::iter() + .filter(|&mode| is_keyboard_mode_supported(mode, version)) + .map(|&mode| mode) + .collect::>() +} + #[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { - pub static ref IS_X11: Mutex = Mutex::new(false); + pub static ref IS_X11: bool = false; } #[cfg(target_os = "linux")] lazy_static::lazy_static! { - pub static ref IS_X11: Mutex = Mutex::new("x11" == hbb_common::platform::linux::get_display_server()); + pub static ref IS_X11: bool = "x11" == hbb_common::platform::linux::get_display_server(); } pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { @@ -706,3 +776,45 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin fd_json.insert("entries".into(), json!(entries_out)); serde_json::to_string(&fd_json).unwrap_or("".into()) } + +/// The function to handle the url scheme sent by the system. +/// +/// 1. Try to send the url scheme from ipc. +/// 2. If failed to send the url scheme, we open a new main window to handle this url scheme. +pub fn handle_url_scheme(url: String) { + if let Err(err) = crate::ipc::send_url_scheme(url.clone()) { + log::debug!("Send the url to the existing flutter process failed, {}. Let's open a new program to handle this.", err); + let _ = crate::run_me(vec![url]); + } +} + +#[inline] +pub fn encode64>(input: T) -> String { + #[allow(deprecated)] + base64::encode(input) +} + +#[inline] +pub fn decode64>(input: T) -> Result, base64::DecodeError> { + #[allow(deprecated)] + base64::decode(input) +} + +pub async fn get_key(sync: bool) -> String { + let mut key = if sync { + Config::get_option("key") + } else { + let mut options = crate::ipc::get_options_async().await; + options.remove("key").unwrap_or_default() + }; + if key.is_empty() { + #[cfg(windows)] + if let Some(lic) = crate::platform::windows::get_license() { + return lic.key; + } + } + if key.is_empty() && !option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() { + key = config::RS_PUB_KEY.to_owned(); + } + key +} diff --git a/src/core_main.rs b/src/core_main.rs index 1f42f8aad..2c209bd07 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,7 @@ +use crate::platform::breakdown_callback; use hbb_common::log; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::platform::register_breakdown_handler; /// shared by flutter and sciter main function /// @@ -6,9 +9,6 @@ use hbb_common::log; /// If it returns [`None`], then the process will terminate, and flutter gui will not be started. /// If it returns [`Some`], then the process will continue, and flutter gui will be started. pub fn core_main() -> Option> { - // https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write - // though async logger more efficient, but it also causes more problems, disable it for now - // let mut _async_logger_holder: Option = None; let mut args = Vec::new(); let mut flutter_args = Vec::new(); let mut i = 0; @@ -38,16 +38,23 @@ pub fn core_main() -> Option> { } i += 1; } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + register_breakdown_handler(breakdown_callback); + #[cfg(target_os = "linux")] + #[cfg(feature = "flutter")] + { + let (k, v) = ("LIBGL_ALWAYS_SOFTWARE", "true"); + if !hbb_common::config::Config::get_option("allow-always-software-render").is_empty() { + std::env::set_var(k, v); + } else { + std::env::remove_var(k); + } + } #[cfg(feature = "flutter")] if _is_flutter_connect { return core_main_invoke_new_connection(std::env::args()); } let click_setup = cfg!(windows) && args.is_empty() && crate::common::is_setup(&arg_exe); - #[cfg(not(feature = "flutter"))] - { - _is_quick_support = - cfg!(windows) && args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe"); - } if click_setup { args.push("--install".to_owned()); flutter_args.push("--install".to_string()); @@ -59,35 +66,22 @@ pub fn core_main() -> Option> { println!("{}", crate::VERSION); return None; } - #[cfg(debug_assertions)] + #[cfg(windows)] { - use hbb_common::env_logger::*; - init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + _is_quick_support |= !crate::platform::is_installed() + && args.is_empty() + && (arg_exe.to_lowercase().ends_with("qs.exe") + || (!click_setup && crate::platform::is_elevated(None).unwrap_or(false))); + crate::portable_service::client::set_quick_support(_is_quick_support); } - #[cfg(not(debug_assertions))] - { - let mut path = hbb_common::config::Config::log_path(); - if args.len() > 0 && args[0].starts_with("--") { - let name = args[0].replace("--", ""); - if !name.is_empty() { - path.push(name); - } - } - use flexi_logger::*; - if let Ok(x) = Logger::try_with_env_or_str("debug") { - // _async_logger_holder = - x.log_to_file(FileSpec::default().directory(path)) - //.write_mode(WriteMode::Async) - .format(opt_format) - .rotate( - Criterion::Age(Age::Day), - Naming::Timestamps, - Cleanup::KeepLogFiles(6), - ) - .start() - .ok(); + let mut log_name = "".to_owned(); + if args.len() > 0 && args[0].starts_with("--") { + let name = args[0].replace("--", ""); + if !name.is_empty() { + log_name = name; } } + hbb_common::init_log(false, &log_name); #[cfg(windows)] if !crate::platform::is_installed() && args.is_empty() @@ -95,7 +89,8 @@ pub fn core_main() -> Option> { && !_is_elevate && !_is_run_as_system { - if let Err(e) = crate::portable_service::client::start_portable_service() { + use crate::portable_service::client; + if let Err(e) = client::start_portable_service(client::StartPara::Direct) { log::error!("Failed to start portable service:{:?}", e); } } @@ -111,7 +106,7 @@ pub fn core_main() -> Option> { { use crate::platform; if args[0] == "--uninstall" { - if let Err(err) = platform::uninstall_me() { + if let Err(err) = platform::uninstall_me(true) { log::error!("Failed to uninstall: {}", err); } return None; @@ -129,9 +124,9 @@ pub fn core_main() -> Option> { hbb_common::allow_err!(platform::update_me()); return None; } else if args[0] == "--reinstall" { - hbb_common::allow_err!(platform::uninstall_me()); + hbb_common::allow_err!(platform::uninstall_me(false)); hbb_common::allow_err!(platform::install_me( - "desktopicon startmenu", + "desktopicon startmenu driverCert", "".to_owned(), false, false, @@ -139,7 +134,7 @@ pub fn core_main() -> Option> { return None; } else if args[0] == "--silent-install" { hbb_common::allow_err!(platform::install_me( - "desktopicon startmenu", + "desktopicon startmenu driverCert", "".to_owned(), true, args.len() > 1, @@ -149,8 +144,9 @@ pub fn core_main() -> Option> { #[cfg(feature = "with_rc")] hbb_common::allow_err!(crate::rc::extract_resources(&args[1])); return None; - } else if args[0] == "--tray" { - crate::tray::start_tray(); + } else if args[0] == "--install-cert" { + #[cfg(windows)] + hbb_common::allow_err!(crate::platform::windows::install_cert(&args[1])); return None; } else if args[0] == "--portable-service" { crate::platform::elevate_or_run_as_system( @@ -168,33 +164,24 @@ pub fn core_main() -> Option> { std::fs::remove_file(&args[1]).ok(); return None; } + } else if args[0] == "--tray" { + crate::tray::start_tray(); + return None; } else if args[0] == "--service" { log::info!("start --service"); crate::start_os_service(); return None; } else if args[0] == "--server" { log::info!("start --server with user {}", crate::username()); - #[cfg(target_os = "windows")] + #[cfg(any(target_os = "linux", target_os = "windows"))] { crate::start_server(true); return None; } #[cfg(target_os = "macos")] - { - std::thread::spawn(move || crate::start_server(true)); - crate::tray::make_tray(); - return None; - } - #[cfg(target_os = "linux")] { let handler = std::thread::spawn(move || crate::start_server(true)); - // Show the tray in linux only when current user is a normal user - // [Note] - // As for GNOME, the tray cannot be shown in user's status bar. - // As for KDE, the tray can be shown without user's theme. - if !crate::platform::is_root() { - crate::tray::start_tray(); - } + crate::tray::start_tray(); // prevent server exit when encountering errors from tray hbb_common::allow_err!(handler.join()); } @@ -221,6 +208,13 @@ pub fn core_main() -> Option> { } } return None; + } else if args[0] == "--get-id" { + if crate::platform::is_root() { + println!("{}", crate::ipc::get_id()); + } else { + println!("Permission denied!"); + } + return None; } else if args[0] == "--check-hwcodec-config" { #[cfg(feature = "hwcodec")] scrap::hwcodec::check_config(); @@ -228,8 +222,6 @@ pub fn core_main() -> Option> { } else if args[0] == "--cm" { // call connection manager to establish connections // meanwhile, return true to call flutter window to show control panel - #[cfg(feature = "flutter")] - crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); } } @@ -276,18 +268,37 @@ fn import_config(path: &str) { fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option> { args.position(|element| { return element == "--connect"; - }) - .unwrap(); + })?; let peer_id = args.next().unwrap_or("".to_string()); if peer_id.is_empty() { eprintln!("please provide a valid peer id"); return None; } + let mut switch_uuid = None; + while let Some(item) = args.next() { + if item == "--switch_uuid" { + switch_uuid = args.next(); + } + } + let mut param_array = vec![]; + if switch_uuid.is_some() { + let switch_uuid = switch_uuid.map_or("".to_string(), |p| format!("switch_uuid={}", p)); + param_array.push(switch_uuid); + } + + let params = param_array.join("&"); + let params_flag = if params.is_empty() { "" } else { "?" }; + #[allow(unused)] + let uni_links = format!( + "rustdesk://connection/new/{}{}{}", + peer_id, params_flag, params + ); + #[cfg(target_os = "linux")] { use crate::dbus::invoke_new_connection; - match invoke_new_connection(peer_id) { + match invoke_new_connection(uni_links) { Ok(()) => { return None; } @@ -301,16 +312,21 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option = Default::default(); + pub static ref CUR_SESSION_ID: RwLock = Default::default(); pub static ref SESSIONS: RwLock>> = Default::default(); pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } +#[cfg(all(target_os = "windows", feature = "flutter_texture_render"))] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open("texture_rgba_renderer_plugin.dll"); +} + +#[cfg(all(target_os = "linux", feature = "flutter_texture_render"))] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open("libtexture_rgba_renderer_plugin.so"); +} + +#[cfg(all(target_os = "macos", feature = "flutter_texture_render"))] +lazy_static::lazy_static! { + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open_self(); +} + /// FFI for rustdesk core's main entry. /// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. #[cfg(not(windows))] @@ -39,6 +70,12 @@ pub extern "C" fn rustdesk_core_main() -> bool { false } +#[cfg(target_os = "macos")] +#[no_mangle] +pub extern "C" fn handle_applicationShouldOpenUntitledFile() { + crate::platform::macos::handle_application_should_open_untitled_file(); +} + #[cfg(windows)] #[no_mangle] pub extern "C" fn rustdesk_core_main_args(args_len: *mut c_int) -> *mut *mut c_char { @@ -101,9 +138,102 @@ pub unsafe extern "C" fn free_c_args(ptr: *mut *mut c_char, len: c_int) { // Afterwards the vector will be dropped and thus freed. } +#[cfg(feature = "flutter_texture_render")] #[derive(Default, Clone)] pub struct FlutterHandler { pub event_stream: Arc>>>, + notify_rendered: Arc>, + renderer: Arc>, + peer_info: Arc>, +} + +#[cfg(not(feature = "flutter_texture_render"))] +#[derive(Default, Clone)] +pub struct FlutterHandler { + pub event_stream: Arc>>>, + // SAFETY: [rgba] is guarded by [rgba_valid], and it's safe to reach [rgba] with `rgba_valid == true`. + // We must check the `rgba_valid` before reading [rgba]. + pub rgba: Arc>>, + pub rgba_valid: Arc, + peer_info: Arc>, +} + +#[cfg(feature = "flutter_texture_render")] +pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn( + texture_rgba: *mut c_void, + buffer: *const u8, + len: c_int, + width: c_int, + height: c_int, + dst_rgba_stride: c_int, +); + +// Video Texture Renderer in Flutter +#[cfg(feature = "flutter_texture_render")] +#[derive(Clone)] +struct VideoRenderer { + // TextureRgba pointer in flutter native. + ptr: usize, + width: i32, + height: i32, + on_rgba_func: Option>, +} + +#[cfg(feature = "flutter_texture_render")] +impl Default for VideoRenderer { + fn default() -> Self { + let on_rgba_func = match &*TEXTURE_RGBA_RENDERER_PLUGIN { + Ok(lib) => { + let find_sym_res = unsafe { + lib.symbol::("FlutterRgbaRendererPluginOnRgba") + }; + match find_sym_res { + Ok(sym) => Some(sym), + Err(e) => { + log::error!("Failed to find symbol FlutterRgbaRendererPluginOnRgba, {e}"); + None + } + } + } + Err(e) => { + log::error!("Failed to load texture rgba renderer plugin, {e}"); + None + } + }; + Self { + ptr: 0, + width: 0, + height: 0, + on_rgba_func, + } + } +} + +#[cfg(feature = "flutter_texture_render")] +impl VideoRenderer { + #[inline] + pub fn set_size(&mut self, width: i32, height: i32) { + self.width = width; + self.height = height; + } + + pub fn on_rgba(&self, rgba: &Vec) { + if self.ptr == usize::default() { + return; + } + if let Some(func) = &self.on_rgba_func { + unsafe { + func( + self.ptr as _, + rgba.as_ptr() as _, + rgba.len() as _, + self.width as _, + self.height as _, + crate::DST_STRIDE_RGBA as _, + ) + }; + } + } } impl FlutterHandler { @@ -123,6 +253,41 @@ impl FlutterHandler { stream.add(EventToUI::Event(out)); } } + + pub fn close_event_stream(&mut self) { + let mut stream_lock = self.event_stream.write().unwrap(); + if let Some(stream) = &*stream_lock { + stream.add(EventToUI::Event("close".to_owned())); + } + *stream_lock = None; + } + + fn make_displays_msg(displays: &Vec) -> String { + let mut msg_vec = Vec::new(); + for ref d in displays.iter() { + let mut h: HashMap<&str, i32> = Default::default(); + h.insert("x", d.x); + h.insert("y", d.y); + h.insert("width", d.width); + h.insert("height", d.height); + h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); + msg_vec.push(h); + } + serde_json::ser::to_string(&msg_vec).unwrap_or("".to_owned()) + } + + #[inline] + #[cfg(feature = "flutter_texture_render")] + pub fn register_texture(&mut self, ptr: usize) { + self.renderer.write().unwrap().ptr = ptr; + } + + #[inline] + #[cfg(feature = "flutter_texture_render")] + pub fn set_size(&mut self, width: i32, height: i32) { + *self.notify_rendered.write().unwrap() = false; + self.renderer.write().unwrap().set_size(width, height); + } } impl InvokeUiSession for FlutterHandler { @@ -255,7 +420,14 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter // TEST flutter 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 override_file_confirm( + &self, + id: i32, + file_num: i32, + to: String, + is_upload: bool, + is_identical: bool, + ) { self.push_event( "override_file_confirm", vec![ @@ -263,6 +435,7 @@ impl InvokeUiSession for FlutterHandler { ("file_num", &file_num.to_string()), ("read_path", &to), ("is_upload", &is_upload.to_string()), + ("is_identical", &is_identical.to_string()), ], ); } @@ -282,24 +455,37 @@ impl InvokeUiSession for FlutterHandler { // unused in flutter fn adapt_size(&self) {} - fn on_rgba(&self, data: &[u8]) { + #[inline] + #[cfg(not(feature = "flutter_texture_render"))] + fn on_rgba(&self, data: &mut Vec) { + // If the current rgba is not fetched by flutter, i.e., is valid. + // We give up sending a new event to flutter. + if self.rgba_valid.load(Ordering::Relaxed) { + return; + } + self.rgba_valid.store(true, Ordering::Relaxed); + // Return the rgba buffer to the video handler for reusing allocated rgba buffer. + std::mem::swap::>(data, &mut *self.rgba.write().unwrap()); if let Some(stream) = &*self.event_stream.read().unwrap() { - stream.add(EventToUI::Rgba(ZeroCopyBuffer(data.to_owned()))); + stream.add(EventToUI::Rgba); + } + } + + #[inline] + #[cfg(feature = "flutter_texture_render")] + fn on_rgba(&self, data: &mut Vec) { + self.renderer.read().unwrap().on_rgba(data); + if *self.notify_rendered.read().unwrap() { + return; + } + if let Some(stream) = &*self.event_stream.read().unwrap() { + stream.add(EventToUI::Rgba); + *self.notify_rendered.write().unwrap() = true; } } fn set_peer_info(&self, pi: &PeerInfo) { - let mut displays = Vec::new(); - for ref d in pi.displays.iter() { - let mut h: HashMap<&str, i32> = Default::default(); - h.insert("x", d.x); - h.insert("y", d.y); - h.insert("width", d.width); - h.insert("height", d.height); - h.insert("cursor_embedded", if d.cursor_embedded { 1 } else { 0 }); - displays.push(h); - } - let displays = serde_json::ser::to_string(&displays).unwrap_or("".to_owned()); + let displays = Self::make_displays_msg(&pi.displays); let mut features: HashMap<&str, i32> = Default::default(); for ref f in pi.features.iter() { features.insert("privacy_mode", if f.privacy_mode { 1 } else { 0 }); @@ -309,6 +495,8 @@ impl InvokeUiSession for FlutterHandler { features.insert("privacy_mode", 0); } let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); + let resolutions = serialize_resolutions(&pi.resolutions.resolutions); + *self.peer_info.write().unwrap() = pi.clone(); self.push_event( "peer_info", vec![ @@ -320,10 +508,20 @@ impl InvokeUiSession for FlutterHandler { ("version", &pi.version), ("features", &features), ("current_display", &pi.current_display.to_string()), + ("resolutions", &resolutions), + ("platform_additions", &pi.platform_additions), ], ); } + fn set_displays(&self, displays: &Vec) { + self.peer_info.write().unwrap().displays = displays.clone(); + self.push_event( + "sync_peer_info", + vec![("displays", &Self::make_displays_msg(displays))], + ); + } + fn on_connected(&self, _conn_type: ConnType) {} fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { @@ -349,6 +547,7 @@ impl InvokeUiSession for FlutterHandler { } fn switch_display(&self, display: &SwitchDisplay) { + let resolutions = serialize_resolutions(&display.resolutions.resolutions); self.push_event( "switch_display", vec![ @@ -357,7 +556,18 @@ impl InvokeUiSession for FlutterHandler { ("y", &display.y.to_string()), ("width", &display.width.to_string()), ("height", &display.height.to_string()), - ("cursor_embedded", &{if display.cursor_embedded {1} else {0}}.to_string()), + ( + "cursor_embedded", + &{ + if display.cursor_embedded { + 1 + } else { + 0 + } + } + .to_string(), + ), + ("resolutions", &resolutions), ], ); } @@ -373,6 +583,48 @@ impl InvokeUiSession for FlutterHandler { fn clipboard(&self, content: String) { self.push_event("clipboard", vec![("content", &content)]); } + + fn switch_back(&self, peer_id: &str) { + self.push_event("switch_back", [("peer_id", peer_id)].into()); + } + + fn portable_service_running(&self, running: bool) { + self.push_event( + "portable_service_running", + [("running", running.to_string().as_str())].into(), + ); + } + + fn on_voice_call_started(&self) { + self.push_event("on_voice_call_started", [].into()); + } + + fn on_voice_call_closed(&self, reason: &str) { + self.push_event("on_voice_call_closed", [("reason", reason)].into()) + } + + fn on_voice_call_waiting(&self) { + self.push_event("on_voice_call_waiting", [].into()); + } + + fn on_voice_call_incoming(&self) { + self.push_event("on_voice_call_incoming", [].into()); + } + + #[inline] + fn get_rgba(&self) -> *const u8 { + #[cfg(not(feature = "flutter_texture_render"))] + if self.rgba_valid.load(Ordering::Relaxed) { + return self.rgba.read().unwrap().as_ptr(); + } + std::ptr::null_mut() + } + + #[inline] + fn next_rgba(&self) { + #[cfg(not(feature = "flutter_texture_render"))] + self.rgba_valid.store(false, Ordering::Relaxed); + } } /// Create a new remote session with the given id. @@ -382,12 +634,23 @@ impl InvokeUiSession for FlutterHandler { /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. /// * `is_port_forward` - If the session is used for port forward. -pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> ResultType<()> { +pub fn session_add( + id: &str, + is_file_transfer: bool, + is_port_forward: bool, + switch_uuid: &str, + force_relay: bool, + password: String, +) -> ResultType<()> { let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); let session: Session = Session { id: session_id.clone(), + password, + 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() }; @@ -400,11 +663,17 @@ pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> R ConnType::DEFAULT_CONN }; + let switch_uuid = if switch_uuid.is_empty() { + None + } else { + Some(switch_uuid.to_string()) + }; + session .lc .write() .unwrap() - .initialize(session_id, conn_type); + .initialize(session_id, conn_type, switch_uuid, force_relay); if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) { same_id_session.close(); @@ -421,6 +690,13 @@ pub fn session_add(id: &str, is_file_transfer: bool, is_port_forward: bool) -> R /// * `events2ui` - The events channel to ui. pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultType<()> { if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + #[cfg(feature = "flutter_texture_render")] + log::info!( + "Session {} start, render by flutter texture rgba plugin", + id + ); + #[cfg(not(feature = "flutter_texture_render"))] + log::info!("Session {} start, render by flutter paint widget", id); *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { @@ -432,6 +708,31 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn update_text_clipboard_required() { + let is_required = SESSIONS + .read() + .unwrap() + .iter() + .any(|(_id, session)| session.is_text_clipboard_required()); + Client::set_is_text_clipboard_required(is_required); +} + +#[inline] +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn other_sessions_running(id: &str) -> bool { + SESSIONS.read().unwrap().keys().filter(|k| *k != id).count() != 0 +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn send_text_clipboard_msg(msg: Message) { + for (_id, session) in SESSIONS.read().unwrap().iter() { + if session.is_text_clipboard_required() { + session.send(Data::Message(msg.clone())); + } + } +} + // Server Side #[cfg(not(any(target_os = "ios")))] pub mod connection_manager { @@ -489,6 +790,11 @@ pub mod connection_manager { fn show_elevation(&self, show: bool) { self.push_event("show_elevation", vec![("show", &show.to_string())]); } + + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + self.push_event("update_voice_call_state", vec![("client", &client_json)]); + } } impl FlutterHandler { @@ -497,12 +803,14 @@ pub mod connection_manager { assert!(h.get("name").is_none()); h.insert("name", name); - if let Some(s) = GLOBAL_EVENT_STREAM - .read() - .unwrap() - .get(super::APP_TYPE_MAIN) - { + if let Some(s) = GLOBAL_EVENT_STREAM.read().unwrap().get(super::APP_TYPE_CM) { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); + } else { + println!( + "Push event {} failed. No {} event stream found.", + name, + super::APP_TYPE_CM + ); }; } } @@ -581,3 +889,78 @@ pub fn set_cur_session_id(id: String) { *CUR_SESSION_ID.write().unwrap() = id; } } + +#[inline] +fn serialize_resolutions(resolutions: &Vec) -> String { + #[derive(Debug, serde::Serialize)] + struct ResolutionSerde { + width: i32, + height: i32, + } + + let mut v = vec![]; + resolutions + .iter() + .map(|r| { + v.push(ResolutionSerde { + width: r.width, + height: r.height, + }) + }) + .count(); + serde_json::ser::to_string(&v).unwrap_or("".to_string()) +} + +#[no_mangle] +#[cfg(not(feature = "flutter_texture_render"))] +pub fn session_get_rgba_size(id: *const char) -> usize { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.read().unwrap().get(id) { + return session.rgba.read().unwrap().len(); + } + } + 0 +} + +#[no_mangle] +#[cfg(feature = "flutter_texture_render")] +pub fn session_get_rgba_size(_id: *const char) -> usize { + 0 +} + +#[no_mangle] +pub fn session_get_rgba(id: *const char) -> *const u8 { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.read().unwrap().get(id) { + return session.get_rgba(); + } + } + std::ptr::null() +} + +#[no_mangle] +pub fn session_next_rgba(id: *const char) { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.read().unwrap().get(id) { + return session.next_rgba(); + } + } +} + +#[no_mangle] +#[cfg(feature = "flutter_texture_render")] +pub fn session_register_texture(id: *const char, ptr: usize) { + let id = unsafe { std::ffi::CStr::from_ptr(id as _) }; + if let Ok(id) = id.to_str() { + if let Some(session) = SESSIONS.write().unwrap().get_mut(id) { + return session.register_texture(ptr); + } + } +} + +#[no_mangle] +#[cfg(not(feature = "flutter_texture_render"))] +pub fn session_register_texture(_id: *const char, _ptr: usize) {} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index bf5ebaf4e..a3e8c38f2 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,27 +1,31 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::get_default_sound_input; +use crate::{ + client::file_trait::FileManager, + common::is_keyboard_mode_supported, + common::make_fd_to_json, + flutter::{self, SESSIONS}, + flutter::{session_add, session_start_}, + ui_interface::{self, *}, +}; +use flutter_rust_bridge::{StreamSink, SyncReturn}; +use hbb_common::{ + config::{self, LocalConfig, PeerConfig, PeerInfoSerde, ONLINE}, + fs, log, + message_proto::KeyboardMode, + ResultType, +}; +use serde_json::json; use std::{ collections::HashMap, ffi::{CStr, CString}, os::raw::c_char, -}; - -use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; -use serde_json::json; - -use hbb_common::ResultType; -use hbb_common::{ - config::{self, LocalConfig, PeerConfig, ONLINE}, - fs, log, + str::FromStr, + time::SystemTime, }; // use crate::hbbs_http::account::AuthResult; -use crate::flutter::{self, SESSIONS}; -use crate::ui_interface::{self, *}; -use crate::{ - client::file_trait::FileManager, - common::make_fd_to_json, - flutter::{session_add, session_start_}, -}; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); #[cfg(target_os = "android")] @@ -46,7 +50,7 @@ fn initialize(app_dir: &str) { pub enum EventToUI { Event(String), - Rgba(ZeroCopyBuffer>), + Rgba, } pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { @@ -81,8 +85,18 @@ pub fn session_add_sync( id: String, is_file_transfer: bool, is_port_forward: bool, + switch_uuid: String, + force_relay: bool, + password: String, ) -> SyncReturn { - if let Err(e) = session_add(&id, is_file_transfer, is_port_forward) { + if let Err(e) = session_add( + &id, + is_file_transfer, + is_port_forward, + &switch_uuid, + force_relay, + password, + ) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -129,10 +143,10 @@ pub fn session_login(id: String, password: String, remember: bool) { } pub fn session_close(id: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Some(mut session) = SESSIONS.write().unwrap().remove(&id) { + session.close_event_stream(); session.close(); } - let _ = SESSIONS.write().unwrap().remove(&id); } pub fn session_refresh(id: String) { @@ -147,16 +161,22 @@ pub fn session_record_screen(id: String, start: bool, width: usize, height: usiz } } -pub fn session_reconnect(id: String) { +pub fn session_reconnect(id: String, force_relay: bool) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.reconnect(); + session.reconnect(force_relay); } } pub fn session_toggle_option(id: String, value: String) { + let mut is_found = false; if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { - log::warn!("toggle option {}", value); - session.toggle_option(value); + is_found = true; + log::warn!("toggle option {}", &value); + session.toggle_option(value.clone()); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if is_found && value == "disable-clipboard" { + crate::flutter::update_text_clipboard_required(); } } @@ -241,8 +261,14 @@ pub fn session_get_keyboard_mode(id: String) -> Option { } pub fn session_set_keyboard_mode(id: String, value: String) { + let mut _mode_updated = false; if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.save_keyboard_mode(value); + _mode_updated = true; + } + #[cfg(windows)] + if _mode_updated { + crate::keyboard::update_grab_get_key_name(); } } @@ -254,6 +280,21 @@ pub fn session_get_custom_image_quality(id: String) -> Option> { } } +pub fn session_is_keyboard_mode_supported(id: String, mode: String) -> SyncReturn { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Ok(mode) = KeyboardMode::from_str(&mode[..]) { + SyncReturn(is_keyboard_mode_supported( + &mode, + session.get_peer_version(), + )) + } else { + SyncReturn(false) + } + } else { + SyncReturn(false) + } +} + pub fn session_set_custom_image_quality(id: String, value: i32) { if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { session.save_custom_image_quality(value); @@ -289,10 +330,11 @@ pub fn session_handle_flutter_key_event( name: String, keycode: i32, scancode: i32, + lock_modes: i32, down_or_up: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.handle_flutter_key_event(&name, keycode, scancode, down_or_up); + session.handle_flutter_key_event(&name, keycode, scancode, lock_modes, down_or_up); } } @@ -300,7 +342,6 @@ pub fn session_enter_or_leave(id: String, enter: bool) { #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Some(session) = SESSIONS.read().unwrap().get(&id) { if enter { - crate::keyboard::set_cur_session(session.clone()); session.enter(); } else { session.leave(); @@ -473,6 +514,37 @@ pub fn session_resume_job(id: String, act_id: i32, is_remote: bool) { } } +pub fn session_elevate_direct(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.elevate_direct(); + } +} + +pub fn session_elevate_with_logon(id: String, username: String, password: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.elevate_with_logon(username, password); + } +} + +pub fn session_switch_sides(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.switch_sides(); + } +} + +pub fn session_change_resolution(id: String, width: i32, height: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.change_resolution(width, height); + } +} + +pub fn session_set_size(_id: String, _width: i32, _height: i32) { + #[cfg(feature = "flutter_texture_render")] + if let Some(session) = SESSIONS.write().unwrap().get_mut(&_id) { + session.set_size(_width, _height); + } +} + pub fn main_get_sound_inputs() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_sound_inputs(); @@ -480,6 +552,17 @@ pub fn main_get_sound_inputs() -> Vec { vec![String::from("")] } +pub fn main_get_default_sound_input() -> Option { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return get_default_sound_input(); + #[cfg(any(target_os = "android", target_os = "ios"))] + None +} + +pub fn main_get_hostname() -> SyncReturn { + SyncReturn(crate::common::hostname()) +} + pub fn main_change_id(new_id: String) { change_id(new_id) } @@ -588,10 +671,6 @@ pub fn main_discover() { discover(); } -pub fn main_has_rendezvous_service() -> bool { - has_rendezvous_service() -} - pub fn main_get_api_server() -> String { get_api_server() } @@ -676,7 +755,28 @@ pub fn main_load_recent_peers() { pub fn main_load_fav_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let favs = get_fav(); - let peers: Vec> = PeerConfig::peers() + let mut recent = PeerConfig::peers(); + let mut lan = config::LanPeers::load() + .peers + .iter() + .filter(|d| recent.iter().all(|r| r.0 != d.id)) + .map(|d| { + ( + d.id.clone(), + SystemTime::UNIX_EPOCH, + PeerConfig { + info: PeerInfoSerde { + username: d.username.clone(), + hostname: d.hostname.clone(), + platform: d.platform.clone(), + }, + ..Default::default() + }, + ) + }) + .collect(); + recent.append(&mut lan); + let peers: Vec> = recent .into_iter() .filter_map(|(id, _, p)| { if favs.contains(&id) { @@ -720,6 +820,10 @@ pub fn main_load_lan_peers() { }; } +pub fn main_remove_discovered(id: String) { + remove_discovered(id); +} + fn main_broadcast_message(data: &HashMap<&str, &str>) { let apps = vec![ flutter::APP_TYPE_DESKTOP_REMOTE, @@ -748,6 +852,18 @@ pub fn main_default_video_save_directory() -> String { default_video_save_directory() } +pub fn main_set_user_default_option(key: String, value: String) { + set_user_default_option(key, value); +} + +pub fn main_get_user_default_option(key: String) -> SyncReturn { + SyncReturn(get_user_default_option(key)) +} + +pub fn main_handle_relay_id(id: String) -> String { + handle_relay_id(id) +} + pub fn session_add_port_forward( id: String, local_port: i32, @@ -771,6 +887,26 @@ pub fn session_new_rdp(id: String) { } } +pub fn session_request_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.request_voice_call(); + } +} + +pub fn session_close_voice_call(id: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.close_voice_call(); + } +} + +pub fn cm_handle_incoming_voice_call(id: i32, accept: bool) { + crate::ui_cm_interface::handle_incoming_voice_call(id, accept); +} + +pub fn cm_close_voice_call(id: i32) { + crate::ui_cm_interface::close_voice_call(id); +} + pub fn main_get_last_remote_id() -> String { LocalConfig::get_remote_id() } @@ -885,9 +1021,11 @@ pub fn session_send_mouse(id: String, msg: String) { } if let Some(buttons) = m.get("buttons") { mask |= match buttons.as_str() { - "left" => 1, - "right" => 2, - "wheel" => 4, + "left" => 0x01, + "right" => 0x02, + "wheel" => 0x04, + "back" => 0x08, + "forward" => 0x10, _ => 0, } << 3; } @@ -1037,11 +1175,8 @@ pub fn cm_elevate_portable(conn_id: i32) { crate::ui_cm_interface::elevate_portable(conn_id); } -pub fn main_get_icon() -> String { - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] - return ui_interface::get_icon(); - #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] - return String::new(); +pub fn cm_switch_back(conn_id: i32) { + crate::ui_cm_interface::switch_back(conn_id); } pub fn main_get_build_date() -> String { @@ -1079,8 +1214,8 @@ pub fn query_onlines(ids: Vec) { crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) } -pub fn version_to_number(v: String) -> i64 { - hbb_common::get_version_number(&v) +pub fn version_to_number(v: String) -> SyncReturn { + SyncReturn(hbb_common::get_version_number(&v)) } pub fn option_synced() -> bool { @@ -1091,8 +1226,16 @@ pub fn main_is_installed() -> SyncReturn { SyncReturn(is_installed()) } -pub fn main_start_grab_keyboard() { +pub fn main_start_grab_keyboard() -> SyncReturn { + #[cfg(target_os = "linux")] + if !*crate::common::IS_X11 { + return SyncReturn(false); + } crate::keyboard::client::start_grab_loop(); + if !is_can_input_monitoring(false) { + return SyncReturn(false); + } + SyncReturn(true) } pub fn main_is_installed_lower_version() -> SyncReturn { @@ -1111,6 +1254,10 @@ pub fn main_is_can_screen_recording(prompt: bool) -> SyncReturn { SyncReturn(is_can_screen_recording(prompt)) } +pub fn main_is_can_input_monitoring(prompt: bool) -> SyncReturn { + SyncReturn(is_can_input_monitoring(prompt)) +} + pub fn main_is_share_rdp() -> SyncReturn { SyncReturn(is_share_rdp()) } @@ -1119,6 +1266,10 @@ pub fn main_is_rdp_service_open() -> SyncReturn { SyncReturn(is_rdp_service_open()) } +pub fn main_set_share_rdp(enable: bool) { + set_share_rdp(enable) +} + pub fn main_goto_install() -> SyncReturn { goto_install(); SyncReturn(true) @@ -1134,7 +1285,9 @@ pub fn main_update_me() -> SyncReturn { } pub fn set_cur_session_id(id: String) { - super::flutter::set_cur_session_id(id) + super::flutter::set_cur_session_id(id); + #[cfg(windows)] + crate::keyboard::update_grab_get_key_name(); } pub fn install_show_run_without_install() -> SyncReturn { @@ -1181,27 +1334,85 @@ pub fn main_is_login_wayland() -> SyncReturn { SyncReturn(is_login_wayland()) } +pub fn main_start_pa() { + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); +} + +pub fn main_hide_docker() -> SyncReturn { + #[cfg(target_os = "macos")] + crate::platform::macos::hide_dock(); + SyncReturn(true) +} + +pub fn main_use_texture_render() -> SyncReturn { + #[cfg(not(feature = "flutter_texture_render"))] + { + SyncReturn(false) + } + #[cfg(feature = "flutter_texture_render")] + { + SyncReturn(true) + } +} + +pub fn cm_start_listen_ipc_thread() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::flutter::connection_manager::start_listen_ipc_thread(); +} + +/// Start an ipc server for receiving the url scheme. +/// +/// * Should only be called in the main flutter window. +/// * macOS only +pub fn main_start_ipc_url_server() { + #[cfg(target_os = "macos")] + std::thread::spawn(move || crate::server::start_ipc_url_server()); +} + +/// Send a url scheme throught the ipc. +/// +/// * macOS only +#[allow(unused_variables)] +pub fn send_url_scheme(_url: String) { + #[cfg(target_os = "macos")] + std::thread::spawn(move || crate::handle_url_scheme(_url)); +} + #[cfg(target_os = "android")] pub mod server_side { + use hbb_common::{config, log}; use jni::{ objects::{JClass, JString}, sys::jstring, JNIEnv, }; - use hbb_common::log; - use crate::start_server; #[no_mangle] pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startServer( env: JNIEnv, _class: JClass, + app_dir: JString, ) { - log::debug!("startServer from java"); + log::debug!("startServer from jvm"); + if let Ok(app_dir) = env.get_string(app_dir) { + *config::APP_DIR.write().unwrap() = app_dir.into(); + } std::thread::spawn(move || start_server(true)); } + #[no_mangle] + pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startService( + env: JNIEnv, + _class: JClass, + ) { + log::debug!("startService from jvm"); + config::Config::set_option("stop-service".into(), "".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } + #[no_mangle] pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_translateLocale( env: JNIEnv, diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index 08ad36eb9..76ced87a0 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -5,6 +5,7 @@ use serde_json::{Map, Value}; #[cfg(feature = "flutter")] pub mod account; pub mod record_upload; +pub mod sync; #[derive(Debug)] pub enum HbbHttpResponse { diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs new file mode 100644 index 000000000..a060d6a20 --- /dev/null +++ b/src/hbbs_http/sync.rs @@ -0,0 +1,116 @@ +use std::{collections::HashMap, sync::Mutex, time::Duration}; + +use hbb_common::{ + config::{Config, LocalConfig}, + tokio::{self, sync::broadcast, time::Instant}, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +use crate::Connection; + +const TIME_HEARTBEAT: Duration = Duration::from_secs(30); +const TIME_CONN: Duration = Duration::from_secs(3); + +lazy_static::lazy_static! { + static ref SENDER : Mutex>> = Mutex::new(start_hbbs_sync()); +} + +pub fn start() { + let _sender = SENDER.lock().unwrap(); +} + +pub fn signal_receiver() -> broadcast::Receiver> { + SENDER.lock().unwrap().subscribe() +} + +fn start_hbbs_sync() -> broadcast::Sender> { + let (tx, _rx) = broadcast::channel::>(16); + std::thread::spawn(move || start_hbbs_sync_async()); + return tx; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StrategyOptions { + pub config_options: HashMap, + pub extra: HashMap, +} + +#[tokio::main(flavor = "current_thread")] +async fn start_hbbs_sync_async() { + tokio::spawn(async move { + let mut interval = tokio::time::interval_at(Instant::now() + TIME_CONN, TIME_CONN); + let mut last_send = Instant::now(); + loop { + tokio::select! { + _ = interval.tick() => { + let url = heartbeat_url(); + let modified_at = LocalConfig::get_option("strategy_timestamp").parse::().unwrap_or(0); + if !url.is_empty() { + let conns = Connection::alive_conns(); + if conns.is_empty() && last_send.elapsed() < TIME_HEARTBEAT { + continue; + } + last_send = Instant::now(); + let mut v = Value::default(); + v["id"] = json!(Config::get_id()); + v["ver"] = json!(hbb_common::get_version_number(crate::VERSION)); + if !conns.is_empty() { + v["conns"] = json!(conns); + } + v["modified_at"] = json!(modified_at); + if let Ok(s) = crate::post_request(url.clone(), v.to_string(), "").await { + if let Ok(mut rsp) = serde_json::from_str::>(&s) { + if let Some(conns) = rsp.remove("disconnect") { + if let Ok(conns) = serde_json::from_value::>(conns) { + SENDER.lock().unwrap().send(conns).ok(); + } + } + if let Some(rsp_modified_at) = rsp.remove("modified_at") { + if let Ok(rsp_modified_at) = serde_json::from_value::(rsp_modified_at) { + if rsp_modified_at != modified_at { + LocalConfig::set_option("strategy_timestamp".to_string(), rsp_modified_at.to_string()); + } + } + } + if let Some(strategy) = rsp.remove("strategy") { + if let Ok(strategy) = serde_json::from_value::(strategy) { + handle_config_options(strategy.config_options); + } + } + } + } + } + } + } + } + }) + .await + .ok(); +} + +fn heartbeat_url() -> String { + let url = crate::common::get_api_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ); + if url.is_empty() || url.contains("rustdesk.com") { + return "".to_owned(); + } + format!("{}/api/heartbeat", url) +} + +fn handle_config_options(config_options: HashMap) { + let mut options = Config::get_options(); + config_options + .iter() + .map(|(k, v)| { + if v.is_empty() { + options.remove(k); + } else { + options.insert(k.to_string(), v.to_string()); + } + }) + .count(); + Config::set_options(options); +} diff --git a/src/ipc.rs b/src/ipc.rs index c562225b4..5f415c6e8 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -9,7 +9,7 @@ use parity_tokio_ipc::{ use serde_derive::{Deserialize, Serialize}; #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use clipboard::ClipbaordFile; +pub use clipboard::ClipboardFile; use hbb_common::{ allow_err, bail, bytes, bytes_codec::BytesCodec, @@ -166,6 +166,7 @@ pub enum Data { file_transfer_enabled: bool, restart: bool, recording: bool, + from_switch: bool, }, ChatMessage { text: String, @@ -189,9 +190,9 @@ pub enum Data { Socks(Option), FS(FS), Test, - SyncConfig(Option<(Config, Config2)>), + SyncConfig(Option>), #[cfg(not(any(target_os = "android", target_os = "ios")))] - ClipbaordFile(ClipbaordFile), + ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), PrivacyModeState((i32, PrivacyModeState)), TestRendezvousServer, @@ -207,6 +208,13 @@ pub enum Data { Empty, Disconnected, DataPortableService(DataPortableService), + SwitchSidesRequest(String), + SwitchSidesBack, + UrlLink(String), + VoiceCallIncoming, + StartVoiceCall, + VoiceCallResponse(bool), + CloseVoiceCall(String), } #[tokio::main(flavor = "current_thread")] @@ -243,7 +251,7 @@ pub async fn start(postfix: &str) -> ResultType<()> { pub async fn new_listener(postfix: &str) -> ResultType { let path = Config::ipc_path(postfix); - #[cfg(not(windows))] + #[cfg(not(any(windows, target_os = "android", target_os = "ios")))] check_pid(postfix).await; let mut endpoint = Endpoint::new(path.clone()); match SecurityAttributes::allow_everyone_create() { @@ -411,7 +419,8 @@ async fn handle(data: Data, stream: &mut Connection) { let t = Config::get_nat_type(); allow_err!(stream.send(&Data::NatType(Some(t))).await); } - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = CheckIfRestart::new(); Config::set(config); Config2::set(config2); @@ -420,13 +429,24 @@ async fn handle(data: Data, stream: &mut Connection) { Data::SyncConfig(None) => { allow_err!( stream - .send(&Data::SyncConfig(Some((Config::get(), Config2::get())))) + .send(&Data::SyncConfig(Some( + (Config::get(), Config2::get()).into() + ))) .await ); } Data::TestRendezvousServer => { crate::test_rendezvous_server(); } + Data::SwitchSidesRequest(id) => { + let uuid = uuid::Uuid::new_v4(); + crate::server::insert_switch_sides_uuid(id, uuid.clone()); + allow_err!( + stream + .send(&Data::SwitchSidesRequest(uuid.to_string())) + .await + ); + } _ => {} } } @@ -521,19 +541,19 @@ fn get_pid_file(postfix: &str) -> String { format!("{}.pid", path) } -#[cfg(not(windows))] +#[cfg(not(any(windows, target_os = "android", target_os = "ios")))] async fn check_pid(postfix: &str) { let pid_file = get_pid_file(postfix); if let Ok(mut file) = File::open(&pid_file) { let mut content = String::new(); file.read_to_string(&mut content).ok(); - let pid = content.parse::().unwrap_or(0); + let pid = content.parse::().unwrap_or(0); if pid > 0 { - use sysinfo::{ProcessExt, System, SystemExt}; + use hbb_common::sysinfo::{ProcessExt, System, SystemExt}; let mut sys = System::new(); sys.refresh_processes(); if let Some(p) = sys.process(pid.into()) { - if let Some(current) = sys.process((std::process::id() as i32).into()) { + if let Some(current) = sys.process((std::process::id() as usize).into()) { if current.name() == p.name() { // double check with connect if connect(1000, postfix).await.is_ok() { @@ -820,3 +840,22 @@ pub async fn test_rendezvous_server() -> ResultType<()> { c.send(&Data::TestRendezvousServer).await?; Ok(()) } + +#[tokio::main(flavor = "current_thread")] +pub async fn send_url_scheme(url: String) -> ResultType<()> { + connect(1_000, "_url") + .await? + .send(&Data::UrlLink(url)) + .await?; + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + #[test] + fn verify_ffi_enum_data_size() { + println!("{}", std::mem::size_of::()); + assert!(std::mem::size_of::() < 96); + } +} diff --git a/src/keyboard.rs b/src/keyboard.rs index a3088c63f..ce68657ea 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -2,11 +2,14 @@ use crate::client::get_key_state; use crate::common::GrabState; #[cfg(feature = "flutter")] -use crate::flutter::FlutterHandler; +use crate::flutter::{CUR_SESSION_ID, SESSIONS}; +#[cfg(target_os = "windows")] +use crate::platform::windows::get_char_by_vk; #[cfg(not(any(feature = "flutter", feature = "cli")))] -use crate::ui::remote::SciterHandler; -use crate::ui_session_interface::Session; -use hbb_common::{log, message_proto::*}; +use crate::ui::CUR_SESSION; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::log; +use hbb_common::message_proto::*; use rdev::{Event, EventType, Key}; #[cfg(any(target_os = "windows", target_os = "macos"))] use std::sync::atomic::{AtomicBool, Ordering}; @@ -19,19 +22,16 @@ use std::{ #[cfg(windows)] static mut IS_ALT_GR: bool = false; +#[allow(dead_code)] +const OS_LOWER_WINDOWS: &str = "windows"; +#[allow(dead_code)] +const OS_LOWER_LINUX: &str = "linux"; +#[allow(dead_code)] +const OS_LOWER_MACOS: &str = "macos"; + #[cfg(any(target_os = "windows", target_os = "macos"))] static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -#[cfg(feature = "flutter")] -lazy_static::lazy_static! { - static ref CUR_SESSION: Arc>>> = Default::default(); -} - -#[cfg(not(any(feature = "flutter", feature = "cli")))] -lazy_static::lazy_static! { - static ref CUR_SESSION: Arc>>> = Default::default(); -} - lazy_static::lazy_static! { static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); static ref MODIFIERS_STATE: Mutex> = { @@ -48,24 +48,22 @@ lazy_static::lazy_static! { }; } -#[cfg(feature = "flutter")] -pub fn set_cur_session(session: Session) { - *CUR_SESSION.lock().unwrap() = Some(session); -} - -#[cfg(not(any(feature = "flutter", feature = "cli")))] -pub fn set_cur_session(session: Session) { - *CUR_SESSION.lock().unwrap() = Some(session); -} - pub mod client { use super::*; pub fn get_keyboard_mode() -> String { - #[cfg(not(feature = "cli"))] - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - return handler.get_keyboard_mode(); - } + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + return session.get_keyboard_mode(); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + return session.get_keyboard_mode(); + } "legacy".to_string() } @@ -77,6 +75,8 @@ pub mod client { match state { GrabState::Ready => {} GrabState::Run => { + #[cfg(windows)] + update_grab_get_key_name(); #[cfg(any(target_os = "windows", target_os = "macos"))] KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); @@ -99,13 +99,17 @@ pub mod client { } } - pub fn process_event(event: &Event) { + pub fn process_event(event: &Event, lock_modes: Option) -> KeyboardMode { + let keyboard_mode = get_keyboard_mode_enum(); + if is_long_press(&event) { - return; + return keyboard_mode; } - if let Some(key_event) = event_to_key_event(&event) { + + for key_event in event_to_key_events(&event, keyboard_mode, lock_modes) { send_key_event(&key_event); } + keyboard_mode } pub fn get_modifiers_state( @@ -164,15 +168,20 @@ pub mod client { } } - pub fn lock_screen() { + pub fn event_lock_screen() -> KeyEvent { let mut key_event = KeyEvent::new(); key_event.set_control_key(ControlKey::LockScreen); key_event.down = true; key_event.mode = KeyboardMode::Legacy.into(); - send_key_event(&key_event); + key_event } - pub fn ctrl_alt_del() { + #[inline] + pub fn lock_screen() { + send_key_event(&event_lock_screen()); + } + + pub fn event_ctrl_alt_del() -> KeyEvent { let mut key_event = KeyEvent::new(); if get_peer_platform() == "Windows" { key_event.set_control_key(ControlKey::CtrlAltDel); @@ -183,11 +192,32 @@ pub mod client { key_event.press = true; } key_event.mode = KeyboardMode::Legacy.into(); - send_key_event(&key_event); + key_event + } + + #[inline] + pub fn ctrl_alt_del() { + send_key_event(&event_ctrl_alt_del()); } } +#[cfg(windows)] +pub fn update_grab_get_key_name() { + match get_keyboard_mode_enum() { + KeyboardMode::Map => rdev::set_get_key_unicode(false), + KeyboardMode::Translate => rdev::set_get_key_unicode(true), + _ => {} + }; +} + +#[cfg(target_os = "windows")] +static mut IS_0X021D_DOWN: bool = false; + +#[cfg(target_os = "macos")] +static mut IS_LEFT_OPTION_DOWN: bool = false; + pub fn start_grab_loop() { + std::env::set_var("KEYBOARD_ONLY", "y"); #[cfg(any(target_os = "windows", target_os = "macos"))] std::thread::spawn(move || { let try_handle_keyboard = move |event: Event, key: Key, is_press: bool| -> Option { @@ -195,22 +225,61 @@ pub fn start_grab_loop() { if key == Key::CapsLock || key == Key::NumLock { return Some(event); } - if KEYBOARD_HOOKED.load(Ordering::SeqCst) { - client::process_event(&event); + + let mut _keyboard_mode = KeyboardMode::Map; + let _scan_code = event.position_code; + let _code = event.platform_code; + let res = if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + _keyboard_mode = client::process_event(&event, None); if is_press { - return None; + None } else { - return Some(event); + Some(event) } } else { - return Some(event); + Some(event) + }; + + #[cfg(target_os = "windows")] + match _scan_code { + 0x1D | 0x021D => rdev::set_modifier(Key::ControlLeft, is_press), + 0xE01D => rdev::set_modifier(Key::ControlRight, is_press), + 0x2A => rdev::set_modifier(Key::ShiftLeft, is_press), + 0x36 => rdev::set_modifier(Key::ShiftRight, is_press), + 0x38 => rdev::set_modifier(Key::Alt, is_press), + // Right Alt + 0xE038 => rdev::set_modifier(Key::AltGr, is_press), + 0xE05B => rdev::set_modifier(Key::MetaLeft, is_press), + 0xE05C => rdev::set_modifier(Key::MetaRight, is_press), + _ => {} } + + #[cfg(target_os = "windows")] + unsafe { + // AltGr + if _scan_code == 0x021D { + IS_0X021D_DOWN = is_press; + } + } + + #[cfg(target_os = "macos")] + unsafe { + if _code as u32 == rdev::kVK_Option { + IS_LEFT_OPTION_DOWN = is_press; + } + } + + return res; }; let func = move |event: Event| match event.event_type { EventType::KeyPress(key) => try_handle_keyboard(event, key, true), EventType::KeyRelease(key) => try_handle_keyboard(event, key, false), _ => Some(event), }; + #[cfg(target_os = "macos")] + rdev::set_is_main_thread(false); + #[cfg(target_os = "windows")] + rdev::set_event_popup(false); if let Err(error) = rdev::grab(func) { log::error!("rdev Error: {:?}", error) } @@ -222,7 +291,7 @@ pub fn start_grab_loop() { if let Key::Unknown(keycode) = key { log::error!("rdev get unknown key, keycode is : {:?}", keycode); } else { - client::process_event(&event); + client::process_event(&event, None); } None } @@ -254,7 +323,9 @@ pub fn release_remote_keys() { for key in to_release { let event_type = EventType::KeyRelease(key); let event = event_type_to_event(event_type); - client::process_event(&event); + // to-do: BUG + // Release events should be sent to the corresponding sessions, instead of current session. + client::process_event(&event, None); } } @@ -267,7 +338,23 @@ pub fn get_keyboard_mode_enum() -> KeyboardMode { } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn add_numlock_capslock_status(key_event: &mut KeyEvent) { +fn add_numlock_capslock_with_lock_modes(key_event: &mut KeyEvent, lock_modes: i32) { + const CAPS_LOCK: i32 = 1; + const NUM_LOCK: i32 = 2; + // const SCROLL_LOCK: i32 = 3; + if lock_modes & (1 << CAPS_LOCK) != 0 { + key_event.modifiers.push(ControlKey::CapsLock.into()); + } + if lock_modes & (1 << NUM_LOCK) != 0 { + key_event.modifiers.push(ControlKey::NumLock.into()); + } + // if lock_modes & (1 << SCROLL_LOCK) != 0 { + // key_event.modifiers.push(ControlKey::ScrollLock.into()); + // } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn add_numlock_capslock_status(key_event: &mut KeyEvent) { if get_key_state(enigo::Key::CapsLock) { key_event.modifiers.push(ControlKey::CapsLock.into()); } @@ -315,7 +402,11 @@ fn update_modifiers_state(event: &Event) { }; } -pub fn event_to_key_event(event: &Event) -> Option { +pub fn event_to_key_events( + event: &Event, + keyboard_mode: KeyboardMode, + lock_modes: Option, +) -> Vec { let mut key_event = KeyEvent::new(); update_modifiers_state(event); @@ -329,61 +420,91 @@ pub fn event_to_key_event(event: &Event) -> Option { _ => {} } - let keyboard_mode = get_keyboard_mode_enum(); + let mut peer = get_peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + key_event.mode = keyboard_mode.into(); - let mut key_event = match keyboard_mode { - KeyboardMode::Map => map_keyboard_mode(event, key_event)?, - KeyboardMode::Translate => translate_keyboard_mode(event, key_event)?, + let mut key_events = match keyboard_mode { + KeyboardMode::Map => match map_keyboard_mode(peer.as_str(), event, key_event) { + Some(event) => [event].to_vec(), + None => Vec::new(), + }, + KeyboardMode::Translate => translate_keyboard_mode(peer.as_str(), event, key_event), _ => { #[cfg(not(any(target_os = "android", target_os = "ios")))] { - legacy_keyboard_mode(event, key_event)? + legacy_keyboard_mode(event, key_event) } #[cfg(any(target_os = "android", target_os = "ios"))] { - None? + Vec::new() } } }; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - add_numlock_capslock_status(&mut key_event); - return Some(key_event); + if keyboard_mode != KeyboardMode::Translate { + for key_event in &mut key_events { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(lock_modes) = lock_modes { + add_numlock_capslock_with_lock_modes(key_event, lock_modes); + } else { + add_numlock_capslock_status(key_event); + } + } + } + key_events } pub fn event_type_to_event(event_type: EventType) -> Event { Event { event_type, time: SystemTime::now(), - name: None, - code: 0, - scan_code: 0, + unicode: None, + platform_code: 0, + position_code: 0, } } pub fn send_key_event(key_event: &KeyEvent) { - #[cfg(not(feature = "cli"))] - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - handler.send_key_event(key_event); + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + session.send_key_event(key_event); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + session.send_key_event(key_event); } } pub fn get_peer_platform() -> String { - #[cfg(not(feature = "cli"))] - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - return handler.peer_platform(); - } + #[cfg(not(any(feature = "flutter", feature = "cli")))] + if let Some(session) = CUR_SESSION.lock().unwrap().as_ref() { + return session.peer_platform(); + } + #[cfg(feature = "flutter")] + if let Some(session) = SESSIONS + .read() + .unwrap() + .get(&*CUR_SESSION_ID.read().unwrap()) + { + return session.peer_platform(); + } "Windows".to_string() } #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { +pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec { + let mut events = Vec::new(); // legacy mode(0): Generate characters locally, look for keycode on other side. let (mut key, down_or_up) = match event.event_type { EventType::KeyPress(key) => (key, true), EventType::KeyRelease(key) => (key, false), _ => { - return None; + return events; } }; @@ -425,11 +546,11 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option> 8) == 0xE0 { + if (event.position_code >> 8) == 0xE0 { unsafe { IS_ALT_GR = true; } - return None; + return events; } Some(ControlKey::Control) } @@ -461,7 +582,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { if is_win && ctrl && alt { client::ctrl_alt_del(); - return None; + return events; } Some(ControlKey::Delete) } @@ -499,7 +620,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Some(ControlKey::Subtract), Key::KpPlus => Some(ControlKey::Add), Key::CapsLock | Key::NumLock | Key::ScrollLock => { - return None; + return events; } Key::Home => Some(ControlKey::Home), Key::End => Some(ControlKey::End), @@ -512,7 +633,11 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { if s.len() <= 2 { // exclude chinese characters @@ -582,12 +707,12 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option Option { +pub fn map_keyboard_mode(peer: &str, event: &Event, mut key_event: KeyEvent) -> Option { match event.event_type { EventType::KeyPress(..) => { key_event.down = true; @@ -610,38 +736,42 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option return None, }; - let mut peer = get_peer_platform().to_lowercase(); - peer.retain(|c| !c.is_whitespace()); - #[cfg(target_os = "windows")] - let keycode = match peer.as_str() { - "windows" => event.scan_code, - "macos" => { + let keycode = match peer { + OS_LOWER_WINDOWS => { + // https://github.com/rustdesk/rustdesk/issues/1371 + // Filter scancodes that are greater than 255 and the hight word is not 0xE0. + if event.position_code > 255 && (event.position_code >> 8) != 0xE0 { + return None; + } + event.position_code + } + OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { - rdev::win_scancode_to_macos_iso_code(event.scan_code)? + rdev::win_scancode_to_macos_iso_code(event.position_code)? } else { - rdev::win_scancode_to_macos_code(event.scan_code)? + rdev::win_scancode_to_macos_code(event.position_code)? } } - _ => rdev::win_scancode_to_linux_code(event.scan_code)?, + _ => rdev::win_scancode_to_linux_code(event.position_code)?, }; #[cfg(target_os = "macos")] - let keycode = match peer.as_str() { - "windows" => rdev::macos_code_to_win_scancode(event.code as _)?, - "macos" => event.code as _, - _ => rdev::macos_code_to_linux_code(event.code as _)?, + let keycode = match peer { + OS_LOWER_WINDOWS => rdev::macos_code_to_win_scancode(event.platform_code as _)?, + OS_LOWER_MACOS => event.platform_code as _, + _ => rdev::macos_code_to_linux_code(event.platform_code as _)?, }; #[cfg(target_os = "linux")] - let keycode = match peer.as_str() { - "windows" => rdev::linux_code_to_win_scancode(event.code as _)?, - "macos" => { + let keycode = match peer { + OS_LOWER_WINDOWS => rdev::linux_code_to_win_scancode(event.position_code as _)?, + OS_LOWER_MACOS => { if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { - rdev::linux_code_to_macos_iso_code(event.scan_code)? + rdev::linux_code_to_macos_iso_code(event.position_code as _)? } else { - rdev::linux_code_to_macos_code(event.code as _)? + rdev::linux_code_to_macos_code(event.position_code as _)? } } - _ => event.code as _, + _ => event.position_code as _, }; #[cfg(any(target_os = "android", target_os = "ios"))] let keycode = 0; @@ -650,6 +780,134 @@ pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option Option { - None +fn try_fill_unicode(event: &Event, key_event: &KeyEvent, events: &mut Vec) { + match &event.unicode { + Some(unicode_info) => { + if let Some(name) = &unicode_info.name { + if name.len() > 0 { + let mut evt = key_event.clone(); + evt.set_seq(name.to_string()); + events.push(evt); + } + } + } + None => + { + #[cfg(target_os = "windows")] + if is_hot_key_modifiers_down() && unsafe { !IS_0X021D_DOWN } { + if let Some(chr) = get_char_by_vk(event.platform_code as u32) { + let mut evt = key_event.clone(); + evt.set_seq(chr.to_string()); + events.push(evt); + } + } + } + } +} + +#[cfg(target_os = "windows")] +fn is_hot_key_modifiers_down() -> bool { + if rdev::get_modifier(Key::ControlLeft) || rdev::get_modifier(Key::ControlRight) { + return true; + } + if rdev::get_modifier(Key::Alt) || rdev::get_modifier(Key::AltGr) { + return true; + } + if rdev::get_modifier(Key::MetaLeft) || rdev::get_modifier(Key::MetaRight) { + return true; + } + return false; +} + +#[inline] +#[cfg(target_os = "windows")] +pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { + let mut key_event = map_keyboard_mode(peer, event, key_event)?; + key_event.set_chr((key_event.chr() & 0x0000FFFF) | ((event.platform_code as u32) << 16)); + Some(key_event) +} + +#[inline] +#[cfg(not(target_os = "windows"))] +pub fn translate_key_code(peer: &str, event: &Event, key_event: KeyEvent) -> Option { + map_keyboard_mode(peer, event, key_event) +} + +#[inline] +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn is_altgr(event: &Event) -> bool { + #[cfg(target_os = "linux")] + if event.platform_code == 0xFE03 { + true + } else { + false + } + + #[cfg(target_os = "windows")] + if unsafe { IS_0X021D_DOWN } && event.position_code == 0xE038 { + true + } else { + false + } +} + +fn is_press(event: &Event) -> bool { + matches!(event.event_type, EventType::KeyPress(_)) +} + +pub fn translate_keyboard_mode(peer: &str, event: &Event, key_event: KeyEvent) -> Vec { + let mut events: Vec = Vec::new(); + if let Some(unicode_info) = &event.unicode { + if unicode_info.is_dead { + #[cfg(target_os = "macos")] + if peer != OS_LOWER_MACOS && unsafe { IS_LEFT_OPTION_DOWN } { + // try clear dead key state + // rdev::clear_dead_key_state(); + } else { + return events; + } + #[cfg(not(target_os = "macos"))] + return events; + } + } + + #[cfg(target_os = "macos")] + // ignore right option key + if event.platform_code as u32 == rdev::kVK_RightOption { + return events; + } + + #[cfg(any(target_os = "linux", target_os = "windows"))] + if is_altgr(event) { + return events; + } + + #[cfg(target_os = "windows")] + if event.position_code == 0x021D { + return events; + } + + #[cfg(any(target_os = "linux", target_os = "windows"))] + if is_press(event) { + try_fill_unicode(event, &key_event, &mut events); + } + + #[cfg(target_os = "windows")] + unsafe { + if IS_0X021D_DOWN { + return events; + } + } + + #[cfg(target_os = "macos")] + if !unsafe { IS_LEFT_OPTION_DOWN } { + try_fill_unicode(event, &key_event, &mut events); + } + + if events.is_empty() { + if let Some(evt) = translate_key_code(peer, event, key_event) { + events.push(evt); + } + } + events } diff --git a/src/lang.rs b/src/lang.rs index 5ea408416..972510325 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -1,6 +1,6 @@ -use serde_json::{json, value::Value}; use std::ops::Deref; +mod ca; mod cn; mod cs; mod da; @@ -8,63 +8,65 @@ mod de; mod en; mod eo; mod es; +mod fa; mod fr; +mod el; mod hu; mod id; mod it; mod ja; mod ko; +mod kz; +mod nl; mod pl; mod ptbr; +mod ro; mod ru; mod sk; -mod tr; -mod tw; -mod vn; -mod kz; -mod ua; -mod fa; -mod ca; -mod gr; -mod sv; +mod sl; mod sq; mod sr; +mod sv; mod th; +mod tr; +mod tw; +mod ua; +mod vn; -lazy_static::lazy_static! { - pub static ref LANGS: Value = - json!(vec![ - ("en", "English"), - ("it", "Italiano"), - ("fr", "Français"), - ("de", "Deutsch"), - ("cn", "简体中文"), - ("tw", "繁體中文"), - ("pt", "Português"), - ("es", "Español"), - ("hu", "Magyar"), - ("ru", "Русский"), - ("sk", "Slovenčina"), - ("id", "Indonesia"), - ("cs", "Čeština"), - ("da", "Dansk"), - ("eo", "Esperanto"), - ("tr", "Türkçe"), - ("vn", "Tiếng Việt"), - ("pl", "Polski"), - ("ja", "日本語"), - ("ko", "한국어"), - ("kz", "Қазақ"), - ("ua", "Українська"), - ("fa", "فارسی"), - ("ca", "Català"), - ("gr", "Ελληνικά"), - ("sv", "Svenska"), - ("sq", "Shqip"), - ("sr", "Srpski"), - ("th", "ภาษาไทย"), - ]); -} +pub const LANGS: &[(&str, &str)] = &[ + ("en", "English"), + ("it", "Italiano"), + ("fr", "Français"), + ("de", "Deutsch"), + ("nl", "Nederlands"), + ("zh-cn", "简体中文"), + ("zh-tw", "繁體中文"), + ("pt", "Português"), + ("es", "Español"), + ("hu", "Magyar"), + ("ru", "Русский"), + ("sk", "Slovenčina"), + ("id", "Indonesia"), + ("cs", "Čeština"), + ("da", "Dansk"), + ("eo", "Esperanto"), + ("tr", "Türkçe"), + ("vn", "Tiếng Việt"), + ("pl", "Polski"), + ("ja", "日本語"), + ("ko", "한국어"), + ("kz", "Қазақ"), + ("ua", "Українська"), + ("fa", "فارسی"), + ("ca", "Català"), + ("el", "Ελληνικά"), + ("sv", "Svenska"), + ("sq", "Shqip"), + ("sr", "Srpski"), + ("th", "ภาษาไทย"), + ("sl", "Slovenščina"), + ("ro", "Română"), +]; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn translate(name: String) -> String { @@ -77,7 +79,12 @@ pub fn translate_locale(name: String, locale: &str) -> String { if lang.is_empty() { // zh_CN on Linux, zh-Hans-CN on mac, zh_CN_#Hans on Android if locale.starts_with("zh") { - lang = (if locale.contains("TW") { "tw" } else { "cn" }).to_owned(); + lang = (if locale.contains("tw") { + "zh-tw" + } else { + "zh-cn" + }) + .to_owned(); } } if lang.is_empty() { @@ -91,16 +98,16 @@ pub fn translate_locale(name: String, locale: &str) -> String { let lang = lang.to_lowercase(); let m = match lang.as_str() { "fr" => fr::T.deref(), - "cn" => cn::T.deref(), + "zh-cn" => cn::T.deref(), "it" => it::T.deref(), - "tw" => tw::T.deref(), + "zh-tw" => tw::T.deref(), "de" => de::T.deref(), + "nl" => nl::T.deref(), "es" => es::T.deref(), "hu" => hu::T.deref(), "ru" => ru::T.deref(), "eo" => eo::T.deref(), "id" => id::T.deref(), - "ptbr" => ptbr::T.deref(), "br" => ptbr::T.deref(), "pt" => ptbr::T.deref(), "tr" => tr::T.deref(), @@ -115,11 +122,13 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ua" => ua::T.deref(), "fa" => fa::T.deref(), "ca" => ca::T.deref(), - "gr" => gr::T.deref(), + "el" => el::T.deref(), "sv" => sv::T.deref(), "sq" => sq::T.deref(), "sr" => sr::T.deref(), "th" => th::T.deref(), + "sl" => sl::T.deref(), + "ro" => ro::T.deref(), _ => en::T.deref(), }; if let Some(v) = m.get(&name as &str) { diff --git a/src/lang/ca.rs b/src/lang/ca.rs index e5c43ccbc..162a48883 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "El portapapers està buit"), ("Stop service", "Aturar servei"), ("Change ID", "Canviar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), ("Website", "Lloc web"), ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Silenciar"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Entrada d'àudio"), ("Enhancements", "Millores"), ("Hardware Codec", "Còdec de hardware"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor API"), ("invalid_http", "ha de començar amb http:// o https://"), ("Invalid IP", "IP incorrecta"), - ("id_change_tip", "Només pots utilitzar caràcters a-z, A-Z, 0-9 e _ (guionet baix). El primer caràcter ha de ser a-z o A-Z. La longitut ha d'estar entre 6 i 16 caràcters."), ("Invalid format", "Format incorrecte"), ("server_not_support", "Encara no és compatible amb el servidor"), ("Not available", "No disponible"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Avís"), ("Login screen using Wayland is not supported", "La pantalla d'inici de sessió amb Wayland no és compatible"), ("Reboot required", "Cal reiniciar"), - ("Unsupported display server ", "Servidor de visualització no compatible"), + ("Unsupported display server", "Servidor de visualització no compatible"), ("x11 expected", "x11 necessari"), ("Port", ""), ("Settings", "Ajustaments"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Tancat manualment pel peer"), ("Enable remote configuration modification", "Habilitar modificació remota de configuració"), ("Run without install", "Executar sense instal·lar"), - ("Always connected via relay", "Connectat sempre a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Connecta sempre a través de relay"), ("whitelist_tip", ""), ("Login", "Inicia sessió"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Sortir"), ("Tags", ""), ("Search ID", "Cerca ID"), - ("Current Wayland display server is not supported", "El servidor de visualització actual de Wayland no és compatible"), ("whitelist_sep", ""), ("Add ID", "Afegir ID"), ("Add Tag", "Afegir tag"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Nota"), ("Connection", "connexió"), ("Share Screen", "Compartir pantalla"), - ("CLOSE", "TANCAR"), - ("OPEN", "OBRIR"), ("Chat", "Xat"), ("Total", "Total"), ("items", "ítems"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciarà el servei automàticament, i permetrà que altres dispositius sol·licitin una connexió des d'aquest dispositiu."), ("android_stop_service_tip", "Tancar el servei tancarà totes les connexions establertes."), ("android_version_audio_tip", "La versión actual de Android no admet la captura d'àudio, actualizi a Android 10 o superior."), - ("android_start_service_tip", "Toqui el permís [Iniciar servei] o OBRIR [Captura de pantalla] per iniciar el servei d'ús compartit de pantalla."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Compte"), ("Overwrite", "Sobreescriure"), ("This file exists, skip or overwrite this file?", "Aquest arxiu ja existeix, ometre o sobreescriure l'arxiu?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mantenir RustDesk com a servei en segon pla"), ("Ignore Battery Optimizations", "Ignorar optimizacions de la bateria"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Connexió no disponible"), ("Legacy mode", "Mode heretat"), ("Map mode", "Mode mapa"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptativa"), ("General", ""), ("Security", "Seguretat"), - ("Account", "Compte"), ("Theme", "Tema"), ("Dark Theme", "Tema Fosc"), + ("Light Theme", ""), ("Dark", "Fosc"), ("Light", "Clar"), ("Follow System", "Tema del sistema"), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Servidor"), ("Direct IP Access", "Accés IP Directe"), ("Proxy", ""), - ("Port", ""), ("Apply", "Aplicar"), ("Disconnect all devices?", "Desconnectar tots els dispositius?"), ("Clear", "Netejar"), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Altre"), ("Confirm before closing multiple tabs", "Confirmar abans de tancar múltiples pestanyes"), ("Keyboard Settings", "Ajustaments de teclat"), - ("Custom", "Personalitzat"), ("Full Access", "Acces complet"), ("Screen Share", "Compartir pantalla"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requereix Ubuntu 21.04 o una versió superior."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 7d8f51ddc..250f9a5b6 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -3,15 +3,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "状态"), ("Your Desktop", "你的桌面"), - ("desk_tip", "你的桌面可以通过下面的ID和密码访问。"), + ("desk_tip", "你的桌面可以通过下面的 ID 和密码访问。"), ("Password", "密码"), ("Ready", "就绪"), ("Established", "已建立"), - ("connecting_status", "正在接入RustDesk网络..."), + ("connecting_status", "正在接入 RustDesk 网络..."), ("Enable Service", "允许服务"), ("Start Service", "启动服务"), ("Service is running", "服务正在运行"), - ("Service is not running", "服务没有启动"), + ("Service is not running", "服务未运行"), ("not_ready_status", "未就绪,请检查网络连接"), ("Control Remote Desktop", "控制远程桌面"), ("Transfer File", "传输文件"), @@ -19,42 +19,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recent Sessions", "最近访问过"), ("Address Book", "地址簿"), ("Confirmation", "确认"), - ("TCP Tunneling", "TCP隧道"), + ("TCP Tunneling", "TCP 隧道"), ("Remove", "删除"), ("Refresh random password", "刷新随机密码"), ("Set your own password", "设置密码"), ("Enable Keyboard/Mouse", "允许控制键盘/鼠标"), ("Enable Clipboard", "允许同步剪贴板"), ("Enable File Transfer", "允许传输文件"), - ("Enable TCP Tunneling", "允许建立TCP隧道"), - ("IP Whitelisting", "IP白名单"), + ("Enable TCP Tunneling", "允许建立 TCP 隧道"), + ("IP Whitelisting", "IP 白名单"), ("ID/Relay Server", "ID/中继服务器"), ("Import Server Config", "导入服务器配置"), ("Export Server Config", "导出服务器配置"), ("Import server configuration successfully", "导入服务器配置信息成功"), ("Export server configuration successfully", "导出服务器配置信息成功"), - ("Invalid server configuration", "无效服务器配置,请修改后重新拷贝配置信息到剪贴板后点击此按钮"), - ("Clipboard is empty", "拷贝配置信息到剪贴板后点击此按钮,可以自动导入配置"), + ("Invalid server configuration", "服务器配置无效,请修改后重新复制配置信息到剪贴板,然后点击此按钮"), + ("Clipboard is empty", "复制配置信息到剪贴板后点击此按钮,可以自动导入配置"), ("Stop service", "停止服务"), - ("Change ID", "改变ID"), + ("Change ID", "更改 ID"), + ("Your new ID", "你的新 ID"), + ("length %min% to %max%", "长度在 %min% 与 %max% 之间"), + ("starts with a letter", "以字母开头"), + ("allowed characters", "使用允许的字符"), + ("id_change_tip", "只可以使用字母 a-z, A-Z, 0-9, _ (下划线)。首字母必须是 a-z, A-Z。长度在 6 与 16 之间。"), ("Website", "网站"), ("About", "关于"), ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Privacy Statement", "隐私声明"), ("Mute", "静音"), + ("Build Date", "构建日期"), + ("Version", "版本"), + ("Home", "主页"), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), ("Hardware Codec", "硬件编解码"), ("Adaptive Bitrate", "自适应码率"), - ("ID Server", "ID服务器"), + ("ID Server", "ID 服务器"), ("Relay Server", "中继服务器"), - ("API Server", "API服务器"), - ("invalid_http", "必须以http://或者https://开头"), - ("Invalid IP", "无效IP"), - ("id_change_tip", "只可以使用字母a-z, A-Z, 0-9, _ (下划线)。首字母必须是a-z, A-Z。长度在6与16之间。"), + ("API Server", "API 服务器"), + ("invalid_http", "必须以 http:// 或者 https:// 开头"), + ("Invalid IP", "无效 IP"), ("Invalid format", "无效格式"), ("server_not_support", "服务器暂不支持"), - ("Not available", "已被占用"), + ("Not available", "不可用"), ("Too frequent", "修改太频繁,请稍后再试"), ("Cancel", "取消"), ("Skip", "跳过"), @@ -65,12 +72,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter your password", "请输入密码"), ("Remember password", "记住密码"), ("Wrong Password", "密码错误"), - ("Do you want to enter again?", "还想输入一次吗?"), + ("Do you want to enter again?", "是否要再次输入?"), ("Connection Error", "连接错误"), ("Error", "错误"), ("Reset by the peer", "连接被对方关闭"), ("Connecting...", "正在连接..."), - ("Connection in progress. Please wait.", "连接进行中,请稍等。"), + ("Connection in progress. Please wait.", "正在进行连接,请稍候。"), ("Please try 1 minute later", "一分钟后再试"), ("Login Error", "登录错误"), ("Successful", "成功"), @@ -95,14 +102,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unselect All", "取消全选"), ("Empty Directory", "空文件夹"), ("Not an empty directory", "这不是一个空文件夹"), - ("Are you sure you want to delete this file?", "是否删除此文件?"), - ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), - ("Are you sure you want to delete the file of this directory?", "是否删除文件夹下的文件?"), + ("Are you sure you want to delete this file?", "是否删除此文件?"), + ("Are you sure you want to delete this empty directory?", "是否删除此空文件夹?"), + ("Are you sure you want to delete the file of this directory?", "是否删除此文件夹下的文件?"), ("Do this for all conflicts", "应用于其它冲突"), ("This is irreversible!", "此操作不可逆!"), ("Deleting", "正在删除"), ("files", "文件"), - ("Waiting", "等待..."), + ("Waiting", "正在等待..."), ("Finished", "完成"), ("Speed", "速度"), ("Custom Image Quality", "设置画面质量"), @@ -115,37 +122,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stretch", "伸展"), ("Scrollbar", "滚动条"), ("ScrollAuto", "自动滚动"), - ("Good image quality", "好画质"), - ("Balanced", "一般画质"), - ("Optimize reaction time", "优化反应时间"), - ("Custom", "自定义画质"), + ("Good image quality", "画质最优化"), + ("Balanced", "平衡"), + ("Optimize reaction time", "速度最优化"), + ("Custom", "自定义"), ("Show remote cursor", "显示远程光标"), ("Show quality monitor", "显示质量监测"), - ("Disable clipboard", "禁止剪贴板"), - ("Lock after session end", "断开后锁定远程电脑"), + ("Disable clipboard", "禁用剪贴板"), + ("Lock after session end", "会话结束后锁定远程电脑"), ("Insert", "插入"), ("Insert Lock", "锁定远程电脑"), ("Refresh", "刷新画面"), - ("ID does not exist", "ID不存在"), + ("ID does not exist", "ID 不存在"), ("Failed to connect to rendezvous server", "连接注册服务器失败"), ("Please try later", "请稍后再试"), - ("Remote desktop is offline", "远程电脑不在线"), - ("Key mismatch", "Key不匹配"), + ("Remote desktop is offline", "远程电脑处于离线状态"), + ("Key mismatch", "Key 不匹配"), ("Timeout", "连接超时"), ("Failed to connect to relay server", "无法连接到中继服务器"), ("Failed to connect via rendezvous server", "无法通过注册服务器建立连接"), ("Failed to connect via relay server", "无法通过中继服务器建立连接"), - ("Failed to make direct connection to remote desktop", "无法建立直接连接"), + ("Failed to make direct connection to remote desktop", "无法直接连接到远程桌面"), ("Set Password", "设置密码"), ("OS Password", "操作系统密码"), - ("install_tip", "你正在运行未安装版本,由于UAC限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将RustDesk安装到系统,从而规避上述问题。"), + ("install_tip", "你正在运行未安装版本,由于 UAC 限制,作为被控端,会在某些情况下无法控制鼠标键盘,或者录制屏幕,请点击下面的按钮将 RustDesk 安装到系统,从而规避上述问题。"), ("Click to upgrade", "点击这里升级"), ("Click to download", "点击这里下载"), ("Click to update", "点击这里更新"), ("Configure", "配置"), - ("config_acc", "为了能够远程控制你的桌面, 请给予RustDesk\"辅助功能\" 权限。"), - ("config_screen", "为了能够远程访问你的桌面, 请给予RustDesk\"屏幕录制\" 权限。"), - ("Installing ...", "安装 ..."), + ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), + ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), + ("Installing ...", "安装中..."), ("Install", "安装"), ("Installation", "安装"), ("Installation Path", "安装路径"), @@ -154,10 +161,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "开始安装即表示接受许可协议。"), ("Accept and Install", "同意并安装"), ("End-user license agreement", "用户协议"), - ("Generating ...", "正在产生 ..."), + ("Generating ...", "正在生成..."), ("Your installation is lower version.", "你安装的版本比当前运行的低。"), ("not_close_tcp_tip", "请在使用隧道的时候,不要关闭本窗口"), - ("Listening ...", "正在等待隧道连接 ..."), + ("Listening ...", "正在等待隧道连接..."), ("Remote Host", "远程主机"), ("Remote Port", "远程端口"), ("Action", "动作"), @@ -166,7 +173,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Address", "当前地址"), ("Change Local Port", "修改本地端口"), ("setup_server_tip", "如果需要更快连接速度,你可以选择自建服务器"), - ("Too short, at least 6 characters.", "太短了,至少6个字符"), + ("Too short, at least 6 characters.", "太短了,至少 6 个字符"), ("The confirmation is not identical.", "两次输入不匹配"), ("Permissions", "权限"), ("Accept", "接受"), @@ -176,28 +183,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow using clipboard", "允许使用剪贴板"), ("Allow hearing sound", "允许听到声音"), ("Allow file copy and paste", "允许复制粘贴文件"), - ("Connected", "已经连接"), + ("Connected", "已连接"), ("Direct and encrypted connection", "加密直连"), ("Relayed and encrypted connection", "加密中继连接"), ("Direct and unencrypted connection", "非加密直连"), ("Relayed and unencrypted connection", "非加密中继连接"), - ("Enter Remote ID", "输入对方ID"), + ("Enter Remote ID", "输入对方 ID"), ("Enter your password", "输入密码"), ("Logging in...", "正在登录..."), - ("Enable RDP session sharing", "允许RDP会话共享"), + ("Enable RDP session sharing", "允许 RDP 会话共享"), ("Auto Login", "自动登录(设置断开后锁定才有效)"), - ("Enable Direct IP Access", "允许IP直接访问"), - ("Rename", "改名"), + ("Enable Direct IP Access", "允许 IP 直接访问"), + ("Rename", "重命名"), ("Space", "空格"), ("Create Desktop Shortcut", "创建桌面快捷方式"), - ("Change Path", "改变路径"), + ("Change Path", "更改路径"), ("Create Folder", "创建文件夹"), ("Please enter the folder name", "请输入文件夹名称"), ("Fix it", "修复"), ("Warning", "警告"), ("Login screen using Wayland is not supported", "不支持使用 Wayland 登录界面"), ("Reboot required", "重启后才能生效"), - ("Unsupported display server ", "不支持当前显示服务器"), + ("Unsupported display server", "不支持当前显示服务器"), ("x11 expected", "请切换到 x11"), ("Port", "端口"), ("Settings", "设置"), @@ -205,25 +212,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid port", "无效端口"), ("Closed manually by the peer", "被对方手动关闭"), ("Enable remote configuration modification", "允许远程修改配置"), - ("Run without install", "无安装运行"), - ("Always connected via relay", "强制走中继连接"), + ("Run without install", "不安装直接运行"), + ("Connect via relay", "中继连接"), ("Always connect via relay", "强制走中继连接"), - ("whitelist_tip", "只有白名单里的ip才能访问我"), + ("whitelist_tip", "只有白名单里的 IP 才能访问本机"), ("Login", "登录"), + ("Verify", "验证"), + ("Remember me", "记住我"), + ("Trust this device", "信任此设备"), + ("Verification code", "验证码"), + ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,请输入验证码继续登录"), ("Logout", "登出"), ("Tags", "标签"), - ("Search ID", "查找ID"), - ("Current Wayland display server is not supported", "不支持 Wayland 显示服务器"), + ("Search ID", "查找 ID"), ("whitelist_sep", "可以使用逗号,分号,空格或者换行符作为分隔符"), - ("Add ID", "增加ID"), + ("Add ID", "增加 ID"), ("Add Tag", "增加标签"), ("Unselect all tags", "取消选择所有标签"), ("Network error", "网络错误"), ("Username missed", "用户名没有填写"), ("Password missed", "密码没有填写"), - ("Wrong credentials", "用户名或者密码错误"), + ("Wrong credentials", "提供的登录信息错误"), ("Edit Tag", "修改标签"), - ("Unremember Password", "忘掉密码"), + ("Unremember Password", "忘记密码"), ("Favorites", "收藏"), ("Add to Favorites", "加入到收藏"), ("Remove from Favorites", "从收藏中删除"), @@ -233,9 +244,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hostname", "主机名"), ("Discovered", "已发现"), ("install_daemon_tip", "为了开机启动,请安装系统服务。"), - ("Remote ID", "远程ID"), + ("Remote ID", "远程 ID"), ("Paste", "粘贴"), - ("Paste here?", "粘贴到这里?"), + ("Paste here?", "粘贴到这里?"), ("Are you sure to close the connection?", "是否确认关闭连接?"), ("Download new version", "下载新版本"), ("Touch mode", "触屏模式"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "备注"), ("Connection", "连接"), ("Share Screen", "共享屏幕"), - ("CLOSE", "关闭"), - ("OPEN", "开启"), ("Chat", "聊天消息"), ("Total", "总计"), ("items", "个项目"), @@ -273,16 +282,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "是否接受?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获取安卓的输入权限?"), - ("android_input_permission_tip1", "為了讓遠程設備通過鼠標或者觸屏控制您的安卓設備,你需要允許RustDesk使用\"無障礙\"服務。"), + ("android_input_permission_tip1", "为了让远程设备通过鼠标或触屏控制您的安卓设备,你需要允许 RustDesk 使用\"无障碍\"服务。"), ("android_input_permission_tip2", "请在接下来的系统设置页面里,找到并进入 [已安装的服务] 页面,将 [RustDesk Input] 服务开启。"), ("android_new_connection_tip", "收到新的连接控制请求,对方想要控制你当前的设备。"), ("android_service_will_start_tip", "开启录屏权限将自动开启服务,允许其他设备向此设备请求建立连接。"), ("android_stop_service_tip", "关闭服务将自动关闭所有已建立的连接。"), - ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓10或更高。"), - ("android_start_service_tip", "点击 [启动服务] 或打开 [屏幕录制] 权限开启手机屏幕共享服务。"), - ("Account", "账号"), + ("android_version_audio_tip", "当前安卓版本不支持音频录制,请升级至安卓 10 或更高。"), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", "对于已建立的连接,权限可能不会立即发生改变,除非重新建立连接。"), + ("Account", "账户"), ("Overwrite", "覆盖"), - ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), + ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), ("Quit", "退出"), ("doc_mac_permission", "https://rustdesk.com/docs/zh-cn/manual/mac#%E5%90%AF%E7%94%A8%E6%9D%83%E9%99%90"), ("Help", "帮助"), @@ -298,12 +308,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "进入隐私模式"), ("Out privacy mode", "退出隐私模式"), ("Language", "语言"), - ("Keep RustDesk background service", "保持RustDesk后台服务"), + ("Keep RustDesk background service", "保持 RustDesk 后台服务"), ("Ignore Battery Optimizations", "忽略电池优化"), - ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的RustDesk应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), + ("android_open_battery_optimizations_tip", "如需关闭此功能,请在接下来的 RustDesk 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), + ("Start on Boot", "开机自启动"), + ("Start the screen sharing service on boot, requires special permissions", "开机自动启动屏幕共享服务,此功能需要一些特殊权限。"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), - ("Map mode", "1:1传输"), + ("Map mode", "1:1 传输"), ("Translate mode", "翻译模式"), ("Use permanent password", "使用固定密码"), ("Use both passwords", "同时使用两种密码"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "适应窗口"), ("General", "常规"), ("Security", "安全"), - ("Account", "账户"), ("Theme", "主题"), ("Dark Theme", "暗黑主题"), + ("Light Theme", "明亮主题"), ("Dark", "黑暗"), ("Light", "明亮"), ("Follow System", "跟随系统"), @@ -345,17 +357,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Audio", "允许传输音频"), ("Unlock Network Settings", "解锁网络设置"), ("Server", "服务器"), - ("Direct IP Access", "IP直接访问"), + ("Direct IP Access", "IP 直接访问"), ("Proxy", "代理"), - ("Port", "端口"), ("Apply", "应用"), - ("Disconnect all devices?", "断开所有远程连接?"), + ("Disconnect all devices?", "断开所有远程连接?"), ("Clear", "清空"), ("Audio Input Device", "音频输入设备"), ("Deny remote access", "拒绝远程访问"), - ("Use IP Whitelisting", "只允许白名单上的IP访问"), + ("Use IP Whitelisting", "只允许白名单上的 IP 访问"), ("Network", "网络"), - ("Enable RDP", "允许RDP访问"), + ("Enable RDP", "允许 RDP 访问"), ("Pin menubar", "固定菜单栏"), ("Unpin menubar", "取消固定菜单栏"), ("Recording", "录屏"), @@ -370,25 +381,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "拒绝局域网发现"), ("Write a message", "输入聊天消息"), ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "请等待对方确认UAC..."), + ("Please wait for confirmation of UAC...", "请等待对方确认 UAC..."), ("elevated_foreground_window_tip", "远端桌面的当前窗口需要更高的权限才能操作, 暂时无法使用鼠标键盘, 可以请求对方最小化当前窗口, 或者在连接管理窗口点击提升。为避免这个问题,建议在远端设备上安装本软件。"), ("Disconnected", "会话已结束"), ("Other", "其他"), ("Confirm before closing multiple tabs", "关闭多个标签页时向您确认"), ("Keyboard Settings", "键盘设置"), - ("Custom", "自定义"), ("Full Access", "完全访问"), ("Screen Share", "仅共享屏幕"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 发行版。 请尝试 X11 桌面或更改您的操作系统。"), ("JumpLink", "查看"), ("Please Select the screen to be shared(Operate on the peer side).", "请选择要分享的画面(对端操作)。"), - ("Show RustDesk", "显示rustdesk"), + ("Show RustDesk", "显示 RustDesk"), ("This PC", "此电脑"), ("or", "或"), ("Continue with", "使用"), ("Elevate", "提权"), - ("Zoom cursor", "缩放鼠标"), + ("Zoom cursor", "缩放光标"), ("Accept sessions via password", "只允许密码访问"), ("Accept sessions via click", "只允许点击访问"), ("Accept sessions via both", "允许密码或点击访问"), @@ -399,14 +409,80 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "请求访问你的设备"), ("Hide connection management window", "隐藏连接管理窗口"), ("hide_cm_tip", "在只允许密码连接并且只用固定密码的情况下才允许隐藏"), - ("wayland_experiment_tip", ""), + ("wayland_experiment_tip", "Wayland 支持处于实验阶段,如果你需要使用无人值守访问,请使用 X11。"), ("Right click to select tabs", "右键选择选项卡"), ("Skipped", "已跳过"), ("Add to Address Book", "添加到地址簿"), ("Group", "小组"), ("Search", "搜索"), - ("Closed manually by the web console", "被web控制台手动关闭"), + ("Closed manually by web console", "被 web 控制台手动关闭"), ("Local keyboard type", "本地键盘类型"), ("Select local keyboard type", "请选择本地键盘类型"), + ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装 nouveau 驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), + ("Always use software rendering", "始终使用软件渲染"), + ("config_input", "为了能够通过键盘控制远程桌面, 请给予 RustDesk \"输入监控\" 权限。"), + ("config_microphone", "为了支持通过麦克风进行音频传输,请给予 RustDesk \"录音\"权限。"), + ("request_elevation_tip", "如果对面有人, 也可以请求提升权限。"), + ("Wait", "等待"), + ("Elevation Error", "提权失败"), + ("Ask the remote user for authentication", "请求远端用户授权"), + ("Choose this if the remote account is administrator", "当对面电脑是管理员账号时选择该选项"), + ("Transmit the username and password of administrator", "发送管理员账号的用户名密码"), + ("still_click_uac_tip", "依然需要被控端用户在运行 RustDesk 的 UAC 窗口点击确认。"), + ("Request Elevation", "请求提权"), + ("wait_accept_uac_tip", "请等待远端用户确认 UAC 对话框。"), + ("Elevate successfully", "提权成功"), + ("uppercase", "大写字母"), + ("lowercase", "小写字母"), + ("digit", "数字"), + ("special character", "特殊字符"), + ("length>=8", "长度不小于 8"), + ("Weak", "弱"), + ("Medium", "中"), + ("Strong", "强"), + ("Switch Sides", "反转访问方向"), + ("Please confirm if you want to share your desktop?", "请确认是否要让对方访问你的桌面?"), + ("Display", "显示"), + ("Default View Style", "默认显示方式"), + ("Default Scroll Style", "默认滚动方式"), + ("Default Image Quality", "默认图像质量"), + ("Default Codec", "默认编解码"), + ("Bitrate", "码率"), + ("FPS", "帧率"), + ("Auto", "自动"), + ("Other Default Options", "其它默认选项"), + ("Voice call", "语音通话"), + ("Text chat", "文字聊天"), + ("Stop voice call", "停止语音通话"), + ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"), + ("Reconnect", "重连"), + ("Codec", "编解码"), + ("Resolution", "分辨率"), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", "安装虚拟显示器驱动,以便在没有连接显示器的情况下启动虚拟显示器进行控制。"), + ("confirm_idd_driver_tip", "安装虚拟显示器驱动的选项已勾选。请注意,测试证书将被安装以信任虚拟显示器驱动。测试证书仅会用于信任Rustdesk的驱动。"), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", "此文件与对方的一致"), + ("show_monitors_tip", ""), + ("View Mode", "浏览模式"), + ("enter_rustdesk_passwd_tip", "请输入 RustDesk 密码"), + ("remember_rustdesk_passwd_tip", "记住 RustDesk 密码"), + ("login_linux_tip", "登录被控端的 Linux 账户"), + ("login_linux_tooltip_tip", "登录被控端的 Linux 账户,才能启用 X 桌面"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 609ebb81e..2f9e52f6c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schránka je prázdná"), ("Stop service", "Zastavit službu"), ("Change ID", "Změnit identifikátor"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Použít je mozné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je třeba aby začínalo na písmeno a-z, A-Z. Délka mezi 6 a 16 znaky."), ("Website", "Webové stránky"), ("About", "O aplikaci"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Ztlumit"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Vstup zvuku"), ("Enhancements", ""), ("Hardware Codec", ""), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Server s API rozhraním"), ("invalid_http", "Je třeba, aby začínalo na http:// nebo https://"), ("Invalid IP", "Neplatná IP adresa"), - ("id_change_tip", "Použít je mozné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je třeba aby začínalo na písmeno a-z, A-Z. Délka mezi 6 a 16 znaky."), ("Invalid format", "Neplatný formát"), ("server_not_support", "Server zatím nepodporuje"), ("Not available", "Není k dispozici"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Upozornení"), ("Login screen using Wayland is not supported", "Přihlašovací obrazovka prostřednictvím Wayland není podporována"), ("Reboot required", "Je třeba restartovat"), - ("Unsupported display server ", "Nepodporovaný zobrazovací server"), + ("Unsupported display server", "Nepodporovaný zobrazovací server"), ("x11 expected", "očekávány x11"), ("Port", ""), ("Settings", "Nastavení"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Ručně ukončeno protějškem"), ("Enable remote configuration modification", "Umožnit upravování nastavení vzdáleného"), ("Run without install", "Spustit bez instalování"), - ("Always connected via relay", "Vždy spojováno prostřednictvím brány pro předávání (relay)"), + ("Connect via relay", ""), ("Always connect via relay", "Vždy se spojovat prostřednictvím brány pro předávání (relay)"), ("whitelist_tip", "Přístup je umožněn pouze z IP adres, nacházejících se na seznamu povolených"), ("Login", "Přihlásit se"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Odhlásit se"), ("Tags", "Štítky"), ("Search ID", "Hledat identifikátor"), - ("Current Wayland display server is not supported", "Zobrazovací server Wayland zatím není podporován"), ("whitelist_sep", "Odělováno čárkou, středníkem, mezerou nebo koncem řádku"), ("Add ID", "Přidat identifikátor"), ("Add Tag", "Přidat štítek"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Poznámka"), ("Connection", "Připojení"), ("Share Screen", "Nasdílet obrazovku"), - ("CLOSE", "ZAVŘÍT"), - ("OPEN", "OTEVŘÍT"), ("Chat", "Chat"), ("Total", "Celkem"), ("items", "Položek"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Zapnutí „Zachytávání obsahu obrazovky“ automaticky spustí službu, což umožní ostatním zařízením žádat o připojení k vašemu zařízení."), ("android_stop_service_tip", "Zastavení služby automaticky ukončí veškerá navázaná spojení."), ("android_version_audio_tip", "Vámi nyní používaná verze systému Android nepodporuje zachytávání zvuku – přejděte na Android 10 nebo novější."), - ("android_start_service_tip", "Službu pro sdílení obrazovky spustíte klepnutím na [Spustit službu] nebo UDĚLTE pověření pro [Zachytávání obsahu obrazovky]."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", ""), ("Overwrite", "Přepsat"), ("This file exists, skip or overwrite this file?", "Tento soubor existuje – přeskočit ho nebo přepsat?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Měřítko adaptivní"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), + ("Light Theme", ""), ("Dark", ""), ("Light", ""), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 nebo vyšší verzi."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 82a0fec94..a9ca8e8a6 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -2,21 +2,21 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), - ("Your Desktop", "dit skrivebord"), + ("Your Desktop", "Dit skrivebord"), ("desk_tip", "Du kan få adgang til dit skrivebord med dette ID og adgangskode."), - ("Password", "Kodeord"), + ("Password", "Adgangskode"), ("Ready", "Klar"), ("Established", "Etableret"), ("connecting_status", "Opretter forbindelse til RustDesk-netværket..."), ("Enable Service", "Tænd forbindelsesserveren"), - ("Start Service", "Starte forbindelsesserveren"), + ("Start Service", "Start forbindelsesserveren"), ("Service is running", "Tjenesten kører"), ("Service is not running", "Den tilknyttede tjeneste kører ikke"), ("not_ready_status", "Ikke klar. Tjek venligst din forbindelse"), ("Control Remote Desktop", "Styr fjernskrivebord"), - ("Transfer File", "Overføre fil"), + ("Transfer File", "Overfør fil"), ("Connect", "Forbind"), - ("Recent Sessions", "Sidste sessioner"), + ("Recent Sessions", "Seneste sessioner"), ("Address Book", "Adressebog"), ("Confirmation", "Bekræftelse"), ("TCP Tunneling", "TCP tunneling"), @@ -25,53 +25,60 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set your own password", "Indstil din egen adgangskode"), ("Enable Keyboard/Mouse", "Tænd for tastatur/mus"), ("Enable Clipboard", "Tænd for udklipsholderen"), - ("Enable File Transfer", "Aktiver filoverførsel"), + ("Enable File Transfer", "Aktivér filoverførsel"), ("Enable TCP Tunneling", "Slå TCP-tunneling til"), - ("IP Whitelisting", "IP-udgivelsesliste"), + ("IP Whitelisting", "IP whitelisting"), ("ID/Relay Server", "ID/forbindelsesserver"), ("Import Server Config", "Importér serverkonfiguration"), - ("Export Server Config", ""), - ("Import server configuration successfully", "Importér serverkonfigurationen"), - ("Export server configuration successfully", ""), + ("Export Server Config", "Eksportér serverkonfiguration"), + ("Import server configuration successfully", "Importering af serverkonfigurationen lykkedes"), + ("Export server configuration successfully", "Eksportering af serverkonfigurationen lykkedes"), ("Invalid server configuration", "Ugyldig serverkonfiguration"), ("Clipboard is empty", "Udklipsholderen er tom"), ("Stop service", "Sluk for forbindelsesserveren"), - ("Change ID", "Ændre ID"), + ("Change ID", "Ændr ID"), + ("Your new ID", "Dit nye ID"), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."), ("Website", "Hjemmeside"), - ("About", "Omkring"), + ("About", "Om"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Sluk for mikrofonen"), - ("Audio Input", "Lydindgang"), - ("Enhancements", ""), - ("Hardware Codec", ""), - ("Adaptive Bitrate", ""), - ("ID Server", "identifikations Server"), - ("Relay Server", "Relæ Server"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), + ("Audio Input", "Lydinput"), + ("Enhancements", "Forbedringer"), + ("Hardware Codec", "Hardware-codec"), + ("Adaptive Bitrate", "Adaptiv Bitrate"), + ("ID Server", "ID Server"), + ("Relay Server", "Relay Server"), ("API Server", "API Server"), ("invalid_http", "Skal begynde med http:// eller https://"), ("Invalid IP", "Ugyldig IP-adresse"), - ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Længde mellem 6 og 16."), ("Invalid format", "Ugyldigt format"), ("server_not_support", "Endnu ikke understøttet af serveren"), - ("Not available", "ikke Tilgængelig"), + ("Not available", "Ikke tilgængelig"), ("Too frequent", "For ofte"), - ("Cancel", "Abort"), + ("Cancel", "Annullér"), ("Skip", "Spring over"), ("Close", "Luk"), ("Retry", "Prøv igen"), ("OK", "OK"), - ("Password Required", "Adgangskode kræves"), + ("Password Required", "Adgangskode påkrævet"), ("Please enter your password", "Indtast venligst dit kodeord"), ("Remember password", "Husk kodeord"), ("Wrong Password", "Forkert kodeord"), ("Do you want to enter again?", "Vil du forbinde igen?"), ("Connection Error", "Forbindelsesfejl"), - ("Error", "fejl"), - ("Reset by the peer", "Nulstil ved peer"), + ("Error", "Fejl"), + ("Reset by the peer", "Nulstillet ved modparten"), ("Connecting...", "Opretter forbindelse..."), ("Connection in progress. Please wait.", "Forbindelsen er etableret. Vent venligst."), - ("Please try 1 minute later", "Prøv igen, 1 minut senere"), + ("Please try 1 minute later", "Prøv igen om et minut"), ("Login Error", "Login fejl"), ("Successful", "Vellykket"), ("Connected, waiting for image...", "Tilsluttet, venter på billede..."), @@ -85,42 +92,42 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh File", "Genopfrisk fil"), ("Local", "Lokalt"), ("Remote", "Remote"), - ("Remote Computer", "Fjern computer"), - ("Local Computer", "Lokal Computer"), + ("Remote Computer", "Fjerncomputer"), + ("Local Computer", "Lokal computer"), ("Confirm Delete", "Bekræft sletning"), ("Delete", "Slet"), ("Properties", "Egenskaber"), ("Multi Select", "Flere valg"), - ("Select All", ""), - ("Unselect All", ""), - ("Empty Directory", "Tom bibliotek"), + ("Select All", "Vælg alt"), + ("Unselect All", "Fravælg alt"), + ("Empty Directory", "Tomt bibliotek"), ("Not an empty directory", "Intet tomt bibliotek"), ("Are you sure you want to delete this file?", "Er du sikker på, at du vil slette denne fil?"), ("Are you sure you want to delete this empty directory?", "Er du sikker på, at du vil slette dette tomme bibliotek?"), ("Are you sure you want to delete the file of this directory?", "Er du sikker på, at du vil slette filen til dette bibliotek?"), ("Do this for all conflicts", "Gør dette for alle konflikter"), - ("This is irreversible!", "Dette er irreversibelt!"), + ("This is irreversible!", "Dette kan ikke gendannes!"), ("Deleting", "Sletter"), ("files", "Filer"), ("Waiting", "Venter"), ("Finished", "Færdig"), - ("Speed", "hastighed"), - ("Custom Image Quality", "Individuel billedkvalitet"), - ("Privacy mode", "Databeskyttelsestilstand (Privatlivstilstand)"), + ("Speed", "Hastighed"), + ("Custom Image Quality", "Brugerdefineret billedkvalitet"), + ("Privacy mode", "Privatlivstilstand"), ("Block user input", "Bloker brugerinput"), ("Unblock user input", "Fjern blokering af brugerinput"), ("Adjust Window", "Juster vinduet"), ("Original", "Original"), ("Shrink", "Krymp"), - ("Stretch", "Strak"), + ("Stretch", "Stræk ud"), ("Scrollbar", "Rullebar"), - ("ScrollAuto", "Rul Auto"), + ("ScrollAuto", "Auto-rul"), ("Good image quality", "God billedkvalitet"), ("Balanced", "Afbalanceret"), ("Optimize reaction time", "Optimeret responstid"), - ("Custom", ""), + ("Custom", "Tilpasset"), ("Show remote cursor", "Vis fjernbetjeningskontrolleret markør"), - ("Show quality monitor", ""), + ("Show quality monitor", "Vis billedkvalitet"), ("Disable clipboard", "Deaktiver udklipsholder"), ("Lock after session end", "Lås efter afslutningen af fjernstyring"), ("Insert", "Indsæt"), @@ -128,8 +135,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh", "Genopfrisk"), ("ID does not exist", "ID findes ikke"), ("Failed to connect to rendezvous server", "Forbindelse til forbindelsesserveren mislykkedes"), - ("Please try later", "Prøv det senere"), - ("Remote desktop is offline", "Fjernet desktop er offline"), + ("Please try later", "Prøv igen senere"), + ("Remote desktop is offline", "Fjernskrivebord er offline"), ("Key mismatch", "Nøgle uoverensstemmelse"), ("Timeout", "Timeout"), ("Failed to connect to relay server", "Forbindelse til relæ-serveren mislykkedes"), @@ -138,93 +145,97 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Direkte forbindelse til fjernskrivebord kunne ikke etableres"), ("Set Password", "Indstil adgangskode"), ("OS Password", "Operativsystemadgangskode"), - ("install_tip", "På grund af UAC kan Rustdesk ikke fungere korrekt på den anden side i nogle tilfælde. For at undgå UAC skal du klikke på knappen nedenfor for at installere Rustdesk på systemet"), + ("install_tip", "På grund af UAC kan RustDesk ikke fungere korrekt i nogle tilfælde på fjernskrivebordet. For at undgå UAC skal du klikke på knappen nedenfor for at installere RustDesk på systemet"), ("Click to upgrade", "Klik for at opgradere"), ("Click to download", "Klik for at downloade"), ("Click to update", "Klik for at opdatere"), ("Configure", "Konfigurer"), ("config_acc", "For at kontrollere dit skrivebord på afstand skal du give Rustdesk \"Access \" Rettigheder."), ("config_screen", "For at kunne få adgang til dit skrivebord langtfra, skal du give Rustdesk \"skærmstøtte \" tilladelser."), - ("Installing ...", "Installere ..."), + ("Installing ...", "Installerer ..."), ("Install", "installere"), ("Installation", "Installation"), ("Installation Path", "Installationsti"), - ("Create start menu shortcuts", "Opret startmenu links"), - ("Create desktop icon", "Opret skrivebords-symbol"), + ("Create start menu shortcuts", "Opret start menu genveje"), + ("Create desktop icon", "Opret skrivebords-genvej"), ("agreement_tip", "Hvis du starter installationen, skal du acceptere licensaftalen"), ("Accept and Install", "Accepter og installer"), ("End-user license agreement", "Licensaftale for slutbrugere"), - ("Generating ...", "Generer kode ..."), - ("Your installation is lower version.", "Din installation er en lavere version."), + ("Generating ...", "Genererer kode ..."), + ("Your installation is lower version.", "Din installation er en ældre version."), ("not_close_tcp_tip", "Luk ikke dette vindue, mens du bruger tunnelen."), ("Listening ...", "Lytter ..."), ("Remote Host", "Fjern-Host"), ("Remote Port", "Fjern-Port"), - ("Action", "Рandling"), + ("Action", "Handling"), ("Add", "Tilføj"), ("Local Port", "Lokal Port"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Lokal adresse"), + ("Change Local Port", "Skift lokal port"), ("setup_server_tip", "For en hurtigere forbindelse skal du indstille din egen forbindelsesserver"), - ("Too short, at least 6 characters.", "For kort, mindst 6 tegn."), + ("Too short, at least 6 characters.", "For kort, brug mindst 6 tegn."), ("The confirmation is not identical.", "Bekræftelsen er ikke identisk."), ("Permissions", "Tilladelser"), - ("Accept", "Acceptere"), - ("Dismiss", "Afvise"), + ("Accept", "Accepter"), + ("Dismiss", "Afvis"), ("Disconnect", "Frakobl"), ("Allow using keyboard and mouse", "Tillad brug af tastatur og mus"), ("Allow using clipboard", "Tillad brug af udklipsholderen"), - ("Allow hearing sound", "Tillader hørelse fra lyd"), - ("Allow file copy and paste", "Tillad fil kopiering og indsættelse"), + ("Allow hearing sound", "Tillad at høre lyd"), + ("Allow file copy and paste", "Tillad kopiering og indsæt af filer"), ("Connected", "Forbundet"), ("Direct and encrypted connection", "Direkte og krypteret forbindelse"), - ("Relayed and encrypted connection", "Brugt relæet og krypteret forbindelse"), + ("Relayed and encrypted connection", "Viderestillet og krypteret forbindelse"), ("Direct and unencrypted connection", "Direkte og ukrypteret forbindelse"), - ("Relayed and unencrypted connection", "Brugt relæet og ukrypteret forbindelse"), + ("Relayed and unencrypted connection", "Viderestillet og ukrypteret forbindelse"), ("Enter Remote ID", "Indtast Remote-ID"), ("Enter your password", "Skriv dit kodeord"), ("Logging in...", "Logger ind..."), - ("Enable RDP session sharing", "RDP-Aktivér sessiongodkendelse"), - ("Auto Login", "Automatisk login (kun gyldigt hvis du har konfigureret \"Lock efter afslutningen af sessionen\")"), + ("Enable RDP session sharing", "Aktivér RDP sessiongodkendelse"), + ("Auto Login", "Automatisk login (kun gyldigt hvis du har konfigureret \"Lås efter afslutningen af sessionen\")"), ("Enable Direct IP Access", "Aktivér direkte IP-adgang"), ("Rename", "Omdøb"), ("Space", "Plads"), ("Create Desktop Shortcut", "Opret skrivebords-genvej"), ("Change Path", "Skift stien"), ("Create Folder", "Opret mappe"), - ("Please enter the folder name", "Indtast venligst mappenavnet"), + ("Please enter the folder name", "Indtast venligst mappens navn"), ("Fix it", "Kør reparation"), ("Warning", "Advarsel"), - ("Login screen using Wayland is not supported", "Registreringsskærm med Wayland understøttes ikke"), + ("Login screen using Wayland is not supported", "Login skærm med Wayland understøttes ikke"), ("Reboot required", "Genstart krævet"), - ("Unsupported display server ", "Ikke-understøttet displayserver"), + ("Unsupported display server", "Ikke-understøttet displayserver"), ("x11 expected", "X11 Forventet"), ("Port", "Port"), ("Settings", "Indstillinger"), ("Username", " Brugernavn"), ("Invalid port", "Ugyldig port"), ("Closed manually by the peer", "Manuelt lukket af peer"), - ("Enable remote configuration modification", "Tillad at ændre afstandskonfigurationen"), + ("Enable remote configuration modification", "Tillad fjernkonfigurering"), ("Run without install", "Kør uden installation"), - ("Always connected via relay", "Tilslut altid via relæ-server"), - ("Always connect via relay", "Forbindelse via relæ-server"), - ("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"), + ("Connect via relay", "Forbind via viderestilling"), + ("Always connect via relay", "Forbindelse via viderestillings-server"), + ("whitelist_tip", "Kun IP'er på whitelisten kan få adgang til mig"), ("Login", "Login"), - ("Logout", "logger af"), + ("Verify", "Verificér"), + ("Remember me", "Husk mig"), + ("Trust this device", "Husk denne enhed"), + ("Verification code", "Verifikationskode"), + ("verification_tip", ""), + ("Logout", "Logger af"), ("Tags", "Nøgleord"), - ("Search ID", "Søg ID"), - ("Current Wayland display server is not supported", "Den aktuelle Wayland-Anzege-server understøttes ikke"), - ("whitelist_sep", "Adskilt af komma, semikolon, rum eller linjepaus"), + ("Search ID", "Søg efter ID"), + ("whitelist_sep", "Adskilt af komma, semikolon, mellemrum eller linjebrud"), ("Add ID", "Tilføj ID"), ("Add Tag", "Tilføj nøgleord"), ("Unselect all tags", "Fravælg alle nøgleord"), ("Network error", "Netværksfejl"), - ("Username missed", "Benutzername fehlt"), + ("Username missed", "Glemt brugernavn"), ("Password missed", "Glemt kodeord"), ("Wrong credentials", "Forkerte registreringsdata"), ("Edit Tag", "Rediger nøgleord"), - ("Unremember Password", "Bemærk ikke adgangskoden"), - ("Favorites", "Favorit"), + ("Unremember Password", "Glem adgangskoden"), + ("Favorites", "Favoritter"), ("Add to Favorites", "Tilføj til favoritter"), ("Remove from Favorites", "Fjern favoritter"), ("Empty", "Tom"), @@ -232,35 +243,33 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Socks5 Proxy", "Socks5 Proxy"), ("Hostname", "Computernavn"), ("Discovered", "Fundet"), - ("install_daemon_tip", "Til at begynde med opstart, skal du installere systemtjenesten"), - ("Remote ID", "Fjern ID"), + ("install_daemon_tip", "For at starte efter PC'en er startet op, skal du installere systemtjenesten"), + ("Remote ID", "Fjern-ID"), ("Paste", "Indsæt"), ("Paste here?", "Indsæt her?"), - ("Are you sure to close the connection?", "Sind Sie sicher, dass Sie die Verbindung schließen wollen?"), - ("Download new version", "Neue Version herunterladen"), + ("Are you sure to close the connection?", "Er du sikker på at du vil afslutte forbindelsen?"), + ("Download new version", "Download ny version"), ("Touch mode", "Touch-tilstand"), ("Mouse mode", "Musse-tilstand"), - ("One-Finger Tap", "En fingerspids-tap"), + ("One-Finger Tap", "En-finger-tryk"), ("Left Mouse", "Venstre mus"), - ("One-Long Tap", "Tryk med en finger lang"), - ("Two-Finger Tap", "Tryk med to fingre-tap"), + ("One-Long Tap", "Tryk og hold med en finger"), + ("Two-Finger Tap", "Tryk med to fingre"), ("Right Mouse", "Højre mus"), - ("One-Finger Move", "En fingerbevægelse"), - ("Double Tap & Move", "Dobbelt og flytte"), - ("Mouse Drag", "Mus"), + ("One-Finger Move", "En-finger bevægelse"), + ("Double Tap & Move", "Dobbeltklik og flyt"), + ("Mouse Drag", "Træk med musen"), ("Three-Finger vertically", "Tre fingre lodret"), ("Mouse Wheel", "Mussehjul"), - ("Two-Finger Move", "To fingreflytning"), + ("Two-Finger Move", "To-fingre bevægelse"), ("Canvas Move", "Flyt lærred"), - ("Pinch to Zoom", "Zoom ind"), + ("Pinch to Zoom", "Knib for at zoome ind"), ("Canvas Zoom", "Lærred zoom"), - ("Reset canvas", "Nulstil skærm"), + ("Reset canvas", "Nulstil lærred"), ("No permission of file transfer", "Ingen tilladelse til at overføre filen"), ("Note", "Note"), ("Connection", "Forbindelse"), ("Share Screen", "Del skærmen"), - ("CLOSE", "LUK"), - ("OPEN", "ÅBEN"), ("Chat", "Chat"), ("Total", "Total"), ("items", "artikel"), @@ -273,140 +282,206 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Accepterer du?"), ("Open System Setting", "Åbn systemindstillingen"), ("How to get Android input permission?", "Hvordan får jeg en Android-input tilladelse?"), - ("android_input_permission_tip1", "For at en ekstern enhed kan kontrollere din Android-enhed via mus eller berøring, skal du give Rustdesk mulighed for at bruge tjenesten \"tilgængelighed \"."), - ("android_input_permission_tip2", "Gå til den næste systemindstillingsside, søg og indtast [installerede tjenester], tænd for [Rustdesk Input] Service."), - ("android_new_connection_tip", "En ny kontrolanmodning blev modtaget, der gerne ville kontrollere din nuværende enhed."), + ("android_input_permission_tip1", "For at en ekstern enhed kan kontrollere din Android-enhed via mus eller berøring, skal du give RustDesk mulighed for at bruge tjenesten \"tilgængelighed \"."), + ("android_input_permission_tip2", "Gå til den næste systemindstillingsside, søg og indtast [installerede tjenester], tænd for [RustDesk Input] tjenesten."), + ("android_new_connection_tip", "En ny anmodning blev modtaget, der gerne vil kontrollere din nuværende enhed."), ("android_service_will_start_tip", "Ved at tænde for skærmoptagelsen startes tjenesten automatisk, så andre enheder kan anmode om en forbindelse fra denne enhed."), ("android_stop_service_tip", "Ved at lukke tjenesten lukkes alle fremstillede forbindelser automatisk."), - ("android_version_audio_tip", "Den aktuelle Android -version understøtter ikke lydoptagelse, skal du opdatere om Android 10 eller højere."), - ("android_start_service_tip", "Tryk på [Start Service] eller åbn autorisationen [skærmoptagelse] for at starte skærmudgivelsen."), + ("android_version_audio_tip", "Den aktuelle Android-version understøtter ikke lydoptagelse. Android 10 eller højere er påkrævet."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Konto"), ("Overwrite", "Overskriv"), - ("This file exists, skip or overwrite this file?", "Denne fil findes, springer over denne fil eller overskriver?"), + ("This file exists, skip or overwrite this file?", "Denne fil findes allerede, vil du springe over eller overskrive denne fil?"), ("Quit", "Afslut"), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("Help", "Hjælp"), ("Failed", "Mislykkedet"), ("Succeeded", "Vellykket"), - ("Someone turns on privacy mode, exit", "Nogen aktiverede databeskyttelsestilstand, slut"), + ("Someone turns on privacy mode, exit", "Nogen aktiverede privatlivstilstand, afslut"), ("Unsupported", "Ikke understøttet"), - ("Peer denied", "Peer nægtet"), - ("Please install plugins", "Venligst Installer plugins"), - ("Peer exit", "Peer-Afslut"), - ("Failed to turn off", "Slukke"), + ("Peer denied", "Modpart nægtet"), + ("Please install plugins", "Installer venligst plugins"), + ("Peer exit", "Modpart-Afslut"), + ("Failed to turn off", "Mislykkedes i at lukke ned"), ("Turned off", "Slukket"), - ("In privacy mode", "I databeskyttelsestilstand"), - ("Out privacy mode", "Databeskyttelsestilstand fra"), + ("In privacy mode", "I privatlivstilstand"), + ("Out privacy mode", "Privatlivstilstand fra"), ("Language", "Sprog"), ("Keep RustDesk background service", "Behold RustDesk baggrundstjeneste"), - ("Ignore Battery Optimizations", "Ignorer betteri optimeringer"), + ("Ignore Battery Optimizations", "Ignorér betteri optimeringer"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", "Start under opstart"), + ("Start the screen sharing service on boot, requires special permissions", "Start skærmdelingstjenesten under opstart, kræver specielle rettigheder"), ("Connection not allowed", "Forbindelse ikke tilladt"), ("Legacy mode", "Bagudkompatibilitetstilstand"), - ("Map mode", ""), - ("Translate mode", ""), + ("Map mode", "Kortmodus"), + ("Translate mode", "Oversættelsesmodus"), ("Use permanent password", "Brug permanent adgangskode"), - ("Use both passwords", "Bug begge adgangskoder"), + ("Use both passwords", "Brug begge adgangskoder"), ("Set permanent password", "Sæt permanent adgangskode"), - ("Enable Remote Restart", "Aktiver fjerngenstart"), + ("Enable Remote Restart", "Aktivér fjerngenstart"), ("Allow remote restart", "Tillad fjerngenstart"), ("Restart Remote Device", "Genstart fjernenhed"), ("Are you sure you want to restart", "Er du sikker på at du vil genstarte"), ("Restarting Remote Device", "Genstarter fjernenhed"), - ("remote_restarting_tip", ""), - ("Copied", "Kopierét"), + ("remote_restarting_tip", "Enheden genstarter - Lukker denne besked ned, og tilslutter igen om et øjeblik"), + ("Copied", "Kopieret"), ("Exit Fullscreen", "Afslut fuldskærm"), ("Fullscreen", "Fuld skærm"), ("Mobile Actions", "Mobile handlinger"), - ("Select Monitor", "Vælg Monitor"), + ("Select Monitor", "Vælg skærm"), ("Control Actions", "Kontrolhandlinger"), ("Display Settings", "Skærmindstillinger"), ("Ratio", "Forhold"), - ("Image Quality", "Billede kvalitet"), - ("Scroll Style", "Rulstil"), + ("Image Quality", "Billedkvalitet"), + ("Scroll Style", "Rullestil"), ("Show Menubar", "Vis menulinje"), - ("Hide Menubar", "skjul menulinjen"), + ("Hide Menubar", "Skjul menulinjen"), ("Direct Connection", "Direkte forbindelse"), - ("Relay Connection", "Relæforbindelse"), + ("Relay Connection", "Viderestillingsforbindelse"), ("Secure Connection", "Sikker forbindelse"), ("Insecure Connection", "Usikker forbindelse"), - ("Scale original", "Skaler original"), - ("Scale adaptive", "Skaler adaptiv"), + ("Scale original", "Original skalering"), + ("Scale adaptive", "Adaptiv skalering"), ("General", "Generelt"), ("Security", "Sikkerhed"), - ("Account", "Konto"), ("Theme", "Thema"), ("Dark Theme", "Mørk Tema"), + ("Light Theme", "Lys Tema"), ("Dark", "Mørk"), ("Light", "Lys"), ("Follow System", "Følg System"), - ("Enable hardware codec", "Aktiver hardware-codec"), - ("Unlock Security Settings", "Lås op for sikkerhedsinstillinger"), - ("Enable Audio", "Aktiver Lyd"), - ("Unlock Network Settings", "Lås op for Netværksinstillinger"), + ("Enable hardware codec", "Aktivér hardware-codec"), + ("Unlock Security Settings", "Lås op for sikkerhedsindstillinger"), + ("Enable Audio", "Aktivér Lyd"), + ("Unlock Network Settings", "Lås op for Netværksindstillinger"), ("Server", "Server"), ("Direct IP Access", "Direkte IP Adgang"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Anvend"), ("Disconnect all devices?", "Afbryd alle enheder?"), ("Clear", "Nulstil"), ("Audio Input Device", "Lydindgangsenhed"), - ("Deny remote access", "Afvis fjeradgang"), + ("Deny remote access", "Afvis fjernadgang"), ("Use IP Whitelisting", "Brug IP Whitelisting"), ("Network", "Netværk"), - ("Enable RDP", "Aktiver RDP"), + ("Enable RDP", "Aktivér RDP"), ("Pin menubar", "Fastgør menulinjen"), ("Unpin menubar", "Frigør menulinjen"), ("Recording", "Optager"), ("Directory", "Mappe"), ("Automatically record incoming sessions", "Optag automatisk indgående sessioner"), - ("Change", "Ændre"), + ("Change", "Ændr"), ("Start session recording", "Start sessionsoptagelse"), ("Stop session recording", "Stop sessionsoptagelse"), - ("Enable Recording Session", "Aktiver optagelsessession"), + ("Enable Recording Session", "Aktivér optagelsessession"), ("Allow recording session", "Tillad optagelsessession"), - ("Enable LAN Discovery", "Aktiver LAN Discovery"), + ("Enable LAN Discovery", "Aktivér LAN Discovery"), ("Deny LAN Discovery", "Afvis LAN Discovery"), ("Write a message", "Skriv en besked"), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Vent venligst på UAC-bekræftelse..."), ("elevated_foreground_window_tip", ""), ("Disconnected", "Afbrudt"), - ("Other", ""), - ("Confirm before closing multiple tabs", ""), - ("Keyboard Settings", ""), - ("Custom", ""), - ("Full Access", ""), - ("Screen Share", ""), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu 21.04 eller nyere version."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kræver en højere version af linux distro. Prøv venligst X11 desktop eller skift dit OS."), - ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på peer-siden)."), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), - ("Elevate", ""), - ("Zoom cursor", ""), - ("Accept sessions via password", ""), - ("Accept sessions via click", ""), - ("Accept sessions via both", ""), - ("Please wait for the remote side to accept your session request...", ""), - ("One-time Password", ""), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), + ("Other", "Andre"), + ("Confirm before closing multiple tabs", "Bekræft før du lukker flere faner"), + ("Keyboard Settings", "Tastaturindstillinger"), + ("Full Access", "Fuld adgang"), + ("Screen Share", "Skærmdeling"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kræver Ubuntu version 21.04 eller nyere."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kræver en højere version af Linux distro. Prøv venligst X11 desktop eller skift dit OS."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Vælg venligst den skærm, der skal deles (Betjen på modtagersiden)."), + ("Show RustDesk", "Vis RustDesk"), + ("This PC", "Denne PC"), + ("or", "eller"), + ("Continue with", "Fortsæt med"), + ("Elevate", "Elevér"), + ("Zoom cursor", "Zoom markør"), + ("Accept sessions via password", "Acceptér sessioner via adgangskode"), + ("Accept sessions via click", "Acceptér sessioner via klik"), + ("Accept sessions via both", "Acceptér sessioner via begge"), + ("Please wait for the remote side to accept your session request...", "Vent venligst på at fjernklienten accepterer din sessionsforespørgsel..."), + ("One-time Password", "Engangskode"), + ("Use one-time password", "Brug engangskode"), + ("One-time password length", "Engangskode længde"), + ("Request access to your device", "Efterspørg adgang til din enhed"), + ("Hide connection management window", "Skjul forbindelseshåndteringsvindue"), ("hide_cm_tip", ""), ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Skipped", ""), - ("Add to Address Book", ""), - ("Group", ""), - ("Search", ""), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Right click to select tabs", "Højreklik for at vælge faner"), + ("Skipped", "Sprunget over"), + ("Add to Address Book", "Tilføj til adressebog"), + ("Group", "Gruppe"), + ("Search", "Søg"), + ("Closed manually by web console", "Lukket ned manuelt af webkonsollen"), + ("Local keyboard type", "Lokal tastatur type"), + ("Select local keyboard type", "Vælg lokal tastatur type"), + ("software_render_tip", ""), + ("Always use software rendering", "Brug altid software rendering"), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", "Vent"), + ("Elevation Error", "Elevationsfejl"), + ("Ask the remote user for authentication", "Spørg fjernbrugeren for godkendelse"), + ("Choose this if the remote account is administrator", "Vælg dette hvis fjernbrugeren er en administrator"), + ("Transmit the username and password of administrator", "Send brugernavnet og adgangskoden på administratoren"), + ("still_click_uac_tip", ""), + ("Request Elevation", "Efterspørger elevation"), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", "Elevation lykkedes"), + ("uppercase", "store bogstaver"), + ("lowercase", "små bogstaver"), + ("digit", "ciffer"), + ("special character", "specielt tegn"), + ("length>=8", "længde>=8"), + ("Weak", "Svag"), + ("Medium", "Mellem"), + ("Strong", "Stærk"), + ("Switch Sides", "Skift sider"), + ("Please confirm if you want to share your desktop?", "Bekræft venligst, om du vil dele dit skrivebord?"), + ("Display", "Visning"), + ("Default View Style", "Standard visningsstil"), + ("Default Scroll Style", "Standard rulle stil"), + ("Default Image Quality", "Standard billedkvalitet"), + ("Default Codec", "Standard codec"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Andre standardindstillinger"), + ("Voice call", "Stemmeopkald"), + ("Text chat", "Tekstchat"), + ("Stop voice call", "Stop stemmeopkald"), + ("relay_hint_tip", ""), + ("Reconnect", "Genopret"), + ("Codec", "Codec"), + ("Resolution", "Opløsning"), + ("No transfers in progress", "Ingen overførsler i gang"), + ("Set one-time password length", "Sæt engangsadgangskode længde"), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", "RDP indstillinger"), + ("Sort by", "Sortér efter"), + ("New Connection", "Ny forbindelse"), + ("Restore", "Gendan"), + ("Minimize", "Minimér"), + ("Maximize", "Maksimér"), + ("Your Device", "Din enhed"), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", "fx: admin"), + ("Empty Username", "Tom brugernavn"), + ("Empty Password", "Tom adgangskode"), + ("Me", "Mig"), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index ddb2a45d2..b70e9f872 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -7,13 +7,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "Passwort"), ("Ready", "Bereit"), ("Established", "Verbunden"), - ("connecting_status", "Verbinden mit dem RustDesk-Netzwerk..."), + ("connecting_status", "Verbinden mit dem RustDesk-Netzwerk …"), ("Enable Service", "Vermittlungsdienst aktivieren"), - ("Start Service", "Starte Vermittlungsdienst"), + ("Start Service", "Vermittlungsdienst starten"), ("Service is running", "Vermittlungsdienst aktiv"), ("Service is not running", "Vermittlungsdienst deaktiviert"), ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung."), - ("Control Remote Desktop", "Entfernten PC steuern"), + ("Control Remote Desktop", "Entfernten Desktop steuern"), ("Transfer File", "Datei übertragen"), ("Connect", "Verbinden"), ("Recent Sessions", "Letzte Sitzungen"), @@ -26,32 +26,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Keyboard/Mouse", "Tastatur und Maus aktivieren"), ("Enable Clipboard", "Zwischenablage aktivieren"), ("Enable File Transfer", "Dateiübertragung aktivieren"), - ("Enable TCP Tunneling", "TCP-Tunnel aktivieren"), + ("Enable TCP Tunneling", "TCP-Tunnelung aktivieren"), ("IP Whitelisting", "IP-Whitelist"), - ("ID/Relay Server", "ID/Vermittlungsserver"), + ("ID/Relay Server", "ID/Relay-Server"), ("Import Server Config", "Serverkonfiguration importieren"), ("Export Server Config", "Serverkonfiguration exportieren"), ("Import server configuration successfully", "Serverkonfiguration erfolgreich importiert"), ("Export server configuration successfully", "Serverkonfiguration erfolgreich exportiert"), ("Invalid server configuration", "Ungültige Serverkonfiguration"), ("Clipboard is empty", "Zwischenablage ist leer"), - ("Stop service", "Vermittlungsdienst deaktivieren"), + ("Stop service", "Vermittlungsdienst stoppen"), ("Change ID", "ID ändern"), + ("Your new ID", "Ihre neue ID"), + ("length %min% to %max%", "Länge %min% bis %max%"), + ("starts with a letter", "Beginnt mit Buchstabe"), + ("allowed characters", "Erlaubte Zeichen"), + ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), ("Website", "Webseite"), ("About", "Über"), - ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt"), + ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), + ("Build Date", "Erstelldatum"), + ("Version", "Version"), + ("Home", "Startseite"), ("Audio Input", "Audioeingang"), ("Enhancements", "Verbesserungen"), ("Hardware Codec", "Hardware-Codec"), ("Adaptive Bitrate", "Bitrate automatisch anpassen"), ("ID Server", "ID-Server"), - ("Relay Server", "Vermittlungsserver"), + ("Relay Server", "Relay-Server"), ("API Server", "API-Server"), ("invalid_http", "Muss mit http:// oder https:// beginnen"), ("Invalid IP", "Ungültige IP-Adresse"), - ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), ("Invalid format", "Ungültiges Format"), ("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt."), ("Not available", "Nicht verfügbar"), @@ -69,12 +76,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection Error", "Verbindungsfehler"), ("Error", "Fehler"), ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt."), - ("Connecting...", "Verbindung wird hergestellt..."), - ("Connection in progress. Please wait.", "Die Verbindung wird hergestellt. Bitte warten..."), + ("Connecting...", "Verbindung wird hergestellt …"), + ("Connection in progress. Please wait.", "Die Verbindung wird hergestellt. Bitte warten …"), ("Please try 1 minute later", "Bitte versuchen Sie es später erneut"), ("Login Error", "Anmeldefehler"), ("Successful", "Erfolgreich"), - ("Connected, waiting for image...", "Verbindung hergestellt. Warten auf Bild..."), + ("Connected, waiting for image...", "Verbindung hergestellt. Warte auf anderen Bildschirm …"), ("Name", "Name"), ("Type", "Typ"), ("Modified", "Geändert"), @@ -118,7 +125,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Hohe Bildqualität"), ("Balanced", "Ausgeglichen"), ("Optimize reaction time", "Geschwindigkeit"), - ("Custom", "Individuell"), + ("Custom", "Benutzerdefiniert"), ("Show remote cursor", "Entfernten Cursor anzeigen"), ("Show quality monitor", "Qualitätsüberwachung anzeigen"), ("Disable clipboard", "Zwischenablage deaktivieren"), @@ -127,15 +134,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert Lock", "Win+L (Sperren) senden"), ("Refresh", "Aktualisieren"), ("ID does not exist", "Diese ID existiert nicht."), - ("Failed to connect to rendezvous server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), + ("Failed to connect to rendezvous server", "Verbindung zum Rendezvous-Server fehlgeschlagen"), ("Please try later", "Bitte versuchen Sie es später erneut."), - ("Remote desktop is offline", "Entfernter PC ist offline."), + ("Remote desktop is offline", "Entfernter Desktop ist offline."), ("Key mismatch", "Schlüssel stimmen nicht überein."), ("Timeout", "Zeitüberschreitung"), - ("Failed to connect to relay server", "Verbindung zum Vermittlungsserver fehlgeschlagen"), - ("Failed to connect via rendezvous server", "Verbindung über Vermittlungsserver ist fehlgeschlagen"), + ("Failed to connect to relay server", "Verbindung zum Relay-Server ist fehlgeschlagen"), + ("Failed to connect via rendezvous server", "Verbindung über Rendezvous-Server ist fehlgeschlagen"), ("Failed to connect via relay server", "Verbindung über Relay-Server ist fehlgeschlagen"), - ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten PC fehlgeschlagen"), + ("Failed to make direct connection to remote desktop", "Direkte Verbindung zum entfernten Desktop ist fehlgeschlagen"), ("Set Password", "Passwort festlegen"), ("OS Password", "Betriebssystem-Passwort"), ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten und installieren RustDesk auf dem System."), @@ -145,7 +152,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Configure", "Konfigurieren"), ("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."), ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk die Berechtigung \"Bildschirmaufnahme\" erteilen."), - ("Installing ...", "Installiere..."), + ("Installing ...", "Wird installiert …"), ("Install", "Installieren"), ("Installation", "Installation"), ("Installation Path", "Installationspfad"), @@ -154,10 +161,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung."), ("Accept and Install", "Akzeptieren und Installieren"), ("End-user license agreement", "Lizenzvereinbarung für Endbenutzer"), - ("Generating ...", "Wird generiert..."), + ("Generating ...", "Wird generiert …"), ("Your installation is lower version.", "Ihre Version ist veraltet."), ("not_close_tcp_tip", "Schließen Sie dieses Fenster nicht, solange Sie den Tunnel benutzen."), - ("Listening ...", "Lauschen..."), + ("Listening ...", "Lauschen …"), ("Remote Host", "Entfernter PC"), ("Remote Port", "Entfernter Port"), ("Action", "Aktion"), @@ -165,7 +172,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Lokaler Port"), ("Local Address", "Lokale Adresse"), ("Change Local Port", "Lokalen Port ändern"), - ("setup_server_tip", "für eine schnellere Verbindung richten Sie bitte Ihren eigenen Verbindungsserver ein."), + ("setup_server_tip", "für eine schnellere Verbindung richten Sie bitte Ihren eigenen Server ein."), ("Too short, at least 6 characters.", "Zu kurz, mindestens 6 Zeichen."), ("The confirmation is not identical.", "Die Passwörter stimmen nicht überein."), ("Permissions", "Berechtigungen"), @@ -183,7 +190,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "Vermittelte und unverschlüsselte Verbindung"), ("Enter Remote ID", "Remote-ID eingeben"), ("Enter your password", "Geben Sie Ihr Passwort ein"), - ("Logging in...", "Anmelden..."), + ("Logging in...", "Anmelden …"), ("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"), ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Nach Sitzungsende sperren\" aktiviert haben)"), ("Enable Direct IP Access", "Direkten IP-Zugang aktivieren"), @@ -197,30 +204,34 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Warnung"), ("Login screen using Wayland is not supported", "Anmeldebildschirm mit Wayland wird nicht unterstützt."), ("Reboot required", "Neustart erforderlich"), - ("Unsupported display server ", "Nicht unterstützter Display-Server"), + ("Unsupported display server", "Nicht unterstützter Anzeigeserver"), ("x11 expected", "X11 erwartet"), ("Port", "Port"), ("Settings", "Einstellungen"), - ("Username", " Benutzername"), + ("Username", "Benutzername"), ("Invalid port", "Ungültiger Port"), - ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen"), + ("Closed manually by the peer", "Von der Gegenstelle manuell geschlossen."), ("Enable remote configuration modification", "Änderung der Konfiguration aus der Ferne zulassen"), ("Run without install", "Ohne Installation ausführen"), - ("Always connected via relay", "Immer über Relay-Server verbunden"), + ("Connect via relay", "Über Relay-Server verbinden"), ("Always connect via relay", "Immer über Relay-Server verbinden"), ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), + ("Verify", "Überprüfen"), + ("Remember me", "Login merken"), + ("Trust this device", "Diesem Gerät vertrauen"), + ("Verification code", "Verifizierungscode"), + ("verification_tip", "Es wurde ein neues Gerät erkannt und ein Verifizierungscode an die registrierte E-Mail-Adresse gesendet. Geben Sie den Verifizierungscode ein, um sich erneut anzumelden."), ("Logout", "Abmelden"), ("Tags", "Schlagworte"), - ("Search ID", "Suche ID"), - ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt."), + ("Search ID", "ID suchen"), ("whitelist_sep", "Getrennt durch Komma, Semikolon, Leerzeichen oder Zeilenumbruch"), ("Add ID", "ID hinzufügen"), ("Add Tag", "Stichwort hinzufügen"), ("Unselect all tags", "Alle Stichworte abwählen"), ("Network error", "Netzwerkfehler"), - ("Username missed", "Benutzername vergessen"), - ("Password missed", "Passwort vergessen"), + ("Username missed", "Benutzername fehlt"), + ("Password missed", "Passwort fehlt"), ("Wrong credentials", "Falsche Anmeldedaten"), ("Edit Tag", "Schlagwort bearbeiten"), ("Unremember Password", "Gespeichertes Passwort löschen"), @@ -236,10 +247,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote ID", "Entfernte ID"), ("Paste", "Einfügen"), ("Paste here?", "Hier einfügen?"), - ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich trennen?"), + ("Are you sure to close the connection?", "Möchten Sie diese Verbindung wirklich schließen?"), ("Download new version", "Neue Version herunterladen"), ("Touch mode", "Touch-Modus"), - ("Mouse mode", "Maus-Modus"), + ("Mouse mode", "Mausmodus"), ("One-Finger Tap", "1-Finger-Tipp"), ("Left Mouse", "Linksklick"), ("One-Long Tap", "1-Finger-Halten"), @@ -255,31 +266,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pinch to Zoom", "2-Finger-Zoom"), ("Canvas Zoom", "Sichtfeld-Zoom"), ("Reset canvas", "Sichtfeld zurücksetzen"), - ("No permission of file transfer", "Keine Berechtigung für den Dateizugriff"), - ("Note", "Anmerkung"), + ("No permission of file transfer", "Keine Berechtigung für die Dateiübertragung"), + ("Note", "Hinweis"), ("Connection", "Verbindung"), ("Share Screen", "Bildschirm freigeben"), - ("CLOSE", "DEAKTIV."), - ("OPEN", "AKTIVIER."), ("Chat", "Chat"), ("Total", "Gesamt"), ("items", "Einträge"), ("Selected", "Ausgewählt"), - ("Screen Capture", "Bildschirmzugr."), - ("Input Control", "Eingabezugriff"), - ("Audio Capture", "Audiozugriff"), - ("File Connection", "Dateizugriff"), - ("Screen Connection", "Bildschirmanschluss"), + ("Screen Capture", "Bildschirmaufnahme"), + ("Input Control", "Eingabesteuerung"), + ("Audio Capture", "Audioaufnahme"), + ("File Connection", "Dateiverbindung"), + ("Screen Connection", "Bildschirmverbindung"), ("Do you accept?", "Verbindung zulassen?"), ("Open System Setting", "Systemeinstellung öffnen"), ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), - ("android_input_permission_tip1", "Damit ein Remote-Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), - ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie [Installierte Dienste] und schalten Sie den Dienst [RustDesk Input] ein."), - ("android_new_connection_tip", "möchte ihr Gerät steuern."), + ("android_input_permission_tip1", "Damit ein entferntes Gerät Ihr Android-Gerät steuern kann, müssen Sie RustDesk erlauben, den Dienst \"Barrierefreiheit\" zu verwenden."), + ("android_input_permission_tip2", "Bitte gehen Sie zur nächsten Systemeinstellungsseite, suchen Sie \"Installierte Dienste\" und schalten Sie den Dienst \"RustDesk Input\" ein."), + ("android_new_connection_tip", "möchte Ihr Gerät steuern."), ("android_service_will_start_tip", "Durch das Aktivieren der Bildschirmfreigabe wird der Dienst automatisch gestartet, sodass andere Geräte dieses Android-Gerät steuern können."), ("android_stop_service_tip", "Durch das Deaktivieren des Dienstes werden automatisch alle hergestellten Verbindungen getrennt."), ("android_version_audio_tip", "Ihre Android-Version unterstützt keine Audioaufnahme, bitte aktualisieren Sie auf Android 10 oder höher, falls möglich."), - ("android_start_service_tip", "Tippen Sie auf [Dienst aktivieren] oder aktivieren Sie die Berechtigung [Bildschirmzugr.], um den Bildschirmfreigabedienst zu starten."), + ("android_start_service_tip", "Tippen Sie auf \"Vermittlungsdienst starten\" oder aktivieren Sie die Berechtigung \"Bildschirmaufnahme\", um den Bildschirmfreigabedienst zu starten."), + ("android_permission_may_not_change_tip", "Die Berechtigungen für bestehende Verbindungen können nicht sofort geändert werden, bis die Verbindung wiederhergestellt ist."), ("Account", "Konto"), ("Overwrite", "Überschreiben"), ("This file exists, skip or overwrite this file?", "Diese Datei existiert; überspringen oder überschreiben?"), @@ -288,11 +298,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Help", "Hilfe"), ("Failed", "Fehlgeschlagen"), ("Succeeded", "Erfolgreich"), - ("Someone turns on privacy mode, exit", "Jemand hat den Datenschutzmodus aktiviert, beende..."), + ("Someone turns on privacy mode, exit", "Jemand hat den Datenschutzmodus aktiviert, wird beendet …"), ("Unsupported", "Nicht unterstützt"), - ("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt"), + ("Peer denied", "Die Gegenstelle hat die Verbindung abgelehnt."), ("Please install plugins", "Bitte installieren Sie Plugins"), - ("Peer exit", "Die Gegenstelle hat die Verbindung getrennt"), + ("Peer exit", "Die Gegenstelle hat die Verbindung getrennt."), ("Failed to turn off", "Ausschalten fehlgeschlagen"), ("Turned off", "Ausgeschaltet"), ("In privacy mode", "Datenschutzmodus aktivieren"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"), ("Ignore Battery Optimizations", "Akkuoptimierung ignorieren"), ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Akkuoptimierung öffnen?"), + ("Start on Boot", "Beim Booten starten"), + ("Start the screen sharing service on boot, requires special permissions", "Bildschirmfreigabedienst beim Booten starten, erfordert zusätzliche Berechtigungen"), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), ("Map mode", "Kartenmodus"), @@ -320,23 +332,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mobile Actions", "Mobile Aktionen"), ("Select Monitor", "Bildschirm auswählen"), ("Control Actions", "Aktionen"), - ("Display Settings", "Bildschirmeinstellungen"), + ("Display Settings", "Anzeigeeinstellungen"), ("Ratio", "Verhältnis"), ("Image Quality", "Bildqualität"), ("Scroll Style", "Scroll-Stil"), ("Show Menubar", "Menüleiste anzeigen"), ("Hide Menubar", "Menüleiste ausblenden"), ("Direct Connection", "Direkte Verbindung"), - ("Relay Connection", "Relaisverbindung"), + ("Relay Connection", "Relay-Verbindung"), ("Secure Connection", "Sichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"), ("Scale original", "Keine Skalierung"), - ("Scale adaptive", "Automatische Skalierung"), + ("Scale adaptive", "Anpassbare Skalierung"), ("General", "Allgemein"), ("Security", "Sicherheit"), - ("Account", "Konto"), ("Theme", "Farbgebung"), ("Dark Theme", "Dunkle Farbgebung"), + ("Light Theme", "Helle Farbgebung"), ("Dark", "Dunkel"), ("Light", "Hell"), ("Follow System", "Systemstandard"), @@ -347,13 +359,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direkter IP-Zugriff"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Anwenden"), ("Disconnect all devices?", "Alle Geräte trennen?"), ("Clear", "Zurücksetzen"), ("Audio Input Device", "Audioeingabegerät"), ("Deny remote access", "Fernzugriff verbieten"), - ("Use IP Whitelisting", "IP-Whitelist benutzen"), + ("Use IP Whitelisting", "IP-Whitelist verwenden"), ("Network", "Netzwerk"), ("Enable RDP", "RDP aktivieren"), ("Pin menubar", "Menüleiste anpinnen"), @@ -370,33 +381,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), ("Prompt", "Meldung"), - ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), - ("elevated_foreground_window_tip", "Das aktuell geöffnete Fenster des ferngesteuerten Computers benötigt höhere Rechte. Deshalb ist es derzeit nicht möglich, die Maus und die Tastatur zu verwenden. Bitten Sie den Nutzer, dessen Computer Sie fernsteuern, das Fenster zu minimieren oder die Rechte zu erhöhen. Um dieses Problem zukünftig zu vermeiden, wird empfohlen, die Software auf dem ferngesteuerten Computer zu installieren."), + ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten …"), + ("elevated_foreground_window_tip", "Das aktuell geöffnete Fenster des ferngesteuerten Computers erfordert höhere Rechte. Deshalb ist es derzeit nicht möglich, die Maus und die Tastatur zu verwenden. Bitten Sie den Nutzer, dessen Computer Sie fernsteuern, das Fenster zu minimieren oder die Rechte zu erhöhen. Um dieses Problem zukünftig zu vermeiden, wird empfohlen, die Software auf dem ferngesteuerten Computer zu installieren."), ("Disconnected", "Verbindung abgebrochen"), ("Other", "Weitere Einstellungen"), ("Confirm before closing multiple tabs", "Nachfragen, wenn mehrere Tabs geschlossen werden"), ("Keyboard Settings", "Tastatureinstellungen"), - ("Custom", "Individuell"), ("Full Access", "Vollzugriff"), ("Screen Share", "Bildschirmfreigabe"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland erfordert Ubuntu 21.04 oder eine höhere Version."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland erfordert eine höhere Version der Linux-Distribution. Bitte versuchen Sie den X11-Desktop oder ändern Sie Ihr Betriebssystem."), ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den Bildschirm aus, der freigegeben werden soll (auf der Peer-Seite arbeiten)."), + ("Please Select the screen to be shared(Operate on the peer side).", "Bitte wählen Sie den freizugebenden Bildschirm aus (Bedienung auf der Gegenseite)."), ("Show RustDesk", "RustDesk anzeigen"), ("This PC", "Dieser PC"), ("or", "oder"), ("Continue with", "Fortfahren mit"), - ("Elevate", "Erheben"), - ("Zoom cursor", "Cursor zoomen"), + ("Elevate", "Zugriff gewähren"), + ("Zoom cursor", "Cursor vergrößern"), ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), ("Accept sessions via both", "Sitzung mit Klick und Passwort bestätigen"), - ("Please wait for the remote side to accept your session request...", "Bitte warten Sie, bis die Gegenseite Ihre Sitzungsanfrage akzeptiert hat..."), + ("Please wait for the remote side to accept your session request...", "Bitte warten Sie, bis die Gegenseite Ihre Sitzungsanfrage akzeptiert hat …"), ("One-time Password", "Einmalpasswort"), ("Use one-time password", "Einmalpasswort verwenden"), ("One-time password length", "Länge des Einmalpassworts"), - ("Request access to your device", "Zugriff zu Ihrem Gerät erbitten"), + ("Request access to your device", "Zugriff auf Ihr Gerät anfordern"), ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff über ein permanentes Passwort erfolgt."), ("wayland_experiment_tip", "Die Unterstützung von Wayland ist nur experimentell. Bitte nutzen Sie X11, wenn Sie einen unbeaufsichtigten Zugriff benötigen."), @@ -405,8 +415,74 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Zum Adressbuch hinzufügen"), ("Group", "Gruppe"), ("Search", "Suchen"), - ("Closed manually by the web console", "Manuell über die Webkonsole beendet"), + ("Closed manually by web console", "Manuell über die Webkonsole geschlossen"), ("Local keyboard type", "Lokaler Tastaturtyp"), ("Select local keyboard type", "Lokalen Tastaturtyp auswählen"), + ("software_render_tip", "Wenn Sie eine Nvidia-Grafikkarte haben und sich das entfernte Fenster sofort nach dem Herstellen der Verbindung schließt, kann es helfen, den Nouveau-Treiber zu installieren und Software-Rendering zu verwenden. Ein Neustart der Software ist erforderlich."), + ("Always use software rendering", "Software-Rendering immer verwenden"), + ("config_input", "Um den entfernten Desktop mit der Tastatur steuern zu können, müssen Sie RustDesk die Berechtigung \"Input Monitoring\" erteilen."), + ("config_microphone", "Um aus der Ferne sprechen zu können, müssen Sie RustDesk die Berechtigung \"Audio aufzeichnen\" erteilen."), + ("request_elevation_tip", "Sie können auch erhöhte Rechte anfordern, wenn sich jemand auf der Gegenseite befindet."), + ("Wait", "Warten"), + ("Elevation Error", "Berechtigungsfehler"), + ("Ask the remote user for authentication", "Den entfernten Benutzer zur Authentifizierung auffordern"), + ("Choose this if the remote account is administrator", "Wählen Sie dies, wenn das entfernte Konto Administrator ist."), + ("Transmit the username and password of administrator", "Übermitteln Sie den Benutzernamen und das Passwort des Administrators"), + ("still_click_uac_tip", "Der entfernte Benutzer muss immer noch im UAC-Fenster von RustDesk auf \"Ja\" klicken."), + ("Request Elevation", "Erhöhte Rechte anfordern"), + ("wait_accept_uac_tip", "Bitte warten Sie, bis der entfernte Benutzer den UAC-Dialog akzeptiert hat."), + ("Elevate successfully", "Erhöhung der Rechte erfolgreich"), + ("uppercase", "Großbuchstaben"), + ("lowercase", "Kleinbuchstaben"), + ("digit", "Ziffern"), + ("special character", "Sonderzeichen"), + ("length>=8", "Länge ≥ 8"), + ("Weak", "Schwach"), + ("Medium", "Mittel"), + ("Strong", "Stark"), + ("Switch Sides", "Seiten wechseln"), + ("Please confirm if you want to share your desktop?", "Bitte bestätigen Sie, wenn Sie Ihren Desktop freigeben möchten."), + ("Display", "Anzeige"), + ("Default View Style", "Standard-Ansichtsstil"), + ("Default Scroll Style", "Standard-Scroll-Stil"), + ("Default Image Quality", "Standard-Bildqualität"), + ("Default Codec", "Standard-Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "fps"), + ("Auto", "Automatisch"), + ("Other Default Options", "Weitere Standardeinstellungen"), + ("Voice call", "Sprachanruf"), + ("Text chat", "Text-Chat"), + ("Stop voice call", "Sprachanruf beenden"), + ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen. \nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" auf der Gegenstelle auswählen."), + ("Reconnect", "Erneut verbinden"), + ("Codec", "Codec"), + ("Resolution", "Auflösung"), + ("No transfers in progress", "Keine Übertragungen im Gange"), + ("Set one-time password length", "Länge des Einmalpassworts festlegen"), + ("idd_driver_tip", "Installieren Sie den virtuellen Anzeigetreiber, der verwendet wird, wenn Sie keine physischen Anzeigen haben."), + ("confirm_idd_driver_tip", "Die Option zur Installation des virtuellen Anzeigetreibers ist aktiviert. Beachten Sie, dass ein Testzertifikat installiert wird, um dem virtuellen Anzeigetreiber zu vertrauen. Dieses Testzertifikat wird nur verwendet, um Rustdesk-Treibern zu vertrauen."), + ("RDP Settings", "RDP-Einstellungen"), + ("Sort by", "Sortieren nach"), + ("New Connection", "Neue Verbindung"), + ("Restore", "Verkleinern"), + ("Minimize", "Minimieren"), + ("Maximize", "Maximieren"), + ("Your Device", "Ihr Gerät"), + ("empty_recent_tip", "Ups, keine aktuellen Sitzungen!\nZeit, eine neue zu planen."), + ("empty_favorite_tip", "Noch keine favorisierte Gegenstelle?\nLassen Sie uns jemanden finden, mit dem wir uns verbinden können und fügen Sie ihn zu Ihren Favoriten hinzu!"), + ("empty_lan_tip", "Oh nein, es sieht so aus, als hätten wir noch keine Gegenstelle entdeckt."), + ("empty_address_book_tip", "Oh je, es scheint, dass in Ihrem Adressbuch derzeit keine Gegenstellen aufgeführt sind."), + ("eg: admin", "z. B.: admin"), + ("Empty Username", "Leerer Benutzername"), + ("Empty Password", "Leeres Passwort"), + ("Me", "Ich"), + ("identical_file_tip", "Diese Datei ist identisch mit der Datei der Gegenstelle."), + ("show_monitors_tip", "Monitore in der Symbolleiste anzeigen"), + ("View Mode", "Ansichtsmodus"), + ("enter_rustdesk_passwd_tip", "RustDesk-Passwort eingeben."), + ("remember_rustdesk_passwd_tip", "RustDesk-Passwort merken."), + ("login_linux_tip", "Anmeldung am entfernten Linux-Konto"), + ("login_linux_tooltip_tip", "Sie müssen sich an einem entfernten Linux-Konto anmelden, um eine X-Desktop-Sitzung zu eröffnen."), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/el.rs similarity index 73% rename from src/lang/gr.rs rename to src/lang/el.rs index 811dca2c6..51ac6f041 100644 --- a/src/lang/gr.rs +++ b/src/lang/el.rs @@ -12,7 +12,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start Service", "Έναρξη υπηρεσίας"), ("Service is running", "Η υπηρεσία εκτελείται"), ("Service is not running", "Η υπηρεσία δεν εκτελείται"), - ("not_ready_status", "Δεν είναι έτοιμο. Ελέγξτε τη σύνδεσή σας"), + ("not_ready_status", "Δεν είναι έτοιμο. Ελέγξτε τη σύνδεσή σας στο δίκτυο"), ("Control Remote Desktop", "Έλεγχος απομακρυσμένου σταθμού εργασίας"), ("Transfer File", "Μεταφορά αρχείου"), ("Connect", "Σύνδεση"), @@ -24,7 +24,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"), ("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"), ("Enable Keyboard/Mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"), - ("Enable Clipboard", "Ενεργοποίηση Προχείρου"), + ("Enable Clipboard", "Ενεργοποίηση προχείρου"), ("Enable File Transfer", "Ενεργοποίηση μεταφοράς αρχείων"), ("Enable TCP Tunneling", "Ενεργοποίηση TCP Tunneling"), ("IP Whitelisting", "Λίστα επιτρεπόμενων IP"), @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Το πρόχειρο είναι κενό"), ("Stop service", "Διακοπή υπηρεσίας"), ("Change ID", "Αλλαγή αναγνωριστικού ID"), + ("Your new ID", "Το νέο σας ID"), + ("length %min% to %max%", "μέγεθος από %min% έως %max%"), + ("starts with a letter", "ξεκινά με γράμμα"), + ("allowed characters", "επιτρεπόμενοι χαρακτήρες"), + ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9 και _ (υπογράμμιση). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Website", "Ιστότοπος"), ("About", "Πληροφορίες"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"), + ("Privacy Statement", "Πολιτική απορρήτου"), ("Mute", "Σίγαση"), + ("Build Date", "Ημερομηνία δημιουργίας"), + ("Version", "Έκδοση"), + ("Home", "Αρχική"), ("Audio Input", "Είσοδος ήχου"), ("Enhancements", "Βελτιώσεις"), ("Hardware Codec", "Κωδικοποιητής υλικού"), @@ -51,16 +59,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Διακομιστής API"), ("invalid_http", "Πρέπει να ξεκινά με http:// ή https://"), ("Invalid IP", "Μη έγκυρη διεύθυνση IP"), - ("id_change_tip", "Επιτρέπονται μόνο οι χαρακτήρες a-z, A-Z, 0-9 και _ (υπογράμμιση). Το πρώτο γράμμα πρέπει να είναι a-z, A-Z και το μήκος πρέπει να είναι μεταξύ 6 και 16 χαρακτήρων."), ("Invalid format", "Μη έγκυρη μορφή"), - ("server_not_support", "Αυτή η δυνατότητα δεν υποστηρίζεται ακόμη από τον διακομιστή"), + ("server_not_support", "Αυτή η δυνατότητα δεν υποστηρίζεται από τον διακομιστή"), ("Not available", "Μη διαθέσιμο"), ("Too frequent", "Πολύ συχνά"), ("Cancel", "Ακύρωση"), ("Skip", "Παράλειψη"), ("Close", "Κλείσιμο"), ("Retry", "Δοκίμασε ξανά"), - ("OK", "Εντάξει"), + ("OK", "ΟΚ"), ("Password Required", "Απαιτείται κωδικός πρόσβασης"), ("Please enter your password", "Παρακαλώ εισάγετε τον κωδικό πρόσβασης"), ("Remember password", "Απομνημόνευση κωδικού πρόσβασης"), @@ -101,7 +108,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do this for all conflicts", "Κάνε αυτό για όλες τις διενέξεις"), ("This is irreversible!", "Αυτό είναι μη αναστρέψιμο!"), ("Deleting", "Διαγραφή"), - ("files", "αρχεία"), + ("files", "αρχείων"), ("Waiting", "Αναμονή"), ("Finished", "Ολοκληρώθηκε"), ("Speed", "Ταχύτητα"), @@ -113,17 +120,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Πρωτότυπο"), ("Shrink", "Συρρίκνωση"), ("Stretch", "Προσαρμογή"), - ("Scrollbar", "Γραμμή κύλισης"), + ("Scrollbar", "Μπάρα κύλισης"), ("ScrollAuto", "Αυτόματη κύλιση"), ("Good image quality", "Καλή ποιότητα εικόνας"), - ("Balanced", "Ισορροπημένο"), - ("Optimize reaction time", "Βελτιστοποίηση χρόνου αντίδρασης"), - ("Custom", "Προσαρμογή ποιότητας εικόνας"), + ("Balanced", "Ισορροπημένη"), + ("Optimize reaction time", "Βελτιστοποίηση απόκρισης"), + ("Custom", "Προσαρμοσμένη ποιότητας εικόνας"), ("Show remote cursor", "Εμφάνιση απομακρυσμένου κέρσορα"), ("Show quality monitor", "Εμφάνιση παρακολούθησης ποιότητας σύνδεσης"), ("Disable clipboard", "Απενεργοποίηση προχείρου"), ("Lock after session end", "Κλείδωμα μετά το τέλος της συνεδρίας"), - ("Insert", ""), + ("Insert", "Εισαγωγή"), ("Insert Lock", "Κλείδωμα απομακρυσμένου σταθμού"), ("Refresh", "Ανανέωση"), ("ID does not exist", "Το αναγνωριστικό ID δεν υπάρχει"), @@ -132,22 +139,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote desktop is offline", "Ο απομακρυσμένος σταθμός εργασίας είναι εκτός σύνδεσης"), ("Key mismatch", "Μη έγκυρο κλειδί"), ("Timeout", "Τέλος χρόνου"), - ("Failed to connect to relay server", "Αποτυχία σύνδεσης με διακομιστή αναμετάδοσης"), + ("Failed to connect to relay server", "Αποτυχία σύνδεσης με τον διακομιστή αναμετάδοσης"), ("Failed to connect via rendezvous server", "Απέτυχε η σύνδεση μέσω διακομιστή"), ("Failed to connect via relay server", "Απέτυχε η σύνδεση μέσω διακομιστή αναμετάδοσης"), ("Failed to make direct connection to remote desktop", "Απέτυχε η απευθείας σύνδεση με τον απομακρυσμένο σταθμό εργασίας"), - ("Set Password", "Ορίστε κωδικό"), + ("Set Password", "Ορίστε κωδικό πρόσβασης"), ("OS Password", "Κωδικός πρόσβασης λειτουργικού συστήματος"), ("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουργεί σωστά σε ορισμένες περιπτώσεις. Για να αποφύγετε το UAC, κάντε κλικ στο κουμπί παρακάτω για να εγκαταστήσετε το RustDesk στο σύστημα"), - ("Click to upgrade", "Κάντε κλικ για αναβάθμιση"), - ("Click to download", "Κάντε κλικ για λήψη"), - ("Click to update", "Κάντε κλικ για ενημέρωση"), + ("Click to upgrade", "Πιέστε για αναβάθμιση"), + ("Click to download", "Πιέστε για λήψη"), + ("Click to update", "Πιέστε για ενημέρωση"), ("Configure", "Διαμόρφωση"), ("config_acc", "Για τον απομακρυσμένο έλεγχο του υπολογιστή σας, πρέπει να εκχωρήσετε δικαιώματα πρόσβασης στο RustDesk."), ("config_screen", "Για να αποκτήσετε απομακρυσμένη πρόσβαση στον υπολογιστή σας, πρέπει να εκχωρήσετε το δικαίωμα RustDesk \"Screen Capture\"."), - ("Installing ...", "Εγκατάσταση ..."), + ("Installing ...", "Γίνεται εγκατάσταση ..."), ("Install", "Εγκατάσταση"), - ("Installation", "Εγκατάσταση"), + ("Installation", "Η εγκατάσταση"), ("Installation Path", "Διαδρομή εγκατάστασης"), ("Create start menu shortcuts", "Δημιουργία συντομεύσεων μενού έναρξης"), ("Create desktop icon", "Δημιουργία εικονιδίου επιφάνειας εργασίας"), @@ -175,7 +182,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow using keyboard and mouse", "Να επιτρέπεται η χρήση πληκτρολογίου και ποντικιού"), ("Allow using clipboard", "Να επιτρέπεται η χρήση του προχείρου"), ("Allow hearing sound", "Να επιτρέπεται η αναπαραγωγή ήχου"), - ("Allow file copy and paste", "Να επιτρέπεται η αντιγραφή και επικόλληση αρχείου"), + ("Allow file copy and paste", "Να επιτρέπεται η αντιγραφή και επικόλληση αρχείων"), ("Connected", "Συνδεδεμένο"), ("Direct and encrypted connection", "Άμεση και κρυπτογραφημένη σύνδεση"), ("Relayed and encrypted connection", "Κρυπτογραφημένη σύνδεση με αναμετάδοση"), @@ -183,21 +190,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "Μη κρυπτογραφημένη σύνδεση με αναμετάδοση"), ("Enter Remote ID", "Εισαγωγή απομακρυσμένου ID"), ("Enter your password", "Εισάγετε τον κωδικό σας"), - ("Logging in...", "Σύνδεση..."), + ("Logging in...", "Γίνεται σύνδεση..."), ("Enable RDP session sharing", "Ενεργοποίηση κοινής χρήσης RDP"), ("Auto Login", "Αυτόματη είσοδος"), ("Enable Direct IP Access", "Ενεργοποίηση άμεσης πρόσβασης IP"), ("Rename", "Μετονομασία"), ("Space", "Χώρος"), ("Create Desktop Shortcut", "Δημιουργία συντόμευσης στην επιφάνεια εργασίας"), - ("Change Path", "Αλλαγή διαδρομής"), + ("Change Path", "Αλλαγή διαδρομής δίσκου"), ("Create Folder", "Δημιουργία φακέλου"), ("Please enter the folder name", "Παρακαλώ εισάγετε το όνομα του φακέλου"), ("Fix it", "Επιδιόρθωσε το"), ("Warning", "Προειδοποίηση"), ("Login screen using Wayland is not supported", "Η οθόνη εισόδου με χρήση του Wayland δεν υποστηρίζεται"), ("Reboot required", "Απαιτείται επανεκκίνηση"), - ("Unsupported display server ", "Μη υποστηριζόμενος διακομιστής εμφάνισης "), + ("Unsupported display server", "Μη υποστηριζόμενος διακομιστής εμφάνισης "), ("x11 expected", "απαιτείται X11"), ("Port", "Θύρα"), ("Settings", "Ρυθμίσεις"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Έκλεισε από τον απομακρυσμένο σταθμό"), ("Enable remote configuration modification", "Ενεργοποίηση απομακρυσμένης τροποποίησης ρυθμίσεων"), ("Run without install", "Εκτέλεση χωρίς εγκατάσταση"), - ("Always connected via relay", "Πάντα συνδεδεμένο μέσω αναμετάδοσης"), - ("Always connect via relay", "Σύνδεση πάντα μέσω αναμετάδοσης"), + ("Connect via relay", "Πραγματοποίηση σύνδεση μέσω αναμεταδότη"), + ("Always connect via relay", "Σύνδεση πάντα μέσω αναμεταδότη"), ("whitelist_tip", "Μόνο οι IP της λίστας επιτρεπόμενων έχουν πρόσβαση"), ("Login", "Σύνδεση"), + ("Verify", "Επαλήθευση"), + ("Remember me", "Να με θυμάσαι"), + ("Trust this device", "Εμπιστεύομαι αυτή την συσκευή"), + ("Verification code", "Κωδικός επαλήθευσης"), + ("verification_tip", "Εντοπίστηκε νέα συσκευή και εστάλη ένας κωδικός επαλήθευσης στην καταχωρισμένη διεύθυνση email. Εισαγάγετε τον κωδικό επαλήθευσης για να συνδεθείτε ξανά."), ("Logout", "Αποσύνδεση"), ("Tags", "Ετικέτες"), ("Search ID", "Αναζήτηση ID"), - ("Current Wayland display server is not supported", "Ο τρέχων διακομιστής εμφάνισης Wayland δεν υποστηρίζεται"), ("whitelist_sep", "Διαχωρίζονται με κόμμα, ερωτηματικό, διάστημα ή νέα γραμμή"), ("Add ID", "Προσθήκη αναγνωριστικού ID"), ("Add Tag", "Προσθήκη ετικέτας"), @@ -231,7 +242,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Invalid folder name", "Μη έγκυρο όνομα φακέλου"), ("Socks5 Proxy", "Διαμεσολαβητής Socks5"), ("Hostname", "Όνομα υπολογιστή"), - ("Discovered", "Ανακαλύφθηκε"), + ("Discovered", "Ανακαλύφθηκαν"), ("install_daemon_tip", "Για να ξεκινά με την εκκίνηση του υπολογιστή, πρέπει να εγκαταστήσετε την υπηρεσία συστήματος"), ("Remote ID", "Απομακρυσμένο ID"), ("Paste", "Επικόλληση"), @@ -259,15 +270,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Σημείωση"), ("Connection", "Σύνδεση"), ("Share Screen", "Κοινή χρήση οθόνης"), - ("CLOSE", "Απενεργοποίηση"), - ("OPEN", "Ενεργοποίηση"), ("Chat", "Κουβέντα"), ("Total", "Σύνολο"), ("items", "στοιχεία"), ("Selected", "Επιλεγμένο"), ("Screen Capture", "Αποτύπωση οθόνης"), ("Input Control", "Έλεγχος εισόδου"), - ("Audio Capture", "Λήψη ήχου"), + ("Audio Capture", "Εγγραφή ήχου"), ("File Connection", "Σύνδεση αρχείου"), ("Screen Connection", "Σύνδεση οθόνης"), ("Do you accept?", "Δέχεσαι;"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."), ("android_stop_service_tip", "Η απενεργοποίηση της υπηρεσίας θα αποσυνδέσει αυτόματα όλες τις εγκατεστημένες συνδέσεις."), ("android_version_audio_tip", "Η έκδοση Android που διαθέτετε δεν υποστηρίζει εγγραφή ήχου, ενημερώστε το σε Android 10 ή νεότερη έκδοση, εάν είναι δυνατόν."), - ("android_start_service_tip", "Πατήστε [Ενεργοποίηση υπηρεσίας] ή ενεργοποιήστε την άδεια [Πρόσβαση στην οθόνη] για να ξεκινήσετε την υπηρεσία κοινής χρήσης οθόνης."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Λογαριασμός"), ("Overwrite", "Αντικατάσταση"), ("This file exists, skip or overwrite this file?", "Αυτό το αρχείο υπάρχει, παράβλεψη ή αντικατάσταση αυτού του αρχείου;"), @@ -291,7 +301,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Someone turns on privacy mode, exit", "Κάποιος ενεργοποιεί τη λειτουργία απορρήτου, έξοδος"), ("Unsupported", "Δεν υποστηρίζεται"), ("Peer denied", "Ο απομακρυσμένος σταθμός απέρριψε τη σύνδεση"), - ("Please install plugins", "Παρακαλώ εγκαταστήστε πρόσθετα"), + ("Please install plugins", "Παρακαλώ εγκαταστήστε τα πρόσθετα"), ("Peer exit", "Ο απομακρυσμένος σταθμός έχει αποσυνδεθεί"), ("Failed to turn off", "Αποτυχία απενεργοποίησης"), ("Turned off", "Απενεργοποιημένο"), @@ -301,7 +311,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Εκτέλεση του RustDesk στο παρασκήνιο"), ("Ignore Battery Optimizations", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"), ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), - ("Connection not allowed", "Η σύνδεση απορρίφθηκε"), + ("Start on Boot", "Έναρξη κατά την εκκίνηση"), + ("Start the screen sharing service on boot, requires special permissions", "Η έναρξη της υπηρεσίας κοινής χρήσης οθόνης κατά την εκκίνηση, απαιτεί ειδικά δικαιώματα"), + ("Connection not allowed", "Η σύνδεση δεν επιτρέπεται"), ("Legacy mode", "Λειτουργία συμβατότητας"), ("Map mode", "Map mode"), ("Translate mode", "Λειτουργία μετάφρασης"), @@ -312,7 +324,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow remote restart", "Να επιτρέπεται η απομακρυσμένη επανεκκίνηση"), ("Restart Remote Device", "Επανεκκίνηση απομακρυσμένης συσκευής"), ("Are you sure you want to restart", "Είστε βέβαιοι ότι θέλετε να κάνετε επανεκκίνηση"), - ("Restarting Remote Device", "Επανεκκίνηση απομακρυσμένης συσκευής"), + ("Restarting Remote Device", "Γίνεται επανεκκίνηση της απομακρυσμένης συσκευής"), ("remote_restarting_tip", "Η απομακρυσμένη συσκευή επανεκκινείται, κλείστε αυτό το μήνυμα και επανασυνδεθείτε χρησιμοποιώντας τον μόνιμο κωδικό πρόσβασης."), ("Copied", "Αντιγράφηκε"), ("Exit Fullscreen", "Έξοδος από πλήρη οθόνη"), @@ -331,12 +343,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Secure Connection", "Ασφαλής σύνδεση"), ("Insecure Connection", "Μη ασφαλής σύνδεση"), ("Scale original", "Κλιμάκωση πρωτότυπου"), - ("Scale adaptive", "Προσαρμοστική κλίμακα"), + ("Scale adaptive", "Προσαρμοσμένη κλίμακα"), ("General", "Γενικά"), ("Security", "Ασφάλεια"), - ("Account", "Λογαριασμός"), ("Theme", "Θέμα"), ("Dark Theme", "Σκούρο θέμα"), + ("Light Theme", "Φωτεινό θέμα"), ("Dark", "Σκούρο"), ("Light", "Φωτεινό"), ("Follow System", "Από το σύστημα"), @@ -345,9 +357,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Audio", "Ενεργοποίηση ήχου"), ("Unlock Network Settings", "Ξεκλείδωμα ρυθμίσεων δικτύου"), ("Server", "Διακομιστής"), - ("Direct IP Access", "Άμεση πρόσβαση IP"), + ("Direct IP Access", "Πρόσβαση με χρήση IP"), ("Proxy", "Διαμεσολαβητής"), - ("Port", "Θύρα"), ("Apply", "Εφαρμογή"), ("Disconnect all devices?", "Αποσύνδεση όλων των συσκευών;"), ("Clear", "Καθαρισμός"), @@ -365,18 +376,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start session recording", "Έναρξη εγγραφής συνεδρίας"), ("Stop session recording", "Διακοπή εγγραφής συνεδρίας"), ("Enable Recording Session", "Ενεργοποίηση εγγραφής συνεδρίας"), - ("Allow recording session", "Να επιτρέπεται η εγγραφή"), + ("Allow recording session", "Να επιτρέπεται η εγγραφή συνεδρίας"), ("Enable LAN Discovery", "Ενεργοποίηση εντοπισμού LAN"), ("Deny LAN Discovery", "Απαγόρευση εντοπισμού LAN"), ("Write a message", "Γράψτε ένα μήνυμα"), - ("Prompt", "Προτροπή"), + ("Prompt", "Υπενθυμίζω"), ("Please wait for confirmation of UAC...", "Παρακαλώ περιμένετε για επιβεβαίωση του UAC..."), ("elevated_foreground_window_tip", "Το τρέχον παράθυρο της απομακρυσμένης επιφάνειας εργασίας απαιτεί υψηλότερα δικαιώματα για να λειτουργήσει, επομένως δεν μπορεί να χρησιμοποιήσει προσωρινά το ποντίκι και το πληκτρολόγιο. Μπορείτε να ζητήσετε από τον απομακρυσμένο χρήστη να ελαχιστοποιήσει το τρέχον παράθυρο ή να κάνετε κλικ στο κουμπί ανύψωσης στο παράθυρο διαχείρισης σύνδεσης. Για να αποφύγετε αυτό το πρόβλημα, συνιστάται η εγκατάσταση του λογισμικού στην απομακρυσμένη συσκευή."), ("Disconnected", "Αποσυνδέθηκε"), ("Other", "Άλλα"), ("Confirm before closing multiple tabs", "Επιβεβαίωση πριν κλείσετε πολλές καρτέλες"), ("Keyboard Settings", "Ρυθμίσεις πληκτρολογίου"), - ("Custom", "Προσαρμογή ποιότητας εικόνας"), ("Full Access", "Πλήρης πρόσβαση"), ("Screen Share", "Κοινή χρήση οθόνης"), ("Wayland requires Ubuntu 21.04 or higher version.", "Το Wayland απαιτεί Ubuntu 21.04 ή νεότερη έκδοση."), @@ -388,10 +398,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "ή"), ("Continue with", "Συνέχεια με"), ("Elevate", "Ανύψωση"), - ("Zoom cursor", "Μεγέθυνση στον κέρσορα"), - ("Accept sessions via password", "Αποδοχή συνεδριών μέσω κωδικού πρόσβασης"), - ("Accept sessions via click", "Αποδοχή συνεδριών μέσω κλικ"), - ("Accept sessions via both", "Αποδοχή συνεδριών και από τα δύο"), + ("Zoom cursor", "Kέρσορας μεγέθυνσης"), + ("Accept sessions via password", "Αποδοχή συνεδριών με κωδικό πρόσβασης"), + ("Accept sessions via click", "Αποδοχή συνεδριών με κλικ"), + ("Accept sessions via both", "Αποδοχή συνεδριών και με τα δύο"), ("Please wait for the remote side to accept your session request...", "Παρακαλώ περιμένετε μέχρι η απομακρυσμένη πλευρά να αποδεχτεί το αίτημα συνεδρίας σας..."), ("One-time Password", "Κωδικός μίας χρήσης"), ("Use one-time password", "Χρήση κωδικού πρόσβασης μίας χρήσης"), @@ -401,12 +411,77 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), ("wayland_experiment_tip", "Η υποστήριξη Wayland βρίσκεται σε πειραματικό στάδιο, χρησιμοποιήστε το X11 εάν χρειάζεστε πρόσβαση χωρίς επίβλεψη."), ("Right click to select tabs", "Κάντε δεξί κλικ για να επιλέξετε καρτέλες"), - ("Skipped", ""), + ("Skipped", "Παράλειψη"), ("Add to Address Book", "Προσθήκη στο Βιβλίο Διευθύνσεων"), ("Group", "Ομάδα"), ("Search", "Αναζήτηση"), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Closed manually by web console", "Κλειστό χειροκίνητα από την κονσόλα web"), + ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), + ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), + ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), + ("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"), + ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο RustDesk"), + ("config_microphone", "Ρύθμιση μικροφώνου"), + ("request_elevation_tip", "αίτημα ανύψωσης δικαιωμάτων χρήστη"), + ("Wait", "Περιμένετε"), + ("Elevation Error", "Σφάλμα ανύψωσης δικαιωμάτων χρήστη"), + ("Ask the remote user for authentication", "Ζητήστε από τον απομακρυσμένο χρήστη έλεγχο ταυτότητας"), + ("Choose this if the remote account is administrator", "Επιλέξτε αυτό εάν ο απομακρυσμένος λογαριασμός είναι διαχειριστής"), + ("Transmit the username and password of administrator", "Αποστολή του ονόματος χρήστη και του κωδικού πρόσβασης του διαχειριστή"), + ("still_click_uac_tip", "Εξακολουθεί να απαιτεί από τον απομακρυσμένο χρήστη να κάνει κλικ στο OK στο παράθυρο UAC όπου εκτελείται το RustDesk."), + ("Request Elevation", "Αίτημα ανύψωσης δικαιωμάτων χρήστη"), + ("wait_accept_uac_tip", "Περιμένετε να αποδεχτεί ο απομακρυσμένος χρήστης το παράθυρο διαλόγου UAC."), + ("Elevate successfully", "Επιτυχής ανύψωση δικαιωμάτων χρήστη"), + ("uppercase", "κεφαλαία γράμματα"), + ("lowercase", "πεζά γράμματα"), + ("digit", "αριθμός"), + ("special character", "ειδικός χαρακτήρας"), + ("length>=8", "μήκος>=8"), + ("Weak", "Αδύναμο"), + ("Medium", "Μέτριο"), + ("Strong", "Δυνατό"), + ("Switch Sides", "Εναλλαγή πλευράς"), + ("Please confirm if you want to share your desktop?", "Παρακαλώ επιβεβαιώστε αν επιθυμείτε την κοινή χρήση της επιφάνειας εργασίας;"), + ("Display", "Εμφάνιση"), + ("Default View Style", "Προκαθορισμένος τρόπος εμφάνισης"), + ("Default Scroll Style", "Προκαθορισμένος τρόπος κύλισης"), + ("Default Image Quality", "Προκαθορισμένη ποιότητα εικόνας"), + ("Default Codec", "Προκαθορισμένη κωδικοποίηση"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Αυτόματο"), + ("Other Default Options", "Άλλες προκαθορισμένες ρυθμίσεις"), + ("Voice call", "Φωνητική κλήση"), + ("Text chat", "Συνομιλία κειμένου"), + ("Stop voice call", "Διακοπή φωνητικής κλήσης"), + ("relay_hint_tip", "Εάν δεν είναι δυνατή η απευθείας σύνδεση, μπορείτε να δοκιμάσετε να συνδεθείτε μέσω διακομιστή αναμετάδοσης"), + ("Reconnect", "Επανασύνδεση"), + ("Codec", "Κωδικοποίηση"), + ("Resolution", "Ανάλυση"), + ("No transfers in progress", "Δεν υπάρχει μεταφορά σε εξέλιξη"), + ("Set one-time password length", "Μέγεθος κωδικού μιας χρήσης"), + ("idd_driver_tip", "Εγκαταστήστε το πρόγραμμα οδήγησης εικονικής οθόνης που χρησιμοποιείται όταν δεν έχετε φυσικές οθόνες."), + ("confirm_idd_driver_tip", "Είναι ενεργοποιημένη η επιλογή εγκατάστασης του προγράμματος οδήγησης εικονικής οθόνης. Λάβετε υπόψη ότι θα εγκατασταθεί ένα δοκιμαστικό πιστοποιητικό για το πρόγραμμα οδήγησης εικονικής οθόνης. Αυτό το πιστοποιητικό θα χρησιμοποιηθεί μόνο για την πιστοποίηση των προγραμμάτων οδήγησης του Rustdesk."), + ("RDP Settings", "Ρυθμίσεις RDP"), + ("Sort by", "Ταξινόμηση κατά"), + ("New Connection", "Νέα σύνδεση"), + ("Restore", "Επαναφορά"), + ("Minimize", "Ελαχιστοποίηση"), + ("Maximize", "Μεγιστοποίηση"), + ("Your Device", "Η συσκευή σας"), + ("empty_recent_tip", "Δεν υπάρχουν πρόσφατες συνεδρίες!\nΔοκιμάστε να ξεκινήσετε μια νέα."), + ("empty_favorite_tip", "Δεν υπάρχουν ακόμη αγαπημένες συνδέσεις;\nΑφού πραγματοποιήσετε σύνδεση με κάποιο απομακρυσμένο σταθμό, μπορείτε να τον προσθέσετε στα αγαπημένα σας!"), + ("empty_lan_tip", "Δεν έχουμε ανακαλυφθεί ακόμη απομακρυσμένοι σταθμοί."), + ("empty_address_book_tip", "Φαίνεται ότι αυτή τη στιγμή δεν υπάρχουν αγαπημένες συνδέσεις στο βιβλίο διευθύνσεών σας."), + ("eg: admin", "π.χ. admin"), + ("Empty Username", "Κενό όνομα χρήστη"), + ("Empty Password", "Κενός κωδικός πρόσβασης"), + ("Me", "Εγώ"), + ("identical_file_tip", "Το αρχείο είναι πανομοιότυπο με αυτό του άλλου υπολογιστή."), + ("show_monitors_tip", "Εμφάνιση οθονών στη γραμμή εργαλείων"), + ("View Mode", "Λειτουργία προβολής"), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index f351b575d..8a981564b 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("setup_server_tip", "For faster connection, please set up your own server"), ("Auto Login", "Auto Login (Only valid if you set \"Lock after session end\")"), ("whitelist_tip", "Only whitelisted IP can access me"), - ("whitelist_sep", "Seperated by comma, semicolon, spaces or new line"), + ("whitelist_sep", "Separated by comma, semicolon, spaces or new line"), ("Wrong credentials", "Wrong username or password"), ("invalid_http", "must start with http:// or https://"), ("install_daemon_tip", "For starting on boot, you need to install system service."), @@ -23,7 +23,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Turning on \"Screen Capture\" will automatically start the service, allowing other devices to request a connection to your device."), ("android_stop_service_tip", "Closing the service will automatically close all established connections."), ("android_version_audio_tip", "The current Android version does not support audio capture, please upgrade to Android 10 or higher."), - ("android_start_service_tip", "Tap [Start Service] or OPEN [Screen Capture] permission to start the screen sharing service."), + ("android_start_service_tip", "Tap [Start Service] or enable [Screen Capture] permission to start the screen sharing service."), + ("android_permission_may_not_change_tip", "Permissions for established connections may not be changed instantly until reconnected."), ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), ("doc_fix_wayland", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), ("server_not_support", "Not yet supported by the server"), @@ -36,5 +37,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Allow hiding only if accepting sessions via password and using permanent password"), ("wayland_experiment_tip", "Wayland support is in experimental stage, please use X11 if you require unattended access."), ("Slogan_tip", "Made with heart in this chaotic world!"), + ("verification_tip", "A new device has been detected, and a verification code has been sent to the registered email address, enter the verification code to continue logging in."), + ("software_render_tip", "If you have an Nvidia graphics card and the remote window closes immediately after connecting, installing the nouveau driver and choosing to use software rendering may help. A software restart is required."), + ("config_input", "In order to control remote desktop with keyboard, you need to grant RustDesk \"Input Monitoring\" permissions."), + ("request_elevation_tip", "You can also request elevation if there is someone on the remote side."), + ("wait_accept_uac_tip", "Please wait for the remote user to accept the UAC dialog."), + ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."), + ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions."), + ("relay_hint_tip", "It may not be possible to connect directly, you can try to connect via relay. \nIn addition, if you want to use relay on your first try, you can add the \"/r\" suffix to the ID, or select the option \"Always connect via relay\" in the peer card."), + ("No transfers in progress", ""), + ("idd_driver_tip", "Install virtual display driver which is used when you have no physical displays."), + ("confirm_idd_driver_tip", "The option to install the virtual display driver is checked. Note that a test certificate will be installed to trust the virtual display driver. This test certificate will only be used to trust Rustdesk drivers."), + ("empty_recent_tip", "Oops, no recent sessions!\nTime to plan a new one."), + ("empty_favorite_tip", "No favorite peers yet?\nLet's find someone to connect with and add it to your favorites!"), + ("empty_lan_tip", "Oh no, it looks like we haven't discovered any peers yet."), + ("empty_address_book_tip", "Oh dear, it appears that there are currently no peers listed in your address book."), + ("identical_file_tip", "This file is identical with the peer's one."), + ("show_monitors_tip", "Show monitors in toolbar."), + ("enter_rustdesk_passwd_tip", "Enter RustDesk password."), + ("remember_rustdesk_passwd_tip", "Remember RustDesk password."), + ("login_linux_tip", "Login to remote Linux account"), + ("login_linux_tooltip_tip", "You need to login to remote Linux account to enable a X desktop session."), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 0de56d0ad..e430f8f67 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "La poŝo estas malplena"), ("Stop service", "Haltu servon"), ("Change ID", "Ŝanĝi identigilon"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), ("Website", "Retejo"), ("About", "Pri"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Muta"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Aŭdia enigo"), ("Enhancements", ""), ("Hardware Codec", ""), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servilo de API"), ("invalid_http", "Devas komenci kun http:// aŭ https://"), ("Invalid IP", "IP nevalida"), - ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), ("Invalid format", "Formato nevalida"), ("server_not_support", "Ankoraŭ ne subtenata de la servilo"), ("Not available", "Nedisponebla"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Averto"), ("Login screen using Wayland is not supported", "Konektajn ekranojn uzantajn Wayland ne estas subtenitaj"), ("Reboot required", "Restarto deviga"), - ("Unsupported display server ", "La aktuala bilda servilo ne estas subtenita"), + ("Unsupported display server", "La aktuala bilda servilo ne estas subtenita"), ("x11 expected", "Bonvolu uzi x11"), ("Port", ""), ("Settings", "Agordoj"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuale fermita de la samtavolano"), ("Enable remote configuration modification", "Permesi foran redaktadon de la konfiguracio"), ("Run without install", "Plenumi sen instali"), - ("Always connected via relay", "Ĉiam konektata per relajso"), + ("Connect via relay", ""), ("Always connect via relay", "Ĉiam konekti per relajso"), ("whitelist_tip", "Nur la IP en la blanka listo povas kontroli mian komputilon"), ("Login", "Konekti"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Malkonekti"), ("Tags", "Etikedi"), ("Search ID", "Serĉi ID"), - ("Current Wayland display server is not supported", "La aktuala bilda servilo Wayland ne estas subtenita"), ("whitelist_sep", "Vi povas uzi komon, punktokomon, spacon aŭ linsalton kiel apartigilo"), ("Add ID", "Aldoni identigilo"), ("Add Tag", "Aldoni etikedo"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Notu"), ("Connection", ""), ("Share Screen", ""), - ("CLOSE", ""), - ("OPEN", ""), ("Chat", ""), ("Total", ""), ("items", ""), @@ -280,6 +289,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", ""), ("android_version_audio_tip", ""), ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", ""), ("Overwrite", ""), ("This file exists, skip or overwrite this file?", ""), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skalo adapta"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), + ("Light Theme", ""), ("Dark", ""), ("Light", ""), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland postulas Ubuntu 21.04 aŭ pli altan version."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index a99ae8551..c4e78943a 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "El portapapeles está vacío"), ("Stop service", "Detener servicio"), ("Change ID", "Cambiar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), ("Website", "Sitio web"), ("About", "Acerca de"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), + ("Privacy Statement", "Declaración de privacidad"), ("Mute", "Silenciar"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), ("Hardware Codec", "Códec de hardware"), @@ -51,13 +59,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor API"), ("invalid_http", "debe comenzar con http:// o https://"), ("Invalid IP", "IP incorrecta"), - ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), ("Invalid format", "Formato incorrecto"), ("server_not_support", "Aún no es compatible con el servidor"), ("Not available", "No disponible"), ("Too frequent", "Demasiado frecuente"), ("Cancel", "Cancelar"), - ("Skip", "Saltar"), + ("Skip", "Omitir"), ("Close", "Cerrar"), ("Retry", "Reintentar"), ("OK", ""), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Aviso"), ("Login screen using Wayland is not supported", "La pantalla de inicio de sesión con Wayland no es compatible"), ("Reboot required", "Reinicio requerido"), - ("Unsupported display server ", "Servidor de visualización no compatible"), + ("Unsupported display server", "Servidor de visualización no compatible"), ("x11 expected", "x11 necesario"), ("Port", "Puerto"), ("Settings", "Ajustes"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Cerrado manualmente por el par"), ("Enable remote configuration modification", "Habilitar modificación remota de configuración"), ("Run without install", "Ejecutar sin instalar"), - ("Always connected via relay", "Siempre conectado a través de relay"), + ("Connect via relay", ""), ("Always connect via relay", "Conéctese siempre a través de relay"), ("whitelist_tip", "Solo las direcciones IP autorizadas pueden conectarse a este escritorio"), ("Login", "Iniciar sesión"), + ("Verify", "Verificar"), + ("Remember me", "Recordarme"), + ("Trust this device", "Confiar en este dispositivo"), + ("Verification code", "Código de verificación"), + ("verification_tip", "Se ha detectado un nuevo dispositivo y se ha enviado un código de verificación a la dirección de correo registrada. Introduzca el código de verificación para continuar con el inicio de sesión."), ("Logout", "Salir"), ("Tags", "Tags"), ("Search ID", "Buscar ID"), - ("Current Wayland display server is not supported", "El servidor de visualización actual de Wayland no es compatible"), ("whitelist_sep", "Separados por coma, punto y coma, espacio o nueva línea"), ("Add ID", "Agregar ID"), ("Add Tag", "Agregar tag"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Nota"), ("Connection", "Conexión"), ("Share Screen", "Compartir pantalla"), - ("CLOSE", "CERRAR"), - ("OPEN", "ABRIR"), ("Chat", "Chat"), ("Total", "Total"), ("items", "items"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Habilitar la captura de pantalla iniciará automáticamente el servicio, lo que permitirá que otros dispositivos soliciten una conexión desde este dispositivo."), ("android_stop_service_tip", "Cerrar el servicio cerrará automáticamente todas las conexiones establecidas."), ("android_version_audio_tip", "La versión actual de Android no admite la captura de audio, actualice a Android 10 o posterior."), - ("android_start_service_tip", "Toque el permiso [Iniciar servicio] o ABRIR [Captura de pantalla] para iniciar el servicio de uso compartido de pantalla."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Cuenta"), ("Overwrite", "Sobrescribir"), ("This file exists, skip or overwrite this file?", "Este archivo existe, ¿omitir o sobrescribir este archivo?"), @@ -300,7 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Idioma"), ("Keep RustDesk background service", "Dejar RustDesk como Servicio en 2do plano"), ("Ignore Battery Optimizations", "Ignorar optimizacioens de bateria"), - ("android_open_battery_optimizations_tip", ""), + ("android_open_battery_optimizations_tip", "Si deseas deshabilitar esta característica, por favor, ve a la página siguiente de ajustes, busca y entra en [Batería] y desmarca [Sin restricción]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexión no disponible"), ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), @@ -313,8 +325,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restart Remote Device", "Reiniciar dispositivo"), ("Are you sure you want to restart", "Esta Seguro que desea reiniciar?"), ("Restarting Remote Device", "Reiniciando dispositivo remoto"), - ("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."), - ("Copied", ""), + ("remote_restarting_tip", "El dispositivo remoto se está reiniciando. Por favor cierre este mensaje y vuelva a conectarse con la contraseña peremanente en unos momentos."), + ("Copied", "Copiado"), ("Exit Fullscreen", "Salir de pantalla completa"), ("Fullscreen", "Pantalla completa"), ("Mobile Actions", "Acciones móviles"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptativa"), ("General", ""), ("Security", "Seguridad"), - ("Account", "Cuenta"), ("Theme", "Tema"), ("Dark Theme", "Tema Oscuro"), + ("Light Theme", ""), ("Dark", "Oscuro"), ("Light", "Claro"), ("Follow System", "Tema del sistema"), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Servidor"), ("Direct IP Access", "Acceso IP Directo"), ("Proxy", ""), - ("Port", "Puerto"), ("Apply", "Aplicar"), ("Disconnect all devices?", "¿Desconectar todos los dispositivos?"), ("Clear", "Borrar"), @@ -370,13 +381,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "Denegar descubrimiento de LAN"), ("Write a message", "Escribir un mensaje"), ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "Por favor, espera confirmación de UAC"), + ("elevated_foreground_window_tip", "La ventana actual del escritorio remoto necesita privilegios elevados para funcionar, así que no puedes usar ratón y teclado temporalmente. Puedes solicitar al usuario remoto que minimize la ventana actual o hacer clic en el botón de elevación de la ventana de gestión de conexión. Para evitar este problema, se recomienda instalar el programa en el dispositivo remto."), ("Disconnected", "Desconectado"), ("Other", "Otro"), ("Confirm before closing multiple tabs", "Confirmar antes de cerrar múltiples pestañas"), ("Keyboard Settings", "Ajustes de teclado"), - ("Custom", "Personalizado"), ("Full Access", "Acceso completo"), ("Screen Share", "Compartir pantalla"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requiere Ubuntu 21.04 o una versión superior."), @@ -401,12 +411,77 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "Permitir ocultar solo si se aceptan sesiones a través de contraseña y usando contraseña permanente"), ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."), ("Right click to select tabs", "Clic derecho para seleccionar pestañas"), - ("Skipped", ""), + ("Skipped", "Omitido"), ("Add to Address Book", "Añadir a la libreta de direcciones"), ("Group", "Grupo"), ("Search", "Búsqueda"), - ("Closed manually by the web console", "Cerrado manualmente por la consola web"), + ("Closed manually by web console", "Cerrado manualmente por la consola web"), ("Local keyboard type", "Tipo de teclado local"), ("Select local keyboard type", "Seleccionar tipo de teclado local"), + ("software_render_tip", "Si tienes una gráfica Nvidia y la ventana remota se cierra inmediatamente, instalar el driver nouveau y elegir renderizado por software podría ayudar. Se requiere reiniciar la aplicación."), + ("Always use software rendering", "Usar siempre renderizado por software"), + ("config_input", "Para controlar el escritorio remoto con el teclado necesitas dar a RustDesk permisos de \"Monitorización de entrada\"."), + ("config_microphone", "Para poder hablar de forma remota necesitas darle a RustDesk permisos de \"Grabar Audio\"."), + ("request_elevation_tip", "También puedes solicitar elevación si hay alguien en el lado remoto."), + ("Wait", "Esperar"), + ("Elevation Error", "Error de elevación"), + ("Ask the remote user for authentication", "Pida autenticación al usuario remoto"), + ("Choose this if the remote account is administrator", "Elegir si la cuenta remota es de administrador"), + ("Transmit the username and password of administrator", "Transmitir usuario y contraseña del administrador"), + ("still_click_uac_tip", "Aún se necesita que el usuario remoto haga click en OK en la ventana UAC del RusDesk en ejecución."), + ("Request Elevation", "Solicitar Elevación"), + ("wait_accept_uac_tip", "Por favor espere a que el usuario remoto acepte el diálogo UAC."), + ("Elevate successfully", "Elevar con éxito"), + ("uppercase", "mayúsculas"), + ("lowercase", "minúsculas"), + ("digit", "dígito"), + ("special character", "carácter especial"), + ("length>=8", "longitud>=8"), + ("Weak", "Débil"), + ("Medium", "Media"), + ("Strong", "Fuerte"), + ("Switch Sides", "Intercambiar lados"), + ("Please confirm if you want to share your desktop?", "Por favor, confirma si quieres compartir tu escritorio"), + ("Display", "Pantalla"), + ("Default View Style", "Estilo de vista predeterminado"), + ("Default Scroll Style", "Estilo de desplazamiento predeterminado"), + ("Default Image Quality", "Calidad de imagen predeterminada"), + ("Default Codec", "Códec predeterminado"), + ("Bitrate", "Tasa de bits"), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", "Otras opciones predeterminadas"), + ("Voice call", "Llamada de voz"), + ("Text chat", "Chat de texto"), + ("Stop voice call", "Detener llamada de voz"), + ("relay_hint_tip", "Puede que no sea posible conectar directamente. Puedes tratar de conectar a través de relay. \nAdicionalmente, si quieres usar relay en el primer intento, puedes añadir el sufijo \"/r\" a la ID o seleccionar la opción \"Conectar siempre a través de relay\" en la tarjeta del par."), + ("Reconnect", "Reconectar"), + ("Codec", "Códec"), + ("Resolution", "Resolución"), + ("No transfers in progress", "No hay transferencias en curso"), + ("Set one-time password length", "Establecer contraseña de un solo uso"), + ("idd_driver_tip", "Instalar controlador virtual de pantalla a usar cuando no hay pantalla física."), + ("confirm_idd_driver_tip", "La opción de instalar el controlador de pantalla virtual está marcada. Hay que tener en cuenta que se instalará un certificado de prueba para confirar en el controlador de pantalla. Este certificado solo se usará para confiar en controladores Rustdesk."), + ("RDP Settings", "Ajustes RDP"), + ("Sort by", "Ordenar por"), + ("New Connection", "Nueva conexión"), + ("Restore", "Restaurar"), + ("Minimize", "Minimizar"), + ("Maximize", "Maximizar"), + ("Your Device", "Tu dispositivo"), + ("empty_recent_tip", "Vaya, no hay conexiones recientes!\nBuen momento para planificar una."), + ("empty_favorite_tip", "¿Sin pares favoritos aún?\nEncuentra uno al que conectarte y añádelo a favoritos!"), + ("empty_lan_tip", "Oh no, parece que aún no has descubierto ningún par."), + ("empty_address_book_tip", "Parece que actualmente no hay pares en tu libreta de direcciones."), + ("eg: admin", "ej.: admin"), + ("Empty Username", "Nombre de usuario vacío"), + ("Empty Password", "Contraseña vacía"), + ("Me", "Yo"), + ("identical_file_tip", "Este archivo es idéntico al del par."), + ("show_monitors_tip", "Mostrar monitores en la barra de herramientas"), + ("View Mode", "Modo Vista"), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index d964db945..e4d026586 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -37,21 +37,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "کلیپبورد خالی است"), ("Stop service", "توقف سرویس"), ("Change ID", "تعویض شناسه"), + ("Your new ID", "جدید ID"), + ("length %min% to %max%", "%max% تا %min% طول از"), + ("starts with a letter", "با حرف شروع می شود"), + ("allowed characters", "کارکترهای مجاز"), + ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), ("Website", "وب سایت"), ("About", "درباره"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "ساخته شده با قلب(عشق) در این دنیای پر هرج و مرج!"), + ("Privacy Statement", "بیانیه حریم خصوصی"), ("Mute", "بستن صدا"), + ("Build Date", "تاریخ ساخت"), + ("Version", "نسخه"), + ("Home", "صفحه اصلی"), ("Audio Input", "ورودی صدا"), ("Enhancements", "بهبودها"), ("Hardware Codec", "کدک سخت افزاری"), - ("Adaptive Bitrate", ""), + ("Adaptive Bitrate", "سازگار Bitrate"), ("ID Server", "شناسه سرور"), ("Relay Server", "Relay سرور"), ("API Server", "API سرور"), ("invalid_http", "شروع شود http:// یا https:// باید با"), ("Invalid IP", "نامعتبر است IP آدرس"), - ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), ("Invalid format", "فرمت نادرست است"), ("server_not_support", "هنوز توسط سرور مورد نظر پشتیبانی نمی شود"), ("Not available", "در دسترسی نیست"), @@ -186,7 +193,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Logging in...", "...در حال ورود"), ("Enable RDP session sharing", "اشتراک گذاری جلسه RDP را فعال کنید"), ("Auto Login", "ورود خودکار"), - ("Enable Direct IP Access", "دسترسی مستقیم IP را فعال کنید"), + ("Enable Direct IP Access", "را فعال کنید IP دسترسی مستقیم"), ("Rename", "تغییر نام"), ("Space", "فضا"), ("Create Desktop Shortcut", "ساخت میانبر روی دسکتاپ"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "هشدار"), ("Login screen using Wayland is not supported", "پشتیبانی نمی شود Wayland ورود به سیستم با استفاده از "), ("Reboot required", "راه اندازی مجدد مورد نیاز است"), - ("Unsupported display server ", "سرور تصویر پشتیبانی نشده است"), + ("Unsupported display server", "سرور تصویر پشتیبانی نشده است"), ("x11 expected", ""), ("Port", "پورت"), ("Settings", "تنظیمات"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), ("Enable remote configuration modification", "فعال بودن اعمال تغییرات پیکربندی از راه دور"), ("Run without install", "بدون نصب اجرا شود"), - ("Always connected via relay", "متصل است Relay همیشه با"), + ("Connect via relay", "اتصال با رله"), ("Always connect via relay", "برای اتصال استفاده شود Relay از"), ("whitelist_tip", "های مجاز می توانند به این دسکتاپ متصل شوند IP فقط"), ("Login", "ورود"), + ("Verify", "تأیید کنید"), + ("Remember me", "مرا به یاد داشته باش"), + ("Trust this device", "به این دستگاه اعتماد کنید"), + ("Verification code", "کد تایید"), + ("verification_tip", "یک دستگاه جدید شناسایی شده است و یک کد تأیید به آدرس ایمیل ثبت شده ارسال شده است، برای ادامه ورود، کد تأیید را وارد کنید."), ("Logout", "خروج"), ("Tags", "برچسب ها"), ("Search ID", "جستجوی شناسه"), - ("Current Wayland display server is not supported", "پشتیبانی نمی شود Wayland سرور نمایش فعلی"), ("whitelist_sep", "با کاما، نقطه ویرگول، فاصله یا خط جدید از هم جدا می شوند"), ("Add ID", "افزودن شناسه"), ("Add Tag", "افزودن برچسب"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "یادداشت"), ("Connection", "ارتباط"), ("Share Screen", "اشتراک گذاری صفحه"), - ("CLOSE", "بستن"), - ("OPEN", "باز کردن"), ("Chat", "چت"), ("Total", "مجموع"), ("items", "آیتم ها"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "فعال کردن ضبط صفحه به طور خودکار سرویس را راه اندازی می کند و به دستگاه های دیگر امکان می دهد درخواست اتصال به آن دستگاه را داشته باشند."), ("android_stop_service_tip", "با بستن سرویس، تمام اتصالات برقرار شده به طور خودکار بسته می شود"), ("android_version_audio_tip", "نسخه فعلی اندروید از ضبط صدا پشتیبانی نمی‌کند، لطفاً به اندروید 10 یا بالاتر به‌روزرسانی کنید"), - ("android_start_service_tip", "برای شروع سرویس اشتراک‌گذاری صفحه، روی مجوز \"شروع مرحله‌بندی سرور\" یا OPEN \"Screen Capture\" کلیک کنید."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "حساب کاربری"), ("Overwrite", "بازنویسی"), ("This file exists, skip or overwrite this file?", "این فایل وجود دارد، از فایل رد شود یا آن را بازنویسی کند؟"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "را در پس زمینه نگه دارید RustDesk سرویس"), ("Ignore Battery Optimizations", "بهینه سازی باتری نادیده گرفته شود"), ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), + ("Start on Boot", "در هنگام بوت شروع شود"), + ("Start the screen sharing service on boot, requires special permissions", "سرویس اشتراک‌گذاری صفحه را در بوت راه‌اندازی کنید، به مجوزهای خاصی نیاز دارد"), ("Connection not allowed", "اتصال مجاز نیست"), ("Legacy mode", "legacy حالت"), ("Map mode", "map حالت"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "مقیاس تطبیقی"), ("General", "عمومی"), ("Security", "امنیت"), - ("Account", "حساب کاربری"), ("Theme", "نمایه"), ("Dark Theme", "نمایه تیره"), + ("Light Theme", "نمایه روشن"), ("Dark", "تیره"), ("Light", "روشن"), ("Follow System", "پیروی از سیستم"), @@ -345,9 +357,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Audio", "فعال شدن صدا"), ("Unlock Network Settings", "آنلاک شدن تنظیمات شبکه"), ("Server", "سرور"), - ("Direct IP Access", "IP دسترسی مستقیم "), + ("Direct IP Access", "IP دسترسی مستقیم به"), ("Proxy", "پروکسی"), - ("Port", "پورت"), ("Apply", "ثبت"), ("Disconnect all devices?", "همه دستگاه ها قطع شوند؟"), ("Clear", "پاک کردن"), @@ -370,24 +381,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Deny LAN Discovery", "غیر فعالسازی جستجو در شبکه"), ("Write a message", "یک پیام بنویسید"), ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "باشید UAC لطفا منتظر تایید"), + ("elevated_foreground_window_tip", "پنجره فعلی دسکتاپ راه دور برای کار کردن به دسترسی بالاتری نیاز دارد، بنابراین نمی‌تواند به طور موقت از ماوس و صفحه کلید استفاده کند. می توانید از کاربر راه دور درخواست کنید که پنجره فعلی را به پایین منتقل کند یا روی دکمه ارتقاء دسترسی در پنجره مدیریت اتصال کلیک کنید. برای جلوگیری از این مشکل، توصیه می شود نرم افزار را روی دستگاه از راه دور نصب کنید."), ("Disconnected", "قطع ارتباط"), ("Other", "سایر"), ("Confirm before closing multiple tabs", "تایید بستن دسته ای برگه ها"), ("Keyboard Settings", "تنظیمات صفحه کلید"), - ("Custom", "سفارشی"), ("Full Access", "دسترسی کامل"), ("Screen Share", "اشتراک گذاری صفحه"), ("Wayland requires Ubuntu 21.04 or higher version.", "نیازمند اوبونتو نسخه 21.04 یا بالاتر است Wayland"), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "استفاده کنید و یا سیستم عامل خود را تغییر دهید X11 نیازمند نسخه بالاتری از توزیع لینوکس است. لطفا از دسکتاپ با سیستم"), - ("JumpLink", ""), + ("JumpLink", "چشم انداز"), ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), ("Show RustDesk", "RustDesk نمایش"), ("This PC", "This PC"), ("or", "یا"), ("Continue with", "ادامه با"), - ("Elevate", "افزایش سطح"), + ("Elevate", "ارتقاء"), ("Zoom cursor", " بزرگنمایی نشانگر ماوس"), ("Accept sessions via password", "قبول درخواست با رمز عبور"), ("Accept sessions via click", "قبول درخواست با کلیک موس"), @@ -401,12 +411,77 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("hide_cm_tip", "فقط در صورت پذیرفتن جلسات از طریق رمز عبور و استفاده از رمز عبور دائمی، مخفی شدن مجاز است"), ("wayland_experiment_tip", "پشتیبانی Wayland در مرحله آزمایشی است، لطفاً در صورت نیاز به دسترسی بدون مراقبت از X11 استفاده کنید."), ("Right click to select tabs", "برای انتخاب تب ها راست کلیک کنید"), - ("Skipped", ""), + ("Skipped", "رد شد"), ("Add to Address Book", "افزودن به دفترچه آدرس"), - ("Group", ""), - ("Search", ""), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Group", "گروه"), + ("Search", "جستجو"), + ("Closed manually by web console", "به صورت دستی توسط کنسول وب بسته شد"), + ("Local keyboard type", "نوع صفحه کلید محلی"), + ("Select local keyboard type", "نوع صفحه کلید محلی را انتخاب کنید"), + ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), + ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), + ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), + ("config_microphone", "را بدهید. RustDesk \"Record Audio\" برای صحبت از راه دور، باید مجوز"), + ("request_elevation_tip", "همچنین می توانید در صورت وجود شخصی در سمت راه دور درخواست ارتقاء دسترسی دهید."), + ("Wait", "صبر کنید"), + ("Elevation Error", "خطای ارتقاء دسترسی"), + ("Ask the remote user for authentication", "درخواست احراز هویت از یک کاربر راه دور"), + ("Choose this if the remote account is administrator", "اگر حساب راه دور یک مدیر است، این را انتخاب کنید"), + ("Transmit the username and password of administrator", "نام کاربری و رمز عبور مدیر را منتقل کنید"), + ("still_click_uac_tip", "همچنان کاربر از راه دور نیاز دارد که روی OK در پنجره UAC اجرای RustDesk کلیک کند."), + ("Request Elevation", "درخواست ارتقاء دسترسی"), + ("wait_accept_uac_tip", "لطفاً منتظر بمانید تا کاربر راه دور درخواست پنجره UAC را بپذیرد."), + ("Elevate successfully", "ارتقاء دسترسی با موفقیت انجام شد"), + ("uppercase", "حروف بزرگ"), + ("lowercase", "حروف کوچک"), + ("digit", "عدد"), + ("special character", "کاراکتر خاص"), + ("length>=8", "حداقل طول 8 کاراکتر"), + ("Weak", "ضعیف"), + ("Medium", "متوسط"), + ("Strong", "قوی"), + ("Switch Sides", "طرفین را عوض کنید"), + ("Please confirm if you want to share your desktop?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), + ("Display", "نمایش دادن"), + ("Default View Style", "سبک نمایش پیش فرض"), + ("Default Scroll Style", "سبک پیش‌ فرض اسکرول"), + ("Default Image Quality", "کیفیت تصویر پیش فرض"), + ("Default Codec", "کدک پیش فرض"), + ("Bitrate", "میزان بیت صفحه نمایش"), + ("FPS", "FPS"), + ("Auto", "خودکار"), + ("Other Default Options", "سایر گزینه های پیش فرض"), + ("Voice call", "تماس صوتی"), + ("Text chat", "گفتگو متنی (چت متنی)"), + ("Stop voice call", "توقف تماس صوتی"), + ("relay_hint_tip", " را به شناسه اضافه کنید یا گزینه \"همیشه از طریق رله متصل شوید\" را در کارت همتا انتخاب کنید. همچنین، اگر می‌خواهید فوراً از سرور رله استفاده کنید، می‌توانید پسوند \"/r\".\n اتصال مستقیم ممکن است امکان پذیر نباشد. در این صورت می توانید سعی کنید از طریق سرور رله متصل شوید"), + ("Reconnect", "اتصال مجدد"), + ("Codec", "کدک"), + ("Resolution", "وضوح"), + ("No transfers in progress", "هیچ انتقالی در حال انجام نیست"), + ("Set one-time password length", "طول رمز یکبار مصرف را تعیین کنید"), + ("idd_driver_tip", "درایور صفحه نمایش مجازی را نصب کنید این برای زمانیست که هیچ نمایشگر فیزیکی ندارید."), + ("confirm_idd_driver_tip", "گزینه نصب درایور نمایش مجازی تیک خورده است. توجه داشته باشید که یک گواهی آزمایشی برای اعتماد به درایور نمایش مجازی نصب خواهد شد. این گواهی آزمایشی فقط برای اعتماد به درایورهای Rustdesk استفاده خواهد شد."), + ("RDP Settings", "RDP تنظیمات"), + ("Sort by", "مرتب سازی بر اساس"), + ("New Connection", "اتصال جدید"), + ("Restore", "بازیابی"), + ("Minimize", "کوچک کردن پنجره"), + ("Maximize", "بزرک کردن پنجره"), + ("Your Device", "دستگاه شما"), + ("empty_recent_tip", "اوه، هیچ جلسه اخیری وجود ندارد!\nزمان برنامه ریزی جلسه جدید است"), + ("empty_favorite_tip", "هنوز همتای مورد علاقه‌ای ندارید؟\nبیایید فردی را برای ارتباط پیدا کنیم و آن را به موارد دلخواه خود اضافه کنیم!"), + ("empty_lan_tip", "اوه نه، به نظر می رسد که ما هنوز همتای خود را پیدا نکرده ایم"), + ("empty_address_book_tip", "اوه ، به نظر می رسد که در حال حاضر هیچ همتایی در دفترچه آدرس شما وجود ندارد"), + ("eg: admin", "مثال : admin"), + ("Empty Username", "نام کاربری خالی است"), + ("Empty Password", "رمز عبور خالی است"), + ("Me", "من"), + ("identical_file_tip", "این فایل با فایل همتا یکسان است."), + ("show_monitors_tip", "نمایش مانیتورها در نوار ابزار"), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 2f13a9807..bf3853d96 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -14,8 +14,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is not running", "Le service ne fonctionne pas"), ("not_ready_status", "Pas prêt, veuillez vérifier la connexion réseau"), ("Control Remote Desktop", "Contrôler le bureau à distance"), - ("Transfer File", "Transférer le fichier"), - ("Connect", "Connecter"), + ("Transfer File", "Transfert de fichiers"), + ("Connect", "Se connecter"), ("Recent Sessions", "Sessions récentes"), ("Address Book", "Carnet d'adresses"), ("Confirmation", "Confirmation"), @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Presse-papier vide"), ("Stop service", "Arrêter le service"), ("Change ID", "Changer d'ID"), + ("Your new ID", "Votre nouvel ID"), + ("length %min% to %max%", "longueur de %min% à %max%"), + ("starts with a letter", "commence par une lettre"), + ("allowed characters", "caractères autorisés"), + ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), ("Website", "Site Web"), ("About", "À propos de"), ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), ("Privacy Statement", "Déclaration de confidentialité"), ("Mute", "Muet"), + ("Build Date", "Date de compilation"), + ("Version", "Version"), + ("Home", "Accueil"), ("Audio Input", "Entrée audio"), ("Enhancements", "Améliorations"), ("Hardware Codec", "Transcodage matériel"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Serveur API"), ("invalid_http", "Doit commencer par http:// ou https://"), ("Invalid IP", "IP invalide"), - ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), ("Invalid format", "Format invalide"), ("server_not_support", "Pas encore supporté par le serveur"), ("Not available", "Indisponible"), @@ -60,7 +67,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Skip", "Ignorer"), ("Close", "Fermer"), ("Retry", "Réessayer"), - ("OK", "Confirmer"), + ("OK", "Valider"), ("Password Required", "Mot de passe requis"), ("Please enter your password", "Veuillez saisir votre mot de passe"), ("Remember password", "Mémoriser le mot de passe"), @@ -77,12 +84,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connected, waiting for image...", "Connecté, en attente de transmission d'image..."), ("Name", "Nom"), ("Type", "Type"), - ("Modified", "Modifié"), + ("Modified", "Modifié le"), ("Size", "Taille"), ("Show Hidden Files", "Afficher les fichiers cachés"), - ("Receive", "Accepter"), + ("Receive", "Recevoir"), ("Send", "Envoyer"), - ("Refresh File", "Actualiser le fichier"), + ("Refresh File", "Rafraîchir le contenu"), ("Local", "Local"), ("Remote", "Distant"), ("Remote Computer", "Ordinateur distant"), @@ -90,7 +97,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Confirm Delete", "Confirmer la suppression"), ("Delete", "Supprimer"), ("Properties", "Propriétés"), - ("Multi Select", "Choix multiple"), + ("Multi Select", "Sélection multiple"), ("Select All", "Tout sélectionner"), ("Unselect All", "Tout déselectionner"), ("Empty Directory", "Répertoire vide"), @@ -123,7 +130,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Afficher le moniteur de qualité"), ("Disable clipboard", "Désactiver le presse-papier"), ("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"), - ("Insert", "Insérer"), + ("Insert", "Envoyer"), ("Insert Lock", "Verrouiller l'ordinateur distant"), ("Refresh", "Rafraîchir l'écran"), ("ID does not exist", "L'ID n'existe pas"), @@ -174,7 +181,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Disconnect", "Déconnecter"), ("Allow using keyboard and mouse", "Autoriser l'utilisation du clavier et de la souris"), ("Allow using clipboard", "Autoriser l'utilisation du presse-papier"), - ("Allow hearing sound", "Autoriser l'audition du son"), + ("Allow hearing sound", "Autoriser l'envoi du son"), ("Allow file copy and paste", "Autoriser le copier-coller de fichiers"), ("Connected", "Connecté"), ("Direct and encrypted connection", "Connexion directe chiffrée"), @@ -183,7 +190,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "Connexion relais non chiffrée"), ("Enter Remote ID", "Entrer l'ID de l'appareil à distance"), ("Enter your password", "Entrer votre mot de passe"), - ("Logging in...", "Se connecter..."), + ("Logging in...", "En cours de connexion ..."), ("Enable RDP session sharing", "Activer le partage de session RDP"), ("Auto Login", "Connexion automatique (le verrouillage ne sera effectif qu'après la désactivation du premier paramètre)"), ("Enable Direct IP Access", "Autoriser l'accès direct par IP"), @@ -196,8 +203,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Fix it", "Réparer"), ("Warning", "Avertissement"), ("Login screen using Wayland is not supported", "L'écran de connexion utilisant Wayland n'est pas pris en charge"), - ("Reboot required", "Redémarrage pour prendre effet"), - ("Unsupported display server ", "Le serveur d'affichage actuel n'est pas pris en charge"), + ("Reboot required", "Redémarrage requis"), + ("Unsupported display server", "Le serveur d'affichage actuel n'est pas pris en charge"), ("x11 expected", "x11 requis"), ("Port", "Port"), ("Settings", "Paramètres"), @@ -206,24 +213,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fermé manuellement par le pair"), ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"), ("Run without install", "Exécuter sans installer"), - ("Always connected via relay", "Forcer la connexion relais"), + ("Connect via relay", ""), ("Always connect via relay", "Forcer la connexion relais"), - ("whitelist_tip", "Seul l'IP dans la liste blanche peut accéder à mon appareil"), + ("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"), ("Login", "Connexion"), + ("Verify", "Vérifier"), + ("Remember me", "Se souvenir de moi"), + ("Trust this device", "Faire confiance à cet appareil"), + ("Verification code", "Code de vérification"), + ("verification_tip", "Un nouvel appareil a été détecté et un code de vérification a été envoyé à l'adresse e-mail enregistrée, entrez le code de vérification pour continuer la connexion."), ("Logout", "Déconnexion"), ("Tags", "Étiqueter"), ("Search ID", "Rechercher un ID"), - ("Current Wayland display server is not supported", "Le serveur d'affichage Wayland n'est pas pris en charge"), ("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"), ("Add ID", "Ajouter un ID"), ("Add Tag", "Ajouter une balise"), ("Unselect all tags", "Désélectionner toutes les balises"), ("Network error", "Erreur réseau"), - ("Username missed", "Nom d'utilisateur manqué"), - ("Password missed", "Mot de passe manqué"), + ("Username missed", "Nom d'utilisateur manquant"), + ("Password missed", "Mot de passe manquant"), ("Wrong credentials", "Identifiant ou mot de passe erroné"), ("Edit Tag", "Modifier la balise"), - ("Unremember Password", "Mot de passe oublié"), + ("Unremember Password", "Oublier le Mot de passe"), ("Favorites", "Favoris"), ("Add to Favorites", "Ajouter aux Favoris"), ("Remove from Favorites", "Retirer des favoris"), @@ -259,12 +270,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Noter"), ("Connection", "Connexion"), ("Share Screen", "Partager l'écran"), - ("CLOSE", "FERMER"), - ("OPEN", "OUVRIR"), ("Chat", "Discuter"), ("Total", "Total"), ("items", "éléments"), - ("Selected", "Choisi"), + ("Selected", "Sélectionné"), ("Screen Capture", "Capture d'écran"), ("Input Control", "Contrôle de saisie"), ("Audio Capture", "Capture audio"), @@ -279,12 +288,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "L'activation de la capture d'écran démarrera automatiquement le service, permettant à d'autres appareils de demander une connexion à partir de cet appareil."), ("android_stop_service_tip", "La fermeture du service fermera automatiquement toutes les connexions établies."), ("android_version_audio_tip", "La version actuelle d'Android ne prend pas en charge la capture audio, veuillez passer à Android 10 ou supérieur."), - ("android_start_service_tip", "Appuyez sur [Démarrer le service] ou sur l'autorisation OUVRIR [Capture d'écran] pour démarrer le service de partage d'écran."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Compte"), ("Overwrite", "Écraser"), ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), ("Quit", "Quitter"), - ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("doc_mac_permission", "https://rustdesk.com/docs/fr/manual/mac/#enable-permissions"), ("Help", "Aider"), ("Failed", "échouer"), ("Succeeded", "Succès"), @@ -298,9 +308,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("In privacy mode", "en mode privé"), ("Out privacy mode", "hors mode de confidentialité"), ("Language", "Langue"), - ("Keep RustDesk background service", "Gardez le service Rustdesk service arrière plan"), + ("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"), ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Connexion non autorisée"), ("Legacy mode", "Mode hérité"), ("Map mode", ""), @@ -318,7 +330,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit Fullscreen", "Quitter le mode plein écran"), ("Fullscreen", "Plein écran"), ("Mobile Actions", "Actions mobiles"), - ("Select Monitor", "Sélectionnez Moniteur"), + ("Select Monitor", "Sélection du Moniteur"), ("Control Actions", "Actions de contrôle"), ("Display Settings", "Paramètres d'affichage"), ("Ratio", "Rapport"), @@ -330,13 +342,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Connexion relais"), ("Secure Connection", "Connexion sécurisée"), ("Insecure Connection", "Connexion non sécurisée"), - ("Scale original", "Échelle d'origine"), - ("Scale adaptive", "Échelle adaptative"), + ("Scale original", "Échelle 100%"), + ("Scale adaptive", "Mise à l'échelle Auto"), ("General", "Général"), ("Security", "Sécurité"), - ("Account", "Compte"), ("Theme", "Thème"), ("Dark Theme", "Thème somble"), + ("Light Theme", ""), ("Dark", "Sombre"), ("Light", "Clair"), ("Follow System", "Suivi système"), @@ -347,23 +359,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Serveur"), ("Direct IP Access", "Accès IP direct"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Appliquer"), ("Disconnect all devices?", "Déconnecter tous les appareils"), ("Clear", "Effacer"), ("Audio Input Device", "Périphérique source audio"), ("Deny remote access", "Interdir l'accès distant"), - ("Use IP Whitelisting", "Utiliser liste blanche d'IP"), + ("Use IP Whitelisting", "Utiliser une liste blanche d'IP"), ("Network", "Réseau"), ("Enable RDP", "Activer RDP"), ("Pin menubar", "Épingler la barre de menus"), ("Unpin menubar", "Détacher la barre de menu"), ("Recording", "Enregistrement"), ("Directory", "Répertoire"), - ("Automatically record incoming sessions", "Enregistrement automatique des session entrantes"), + ("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"), ("Change", "Modifier"), - ("Start session recording", "Commerce l'enregistrement"), - ("Stop session recording", "Stoper l'enregistrement"), + ("Start session recording", "Commencer l'enregistrement"), + ("Stop session recording", "Stopper l'enregistrement"), ("Enable Recording Session", "Activer l'enregistrement de session"), ("Allow recording session", "Autoriser l'enregistrement de session"), ("Enable LAN Discovery", "Activer la découverte réseau local"), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Divers"), ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"), ("Keyboard Settings", "Configuration clavier"), - ("Custom", "Personnalisé"), ("Full Access", "Accès total"), ("Screen Share", "Partage d'écran"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version supérieure."), @@ -399,14 +409,79 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Demande d'accès à votre appareil"), ("Hide connection management window", "Masquer la fenêtre de gestion des connexions"), ("hide_cm_tip", "Autoriser le masquage uniquement si vous acceptez des sessions via un mot de passe et utilisez un mot de passe permanent"), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Skipped", ""), + ("wayland_experiment_tip", "Le support Wayland est en phase expérimentale, veuillez utiliser X11 si vous avez besoin d'un accès sans surveillance."), + ("Right click to select tabs", "Clique droit pour selectionner les onglets"), + ("Skipped", "Ignoré"), ("Add to Address Book", "Ajouter au carnet d'adresses"), ("Group", "Groupe"), ("Search", "Rechercher"), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Closed manually by web console", "Fermé manuellement par la console Web"), + ("Local keyboard type", "Disposition du clavier local"), + ("Select local keyboard type", "Selectionner la disposition du clavier local"), + ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), + ("Always use software rendering", "Utiliser toujours le rendu logiciel"), + ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), + ("config_microphone", ""), + ("request_elevation_tip", "Vous pouvez également demander une augmentation des privilèges s'il y a quelqu'un du côté distant."), + ("Wait", "En cours"), + ("Elevation Error", "Erreur d'augmentation des privilèges"), + ("Ask the remote user for authentication", "Demander à l'utilisateur distant de s'authentifier"), + ("Choose this if the remote account is administrator", "Choisissez ceci si le compte distant est le compte d'administrateur"), + ("Transmit the username and password of administrator", "Transmettre le nom d'utilisateur et le mot de passe de l'administrateur"), + ("still_click_uac_tip", "Nécessite toujours que l'utilisateur distant confirme par la fenêtre UAC de RustDesk en cours d'éxécution."), + ("Request Elevation", "Demande d'augmentation des privilèges"), + ("wait_accept_uac_tip", "Veuillez attendre que l'utilisateur distant accepte la boîte de dialogue UAC."), + ("Elevate successfully", "Augmentation des privilèges avec succès"), + ("uppercase", "majuscule"), + ("lowercase", "minuscule"), + ("digit", "chiffre"), + ("special character", "caractère spécial"), + ("length>=8", "longueur>=8"), + ("Weak", "Faible"), + ("Medium", "Moyen"), + ("Strong", "Fort"), + ("Switch Sides", "Inverser la prise de contrôle"), + ("Please confirm if you want to share your desktop?", "Veuillez confirmer le partager de votre bureau ?"), + ("Display", "Affichage"), + ("Default View Style", "Style de vue par défaut"), + ("Default Scroll Style", "Style de défilement par défaut"), + ("Default Image Quality", "Qualité d'image par défaut"), + ("Default Codec", "Codec par défaut"), + ("Bitrate", "Débit"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Autres options par défaut"), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 3aaf0c87d..2e6a20b56 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A vágólap üres"), ("Stop service", "Szolgáltatás leállítása"), ("Change ID", "Azonosító megváltoztatása"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), ("Website", "Weboldal"), ("About", "Rólunk"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Némítás"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Hangátvitel"), ("Enhancements", "Fejlesztések"), ("Hardware Codec", "Hardware kodek"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API szerver"), ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdődnie."), ("Invalid IP", "A megadott IP cím helytelen."), - ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az első karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), ("Invalid format", "Érvénytelen formátum"), ("server_not_support", "Nem támogatott a szerver által"), ("Not available", "Nem elérhető"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Figyelmeztetés"), ("Login screen using Wayland is not supported", "Bejelentkezéskori Wayland használata nem támogatott"), ("Reboot required", "Újraindítás szükséges"), - ("Unsupported display server ", "Nem támogatott megjelenítő szerver"), + ("Unsupported display server", "Nem támogatott megjelenítő szerver"), ("x11 expected", "x11-re számítottt"), ("Port", "Port"), ("Settings", "Beállítások"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "A kapcsolatot a másik fél manuálisan bezárta"), ("Enable remote configuration modification", "Távoli konfiguráció módosítás engedélyezése"), ("Run without install", "Futtatás feltelepítés nélkül"), - ("Always connected via relay", "Mindig közvetítőn keresztül csatlakozik"), + ("Connect via relay", ""), ("Always connect via relay", "Mindig közvetítőn keresztüli csatlakozás"), ("whitelist_tip", "Csak az engedélyezési listán szereplő címek csatlakozhatnak"), ("Login", "Belépés"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Kilépés"), ("Tags", "Tagok"), ("Search ID", "Azonosító keresése..."), - ("Current Wayland display server is not supported", "A Wayland display szerver nem támogatott"), ("whitelist_sep", "A címeket veszővel, pontosvesszővel, szóközzel, vagy új sorral válassza el"), ("Add ID", "Azonosító hozzáadása"), ("Add Tag", "Címke hozzáadása"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Megyjegyzés"), ("Connection", "Kapcsolat"), ("Share Screen", "Képernyőmegosztás"), - ("CLOSE", "BEZÁRÁS"), - ("OPEN", "MEGNYITÁS"), ("Chat", "Chat"), ("Total", "Összes"), ("items", "elemek"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "A \"Képernyőrögzítés\" bekapcsolásával automatikus elindul a szolgáltatás, lehetővé téve, hogy más eszközök csatlakozási kérelmet küldhessenek"), ("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létező kapcsolatot."), ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."), - ("android_start_service_tip", "Nyomjon a [Szolgáltatás indítása] gombra, vagy adjon [Képernyőrözítési] engedélyt az applikációnak hogy elindítsa a képernyőmegosztó szolgáltatást."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Fiók"), ("Overwrite", "Felülírás"), ("This file exists, skip or overwrite this file?", "Ez a fájl már létezik, kihagyja vagy felülírja ezt a fájlt?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk futtatása a háttérben"), ("Ignore Battery Optimizations", "Akkumulátorkímélő figyelmen kívűl hagyása"), ("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállítási oldalára, keresse meg az [Akkumulátorkímélő] lehetőséget és válassza a nincs korlátozás lehetőséget."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "A csatlakozás nem engedélyezett"), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptív méretarány"), ("General", "Általános"), ("Security", "Biztonság"), - ("Account", "Fiók"), ("Theme", "Téma"), ("Dark Theme", "Sötét téma"), + ("Light Theme", ""), ("Dark", "Sötét"), ("Light", "Világos"), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Szerver"), ("Direct IP Access", "Közvetlen IP hozzáférés"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Alkalmaz"), ("Disconnect all devices?", "Leválasztja az összes eszközt?"), ("Clear", "Tisztítás"), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Egyéb"), ("Confirm before closing multiple tabs", "Biztos, hogy bezárja az összes lapot?"), ("Keyboard Settings", "Billentyűzet beállítások"), - ("Custom", "Egyedi"), ("Full Access", "Teljes hozzáférés"), ("Screen Share", "Képernyőmegosztás"), ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhoz Ubuntu 21.04 vagy újabb verzió szükséges."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index ccb4bbd8a..cca4ba6c1 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9 dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), ("Website", "Website"), ("About", "Tentang"), ("Slogan_tip", ""), ("Privacy Statement", "Pernyataan Privasi"), ("Mute", "Bisukan"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Masukkan Audio"), ("Enhancements", "Peningkatan"), ("Hardware Codec", "Codec Perangkat Keras"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "harus dimulai dengan http:// atau https://"), ("Invalid IP", "IP tidak valid"), - ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9 dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), ("Invalid format", "Format tidak valid"), ("server_not_support", "Belum didukung oleh server"), ("Not available", "Tidak tersedia"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Peringatan"), ("Login screen using Wayland is not supported", "Layar masuk menggunakan Wayland tidak didukung"), ("Reboot required", "Diperlukan boot ulang"), - ("Unsupported display server ", "Server tampilan tidak didukung "), + ("Unsupported display server", "Server tampilan tidak didukung "), ("x11 expected", "x11 diharapkan"), ("Port", "Port"), ("Settings", "Pengaturan"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Ditutup secara manual oleh peer"), ("Enable remote configuration modification", "Aktifkan modifikasi konfigurasi jarak jauh"), ("Run without install", "Jalankan tanpa menginstal"), - ("Always connected via relay", "Selalu terhubung melalui relai"), + ("Connect via relay", ""), ("Always connect via relay", "Selalu terhubung melalui relai"), ("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"), ("Login", "Masuk"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Keluar"), ("Tags", "Tag"), ("Search ID", "Cari ID"), - ("Current Wayland display server is not supported", "Server tampilan Wayland saat ini tidak didukung"), ("whitelist_sep", "Dipisahkan dengan koma, titik koma, spasi, atau baris baru"), ("Add ID", "Tambah ID"), ("Add Tag", "Tambah Tag"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Catatan"), ("Connection", "Koneksi"), ("Share Screen", "Bagikan Layar"), - ("CLOSE", "TUTUP"), - ("OPEN", "BUKA"), ("Chat", "Obrolan"), ("Total", "Total"), ("items", "item"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Mengaktifkan \"Tangkapan Layar\" akan memulai layanan secara otomatis, memungkinkan perangkat lain untuk meminta sambungan ke perangkat Anda."), ("android_stop_service_tip", "Menutup layanan akan secara otomatis menutup semua koneksi yang dibuat."), ("android_version_audio_tip", "Versi Android saat ini tidak mendukung pengambilan audio, harap tingkatkan ke Android 10 atau lebih tinggi."), - ("android_start_service_tip", "Ketuk izin [Mulai Layanan] atau BUKA [Tangkapan Layar] untuk memulai layanan berbagi layar."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Akun"), ("Overwrite", "Timpa"), ("This file exists, skip or overwrite this file?", "File ini sudah ada, lewati atau timpa file ini?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Pertahankan RustDesk berjalan pada background service"), ("Ignore Battery Optimizations", "Abaikan Pengoptimalan Baterai"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Koneksi tidak dijinkan"), ("Legacy mode", "Mode lama"), ("Map mode", "Mode peta"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptif"), ("General", "Umum"), ("Security", "Keamanan"), - ("Account", "Akun"), ("Theme", "Tema"), ("Dark Theme", "Tema gelap"), + ("Light Theme", ""), ("Dark", "Gelap"), ("Light", "Terang"), ("Follow System", "Ikuti sistem"), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direct IP Access"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Terapkan"), ("Disconnect all devices?", "Putuskan sambungan semua perangkat?"), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Lainnya"), ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"), ("Keyboard Settings", "Pengaturan Papan Ketik"), - ("Custom", "Kustom"), ("Full Access", "Akses penuh"), ("Screen Share", "Berbagi Layar"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", "Pencarian"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 0280d1700..f5a7d174c 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Il servizio è in esecuzione"), ("Service is not running", "Il servizio non è in esecuzione"), ("not_ready_status", "Non pronto. Verifica la tua connessione"), - ("Control Remote Desktop", "Controlla una scrivania remota"), + ("Control Remote Desktop", "Controlla un desktop remoto"), ("Transfer File", "Trasferisci file"), ("Connect", "Connetti"), ("Recent Sessions", "Sessioni recenti"), @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), ("Change ID", "Cambia ID"), + ("Your new ID", "Il tuo nuovo ID"), + ("length %min% to %max%", "da lunghezza %min% a %max%"), + ("starts with a letter", "inizia con una lettera"), + ("allowed characters", "caratteri consentiti"), + ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), ("Website", "Sito web"), ("About", "Informazioni"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), + ("Privacy Statement", "Informativa sulla privacy"), ("Mute", "Silenzia"), + ("Build Date", "Data della build"), + ("Version", "Versione"), + ("Home", "Home"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), ("Hardware Codec", "Codifica Hardware"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Server API"), ("invalid_http", "deve iniziare con http:// o https://"), ("Invalid IP", "Indirizzo IP non valido"), - ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (underscore). Il primo carattere deve essere a-z o A-Z. La lunghezza deve essere fra 6 e 16 caratteri."), ("Invalid format", "Formato non valido"), ("server_not_support", "Non ancora supportato dal server"), ("Not available", "Non disponibile"), @@ -185,7 +192,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter your password", "Inserisci la tua password"), ("Logging in...", "Autenticazione..."), ("Enable RDP session sharing", "Abilita la condivisione della sessione RDP"), - ("Auto Login", "Login automatico"), + ("Auto Login", "Accesso automatico"), ("Enable Direct IP Access", "Abilita l'accesso diretto tramite IP"), ("Rename", "Rinomina"), ("Space", "Spazio"), @@ -195,32 +202,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Inserisci il nome della cartella"), ("Fix it", "Risolvi"), ("Warning", "Avviso"), - ("Login screen using Wayland is not supported", "La schermata di login non è supportata utilizzando Wayland"), + ("Login screen using Wayland is not supported", "La schermata di accesso non è supportata utilizzando Wayland"), ("Reboot required", "Riavvio necessario"), - ("Unsupported display server ", "Display server non supportato"), + ("Unsupported display server", "Display server non supportato"), ("x11 expected", "x11 necessario"), ("Port", "Porta"), ("Settings", "Impostazioni"), - ("Username", " Nome utente"), - ("Invalid port", "Porta non valida"), + ("Username", "Nome utente"), + ("Invalid port", "Numero di porta non valido"), ("Closed manually by the peer", "Chiuso manualmente dal peer"), ("Enable remote configuration modification", "Abilita la modifica remota della configurazione"), - ("Run without install", "Avvia senza installare"), - ("Always connected via relay", "Connesso sempre tramite relay"), - ("Always connect via relay", "Connetti sempre tramite relay"), + ("Run without install", "Esegui senza installare"), + ("Connect via relay", "Collegati tramite relay"), + ("Always connect via relay", "Collegati sempre tramite relay"), ("whitelist_tip", "Solo gli indirizzi IP autorizzati possono connettersi a questo desktop"), ("Login", "Accedi"), + ("Verify", "Verifica"), + ("Remember me", "Ricordami"), + ("Trust this device", "Registra questo dispositivo come attendibile"), + ("Verification code", "Codice di verifica"), + ("verification_tip", "È stato rilevato un nuovo dispositivo e un codice di verifica è stato inviato all'indirizzo e-mail registrato; inserire il codice di verifica per continuare l'accesso."), ("Logout", "Esci"), ("Tags", "Tag"), ("Search ID", "Cerca ID"), - ("Current Wayland display server is not supported", "Questo display server Wayland non è supportato"), ("whitelist_sep", "Separati da virgola, punto e virgola, spazio o a capo"), ("Add ID", "Aggiungi ID"), ("Add Tag", "Aggiungi tag"), ("Unselect all tags", "Deseleziona tutti i tag"), ("Network error", "Errore di rete"), - ("Username missed", "Nome utente dimenticato"), - ("Password missed", "Password dimenticata"), + ("Username missed", "Nome utente mancante"), + ("Password missed", "Password mancante"), ("Wrong credentials", "Credenziali errate"), ("Edit Tag", "Modifica tag"), ("Unremember Password", "Dimentica password"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Nota"), ("Connection", "Connessione"), ("Share Screen", "Condividi schermo"), - ("CLOSE", "CHIUDERE"), - ("OPEN", "APRIRE"), ("Chat", "Chat"), ("Total", "Totale"), ("items", "Oggetti"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "L'attivazione di Cattura schermo avvierà automaticamente il servizio, consentendo ad altri dispositivi di richiedere una connessione da questo dispositivo."), ("android_stop_service_tip", "La chiusura del servizio chiuderà automaticamente tutte le connessioni stabilite."), ("android_version_audio_tip", "L'attuale versione di Android non supporta l'acquisizione audio, esegui l'upgrade ad Android 10 o versioni successive."), - ("android_start_service_tip", "Toccare [Avvia servizio] o APRI l'autorizzazione [Cattura schermo] per avviare il servizio di condivisione dello schermo."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Account"), ("Overwrite", "Sovrascrivi"), ("This file exists, skip or overwrite this file?", "Questo file esiste, saltare o sovrascrivere questo file?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mantieni il servizio di RustDesk in background"), ("Ignore Battery Optimizations", "Ignora le ottimizzazioni della batteria"), ("android_open_battery_optimizations_tip", "Se si desidera disabilitare questa funzione, andare nelle impostazioni dell'applicazione RustDesk, aprire la sezione [Batteria] e deselezionare [Senza restrizioni]."), + ("Start on Boot", "Avvia all'accensione"), + ("Start the screen sharing service on boot, requires special permissions", "L'avvio del servizio di condivisione dello schermo all'accensione, richiede autorizzazioni speciali"), ("Connection not allowed", "Connessione non consentita"), ("Legacy mode", "Modalità legacy"), ("Map mode", "Modalità mappa"), @@ -327,16 +339,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show Menubar", "Mostra la barra dei menu"), ("Hide Menubar", "nascondi la barra dei menu"), ("Direct Connection", "Connessione diretta"), - ("Relay Connection", "Collegamento a relè"), + ("Relay Connection", "Connessione relay"), ("Secure Connection", "Connessione sicura"), - ("Insecure Connection", "Connessione insicura"), + ("Insecure Connection", "Connessione non sicura"), ("Scale original", "Scala originale"), ("Scale adaptive", "Scala adattiva"), ("General", "Generale"), ("Security", "Sicurezza"), - ("Account", "Account"), ("Theme", "Tema"), ("Dark Theme", "Tema Scuro"), + ("Light Theme", ""), ("Dark", "Scuro"), ("Light", "Chiaro"), ("Follow System", "Segui il sistema"), @@ -344,10 +356,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unlock Security Settings", "Sblocca impostazioni di sicurezza"), ("Enable Audio", "Abilita audio"), ("Unlock Network Settings", "Sblocca impostazioni di rete"), - ("Server", ""), + ("Server", "Server"), ("Direct IP Access", "Accesso IP diretto"), - ("Proxy", ""), - ("Port", "Porta"), + ("Proxy", "Proxy"), ("Apply", "Applica"), ("Disconnect all devices?", "Disconnettere tutti i dispositivi?"), ("Clear", "Ripulisci"), @@ -369,14 +380,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "Abilita il rilevamento della LAN"), ("Deny LAN Discovery", "Nega il rilevamento della LAN"), ("Write a message", "Scrivi un messaggio"), - ("Prompt", ""), + ("Prompt", "Richiede"), ("Please wait for confirmation of UAC...", "Attendi la conferma dell'UAC..."), ("elevated_foreground_window_tip", "La finestra corrente del desktop remoto richiede privilegi più elevati per funzionare, quindi non è in grado di utilizzare temporaneamente il mouse e la tastiera. È possibile chiedere all'utente remoto di ridurre a icona la finestra corrente o di fare clic sul pulsante di elevazione nella finestra di gestione della connessione. Per evitare questo problema, si consiglia di installare il software sul dispositivo remoto."), ("Disconnected", "Disconnesso"), ("Other", "Altro"), ("Confirm before closing multiple tabs", "Conferma prima di chiudere più schede"), ("Keyboard Settings", "Impostazioni tastiera"), - ("Custom", "Personalizzato"), ("Full Access", "Accesso completo"), ("Screen Share", "Condivisione dello schermo"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland richiede Ubuntu 21.04 o successiva."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Aggiungi alla rubrica"), ("Group", "Gruppo"), ("Search", "Cerca"), - ("Closed manually by the web console", "Chiudi manualmente dalla console Web"), + ("Closed manually by web console", "Chiudi manualmente dalla console Web"), ("Local keyboard type", "Tipo di tastiera locale"), ("Select local keyboard type", "Seleziona il tipo di tastiera locale"), + ("software_render_tip", "Se si dispone di una scheda grafica Nvidia e la finestra remota si chiude immediatamente dopo la connessione, l'installazione del driver nouveau e la scelta di utilizzare il rendering software possono aiutare. È necessario un riavvio del software."), + ("Always use software rendering", "Usa sempre il render Software"), + ("config_input", "Per controllare il desktop remoto con la tastiera, è necessario concedere le autorizzazioni a RustDesk \"Monitoraggio dell'input\"."), + ("config_microphone", "Per poter chiamare, è necessario concedere l'autorizzazione a RustDesk \"Registra audio\"."), + ("request_elevation_tip", "È possibile richiedere l'elevazione se c'è qualcuno sul lato remoto."), + ("Wait", "Attendi"), + ("Elevation Error", "Errore durante l'elevazione dei diritti"), + ("Ask the remote user for authentication", "Chiedere l'autenticazione all'utente remoto"), + ("Choose this if the remote account is administrator", "Scegliere questa opzione se l'account remoto è amministratore"), + ("Transmit the username and password of administrator", "Trasmettere il nome utente e la password dell'amministratore"), + ("still_click_uac_tip", "Richiede ancora che l'utente remoto faccia clic su OK nella finestra UAC dell'esecuzione di RustDesk."), + ("Request Elevation", "Richiedi elevazione dei diritti"), + ("wait_accept_uac_tip", "Attendere che l'utente remoto accetti la finestra di dialogo UAC."), + ("Elevate successfully", "Elevazione dei diritti effettuata con successo"), + ("uppercase", "Maiuscola"), + ("lowercase", "Minuscola"), + ("digit", "Numero"), + ("special character", "Carattere speciale"), + ("length>=8", "Lunghezza >= 8"), + ("Weak", "Debole"), + ("Medium", "Media"), + ("Strong", "Forte"), + ("Switch Sides", "Cambia lato"), + ("Please confirm if you want to share your desktop?", "Vuoi condividere il tuo desktop?"), + ("Display", "Visualizzazione"), + ("Default View Style", "Stile Visualizzazione Predefinito"), + ("Default Scroll Style", "Stile Scorrimento Predefinito"), + ("Default Image Quality", "Qualità Immagine Predefinita"), + ("Default Codec", "Codec Predefinito"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Altre Opzioni Predefinite"), + ("Voice call", "Chiamata vocale"), + ("Text chat", "Chat testuale"), + ("Stop voice call", "Interrompi la chiamata vocale"), + ("relay_hint_tip", "Se non è possibile connettersi direttamente, si può provare a farlo tramite relay.\nInoltre, se si desidera utilizzare il relay al primo tentativo, è possibile aggiungere il suffisso \"/r\" all'ID o selezionare l'opzione \"Collegati sempre tramite relay\" nella scheda peer."), + ("Reconnect", "Riconnetti"), + ("Codec", "Codec"), + ("Resolution", "Risoluzione"), + ("No transfers in progress", "Nessun trasferimento in corso"), + ("Set one-time password length", "Imposta la lunghezza della password monouso"), + ("idd_driver_tip", "Installa il driver per lo schermo virtuale che sarà utilizzato quando non si dispone di schermi fisici."), + ("confirm_idd_driver_tip", "L'opzione per installare il driver per lo schermo virtuale è selezionata. Nota che un certificato di test sarà installato per l'attendibilità del driver dello schermo virtuale. Questo certificato di test verrà utilizzato solo per l'attendibilità dei driver di RustDesk."), + ("RDP Settings", "Impostazioni RDP"), + ("Sort by", "Ordina per"), + ("New Connection", "Nuova connessione"), + ("Restore", "Ripristina"), + ("Minimize", "Minimizza"), + ("Maximize", "Massimizza"), + ("Your Device", "Il tuo dispositivo"), + ("empty_recent_tip", "Oops, non c'è nessuna sessione recente!\nTempo di pianificarne una."), + ("empty_favorite_tip", "Ancora nessun peer?\nTrova qualcuno con cui connetterti e aggiungilo ai tuoi preferiti!"), + ("empty_lan_tip", "Oh no, sembra proprio che non abbiamo ancora rilevato nessun peer."), + ("empty_address_book_tip", "Oh diamine, sembra che per ora non ci siano peer nella tua rubrica."), + ("eg: admin", "es: admin"), + ("Empty Username", "Nome Utente Vuoto"), + ("Empty Password", "Password Vuota"), + ("Me", "Io"), + ("identical_file_tip", "Questo file è identico a quello del peer."), + ("show_monitors_tip", "Mostra schermi nella barra degli strumenti"), + ("View Mode", "Modalità di visualizzazione"), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 5f6dcc79e..29bf3c346 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "クリップボードは空です"), ("Stop service", "サービスを停止"), ("Change ID", "IDを変更"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "使用できるのは大文字・小文字のアルファベット、数字、アンダースコア(_)のみです。初めの文字はアルファベットにする必要があります。6文字から16文字までです。"), ("Website", "公式サイト"), ("About", "情報"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "ミュート"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "音声入力デバイス"), ("Enhancements", "追加機能"), ("Hardware Codec", "ハードウェア コーデック"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "APIサーバー"), ("invalid_http", "http:// もしくは https:// から入力してください"), ("Invalid IP", "無効なIP"), - ("id_change_tip", "使用できるのは大文字・小文字のアルファベット、数字、アンダースコア(_)のみです。初めの文字はアルファベットにする必要があります。6文字から16文字までです。"), ("Invalid format", "無効な形式"), ("server_not_support", "サーバー側でまだサポートされていません"), ("Not available", "利用不可"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "注意"), ("Login screen using Wayland is not supported", "Waylandを使用したログインスクリーンはサポートされていません"), ("Reboot required", "再起動が必要"), - ("Unsupported display server ", "サポートされていないディスプレイサーバー"), + ("Unsupported display server", "サポートされていないディスプレイサーバー"), ("x11 expected", "X11 が必要です"), ("Port", ""), ("Settings", "設定"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "相手が手動で切断しました"), ("Enable remote configuration modification", "リモート設定変更を有効化"), ("Run without install", "インストールせずに実行"), - ("Always connected via relay", "常に中継サーバー経由で接続"), + ("Connect via relay", ""), ("Always connect via relay", "常に中継サーバー経由で接続"), ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), ("Login", "ログイン"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "ログアウト"), ("Tags", "タグ"), ("Search ID", "IDを検索"), - ("Current Wayland display server is not supported", "現在のWaylandディスプレイサーバーはサポートされていません"), ("whitelist_sep", "カンマやセミコロン、空白、改行で区切ってください"), ("Add ID", "IDを追加"), ("Add Tag", "タグを追加"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "ノート"), ("Connection", "接続"), ("Share Screen", "画面を共有"), - ("CLOSE", "閉じる"), - ("OPEN", "開く"), ("Chat", "チャット"), ("Total", "計"), ("items", "個のアイテム"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "「画面キャプチャ」をオンにするとサービスが自動的に開始され、他の端末がこの端末への接続をリクエストできるようになります。"), ("android_stop_service_tip", "サービスを停止すると、現在確立されている接続が全て自動的に閉じられます。"), ("android_version_audio_tip", "現在のAndroidバージョンでは音声キャプチャはサポートされていません。Android 10以降にアップグレードしてください。"), - ("android_start_service_tip", "「サービスを開始」をタップするか「画面キャプチャ」を開くと、画面共有サービスが開始されます。"), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", ""), ("Overwrite", "上書き"), ("This file exists, skip or overwrite this file?", "このファイルは存在しています。スキップするか上書きしますか?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk バックグラウンドサービスを維持"), ("Ignore Battery Optimizations", "バッテリーの最適化を無効にする"), ("android_open_battery_optimizations_tip", "この機能を使わない場合は、次のRestDeskアプリ設定ページから「バッテリー」に進み、「制限なし」の選択を外してください"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "接続が許可されていません"), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "フィットウィンドウ"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), + ("Light Theme", ""), ("Dark", ""), ("Light", ""), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "他の"), ("Confirm before closing multiple tabs", "同時に複数のタブを閉じる前に確認する"), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland には、Ubuntu 21.04 以降のバージョンが必要です。"), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 0f7df38b9..929934016 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "클립보드가 비어있습니다"), ("Stop service", "서비스 중단"), ("Change ID", "ID 변경"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "a-z, A-Z, 0-9, _(밑줄 문자)만 입력 가능합니다. 첫 문자는 a-z 혹은 A-Z로 시작해야 합니다. 길이는 6 ~ 16글자가 요구됩니다."), ("Website", "웹사이트"), ("About", "정보"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "음소거"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "오디오 입력"), ("Enhancements", ""), ("Hardware Codec", "하드웨어 코덱"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API 서버"), ("invalid_http", "다음과 같이 시작해야 합니다. http:// 또는 https://"), ("Invalid IP", "유효하지 않은 IP"), - ("id_change_tip", "a-z, A-Z, 0-9, _(밑줄 문자)만 입력 가능합니다. 첫 문자는 a-z 혹은 A-Z로 시작해야 합니다. 길이는 6 ~ 16글자가 요구됩니다."), ("Invalid format", "유효하지 않은 형식"), ("server_not_support", "해당 서버가 아직 지원하지 않습니다"), ("Not available", "불가능"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "경고"), ("Login screen using Wayland is not supported", "Wayland를 사용한 로그인 화면이 지원되지 않습니다"), ("Reboot required", "재부팅이 필요합니다"), - ("Unsupported display server ", "지원하지 않는 디스플레이 서버"), + ("Unsupported display server", "지원하지 않는 디스플레이 서버"), ("x11 expected", "x11 예상됨"), ("Port", ""), ("Settings", "설정"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "다른 사용자에 의해 종료됨"), ("Enable remote configuration modification", "원격 구성 변경 활성화"), ("Run without install", "설치 없이 실행"), - ("Always connected via relay", "항상 relay를 통해 접속됨"), + ("Connect via relay", ""), ("Always connect via relay", "항상 relay를 통해 접속하기"), ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), ("Login", "로그인"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "로그아웃"), ("Tags", "태그"), ("Search ID", "ID 검색"), - ("Current Wayland display server is not supported", "현재 Wayland 디스플레이 서버가 지원되지 않습니다"), ("whitelist_sep", "다음 글자로 구분합니다. ',(콤마) ;(세미콜론) 띄어쓰기 혹은 줄바꿈'"), ("Add ID", "ID 추가"), ("Add Tag", "태그 추가"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "노트"), ("Connection", "연결"), ("Share Screen", "화면 공유"), - ("CLOSE", "종료"), - ("OPEN", "열기"), ("Chat", "채팅"), ("Total", "총합"), ("items", "개체"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "\"화면 캡처\"를 켜면 서비스가 자동으로 시작되어 다른 장치에서 사용자 장치에 대한 연결을 요청할 수 있습니다."), ("android_stop_service_tip", "서비스를 종료하면 모든 연결이 자동으로 닫힙니다."), ("android_version_audio_tip", "현재 Android 버전은 오디오 캡처를 지원하지 않습니다. Android 10 이상으로 업그레이드하십시오."), - ("android_start_service_tip", "[서비스 시작] 또는 [화면 캡처] 권한을 눌러 화면 공유 서비스를 시작합니다."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", ""), ("Overwrite", "덮어쓰기"), ("This file exists, skip or overwrite this file?", "해당 파일이 이미 존재합니다, 넘어가거나 덮어쓰시겠습니까?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk 백그라운드 서비스로 유지하기"), ("Ignore Battery Optimizations", "배터리 최적화 무시하기"), ("android_open_battery_optimizations_tip", "해당 기능을 비활성화하려면 RustDesk 응용 프로그램 설정 페이지로 이동하여 [배터리]에서 [제한 없음] 선택을 해제하십시오."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "연결이 허용되지 않음"), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "맞는 창"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), + ("Light Theme", ""), ("Dark", ""), ("Light", ""), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland는 Ubuntu 21.04 이상 버전이 필요합니다."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index f352be343..a2c7c5983 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Көшіру-тақта бос"), ("Stop service", "Сербесті тоқтату"), ("Change ID", "ID ауыстыру"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Тек a-z, A-Z, 0-9 және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), ("Website", "Web-сайт"), ("About", "Туралы"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Дыбыссыздандыру"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Аудио Еңгізу"), ("Enhancements", "Жақсартулар"), ("Hardware Codec", "Hardware Codec"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Сербері"), ("invalid_http", "http:// немесе https://'пен басталуы қажет"), ("Invalid IP", "Бұрыс IP-Мекенжай"), - ("id_change_tip", "Тек a-z, A-Z, 0-9 және _ (астынғы-сызық) таңбалары рұқсат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 арасы."), ("Invalid format", "Бұрыс формат"), ("server_not_support", "Сербер әзірше қолдамайды"), ("Not available", "Қолжетімсіз"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Ескерту"), ("Login screen using Wayland is not supported", "Wayland қолданған Кіру екіреніне қолдау көрсетілмейді"), ("Reboot required", "Қайта-қосу қажет"), - ("Unsupported display server ", "Қолдаусыз дисплей сербері"), + ("Unsupported display server", "Қолдаусыз дисплей сербері"), ("x11 expected", "x11 күтілген"), ("Port", "Порт"), ("Settings", "Орнатпалар"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Пир қолымен жабылған"), ("Enable remote configuration modification", "Қашықтан қалыптарды өзгертуді іске қосу"), ("Run without install", "Орнатпай-ақ Іске қосу"), - ("Always connected via relay", "Әрқашан да релай сербері арқылы қосулы"), + ("Connect via relay", ""), ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), ("Login", "Кіру"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Шығу"), ("Tags", "Тақтар"), ("Search ID", "ID Іздеу"), - ("Current Wayland display server is not supported", "Ағымдағы Wayland дисплей серберіне қолдау көрсетілмейді"), ("whitelist_sep", "Үтір, нүктелі үтір, бос орын және жаңа жолал арқылы бөлінеді"), ("Add ID", "ID Қосу"), ("Add Tag", "Тақ Қосу"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Нота"), ("Connection", "Қосылым"), ("Share Screen", "Екіренді Бөлісу"), - ("CLOSE", "ЖАБУ"), - ("OPEN", "АШУ"), ("Chat", "Чат"), ("Total", "Барлығы"), ("items", "зат"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "\"Екіренді Тұсіру\" қосылған кезде сербес аутыматты іске қосылып, басқа құрылғыларға сіздің құрылғыға қосылым сұраныстауға мүмкіндің береді."), ("android_stop_service_tip", "Сербесті жабу аутыматты түрде барлық орнатылған қосылымдарды жабады."), ("android_version_audio_tip", "Ағымдағы Android нұсқасы аудионы түсіруді қолдамайды, Android 10 не жоғарғысына жаңғыртуды өтінеміз."), - ("android_start_service_tip", "[Сербесті Іске қосу]'ды түртіңіз не [Екіренді Түсіру] рұқсатын АШУ арқылы екіренді бөлісу сербесін іске қосыңыз."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Есепкі"), ("Overwrite", "Үстінен қайта жазу"), ("This file exists, skip or overwrite this file?", "Бұл файыл бар, өткізіп жіберу әлде үстінен қайта жазу керек пе?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Артжақтағы RustDesk сербесін сақтап тұру"), ("Ignore Battery Optimizations", "Бәтері Оңтайландыруларын Елемеу"), ("android_open_battery_optimizations_tip", "Егер де бұл ерекшелікті өшіруді қаласаңыз, келесі RustDesk апылқат орнатпалары бетіне барып, [Бәтері]'ні тауып кіріңіз де [Шектеусіз]'ден құсбелгіні алып тастауды өтінеміз"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Қосылу рұқсат етілмеген"), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Scale adaptive"), ("General", ""), ("Security", ""), - ("Account", "Есепкі"), ("Theme", ""), ("Dark Theme", ""), + ("Light Theme", ""), ("Dark", ""), ("Light", ""), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", "Порт"), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland Ubuntu 21.04 немесе одан жоғары нұсқасын қажет етеді."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs new file mode 100644 index 000000000..d46be2b3f --- /dev/null +++ b/src/lang/nl.rs @@ -0,0 +1,487 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Uw Bureaublad"), + ("desk_tip", "Uw bureaublad is toegankelijk via de ID en het wachtwoord hieronder."), + ("Password", "Wachtwoord"), + ("Ready", "Klaar"), + ("Established", "Opgezet"), + ("connecting_status", "Verbinding maken met het RustDesk netwerk..."), + ("Enable Service", "Service Inschakelen"), + ("Start Service", "Start Service"), + ("Service is running", "De service loopt."), + ("Service is not running", "De service loopt niet"), + ("not_ready_status", "Niet klaar, controleer de netwerkverbinding"), + ("Control Remote Desktop", "Beheer Extern Bureaublad"), + ("Transfer File", "Bestand Overzetten"), + ("Connect", "Verbinden"), + ("Recent Sessions", "Recente Behandelingen"), + ("Address Book", "Adresboek"), + ("Confirmation", "Bevestiging"), + ("TCP Tunneling", "TCP Tunneling"), + ("Remove", "Verwijder"), + ("Refresh random password", "Vernieuw willekeurig wachtwoord"), + ("Set your own password", "Stel je eigen wachtwoord in"), + ("Enable Keyboard/Mouse", "Toetsenbord/Muis Inschakelen"), + ("Enable Clipboard", "Klembord Inschakelen"), + ("Enable File Transfer", "Bestandsoverdracht Inschakelen"), + ("Enable TCP Tunneling", "TCP Tunneling Inschakelen"), + ("IP Whitelisting", "IP Witte Lijst"), + ("ID/Relay Server", "ID/Relay Server"), + ("Import Server Config", "Importeer Serverconfiguratie"), + ("Export Server Config", "Exporteer Serverconfiguratie"), + ("Import server configuration successfully", "Importeren serverconfiguratie succesvol"), + ("Export server configuration successfully", "Exporteren serverconfiguratie succesvol"), + ("Invalid server configuration", "Ongeldige Serverconfiguratie"), + ("Clipboard is empty", "Klembord is leeg"), + ("Stop service", "Stop service"), + ("Change ID", "Wijzig ID"), + ("Your new ID", "Uw nieuw ID"), + ("length %min% to %max%", "lengte %min% tot %max%"), + ("starts with a letter", "begint met een letter"), + ("allowed characters", "toegestane tekens"), + ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), + ("Website", "Website"), + ("About", "Over"), + ("Slogan_tip", "Ontwikkeld met het hart voor deze chaotische wereld!"), + ("Privacy Statement", "Privacyverklaring"), + ("Mute", "Geluid uit"), + ("Build Date", "Versie datum"), + ("Version", "Versie"), + ("Home", "Startpagina"), + ("Audio Input", "Audio Ingang"), + ("Enhancements", "Verbeteringen"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive Bitrate", "Aangepaste Bitsnelheid"), + ("ID Server", "Server ID"), + ("Relay Server", "Relay Server"), + ("API Server", "API Server"), + ("invalid_http", "Moet beginnen met http:// of https://"), + ("Invalid IP", "Ongeldig IP"), + ("Invalid format", "Ongeldig formaat"), + ("server_not_support", "Nog niet ondersteund door de server"), + ("Not available", "Niet beschikbaar"), + ("Too frequent", "Te vaak"), + ("Cancel", "Annuleer"), + ("Skip", "Overslaan"), + ("Close", "Sluit"), + ("Retry", "Probeer opnieuw"), + ("OK", "OK"), + ("Password Required", "Wachtwoord vereist"), + ("Please enter your password", "Geef uw wachtwoord in"), + ("Remember password", "Wachtwoord onthouden"), + ("Wrong Password", "Verkeerd wachtwoord"), + ("Do you want to enter again?", "Wil je opnieuw ingeven?"), + ("Connection Error", "Fout bij verbinding"), + ("Error", "Fout"), + ("Reset by the peer", "Reset door de peer"), + ("Connecting...", "Verbinding maken..."), + ("Connection in progress. Please wait.", "Verbinding in uitvoering. Even geduld a.u.b."), + ("Please try 1 minute later", "Probeer 1 minuut later"), + ("Login Error", "Login Fout"), + ("Successful", "Succesvol"), + ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), + ("Name", "Naam"), + ("Type", "Type"), + ("Modified", "Gewijzigd"), + ("Size", "Grootte"), + ("Show Hidden Files", "Toon verborgen bestanden"), + ("Receive", "Ontvangen"), + ("Send", "Verzenden"), + ("Refresh File", "Bestand Verversen"), + ("Local", "Lokaal"), + ("Remote", "Op afstand"), + ("Remote Computer", "Externe Computer"), + ("Local Computer", "Lokale Computer"), + ("Confirm Delete", "Bevestig Verwijderen"), + ("Delete", "Verwijder"), + ("Properties", "Eigenschappen"), + ("Multi Select", "Meervoudig selecteren"), + ("Select All", "Selecteer Alle"), + ("Unselect All", "Deselecteer alles"), + ("Empty Directory", "Lege Map"), + ("Not an empty directory", "Geen Lege Map"), + ("Are you sure you want to delete this file?", "Weet je zeker dat je dit bestand wilt verwijderen?"), + ("Are you sure you want to delete this empty directory?", "Weet je zeker dat je deze lege map wilt verwijderen?"), + ("Are you sure you want to delete the file of this directory?", "Weet je zeker dat je het bestand uit deze map wilt verwijderen?"), + ("Do this for all conflicts", "Doe dit voor alle conflicten"), + ("This is irreversible!", "Dit is onomkeerbaar!"), + ("Deleting", "Verwijderen"), + ("files", "bestanden"), + ("Waiting", "Wachten"), + ("Finished", "Voltooid"), + ("Speed", "Snelheid"), + ("Custom Image Quality", "Aangepaste beeldkwaliteit"), + ("Privacy mode", "Privacymodus"), + ("Block user input", "Gebruikersinvoer blokkeren"), + ("Unblock user input", "Gebruikersinvoer opheffen"), + ("Adjust Window", "Venster Aanpassen"), + ("Original", "Origineel"), + ("Shrink", "Verkleinen"), + ("Stretch", "Uitrekken"), + ("Scrollbar", "Schuifbalk"), + ("ScrollAuto", "Auto Schuiven"), + ("Good image quality", "Goede beeldkwaliteit"), + ("Balanced", "Gebalanceerd"), + ("Optimize reaction time", "Optimaliseer reactietijd"), + ("Custom", "Aangepast"), + ("Show remote cursor", "Toon cursor van extern bureaublad"), + ("Show quality monitor", "Kwaliteitsmonitor tonen"), + ("Disable clipboard", "Klembord uitschakelen"), + ("Lock after session end", "Vergrendelen na einde sessie"), + ("Insert", "Invoegen"), + ("Insert Lock", "Vergrendeling Invoegen"), + ("Refresh", "Vernieuwen"), + ("ID does not exist", "ID bestaat niet"), + ("Failed to connect to rendezvous server", "Verbinding met rendez-vous-server mislukt"), + ("Please try later", "Probeer later opnieuw"), + ("Remote desktop is offline", "Extern bureaublad is offline"), + ("Key mismatch", "Code onjuist"), + ("Timeout", "Time-out"), + ("Failed to connect to relay server", "Verbinding met relayserver mislukt"), + ("Failed to connect via rendezvous server", "Verbinding via rendez-vous-server mislukt"), + ("Failed to connect via relay server", "Verbinding via relaisserver mislukt"), + ("Failed to make direct connection to remote desktop", "Onmogelijk direct verbinding te maken met extern bureaublad"), + ("Set Password", "Wachtwoord Instellen"), + ("OS Password", "OS Wachtwoord"), + ("install_tip", "Je gebruikt een niet geinstalleerde versie. Als gevolg van UAC-beperkingen is het in sommige gevallen niet mogelijk om als controleterminal de muis en het toetsenbord te bedienen of het scherm over te nemen. Klik op de knop hieronder om RustDesk op het systeem te installeren om het bovenstaande probleem te voorkomen."), + ("Click to upgrade", "Klik voor upgrade"), + ("Click to download", "Klik om te downloaden"), + ("Click to update", "Klik om bij te werken"), + ("Configure", "Configureren"), + ("config_acc", "Om je bureaublad op afstand te kunnen bedienen, moet je RustDesk \"toegankelijkheid\" toestemming geven."), + ("config_screen", "Om toegang te krijgen tot het externe bureaublad, moet je RustDesk de toestemming \"schermregistratie\" geven."), + ("Installing ...", "Installeren ..."), + ("Install", "Installeer"), + ("Installation", "Installatie"), + ("Installation Path", "Installatie Pad"), + ("Create start menu shortcuts", "Startmenu snelkoppelingen maken"), + ("Create desktop icon", "Bureaubladpictogram maken"), + ("agreement_tip", "Het starten van de installatie betekent het accepteren van de licentieovereenkomst."), + ("Accept and Install", "Accepteren en installeren"), + ("End-user license agreement", "Licentieovereenkomst eindgebruiker"), + ("Generating ...", "Genereert ..."), + ("Your installation is lower version.", "Uw installatie is een lagere versie."), + ("not_close_tcp_tip", "Gelieve dit venster niet te sluiten wanneer u de tunnel gebruikt"), + ("Listening ...", "Luisteren ..."), + ("Remote Host", "Externe Host"), + ("Remote Port", "Externe Poort"), + ("Action", "Actie"), + ("Add", "Toevoegen"), + ("Local Port", "Lokale Poort"), + ("Local Address", "Lokaal Adres"), + ("Change Local Port", "Wijzig Lokale Poort"), + ("setup_server_tip", "Als u een snellere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server aan te maken"), + ("Too short, at least 6 characters.", "e kort, minstens 6 tekens."), + ("The confirmation is not identical.", "De bevestiging is niet identiek."), + ("Permissions", "Machtigingen"), + ("Accept", "Accepteren"), + ("Dismiss", "Afwijzen"), + ("Disconnect", "Verbinding verbreken"), + ("Allow using keyboard and mouse", "Gebruik toetsenbord en muis toestaan"), + ("Allow using clipboard", "Gebruik klembord toestaan"), + ("Allow hearing sound", "Geluidsweergave toestaan"), + ("Allow file copy and paste", "Kopieren en plakken van bestanden toestaan"), + ("Connected", "Verbonden"), + ("Direct and encrypted connection", "Directe en versleutelde verbinding"), + ("Relayed and encrypted connection", "Doorgeschakelde en versleutelde verbinding"), + ("Direct and unencrypted connection", "Directe en niet-versleutelde verbinding"), + ("Relayed and unencrypted connection", "Doorgeschakelde en niet-versleutelde verbinding"), + ("Enter Remote ID", "Voer Extern ID in"), + ("Enter your password", "Voer uw wachtwoord in"), + ("Logging in...", "Aanmelden..."), + ("Enable RDP session sharing", "Delen van RDP-sessie inschakelen"), + ("Auto Login", "Automatisch Aanmelden"), + ("Enable Direct IP Access", "Directe IP-toegang inschakelen"), + ("Rename", "Naam wijzigen"), + ("Space", "Spatie"), + ("Create Desktop Shortcut", "Snelkoppeling op bureaublad maken"), + ("Change Path", "Pad wijzigen"), + ("Create Folder", "Map Maken"), + ("Please enter the folder name", "Geef de mapnaam op"), + ("Fix it", "Repareer het"), + ("Warning", "Waarschuwing"), + ("Login screen using Wayland is not supported", "Aanmeldingsscherm via Wayland wordt niet ondersteund"), + ("Reboot required", "Opnieuw opstarten vereist"), + ("Unsupported display server", "Niet-ondersteunde weergaveserver"), + ("x11 expected", "x11 verwacht"), + ("Port", "Poort"), + ("Settings", "Instellingen"), + ("Username", "Gebruikersnaam"), + ("Invalid port", "Ongeldige poort"), + ("Closed manually by the peer", "Handmatig gesloten door de peer"), + ("Enable remote configuration modification", "Wijziging configuratie op afstand inschakelen"), + ("Run without install", "Uitvoeren zonder installatie"), + ("Connect via relay", "Verbinden via relais"), + ("Always connect via relay", "Altijd verbinden via relay"), + ("whitelist_tip", "Alleen een IP-adres op de witte lijst krijgt toegang tot mijn toestel"), + ("Login", "Log In"), + ("Verify", "Controleer"), + ("Remember me", "Herinner mij"), + ("Trust this device", "Vertrouw dit apparaat"), + ("Verification code", "Verificatie code"), + ("verification_tip", "Er is een nieuw apparaat gedetecteerd en er is een verificatiecode naar het geregistreerde e-mailadres gestuurd, voer de verificatiecode in om de verbinding voort te zetten."), + ("Logout", "Log Uit"), + ("Tags", "Labels"), + ("Search ID", "Zoek ID"), + ("whitelist_sep", "Gescheiden door komma, puntkomma, spatie of nieuwe regel"), + ("Add ID", "ID Toevoegen"), + ("Add Tag", "Label Toevoegen"), + ("Unselect all tags", "Alle labels verwijderen"), + ("Network error", "Netwerkfout"), + ("Username missed", "Gebruikersnaam gemist"), + ("Password missed", "Wachtwoord vergeten"), + ("Wrong credentials", "Verkeerde inloggegevens"), + ("Edit Tag", "Label Bewerken"), + ("Unremember Password", "Wachtwoord vergeten"), + ("Favorites", "Favorieten"), + ("Add to Favorites", "Toevoegen aan Favorieten"), + ("Remove from Favorites", "Verwijderen uit Favorieten"), + ("Empty", "Leeg"), + ("Invalid folder name", "Ongeldige mapnaam"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Hostnaam"), + ("Discovered", "Ontdekt"), + ("install_daemon_tip", "Om bij het opstarten van de computer te kunnen beginnen, moet je de systeemdienst installeren."), + ("Remote ID", "Externe ID"), + ("Paste", "Plakken"), + ("Paste here?", "Hier plakken"), + ("Are you sure to close the connection?", "Weet je zeker dat je de verbinding wilt sluiten?"), + ("Download new version", "Download nieuwe versie"), + ("Touch mode", "Aanraak modus"), + ("Mouse mode", "Muismodus"), + ("One-Finger Tap", "Een-Vinger Tik"), + ("Left Mouse", "Linkermuis"), + ("One-Long Tap", "Een-Vinger-Lange-Tik"), + ("Two-Finger Tap", "Twee-Vingers-Tik"), + ("Right Mouse", "Rechter muis"), + ("One-Finger Move", "Een-Vinger-Verplaatsing"), + ("Double Tap & Move", "Dubbel Tik en Verplaatsen"), + ("Mouse Drag", "Muis Slepen"), + ("Three-Finger vertically", "Drie-Vinger verticaal"), + ("Mouse Wheel", "Muiswiel"), + ("Two-Finger Move", "Twee-Vingers Verplaatsen"), + ("Canvas Move", "Canvas Verplaatsen"), + ("Pinch to Zoom", "Knijp om te Zoomen"), + ("Canvas Zoom", "Canvas Zoom"), + ("Reset canvas", "Reset canvas"), + ("No permission of file transfer", "Geen toestemming voor bestandsoverdracht"), + ("Note", "Opmerking"), + ("Connection", "Verbinding"), + ("Share Screen", "Scherm Delen"), + ("Chat", "Chat"), + ("Total", "Totaal"), + ("items", "items"), + ("Selected", "Geselecteerd"), + ("Screen Capture", "Schermopname"), + ("Input Control", "Invoercontrole"), + ("Audio Capture", "Audio Opnemen"), + ("File Connection", "Bestandsverbinding"), + ("Screen Connection", "Schermverbinding"), + ("Do you accept?", "Sta je toe?"), + ("Open System Setting", "Systeeminstelling Openen"), + ("How to get Android input permission?", "Hoe krijg ik Android invoer toestemming?"), + ("android_input_permission_tip1", "Om ervoor te zorgen dat een extern apparaat uw Android-apparaat kan besturen via muis of aanraking, moet u RustDesk toestaan om de \"Toegankelijkheid\" service te gebruiken."), + ("android_input_permission_tip2", "Ga naar de volgende pagina met systeeminstellingen, zoek en ga naar [Geinstalleerde Services], schakel de service [RustDesk Input] in."), + ("android_new_connection_tip", "Er is een nieuw controleverzoek binnengekomen, dat uw huidige apparaat wil controleren."), + ("android_service_will_start_tip", "Als u \"Schermopname\" inschakelt, wordt de service automatisch gestart, zodat andere apparaten een verbinding met uw apparaat kunnen aanvragen."), + ("android_stop_service_tip", "Het sluiten van de service zal automatisch alle gemaakte verbindingen sluiten."), + ("android_version_audio_tip", "De huidige versie van Android ondersteunt geen audio-opname, upgrade naar Android 10 of hoger."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", "Account"), + ("Overwrite", "Overschrijven"), + ("This file exists, skip or overwrite this file?", "Dit bestand bestaat reeds, overslaan of overschrijven?"), + ("Quit", "Afsluiten"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "https://rustdesk.com/docs/en/manual/linux/#x11-required"), + ("Failed", "Mislukt"), + ("Succeeded", "Geslaagd"), + ("Someone turns on privacy mode, exit", "Iemand schakelt privacymodus in, afsluiten"), + ("Unsupported", "Niet Ondersteund"), + ("Peer denied", "Peer geweigerd"), + ("Please install plugins", "Installeer plugins"), + ("Peer exit", "Peer afgesloten"), + ("Failed to turn off", "Uitschakelen mislukt"), + ("Turned off", "Uitgeschakeld"), + ("In privacy mode", "In privacymodus"), + ("Out privacy mode", "Uit privacymodus"), + ("Language", "Taal"), + ("Keep RustDesk background service", "RustDesk achtergronddienst behouden"), + ("Ignore Battery Optimizations", "Negeer Batterij Optimalisaties"), + ("android_open_battery_optimizations_tip", "Ga naar de volgende pagina met instellingen"), + ("Start on Boot", "Starten bij Opstarten"), + ("Start the screen sharing service on boot, requires special permissions", "Start de schermdelings service bij het opstarten, vereist speciale rechten"), + ("Connection not allowed", "Verbinding niet toegestaan"), + ("Legacy mode", "Verouderde modus"), + ("Map mode", "Map mode"), + ("Translate mode", "Vertaalmodus"), + ("Use permanent password", "Gebruik permanent wachtwoord"), + ("Use both passwords", "Gebruik beide wachtwoorden"), + ("Set permanent password", "Stel permanent wachtwoord in"), + ("Enable Remote Restart", "Schakel Herstart op afstand in"), + ("Allow remote restart", "Opnieuw Opstarten op afstand toestaan"), + ("Restart Remote Device", "Apparaat op afstand herstarten"), + ("Are you sure you want to restart", "Weet je zeker dat je wilt herstarten"), + ("Restarting Remote Device", "Apparaat op afstand herstarten"), + ("remote_restarting_tip", "Apparaat op afstand wordt opnieuw opgestart, sluit dit bericht en maak na een ogenblik opnieuw verbinding met het permanente wachtwoord."), + ("Copied", "Gekopieerd"), + ("Exit Fullscreen", "Volledig Scherm sluiten"), + ("Fullscreen", "Volledig Scherm"), + ("Mobile Actions", "Mobiele Acties"), + ("Select Monitor", "Selecteer Monitor"), + ("Control Actions", "Controleacties"), + ("Display Settings", "Beeldscherminstellingen"), + ("Ratio", "Verhouding"), + ("Image Quality", "Beeldkwaliteit"), + ("Scroll Style", "Scroll Stijl"), + ("Show Menubar", "Toon Menubalk"), + ("Hide Menubar", "Verberg Menubalk"), + ("Direct Connection", "Directe Verbinding"), + ("Relay Connection", "Relaisverbinding"), + ("Secure Connection", "Beveiligde Verbinding"), + ("Insecure Connection", "Onveilige Verbinding"), + ("Scale original", "Oorspronkelijke schaal"), + ("Scale adaptive", "Schaalaanpassing"), + ("General", "Algemeen"), + ("Security", "Beveiliging"), + ("Theme", "Thema"), + ("Dark Theme", "Donker Thema"), + ("Light Theme", "Lichte Thema"), + ("Dark", "Donker"), + ("Light", "Licht"), + ("Follow System", "Volg Systeem"), + ("Enable hardware codec", "Hardware codec inschakelen"), + ("Unlock Security Settings", "Beveiligingsinstellingen vrijgeven"), + ("Enable Audio", "Audio Inschakelen"), + ("Unlock Network Settings", "Netwerkinstellingen Vrijgeven"), + ("Server", "Server"), + ("Direct IP Access", "Directe IP toegang"), + ("Proxy", "Proxy"), + ("Apply", "Toepassen"), + ("Disconnect all devices?", "Alle apparaten uitschakelen?"), + ("Clear", "Wis"), + ("Audio Input Device", "Audio-invoerapparaat"), + ("Deny remote access", "Toegang op afstand weigeren"), + ("Use IP Whitelisting", "Gebruik een witte lijst van IP-adressen"), + ("Network", "Netwerk"), + ("Enable RDP", "Zet RDP aan"), + ("Pin menubar", "Menubalk Vastzetten"), + ("Unpin menubar", "Menubalk vrijmaken"), + ("Recording", "Opnemen"), + ("Directory", "Map"), + ("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"), + ("Change", "Wissel"), + ("Start session recording", "Start de sessieopname"), + ("Stop session recording", "Stop de sessieopname"), + ("Enable Recording Session", "Opnamesessie Activeren"), + ("Allow recording session", "Opnamesessie toestaan"), + ("Enable LAN Discovery", "LAN-detectie inschakelen"), + ("Deny LAN Discovery", "LAN-detectie Weigeren"), + ("Write a message", "Schrijf een bericht"), + ("Prompt", "Verzoek"), + ("Please wait for confirmation of UAC...", "Wacht op bevestiging van UAC..."), + ("elevated_foreground_window_tip", "Het momenteel geopende venster van de op afstand bediende computer vereist hogere rechten. Daarom is het momenteel niet mogelijk de muis en het toetsenbord te gebruiken. Vraag de gebruiker wiens computer u op afstand bedient om het venster te minimaliseren of de rechten te verhogen. Om dit probleem in de toekomst te voorkomen, wordt aanbevolen de software te installeren op de op afstand bediende computer."), + ("Disconnected", "Afgesloten"), + ("Other", "Andere"), + ("Confirm before closing multiple tabs", "Bevestig voordat u meerdere tabbladen sluit"), + ("Keyboard Settings", "Toetsenbord instellingen"), + ("Full Access", "Volledige Toegang"), + ("Screen Share", "Scherm Delen"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of een hogere versie."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander je OS."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Selecteer het scherm dat moet worden gedeeld (Bediening aan de kant van de peer)."), + ("Show RustDesk", "Toon RustDesk"), + ("This PC", "Deze PC"), + ("or", "of"), + ("Continue with", "Ga verder met"), + ("Elevate", "Verhoog"), + ("Zoom cursor", "Cursor Zoomen"), + ("Accept sessions via password", "Sessies accepteren via wachtwoord"), + ("Accept sessions via click", "Sessies accepteren via klik"), + ("Accept sessions via both", "Accepteer sessies via beide"), + ("Please wait for the remote side to accept your session request...", "Wacht tot de andere kant uw sessieverzoek accepteert..."), + ("One-time Password", "Eenmalig Wachtwoord"), + ("Use one-time password", "Gebruik een eenmalig Wachtwoord"), + ("One-time password length", "Eenmalig Wachtwoord lengre"), + ("Request access to your device", "Toegang tot uw toestel aanvragen"), + ("Hide connection management window", "Verberg het venster voor verbindingsbeheer"), + ("hide_cm_tip", "Dit kan alleen als de toegang via een permanent wachtwoord verloopt."), + ("wayland_experiment_tip", "Wayland ondersteuning is slechts experimenteel. Gebruik alsjeblieft X11 als je onbeheerde toegang nodig hebt."), + ("Right click to select tabs", "Rechts klikken om tabbladen te selecteren"), + ("Skipped", "Overgeslagen"), + ("Add to Address Book", "Toevoegen aan Adresboek"), + ("Group", "Groep"), + ("Search", "Zoek"), + ("Closed manually by web console", "Handmatig gesloten door webconsole"), + ("Local keyboard type", "Lokaal toetsenbord"), + ("Select local keyboard type", "Selecteer lokaal toetsenbord"), + ("software_render_tip", "Als u een NVIDIA grafische kaart hebt en het externe venster sluit onmiddellijk na verbinding, kan het helpen om het nieuwe stuurprogramma te installeren en te kiezen voor software rendering. Een software herstart is vereist."), + ("Always use software rendering", "Gebruik altijd software rendering"), + ("config_input", "config_invoer"), + ("config_microphone", "config_microfoon"), + ("request_elevation_tip", "U kunt ook meer rechten vragen als iemand aan de andere kant aanwezig is."), + ("Wait", "Wacht"), + ("Elevation Error", "Verhogingsfout"), + ("Ask the remote user for authentication", "Vraag de gebruiker op afstand om bevestiging"), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", "De gebruiker op afstand moet altijd bevestigen via het UAC-venster van de werkende RustDesk."), + ("Request Elevation", "Verzoek om meer rechten"), + ("wait_accept_uac_tip", "Wacht tot de gebruiker op afstand het UAC-dialoogvenster accepteert."), + ("Elevate successfully", "Succesvolle verhoging van privileges"), + ("uppercase", "Hoofdletter"), + ("lowercase", "kleine letter"), + ("digit", "cijfer"), + ("special character", "speciaal teken"), + ("length>=8", "lengte>=8"), + ("Weak", "Zwak"), + ("Medium", "Midelmatig"), + ("Strong", "Sterk"), + ("Switch Sides", "Wissel van kant"), + ("Please confirm if you want to share your desktop?", "bevestig als je je bureaublad wilt delen?"), + ("Display", "Weergave"), + ("Default View Style", "Standaard Weergave Stijl"), + ("Default Scroll Style", "Standaard Scroll Stijl"), + ("Default Image Quality", "Standaard Beeldkwaliteit"), + ("Default Codec", "Standaard Codec"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Andere Standaardopties"), + ("Voice call", "Spraakoproep"), + ("Text chat", "Tekst chat"), + ("Stop voice call", "Stop spraakoproep"), + ("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server. \nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."), + ("Reconnect", "Herverbinden"), + ("Codec", "Codec"), + ("Resolution", "Resolutie"), + ("No transfers in progress", "Geen overdrachten in uitvoering"), + ("Set one-time password length", "Stel de lengte van het eenmalige wachtwoord in"), + ("idd_driver_tip", "Installeer het virtuele beeldschermstuurprogramma dat wordt gebruikt wanneer u geen fysieke beeldschermen hebt."), + ("confirm_idd_driver_tip", "De optie om het virtuele displaystuurprogramma te installeren is ingeschakeld. Er wordt een testcertificaat geplaatst om het virtuele displaystuurprogramma te vertrouwen. Dit testcertificaat wordt alleen gebruikt om RustDesk-stuurprogramma's te vertrouwen."), + ("RDP Settings", "RDP Instellingen"), + ("Sort by", "Sorteren op"), + ("New Connection", "Nieuwe Verbinding"), + ("Restore", "Herstel"), + ("Minimize", "Minimaliseren"), + ("Maximize", "Maximaliseren"), + ("Your Device", "Uw Apparaat"), + ("empty_recent_tip", "Oeps, geen actuele situatie!\nTijd om een nieuwe te plannen."), + ("empty_favorite_tip", "Nog geen favoriete Station op afstand? Laat ons iemand vinden om mee te verbinden en voeg hem toe aan je favorieten!"), + ("empty_lan_tip", "Oh nee, het lijkt erop dat we nog geen extern station hebben ontdekt."), + ("empty_address_book_tip", "Oh jee, het lijkt erop dat er momenteel geen externe stations in je adresboek staan."), + ("eg: admin", "bijv: admin"), + ("Empty Username", "Gebruikersnaam Leeg"), + ("Empty Password", "Wachtwoord Leeg"), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 9593ffe54..30459ffa0 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Twój pulpit"), - ("desk_tip", "W celu zestawienia połączenia z tym urządzeniem należy poniższego ID i hasła."), + ("desk_tip", "W celu połączenia się z tym urządzeniem należy użyć poniższego ID i hasła"), ("Password", "Hasło"), ("Ready", "Gotowe"), ("Established", "Nawiązano"), @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schowek jest pusty"), ("Stop service", "Zatrzymaj usługę"), ("Change ID", "Zmień ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9 oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), ("Website", "Strona internetowa"), - ("About", "O"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), + ("About", "O aplikacji"), + ("Slogan_tip", "Tworzone z miłością w tym pełnym chaosu świecie!"), + ("Privacy Statement", "Oświadczenie o ochronie prywatności"), ("Mute", "Wycisz"), + ("Build Date", "Zbudowano"), + ("Version", "Wersja"), + ("Home", "Pulpit"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), ("Hardware Codec", "Kodek sprzętowy"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Serwer API"), ("invalid_http", "Nieprawidłowe żądanie http"), ("Invalid IP", "Nieprawidłowe IP"), - ("id_change_tip", "Nowy ID może być złożony z małych i dużych liter a-zA-z, cyfry 0-9 oraz _ (podkreślenie). Pierwszym znakiem powinna być litera a-zA-Z, a całe ID powinno składać się z 6 do 16 znaków."), ("Invalid format", "Nieprawidłowy format"), ("server_not_support", "Serwer nie obsługuje tej funkcji"), ("Not available", "Niedostępne"), @@ -96,7 +103,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Empty Directory", "Pusty katalog"), ("Not an empty directory", "Katalog nie jest pusty"), ("Are you sure you want to delete this file?", "Czy na pewno chcesz usunąć ten plik?"), - ("Are you sure you want to delete this empty directory?", "Czy na pewno chcesz usunać ten pusty katalog?"), + ("Are you sure you want to delete this empty directory?", "Czy na pewno chcesz usunąć ten pusty katalog?"), ("Are you sure you want to delete the file of this directory?", "Czy na pewno chcesz usunąć pliki z tego katalogu?"), ("Do this for all conflicts", "wykonaj dla wszystkich konfliktów"), ("This is irreversible!", "To jest nieodwracalne!"), @@ -118,7 +125,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Dobra jakość obrazu"), ("Balanced", "Zrównoważony"), ("Optimize reaction time", "Zoptymalizuj czas reakcji"), - ("Custom", "Własne"), + ("Custom", "Niestandardowe"), ("Show remote cursor", "Pokazuj zdalny kursor"), ("Show quality monitor", "Parametry połączenia"), ("Disable clipboard", "Wyłącz schowek"), @@ -138,10 +145,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Failed to make direct connection to remote desktop", "Nie udało się nawiązać bezpośredniego połączenia z pulpitem zdalnym"), ("Set Password", "Ustaw hasło"), ("OS Password", "Hasło systemu operacyjnego"), - ("install_tip", "RustDesk może nie działać poprawnie na maszynie zdalnej z przyczyn związanych z UAC. W celu uniknięcią problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), + ("install_tip", "RustDesk może nie działać poprawnie na maszynie zdalnej z przyczyn związanych z UAC. W celu uniknięcia problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), ("Click to upgrade", "Zaktualizuj"), ("Click to download", "Pobierz"), - ("Click to update", "Uaktualinij"), + ("Click to update", "Uaktualnij"), ("Configure", "Konfiguruj"), ("config_acc", "Konfiguracja konta"), ("config_screen", "Konfiguracja ekranu"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Ostrzeżenie"), ("Login screen using Wayland is not supported", "Ekran logowania korzystający z Wayland nie jest obsługiwany"), ("Reboot required", "Wymagany ponowne uruchomienie"), - ("Unsupported display server ", "Nieobsługiwany serwer wyświetlania"), + ("Unsupported display server", "Nieobsługiwany serwer wyświetlania"), ("x11 expected", "Wymagany jest X11"), ("Port", "Port"), ("Settings", "Ustawienia"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Połączenie zakończone ręcznie przez peer"), ("Enable remote configuration modification", "Włącz zdalną modyfikację konfiguracji"), ("Run without install", "Uruchom bez instalacji"), - ("Always connected via relay", "Zawsze połączony pośrednio"), + ("Connect via relay", ""), ("Always connect via relay", "Zawsze łącz pośrednio"), - ("whitelist_tip", "Zezwlaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), + ("whitelist_tip", "Zezwalaj na łączenie z tym komputerem tylko z adresów IP znajdujących się na białej liście"), ("Login", "Zaloguj"), + ("Verify", "Zweryfikuj"), + ("Remember me", "Zapamiętaj mnie"), + ("Trust this device", "Dodaj to urządzenie do zaufanych"), + ("Verification code", "Kod weryfikacyjny"), + ("verification_tip", "Nastąpiło logowanie z nowego urządzenia, kod weryfikacyjny został wysłany na podany adres email, wprowadź kod by kontynuować proces logowania"), ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), - ("Current Wayland display server is not supported", "Obecny serwer wyświetlania Wayland nie jest obsługiwany"), ("whitelist_sep", "Oddzielone przecinkiem, średnikiem, spacją lub w nowej linii"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj Tag"), @@ -227,7 +238,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Favorites", "Ulubione"), ("Add to Favorites", "Dodaj do ulubionych"), ("Remove from Favorites", "Usuń z ulubionych"), - ("Empty", "Pusty"), + ("Empty", "Pusto"), ("Invalid folder name", "Nieprawidłowa nazwa folderu"), ("Socks5 Proxy", "Socks5 Proxy"), ("Hostname", "Nazwa hosta"), @@ -259,12 +270,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Notatka"), ("Connection", "Połączenie"), ("Share Screen", "Udostępnij ekran"), - ("CLOSE", "Zamknij"), - ("OPEN", "Otwórz"), ("Chat", "Czat"), ("Total", "Łącznie"), - ("items", "elementy"), - ("Selected", "Zaznaczone"), + ("items", "elementów"), + ("Selected", "Zaznaczonych"), ("Screen Capture", "Przechwytywanie ekranu"), ("Input Control", "Kontrola wejścia"), ("Audio Capture", "Przechwytywanie dźwięku"), @@ -273,13 +282,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Do you accept?", "Akceptujesz?"), ("Open System Setting", "Otwórz ustawienia systemowe"), ("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"), - ("android_input_permission_tip1", "android_input_permission_tip1"), - ("android_input_permission_tip2", "android_input_permission_tip2"), - ("android_new_connection_tip", "android_new_connection_tip"), - ("android_service_will_start_tip", "android_service_will_start_tip"), - ("android_stop_service_tip", "android_stop_service_tip"), - ("android_version_audio_tip", "android_version_audio_tip"), - ("android_start_service_tip", "android_start_service_tip"), + ("android_input_permission_tip1", "Aby można było sterować Twoim urządzeniem za pomocą myszy lub dotyku, musisz zezwolić RustDesk na korzystanie z usługi \"Ułatwienia dostępu\"."), + ("android_input_permission_tip2", "Przejdź do następnej strony ustawień systemowych, znajdź i wejdź w [Zainstalowane usługi], włącz usługę [RustDesk Input]."), + ("android_new_connection_tip", "Otrzymano nowe żądanie zdalnego dostępu, które chce przejąć kontrolę nad Twoim urządzeniem."), + ("android_service_will_start_tip", "Włączenie opcji „Przechwytywanie ekranu” spowoduje automatyczne uruchomienie usługi, umożliwiając innym urządzeniom żądanie połączenia z Twoim urządzeniem."), + ("android_stop_service_tip", "Zamknięcie usługi spowoduje automatyczne zamknięcie wszystkich nawiązanych połączeń."), + ("android_version_audio_tip", "Bieżąca wersja systemu Android nie obsługuje przechwytywania dźwięku, zaktualizuj system do wersji Android 10 lub nowszej."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Konto"), ("Overwrite", "Nadpisz"), ("This file exists, skip or overwrite this file?", "Ten plik istnieje, pominąć czy nadpisać ten plik?"), @@ -300,7 +310,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Language", "Język"), ("Keep RustDesk background service", "Zachowaj usługę RustDesk w tle"), ("Ignore Battery Optimizations", "Ignoruj optymalizację baterii"), - ("android_open_battery_optimizations_tip", "android_open_battery_optimizations_tip"), + ("android_open_battery_optimizations_tip", "Jeśli chcesz wyłączyć tę funkcję, przejdź do następnej strony ustawień aplikacji RustDesk, znajdź i wprowadź [Bateria], odznacz [Bez ograniczeń]"), + ("Start on Boot", "Autostart"), + ("Start the screen sharing service on boot, requires special permissions", "Uruchom usługę udostępniania ekranu podczas startu, wymaga specjalnych uprawnień"), ("Connection not allowed", "Połączenie niedozwolone"), ("Legacy mode", "Tryb kompatybilności wstecznej (legacy)"), ("Map mode", "Tryb mapowania"), @@ -326,7 +338,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scroll Style", "Styl przewijania"), ("Show Menubar", "Pokaż pasek menu"), ("Hide Menubar", "Ukryj pasek menu"), - ("Direct Connection", "Połącznie bezpośrednie"), + ("Direct Connection", "Połączenie bezpośrednie"), ("Relay Connection", "Połączenie przez bramkę"), ("Secure Connection", "Połączenie szyfrowane"), ("Insecure Connection", "Połączenie nieszyfrowane"), @@ -334,20 +346,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skaluj adaptacyjnie"), ("General", "Ogólne"), ("Security", "Zabezpieczenia"), - ("Account", "Konto"), ("Theme", "Motyw"), ("Dark Theme", "Ciemny motyw"), + ("Light Theme", ""), ("Dark", "Ciemny"), ("Light", "Jasny"), - ("Follow System", "Zgodne z systemem"), - ("Enable hardware codec", "Włącz wsparcie sprzętowe dla kodeków"), - ("Unlock Security Settings", "Odblokuj Ustawienia Zabezpieczeń"), - ("Enable Audio", "Włącz Dźwięk"), + ("Follow System", "Zgodny z systemem"), + ("Enable hardware codec", "Włącz akcelerację sprzętową kodeków"), + ("Unlock Security Settings", "Odblokuj ustawienia zabezpieczeń"), + ("Enable Audio", "Włącz dźwięk"), ("Unlock Network Settings", "Odblokuj ustawienia Sieciowe"), ("Server", "Serwer"), - ("Direct IP Access", "Bezpośredni Adres IP"), + ("Direct IP Access", "Bezpośredni adres IP"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Zastosuj"), ("Disconnect all devices?", "Czy rozłączyć wszystkie urządzenia?"), ("Clear", "Wyczyść"), @@ -358,29 +369,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable RDP", "Włącz RDP"), ("Pin menubar", "Przypnij pasek menu"), ("Unpin menubar", "Odepnij pasek menu"), - ("Recording", "Trwa nagrywanie"), + ("Recording", "Nagrywanie"), ("Directory", "Katalog"), ("Automatically record incoming sessions", "Automatycznie nagrywaj sesje przychodzące"), ("Change", "Zmień"), ("Start session recording", "Zacznij nagrywać sesję"), ("Stop session recording", "Zatrzymaj nagrywanie sesji"), - ("Enable Recording Session", "Włącz Nagrywanie Sesji"), + ("Enable Recording Session", "Włącz nagrywanie Sesji"), ("Allow recording session", "Zezwól na nagrywanie sesji"), ("Enable LAN Discovery", "Włącz wykrywanie urządzenia w sieci LAN"), ("Deny LAN Discovery", "Zablokuj wykrywanie urządzenia w sieci LAN"), ("Write a message", "Napisz wiadomość"), ("Prompt", "Monit"), - ("Please wait for confirmation of UAC...", "Oczekuje potwierdzenia ustawień UAC"), - ("elevated_foreground_window_tip", ""), + ("Please wait for confirmation of UAC...", "Poczekaj na potwierdzenie uprawnień UAC"), + ("elevated_foreground_window_tip", "Aktualne okno zdalnego urządzenia wymaga wyższych uprawnień by poprawnie działać, chwilowo niemożliwym jest korzystanie z myszy i klawiatury. Możesz poprosić zdalnego użytkownika o minimalizację okna, lub nacisnąć przycisk podniesienia uprawnień w oknie zarządzania połączeniami. By uniknąć tego problemu, rekomendujemy instalację oprogramowania na urządzeniu zdalnym."), ("Disconnected", "Rozłączone"), ("Other", "Inne"), ("Confirm before closing multiple tabs", "Potwierdź przed zamknięciem wielu kart"), ("Keyboard Settings", "Ustawienia klawiatury"), - ("Custom", "Własne"), ("Full Access", "Pełny dostęp"), ("Screen Share", "Udostępnianie ekranu"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland wymaga Ubuntu 21.04 lub nowszego."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga wyższej wersji dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland wymaga nowszej dystrybucji Linuksa. Wypróbuj pulpit X11 lub zmień system operacyjny."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Wybierz ekran do udostępnienia (działaj po stronie równorzędnej)."), ("Show RustDesk", "Pokaż RustDesk"), @@ -388,7 +398,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "albo"), ("Continue with", "Kontynuuj z"), ("Elevate", "Uzyskaj uprawnienia"), - ("Zoom cursor", "Zoom kursora"), + ("Zoom cursor", "Powiększenie kursora"), ("Accept sessions via password", "Uwierzytelnij sesję używając hasła"), ("Accept sessions via click", "Uwierzytelnij sesję poprzez kliknięcie"), ("Accept sessions via both", "Uwierzytelnij sesję za pomocą obu sposobów"), @@ -398,15 +408,80 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-time password length", "Długość hasła jednorazowego"), ("Request access to your device", "Żądanie dostępu do Twojego urządzenia"), ("Hide connection management window", "Ukryj okno zarządzania połączeniem"), - ("hide_cm_tip", ""), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", ""), - ("Skipped", ""), + ("hide_cm_tip", "Pozwalaj na ukrycie tylko gdy akceptujesz sesje za pośrednictwem hasła i używasz hasła permanentnego"), + ("wayland_experiment_tip", "Wsparcie dla Wayland jest niekompletne, użyj X11 jeżeli chcesz korzystać z dostępu nienadzorowanego"), + ("Right click to select tabs", "Kliknij prawym przyciskiem myszy by wybrać zakładkę"), + ("Skipped", "Pominięte"), ("Add to Address Book", "Dodaj do Książki Adresowej"), - ("Group", "Grypy"), + ("Group", "Grupy"), ("Search", "Szukaj"), - ("Closed manually by the web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Closed manually by web console", "Zakończone manualnie z konsoli Web"), + ("Local keyboard type", "Lokalny typ klawiatury"), + ("Select local keyboard type", "Wybierz lokalny typ klawiatury"), + ("software_render_tip", "Jeżeli posiadasz kartę graficzną Nvidia i okno zamyka się natychmiast po nawiązaniu połączenia, instalacja sterownika nouveau i wybór renderowania programowego mogą pomóc. Restart aplikacji jest wymagany."), + ("Always use software rendering", "Zawsze używaj renderowania programowego"), + ("config_input", "By kontrolować zdalne urządzenie przy pomocy klawiatury, musisz udzielić aplikacji RustDesk uprawnień do \"Urządzeń Wejściowych\"."), + ("config_microphone", ""), + ("request_elevation_tip", "Możesz poprosić o podniesienie uprawnień jeżeli ktoś posiada dostęp do zdalnego urządzenia."), + ("Wait", "Czekaj"), + ("Elevation Error", "Błąd przy podnoszeniu uprawnień"), + ("Ask the remote user for authentication", "Poproś użytkownika zdalnego o uwierzytelnienie"), + ("Choose this if the remote account is administrator", "Wybierz to jeżeli zdalne konto jest administratorem"), + ("Transmit the username and password of administrator", "Prześlij nazwę użytkownika i hasło administratora"), + ("still_click_uac_tip", "Nadal wymaga od zdalnego użytkownika potwierdzenia uprawnień UAC."), + ("Request Elevation", "Poproś o podniesienie uprawnień"), + ("wait_accept_uac_tip", "Prosimy czekać aż zdalny użytkownik potwierdzi uprawnienia UAC."), + ("Elevate successfully", "Pomyślnie podniesiono uprawnienia"), + ("uppercase", "wielkie litery"), + ("lowercase", "małe litery"), + ("digit", "cyfra"), + ("special character", "znak specjalny"), + ("length>=8", "długość>=8"), + ("Weak", "Słabe"), + ("Medium", "Średnie"), + ("Strong", "Mocne"), + ("Switch Sides", "Zamień Strony"), + ("Please confirm if you want to share your desktop?", "Czy na pewno chcesz udostępnić swój ekran?"), + ("Display", "Wyświetlanie"), + ("Default View Style", "Domyślny styl wyświetlania"), + ("Default Scroll Style", "Domyślny styl przewijania"), + ("Default Image Quality", "Domyślna jakość obrazu"), + ("Default Codec", "Dokyślny kodek"), + ("Bitrate", "Bitrate"), + ("FPS", "FPS"), + ("Auto", "Auto"), + ("Other Default Options", "Inne opcje domyślne"), + ("Voice call", "Rozmowa głosowa"), + ("Text chat", "Chat tekstowy"), + ("Stop voice call", "Rozłącz"), + ("relay_hint_tip", "Bezpośrednie połączenie może nie być możliwe, możesz spróbować połączyć się przez serwer przekazujący. \nDodatkowo, jeśli chcesz użyć serwera przekazującego przy pierwszej próbie, możesz dodać sufiks \"/r\" do identyfikatora lub wybrać opcję \"Zawsze łącz przez serwer przekazujący\" na karcie peer-ów."), + ("Reconnect", "Połącz ponownie"), + ("Codec", "Kodek"), + ("Resolution", "Rozdzielczość"), + ("No transfers in progress", "Brak transferów w toku"), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index dcadfb0e7..45226dec6 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Silenciar"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), ("Hardware Codec", ""), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor da API"), ("invalid_http", "deve iniciar com http:// ou https://"), ("Invalid IP", "IP inválido"), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Invalid format", "Formato inválido"), ("server_not_support", "Ainda não suportado pelo servidor"), ("Not available", "Indisponível"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Aviso"), ("Login screen using Wayland is not supported", "Tela de Login com Wayland não é suportada"), ("Reboot required", "Reinicialização necessária"), - ("Unsupported display server ", "Servidor de display não suportado"), + ("Unsupported display server", "Servidor de display não suportado"), ("x11 expected", "x11 em falha"), ("Port", ""), ("Settings", "Configurações"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fechada manualmente pelo destino"), ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), ("Run without install", "Executar sem instalar"), - ("Always connected via relay", "Sempre conectado via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs na whitelist podem me acessar"), ("Login", "Login"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Procurar ID"), - ("Current Wayland display server is not supported", "Servidor de display Wayland atual não é suportado"), ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), ("Add ID", "Adicionar ID"), ("Add Tag", "Adicionar Tag"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Nota"), ("Connection", "Ligação"), ("Share Screen", "Partilhar ecrã"), - ("CLOSE", "FECHAR"), - ("OPEN", "ABRIR"), ("Chat", "Conversar"), ("Total", "Total"), ("items", "itens"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Activar a Captura de Ecran irá automaticamente inicializar o serviço, permitindo que outros dispositivos solicitem uma ligação deste dispositivo."), ("android_stop_service_tip", "Fechar o serviço irá automaticamente fechar todas as ligações estabelecidas."), ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor actualize para o Android 10 ou maior."), - ("android_start_service_tip", "Toque [Iniciar Serviço] ou abra a permissão [Captura de Ecran] para iniciar o serviço de partilha de ecran."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", ""), ("Overwrite", "Substituir"), ("This file exists, skip or overwrite this file?", "Este ficheiro já existe, ignorar ou substituir este ficheiro?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Manter o serviço RustDesk em funcionamento"), ("Ignore Battery Optimizations", "Ignorar optimizações de Bateria"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Ligação não autorizada"), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptável"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), + ("Light Theme", ""), ("Dark", ""), ("Light", ""), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Outro"), ("Confirm before closing multiple tabs", "Confirme antes de fechar vários separadores"), ("Keyboard Settings", "Configurações do teclado"), - ("Custom", ""), ("Full Access", "Controlo total"), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 48e964f7d..8cf8ca6ef 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "A área de transferência está vazia"), ("Stop service", "Parar serviço"), ("Change ID", "Alterar ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Desativar som"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), ("Hardware Codec", "Codec de hardware"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Servidor da API"), ("invalid_http", "deve iniciar com http:// ou https://"), ("Invalid IP", "IP inválido"), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Invalid format", "Formato inválido"), ("server_not_support", "Ainda não suportado pelo servidor"), ("Not available", "Indisponível"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Aviso"), ("Login screen using Wayland is not supported", "Tela de Login utilizando Wayland não é suportada"), ("Reboot required", "Reinicialização necessária"), - ("Unsupported display server ", "Servidor de display não suportado"), + ("Unsupported display server", "Servidor de display não suportado"), ("x11 expected", "x11 esperado"), ("Port", "Porta"), ("Settings", "Configurações"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Fechada manualmente pelo parceiro"), ("Enable remote configuration modification", "Habilitar modificações de configuração remotas"), ("Run without install", "Executar sem instalar"), - ("Always connected via relay", "Sempre conectado via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Sempre conectar via relay"), ("whitelist_tip", "Somente IPs confiáveis podem me acessar"), ("Login", "Login"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Sair"), ("Tags", "Tags"), ("Search ID", "Pesquisar ID"), - ("Current Wayland display server is not supported", "Servidor de display Wayland atual não é suportado"), ("whitelist_sep", "Separado por vírcula, ponto-e-vírgula, espaços ou nova linha"), ("Add ID", "Adicionar ID"), ("Add Tag", "Adicionar Tag"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Nota"), ("Connection", "Conexão"), ("Share Screen", "Compartilhar Tela"), - ("CLOSE", "FECHAR"), - ("OPEN", "ABRIR"), ("Chat", "Chat"), ("Total", "Total"), ("items", "itens"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Habilitar a Captura de Tela irá automaticamente inicalizar o serviço, permitindo que outros dispositivos solicitem uma conexão deste dispositivo."), ("android_stop_service_tip", "Fechar o serviço irá automaticamente fechar todas as conexões estabelecidas."), ("android_version_audio_tip", "A versão atual do Android não suporta captura de áudio, por favor atualize para o Android 10 ou superior."), - ("android_start_service_tip", "Toque [Iniciar Serviço] ou abra a permissão [Captura de Tela] para iniciar o serviço de compartilhamento de tela."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Conta"), ("Overwrite", "Substituir"), ("This file exists, skip or overwrite this file?", "Este arquivo existe, pular ou substituir este arquivo?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Manter o serviço do RustDesk executando em segundo plano"), ("Ignore Battery Optimizations", "Ignorar otimizações de bateria"), ("android_open_battery_optimizations_tip", "Abrir otimizações de bateria"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Conexão não permitida"), ("Legacy mode", "Modo legado"), ("Map mode", "Modo mapa"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptada"), ("General", "Geral"), ("Security", "Segurança"), - ("Account", "Conta"), ("Theme", "Tema"), ("Dark Theme", "Tema escuro"), + ("Light Theme", ""), ("Dark", "Escuro"), ("Light", "Claro"), ("Follow System", "Seguir sistema"), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Servidor"), ("Direct IP Access", "Acesso direto por IP"), ("Proxy", "Proxy"), - ("Port", "Porta"), ("Apply", "Aplicar"), ("Disconnect all devices?", "Desconectar todos os dispositivos?"), ("Clear", "Limpar"), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Outro"), ("Confirm before closing multiple tabs", "Confirmar antes de fechar múltiplas abas"), ("Keyboard Settings", "Configurações de teclado"), - ("Custom", "Personalizado"), ("Full Access", "Acesso completo"), ("Screen Share", "Compartilhamento de tela"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland requer Ubuntu 21.04 ou versão superior."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs new file mode 100644 index 000000000..e81fa0cfd --- /dev/null +++ b/src/lang/ro.rs @@ -0,0 +1,487 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stare"), + ("Your Desktop", "Desktopul tău"), + ("desk_tip", "Desktopul tău poate fi accesat folosind ID-ul și parola de mai jos."), + ("Password", "Parola"), + ("Ready", "Pregătit"), + ("Established", "Stabilit"), + ("connecting_status", "În curs de conectare la rețeaua RustDesk..."), + ("Enable Service", "Activează serviciu"), + ("Start Service", "Pornește serviciu"), + ("Service is running", "Serviciul este în curs de executare..."), + ("Service is not running", "Serviciul nu funcționează"), + ("not_ready_status", "Nepregătit. Verifică conexiunea la rețea."), + ("Control Remote Desktop", "Controlează desktop-ul la distanță"), + ("Transfer File", "Transferă fișier"), + ("Connect", "Conectează-te"), + ("Recent Sessions", "Sesiuni recente"), + ("Address Book", "Agendă"), + ("Confirmation", "Confirmare"), + ("TCP Tunneling", "Tunel TCP"), + ("Remove", "Elimină"), + ("Refresh random password", "Actualizează parolă aleatorie"), + ("Set your own password", "Setează propria parolă"), + ("Enable Keyboard/Mouse", "Activează control tastatură/mouse"), + ("Enable Clipboard", "Activează clipboard"), + ("Enable File Transfer", "Activează transfer fișiere"), + ("Enable TCP Tunneling", "Activează tunel TCP"), + ("IP Whitelisting", "Listă de IP-uri autorizate"), + ("ID/Relay Server", "Server de ID/retransmisie"), + ("Import Server Config", "Importă configurație server"), + ("Export Server Config", "Exportă configurație server"), + ("Import server configuration successfully", "Configurație server importată cu succes"), + ("Export server configuration successfully", "Configurație server exportată cu succes"), + ("Invalid server configuration", "Configurație server nevalidă"), + ("Clipboard is empty", "Clipboard gol"), + ("Stop service", "Oprește serviciu"), + ("Change ID", "Schimbă ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 și 16 caractere."), + ("Website", "Site web"), + ("About", "Despre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Fără sunet"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), + ("Audio Input", "Intrare audio"), + ("Enhancements", "Îmbunătățiri"), + ("Hardware Codec", "Codec hardware"), + ("Adaptive Bitrate", "Rată de biți adaptabilă"), + ("ID Server", "Server de ID"), + ("Relay Server", "Server de retransmisie"), + ("API Server", "Server API"), + ("invalid_http", "Trebuie să înceapă cu http:// sau https://"), + ("Invalid IP", "IP nevalid"), + ("Invalid format", "Format nevalid"), + ("server_not_support", "Încă nu este compatibil cu serverul"), + ("Not available", "Indisponibil"), + ("Too frequent", "Modificat prea frecvent"), + ("Cancel", "Anulează"), + ("Skip", "Omite"), + ("Close", "Închide"), + ("Retry", "Reîncearcă"), + ("OK", "OK"), + ("Password Required", "Parolă necesară"), + ("Please enter your password", "Introdu parola"), + ("Remember password", "Memorează parola"), + ("Wrong Password", "Parolă incorectă"), + ("Do you want to enter again?", "Vrei să intri din nou?"), + ("Connection Error", "Eroare de conexiune"), + ("Error", "Eroare"), + ("Reset by the peer", "Conexiunea a fost închisă de dispozitivul pereche"), + ("Connecting...", "Conectare..."), + ("Connection in progress. Please wait.", "Conectare în curs. Te rugăm așteaptă."), + ("Please try 1 minute later", "Reîncearcă într-un minut"), + ("Login Error", "Eroare de autentificare"), + ("Successful", "Succes"), + ("Connected, waiting for image...", "Conectat, se așteaptă transmiterea imaginii..."), + ("Name", "Denumire"), + ("Type", "Tip"), + ("Modified", "Modificat"), + ("Size", "Dimensiune"), + ("Show Hidden Files", "Afișează fișiere ascunse"), + ("Receive", "Acceptă"), + ("Send", "Trimite"), + ("Refresh File", "Actualizează fișier"), + ("Local", "Local"), + ("Remote", "La distanță"), + ("Remote Computer", "Computer la distanță"), + ("Local Computer", "Computer local"), + ("Confirm Delete", "Confirmă ștergerea"), + ("Delete", "Șterge"), + ("Properties", "Caracteristici"), + ("Multi Select", "Alegere multiplă"), + ("Select All", "Selectează tot"), + ("Unselect All", "Deselectează tot"), + ("Empty Directory", "Director gol"), + ("Not an empty directory", "Directorul nu este gol"), + ("Are you sure you want to delete this file?", "Sigur vrei să ștergi acest fișier?"), + ("Are you sure you want to delete this empty directory?", "Sigur vrei să ștergi acest director gol?"), + ("Are you sure you want to delete the file of this directory?", "Sigur vrei să ștergi fișierul din acest director?"), + ("Do this for all conflicts", "Aplică pentru toate conflictele"), + ("This is irreversible!", "Această acțiune este ireversibilă!"), + ("Deleting", "În curs de ștergere..."), + ("files", "fișier"), + ("Waiting", "În așteptare..."), + ("Finished", "Finalizat"), + ("Speed", "Viteză"), + ("Custom Image Quality", "Setează calitatea imaginii"), + ("Privacy mode", "Mod privat"), + ("Block user input", "Blochează utilizator"), + ("Unblock user input", "Deblochează utilizator"), + ("Adjust Window", "Ajustează fereastra"), + ("Original", "Dimensiune originală"), + ("Shrink", "Micșorează"), + ("Stretch", "Extinde"), + ("Scrollbar", "Bară de derulare"), + ("ScrollAuto", "Derulare automată"), + ("Good image quality", "Calitate bună a imaginii"), + ("Balanced", "Calitate normală a imaginii"), + ("Optimize reaction time", "Optimizează timpul de reacție"), + ("Custom", "Personalizat"), + ("Show remote cursor", "Afișează cursor la distanță"), + ("Show quality monitor", "Afișează indicator de calitate"), + ("Disable clipboard", "Dezactivează clipboard"), + ("Lock after session end", "Blochează după deconectare"), + ("Insert", "Introdu"), + ("Insert Lock", "Blochează computer"), + ("Refresh", "Reîmprospătează"), + ("ID does not exist", "ID neexistent"), + ("Failed to connect to rendezvous server", "Conectare la server rendezvous eșuată"), + ("Please try later", "Încearcă mai târziu"), + ("Remote desktop is offline", "Desktopul la distanță este offline"), + ("Key mismatch", "Nepotrivire chei"), + ("Timeout", "Conexiune expirată"), + ("Failed to connect to relay server", "Conectare la server de retransmisie eșuată"), + ("Failed to connect via rendezvous server", "Conectare prin intermediul serverului rendezvous eșuată"), + ("Failed to connect via relay server", "Conectare prin intermediul serverului de retransmisie eșuată"), + ("Failed to make direct connection to remote desktop", "Imposibil de stabilit o conexiune directă cu desktopul la distanță"), + ("Set Password", "Setează parola"), + ("OS Password", "Parolă OS"), + ("install_tip", "Din cauza restricțiilor UAC, e posibil ca RustDesk să nu funcționeze corespunzător. Pentru a evita acest lucru, fă clic pe butonul de mai jos pentru a instala RustDesk."), + ("Click to upgrade", "Fă clic pentru a face upgrade"), + ("Click to download", "Fă clic pentru a descărca"), + ("Click to update", "Fă clic pentru a actualiza"), + ("Configure", "Configurează"), + ("config_acc", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Accesibilitate."), + ("config_screen", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Înregistrare ecran."), + ("Installing ...", "Instalare în curs..."), + ("Install", "Instalează"), + ("Installation", "Instalare"), + ("Installation Path", "Cale de instalare"), + ("Create start menu shortcuts", "Creează comenzi rapide în meniul Start"), + ("Create desktop icon", "Creează pictogramă pe desktop"), + ("agreement_tip", "Începerea procesului de instalare înseamnă acceptarea acordului de licență."), + ("Accept and Install", "Acceptă și instalează"), + ("End-user license agreement", "Acord de licență pentru utilizatorul final"), + ("Generating ...", "Se generează..."), + ("Your installation is lower version.", "Versiunea instalată este una inferioară."), + ("not_close_tcp_tip", "Nu închide această fereastră în timp ce folosești tunelul"), + ("Listening ...", "În așteptarea conexiunii tunel..."), + ("Remote Host", "Gazdă la distanță"), + ("Remote Port", "Port la distanță"), + ("Action", "Acțiune"), + ("Add", "Adaugă"), + ("Local Port", "Port local"), + ("Local Address", "Adresă locală"), + ("Change Local Port", "Schimbă port local"), + ("setup_server_tip", "Pentru o conexiune mai rapidă, îți poți configura propriul server."), + ("Too short, at least 6 characters.", "Prea scurt; trebuie cel puțin 6 caractere."), + ("The confirmation is not identical.", "Cele două intrări nu corespund."), + ("Permissions", "Permisiuni"), + ("Accept", "Acceptă"), + ("Dismiss", "Respinge"), + ("Disconnect", "Deconectează-te"), + ("Allow using keyboard and mouse", "Permite utilizarea tastaturii și mouse-ului"), + ("Allow using clipboard", "Permite utilizarea clipboardului"), + ("Allow hearing sound", "Permite auzirea sunetului"), + ("Allow file copy and paste", "Permite copierea/lipirea fișierelor"), + ("Connected", "Conectat"), + ("Direct and encrypted connection", "Conexiune directă criptată"), + ("Relayed and encrypted connection", "Conexiune retransmisă criptată"), + ("Direct and unencrypted connection", "Conexiune directă necriptată"), + ("Relayed and unencrypted connection", "Conexiune retransmisă necriptată"), + ("Enter Remote ID", "Introdu ID-ul dispozitivului la distanță"), + ("Enter your password", "Introdu parola"), + ("Logging in...", "Se conectează..."), + ("Enable RDP session sharing", "Activează partajarea sesiunii RDP"), + ("Auto Login", "Conectare automată (valid doar dacă funcția de Blocare după deconectare este activă)"), + ("Enable Direct IP Access", "Activează accesul direct cu IP"), + ("Rename", "Redenumește"), + ("Space", "Spațiu"), + ("Create Desktop Shortcut", "Creează comandă rapidă de desktop"), + ("Change Path", "Schimbă calea"), + ("Create Folder", "Creează folder"), + ("Please enter the folder name", "Introdu numele folderului"), + ("Fix it", "Repară"), + ("Warning", "Avertisment"), + ("Login screen using Wayland is not supported", "Ecranele de conectare care folosesc Wayland nu sunt acceptate"), + ("Reboot required", "Repornire necesară"), + ("Unsupported display server", "Tipul de server de afișaj nu este acceptat"), + ("x11 expected", "E necesar X11"), + ("Port", "Port"), + ("Settings", "Setări"), + ("Username", " Nume de utilizator"), + ("Invalid port", "Port nevalid"), + ("Closed manually by the peer", "Închis manual de dispozitivul pereche"), + ("Enable remote configuration modification", "Activează modificarea configurației de la distanță"), + ("Run without install", "Rulează fără instalare"), + ("Connect via relay", ""), + ("Always connect via relay", "Se conectează mereu prin retransmisie"), + ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"), + ("Login", "Conectare"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "Deconectare"), + ("Tags", "Etichetare"), + ("Search ID", "Caută după ID"), + ("whitelist_sep", "Poți folosi ca separator virgula, punctul și virgula, spațiul sau linia nouă"), + ("Add ID", "Adaugă ID"), + ("Add Tag", "Adaugă etichetă"), + ("Unselect all tags", "Deselectează toate etichetele"), + ("Network error", "Eroare de rețea"), + ("Username missed", "Lipsește numele de utilizator"), + ("Password missed", "Lipsește parola"), + ("Wrong credentials", "Nume sau parolă greșită"), + ("Edit Tag", "Modifică etichetă"), + ("Unremember Password", "Uită parola"), + ("Favorites", "Favorite"), + ("Add to Favorites", "Adaugă la Favorite"), + ("Remove from Favorites", "Șterge din Favorite"), + ("Empty", "Gol"), + ("Invalid folder name", "Denumire folder nevalidă"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Hostname", "Nume gazdă"), + ("Discovered", "Descoperite"), + ("install_daemon_tip", "Pentru executare la pornirea sistemului, instalează serviciul de sistem."), + ("Remote ID", "ID dispozitiv la distanță"), + ("Paste", "Lipește"), + ("Paste here?", "Lipește aici?"), + ("Are you sure to close the connection?", "Sigur vrei să închizi conexiunea?"), + ("Download new version", "Descarcă noua versiune"), + ("Touch mode", "Mod tactil"), + ("Mouse mode", "Mod mouse"), + ("One-Finger Tap", "Apasă cu un deget"), + ("Left Mouse", "Clic stânga"), + ("One-Long Tap", "Apasă lung"), + ("Two-Finger Tap", "Apasă cu două degete"), + ("Right Mouse", "Clic dreapta"), + ("One-Finger Move", "Mișcă cu un deget"), + ("Double Tap & Move", "Apasă dublu și mișcă"), + ("Mouse Drag", "Tragere mouse"), + ("Three-Finger vertically", "Trei degete vertical"), + ("Mouse Wheel", "Rotiță mouse"), + ("Two-Finger Move", "Mișcă cu două degete"), + ("Canvas Move", "Mută ecran"), + ("Pinch to Zoom", "Apropie degetele pentru zoom"), + ("Canvas Zoom", "Zoom ecran"), + ("Reset canvas", "Reinițializează ecranul"), + ("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"), + ("Note", "Reține"), + ("Connection", "Conexiune"), + ("Share Screen", "Partajează ecran"), + ("Chat", "Discută"), + ("Total", "Total"), + ("items", "elemente"), + ("Selected", "Selectat"), + ("Screen Capture", "Captură ecran"), + ("Input Control", "Control intrări"), + ("Audio Capture", "Captură audio"), + ("File Connection", "Conexiune fișier"), + ("Screen Connection", "Conexiune ecran"), + ("Do you accept?", "Accepți?"), + ("Open System Setting", "Deschide setări sistem"), + ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"), + ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilize serviciul Accesibilitate."), + ("android_input_permission_tip2", "Accesează următoarea pagină din Setări, caută și deschide [Aplicații instalate] și activează serviciul [RustDesk Input]."), + ("android_new_connection_tip", "Ai primit o nouă solicitare de control pentru dispozitivul actual."), + ("android_service_will_start_tip", "Activarea setării Captură ecran va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."), + ("android_stop_service_tip", "Închiderea serviciului va închide automat toate conexiunile stabilite."), + ("android_version_audio_tip", "Versiunea actuală de Android nu suportă captura audio. Fă upgrade la Android 10 sau la o versiune superioară."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", "Cont"), + ("Overwrite", "Suprascrie"), + ("This file exists, skip or overwrite this file?", "Fișier deja existent. Omite sau suprascrie?"), + ("Quit", "Ieși"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Ajutor"), + ("Failed", "Nereușit"), + ("Succeeded", "Reușit"), + ("Someone turns on privacy mode, exit", "Cineva activează modul privat, ieși din"), + ("Unsupported", "Neacceptat"), + ("Peer denied", "Dispozitiv pereche refuzat"), + ("Please install plugins", "Instalează pluginuri"), + ("Peer exit", "Ieșire dispozitiv pereche"), + ("Failed to turn off", "Dezactivare nereușită"), + ("Turned off", "Închis"), + ("In privacy mode", "În modul privat"), + ("Out privacy mode", "Ieșit din modul privat"), + ("Language", "Limbă"), + ("Keep RustDesk background service", "Rulează serviciul RustDesk în fundal"), + ("Ignore Battery Optimizations", "Ignoră optimizările de baterie"), + ("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", "Conexiune neautoriztă"), + ("Legacy mode", "Mod legacy"), + ("Map mode", "Mod hartă"), + ("Translate mode", "Mod traducere"), + ("Use permanent password", "Folosește parola permanentă"), + ("Use both passwords", "Folosește parola unică și cea permanentă"), + ("Set permanent password", "Setează parola permanentă"), + ("Enable Remote Restart", "Activează repornirea la distanță"), + ("Allow remote restart", "Permite repornirea la distanță"), + ("Restart Remote Device", "Repornește dispozivul la distanță"), + ("Are you sure you want to restart", "Sigur vrei să repornești dispozitivul?"), + ("Restarting Remote Device", "Se repornește dispozitivul la distanță"), + ("remote_restarting_tip", "Dispozitivul este în curs de repornire. Închide acest mesaj și reconectează-te cu parola permanentă după un timp."), + ("Copied", "Copiat"), + ("Exit Fullscreen", "Ieși din modul ecran complet"), + ("Fullscreen", "Ecran complet"), + ("Mobile Actions", "Acțiuni mobile"), + ("Select Monitor", "Selectează monitor"), + ("Control Actions", "Acțiuni de control"), + ("Display Settings", "Setări afișaj"), + ("Ratio", "Raport"), + ("Image Quality", "Calitate imagine"), + ("Scroll Style", "Stil de derulare"), + ("Show Menubar", "Arată bara de meniu"), + ("Hide Menubar", "Ascunde bara de meniu"), + ("Direct Connection", "Conexiune directă"), + ("Relay Connection", "Conexiune prin retransmisie"), + ("Secure Connection", "Conexiune securizată"), + ("Insecure Connection", "Conexiune nesecurizată"), + ("Scale original", "Scală originală"), + ("Scale adaptive", "Scală adaptivă"), + ("General", "General"), + ("Security", "Securitate"), + ("Theme", "Temă"), + ("Dark Theme", "Temă întunecată"), + ("Light Theme", ""), + ("Dark", "Întunecat"), + ("Light", "Luminos"), + ("Follow System", "Urmărește sistem"), + ("Enable hardware codec", "Activează codec hardware"), + ("Unlock Security Settings", "Deblochează setări de securitate"), + ("Enable Audio", "Activează audio"), + ("Unlock Network Settings", "Deblochează setări de rețea"), + ("Server", "Server"), + ("Direct IP Access", "Acces direct IP"), + ("Proxy", "Proxy"), + ("Apply", "Aplică"), + ("Disconnect all devices?", "Vrei să deconectezi toate dispozitivele?"), + ("Clear", "Golește"), + ("Audio Input Device", "Dispozitiv de intrare audio"), + ("Deny remote access", "Interzice acces la distanță"), + ("Use IP Whitelisting", "Folosește lista de IP-uri autorizate"), + ("Network", "Rețea"), + ("Enable RDP", "Activează RDP"), + ("Pin menubar", "Fixează bara de meniu"), + ("Unpin menubar", "Detașează bara de meniu"), + ("Recording", "Înregistrare"), + ("Directory", "Director"), + ("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"), + ("Change", "Modifică"), + ("Start session recording", "Începe înregistrare"), + ("Stop session recording", "Oprește înregistrare"), + ("Enable Recording Session", "Activează înregistrarea sesiunii"), + ("Allow recording session", "Permite înregistrarea sesiunii"), + ("Enable LAN Discovery", "Activează descoperire LAN"), + ("Deny LAN Discovery", "Interzice descoperire LAN"), + ("Write a message", "Scrie un mesaj"), + ("Prompt", "Solicită"), + ("Please wait for confirmation of UAC...", "Așteaptă confirmarea UAC..."), + ("elevated_foreground_window_tip", "Fereastra actuală a dispozitivului la distanță necesită privilegii sporite pentru a funcționa, astfel că mouse-ul și tastatura nu pot fi folosite. Poți cere utilizatorului la distanță să minimizeze fereastra actuală sau să facă clic pe butonul de sporire a privilegiilor din fereastra de gestionare a conexiunilor. Pentru a evita această problemă, recomandăm instalarea software-ului pe dispozitivul la distanță."), + ("Disconnected", "Deconectat"), + ("Other", "Altele"), + ("Confirm before closing multiple tabs", "Confirmă înainte de a închide mai multe file"), + ("Keyboard Settings", "Configurare tastatură"), + ("Full Access", "Acces total"), + ("Screen Share", "Partajare ecran"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland necesită o versiune superioară a distribuției Linux. Încearcă desktopul X11 sau schimbă sistemul de operare."), + ("JumpLink", "Afișează"), + ("Please Select the screen to be shared(Operate on the peer side).", "Partajează ecranul care urmează să fie partajat (operează din partea dispozitivului pereche)."), + ("Show RustDesk", "Afișează RustDesk"), + ("This PC", "Acest PC"), + ("or", "sau"), + ("Continue with", "Continuă cu"), + ("Elevate", "Sporește"), + ("Zoom cursor", "Cursor lupă"), + ("Accept sessions via password", "Acceptă sesiunile folosind parola"), + ("Accept sessions via click", "Acceptă sesiunile cu un clic de confirmare"), + ("Accept sessions via both", "Acceptă sesiunile folosind ambele moduri"), + ("Please wait for the remote side to accept your session request...", "Așteaptă ca solicitarea ta de conectare la distanță să fie acceptată..."), + ("One-time Password", "Parolă unică"), + ("Use one-time password", "Folosește parola unică"), + ("One-time password length", "Lungimea parolei unice"), + ("Request access to your device", "Solicită acces la dispozitivul tău"), + ("Hide connection management window", "Ascunde fereastra de gestionare a conexiunilor"), + ("hide_cm_tip", "Permite ascunderea ferestrei de gestionare doar dacă accepți începerea sesiunilor folosind parola permanentă"), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to Address Book", ""), + ("Group", ""), + ("Search", ""), + ("Closed manually by web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 82715758b..b9eaf71a5 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -12,7 +12,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start Service", "Запустить службу"), ("Service is running", "Служба запущена"), ("Service is not running", "Служба не запущена"), - ("not_ready_status", "Не выполнено. Проверьте подключение."), + ("not_ready_status", "Не подключено. Проверьте соединение."), ("Control Remote Desktop", "Управление удалённым рабочим столом"), ("Transfer File", "Передать файл"), ("Connect", "Подключиться"), @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Буфер обмена пуст"), ("Stop service", "Остановить службу"), ("Change ID", "Изменить ID"), + ("Your new ID", "Новый ID"), + ("length %min% to %max%", "длина %min%...%max%"), + ("starts with a letter", "начинается с буквы"), + ("allowed characters", "допустимые символы"), + ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Website", "Сайт"), ("About", "О программе"), ("Slogan_tip", "Сделано с душой в этом безумном мире!"), ("Privacy Statement", "Заявление о конфиденциальности"), ("Mute", "Отключить звук"), + ("Build Date", "Дата сборки"), + ("Version", "Версия"), + ("Home", "Главная"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), ("Hardware Codec", "Аппаратный кодек"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API-сервер"), ("invalid_http", "Должен начинаться с http:// или https://"), ("Invalid IP", "Неправильный IP-адрес"), - ("id_change_tip", "Допускаются только символы a-z, A-Z, 0-9 и _ (подчёркивание). Первая буква должна быть a-z, A-Z. Длина от 6 до 16"), ("Invalid format", "Неправильный формат"), ("server_not_support", "Пока не поддерживается сервером"), ("Not available", "Недоступно"), @@ -165,8 +172,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Локальный порт"), ("Local Address", "Локальный адрес"), ("Change Local Port", "Изменить локальный порт"), - ("setup_server_tip", "Для более быстрого подключения настройте свой собственный сервер подключения"), - ("Too short, at least 6 characters.", "Слишком коротко, минимум 6 символов"), + ("setup_server_tip", "Для более быстрого подключения настройте собственный сервер"), + ("Too short, at least 6 characters.", "Слишком короткий, минимум 6 символов"), ("The confirmation is not identical.", "Подтверждение не совпадает"), ("Permissions", "Разрешения"), ("Accept", "Принять"), @@ -177,10 +184,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Allow hearing sound", "Разрешить передачу звука"), ("Allow file copy and paste", "Разрешить копирование и вставку файлов"), ("Connected", "Подключено"), - ("Direct and encrypted connection", "Прямое и зашифрованное соединение"), - ("Relayed and encrypted connection", "Ретранслируемое и зашифрованное соединение"), - ("Direct and unencrypted connection", "Прямое и незашифрованное соединение"), - ("Relayed and unencrypted connection", "Ретранслируемое и незашифрованное соединение"), + ("Direct and encrypted connection", "Прямое и зашифрованное подключение"), + ("Relayed and encrypted connection", "Ретранслируемое и зашифрованное подключение"), + ("Direct and unencrypted connection", "Прямое и незашифрованное подключение"), + ("Relayed and unencrypted connection", "Ретранслируемое и незашифрованное подключение"), ("Enter Remote ID", "Введите удалённый ID"), ("Enter your password", "Введите пароль"), ("Logging in...", "Вход..."), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Предупреждение"), ("Login screen using Wayland is not supported", "Вход в систему с использованием Wayland не поддерживается"), ("Reboot required", "Требуется перезагрузка"), - ("Unsupported display server ", "Неподдерживаемый сервер отображения"), + ("Unsupported display server", "Неподдерживаемый сервер отображения"), ("x11 expected", "Ожидается X11"), ("Port", "Порт"), ("Settings", "Настройки"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), - ("Always connected via relay", "Всегда подключён через ретрансляционный сервер"), - ("Always connect via relay", "Всегда подключаться через ретрансляционный сервер"), + ("Connect via relay", "Подключится через ретранслятор"), + ("Always connect via relay", "Всегда подключаться через ретранслятор"), ("whitelist_tip", "Только IP-адреса из белого списка могут получить доступ ко мне"), ("Login", "Войти"), + ("Verify", "Проверить"), + ("Remember me", "Запомнить"), + ("Trust this device", "Доверенное устройство"), + ("Verification code", "Проверочный код"), + ("verification_tip", "Обнаружено новое устройство, на зарегистрированный адрес электронной почты отправлен проверочный код. Введите его, чтобы продолжить вход в систему."), ("Logout", "Выйти"), ("Tags", "Метки"), ("Search ID", "Поиск по ID"), - ("Current Wayland display server is not supported", "Текущий сервер отображения Wayland не поддерживается"), ("whitelist_sep", "Раздельно запятой, точкой с запятой, пробелом или новой строкой"), ("Add ID", "Добавить ID"), ("Add Tag", "Добавить ключевое слово"), @@ -258,9 +269,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Нет разрешения на передачу файлов"), ("Note", "Примечание"), ("Connection", "Соединение"), - ("Share Screen", "Поделиться экраном"), - ("CLOSE", "ЗАКРЫТЬ"), - ("OPEN", "ОТКРЫТЬ"), + ("Share Screen", "Демонстрация экрана"), ("Chat", "Чат"), ("Total", "Всего"), ("items", "элементы"), @@ -268,7 +277,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Захват экрана"), ("Input Control", "Входной контроль"), ("Audio Capture", "Захват аудио"), - ("File Connection", "Файловое соединение"), + ("File Connection", "Файловое подключение"), ("Screen Connection", "Подключение экрана"), ("Do you accept?", "Вы согласны?"), ("Open System Setting", "Открыть настройки системы"), @@ -276,10 +285,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_input_permission_tip1", "Чтобы удалённое устройство могло управлять вашим Android-устройством с помощью мыши или касания, вам необходимо разрешить RustDesk использовать службу \"Специальные возможности\"."), ("android_input_permission_tip2", "Перейдите на следующую страницу системных настроек, найдите и войдите в \"Установленные службы\", включите службу \"RustDesk Input\"."), ("android_new_connection_tip", "Получен новый запрос на управление вашим текущим устройством."), - ("android_service_will_start_tip", "Включение захвата экрана автоматически запускает службу, позволяя другим устройствам запрашивать соединение с этим устройством."), - ("android_stop_service_tip", "Закрытие службы автоматически закроет все установленные соединения."), + ("android_service_will_start_tip", "Включение захвата экрана автоматически запускает службу, позволяя другим устройствам запрашивать подключение к этому устройству."), + ("android_stop_service_tip", "Закрытие службы автоматически закроет все установленные подключения."), ("android_version_audio_tip", "Текущая версия Android не поддерживает захват звука, обновите её до Android 10 или выше."), - ("android_start_service_tip", "Нажмите \"Запуск промежуточного сервера\" или ОТКРЫТЬ разрешение \"Захват экрана\", чтобы запустить службу демонстрации экрана."), + ("android_start_service_tip", "Нажмите [Запустить службу] или разрешите [Захват экрана], чтобы запустить службу демонстрации экрана."), + ("android_permission_may_not_change_tip", "Разрешения для установленных подключений не могут быть изменены, необходимо переподключение."), ("Account", "Аккаунт"), ("Overwrite", "Перезаписать"), ("This file exists, skip or overwrite this file?", "Этот файл существует, пропустить или перезаписать файл?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Держать в фоне службу RustDesk"), ("Ignore Battery Optimizations", "Игнорировать оптимизацию батареи"), ("android_open_battery_optimizations_tip", "Перейдите на следующую страницу настроек"), + ("Start on Boot", "Начинать при загрузке"), + ("Start the screen sharing service on boot, requires special permissions", "Запускать службу демонстрации экрана при загрузке (требуются специальные разрешения)"), ("Connection not allowed", "Подключение не разрешено"), ("Legacy mode", "Устаревший режим"), ("Map mode", "Режим сопоставления"), @@ -327,27 +339,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show Menubar", "Показать строку меню"), ("Hide Menubar", "Скрыть строку меню"), ("Direct Connection", "Прямая связь"), - ("Relay Connection", "Ретранслируемое соединение"), - ("Secure Connection", "Безопасное соединение"), - ("Insecure Connection", "Небезопасное соединение"), + ("Relay Connection", "Ретранслируемое подключение"), + ("Secure Connection", "Безопасное подключение"), + ("Insecure Connection", "Небезопасное подключение"), ("Scale original", "Оригинальный масштаб"), ("Scale adaptive", "Адаптивный масштаб"), ("General", "Общие"), ("Security", "Безопасность"), - ("Account", "Аккаунт"), ("Theme", "Тема"), ("Dark Theme", "Тёмная тема"), + ("Light Theme", "Светлая тема"), ("Dark", "Тёмная"), ("Light", "Светлая"), ("Follow System", "Системная"), ("Enable hardware codec", "Использовать аппаратный кодек"), ("Unlock Security Settings", "Разблокировать настройки безопасности"), ("Enable Audio", "Включить звук"), - ("Unlock Network Settings", "Разблокировать настройки соединения"), + ("Unlock Network Settings", "Разблокировать настройки подключения"), ("Server", "Сервер"), ("Direct IP Access", "Прямой IP-доступ"), ("Proxy", "Прокси"), - ("Port", "Порт"), ("Apply", "Применить"), ("Disconnect all devices?", "Отключить все устройства?"), ("Clear", "Очистить"), @@ -376,13 +387,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Другое"), ("Confirm before closing multiple tabs", "Подтверждать закрытие несколько вкладок"), ("Keyboard Settings", "Настройки клавиатуры"), - ("Custom", "Своё"), ("Full Access", "Полный доступ"), - ("Screen Share", "Поделиться экраном"), + ("Screen Share", "Демонстрация экрана"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland требует Ubuntu 21.04 или более позднюю версию."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Для Wayland требуется более поздняя версия дистрибутива Linux. Пожалуйста, попробуйте рабочий стол X11 или смените ОС."), ("JumpLink", "Просмотр"), - ("Please Select the screen to be shared(Operate on the peer side).", "Выберите экран для совместного использования (работайте на одноранговой стороне)."), + ("Please Select the screen to be shared(Operate on the peer side).", "Выберите экран для демонстрации (работайте на одноранговой стороне)."), ("Show RustDesk", "Показать RustDesk"), ("This PC", "Этот компьютер"), ("or", "или"), @@ -397,7 +407,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Use one-time password", "Использовать одноразовый пароль"), ("One-time password length", "Длина одноразового пароля"), ("Request access to your device", "Запрос доступа к вашему устройству"), - ("Hide connection management window", "Скрывать окно управления соединениями"), + ("Hide connection management window", "Скрывать окно управления подключениями"), ("hide_cm_tip", "Разрешать скрытие случае, если принимаются сеансы по паролю или используется постоянный пароль"), ("wayland_experiment_tip", "Поддержка Wayland находится на экспериментальной стадии, используйте X11, если вам требуется автоматический доступ."), ("Right click to select tabs", "Выбор вкладок щелчком правой кнопки мыши"), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Добавить в адресную книгу"), ("Group", "Группа"), ("Search", "Поиск"), - ("Closed manually by the web console", "Закрыто вручную через веб-консоль"), + ("Closed manually by web console", "Закрыто вручную через веб-консоль"), ("Local keyboard type", "Тип локальной клавиатуры"), ("Select local keyboard type", "Выберите тип локальной клавиатуры"), + ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), + ("Always use software rendering", "Использовать программную визуализацию"), + ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить RustDesk разрешения \"Мониторинг ввода\"."), + ("config_microphone", "Чтобы разговаривать с удалённой стороной, необходимо предоставить RustDesk разрешение \"Запись аудио\"."), + ("request_elevation_tip", "Также можно запросить повышение прав, если кто-то есть на удалённой стороне."), + ("Wait", "Ждите"), + ("Elevation Error", "Ошибка повышения прав"), + ("Ask the remote user for authentication", "Запросить аутентификацию у удалённого пользователя"), + ("Choose this if the remote account is administrator", "Выберите это, если удалённый аккаунт является администратором"), + ("Transmit the username and password of administrator", "Передать имя пользователя и пароль администратора"), + ("still_click_uac_tip", "По-прежнему требуется, чтобы удалённый пользователь нажал \"OK\" в окне UAC при запуске RustDesk."), + ("Request Elevation", "Запросить повышение"), + ("wait_accept_uac_tip", "Подождите, пока удалённый пользователь подтвердит запрос UAC."), + ("Elevate successfully", "Права повышены"), + ("uppercase", "заглавные"), + ("lowercase", "строчные"), + ("digit", "цифры"), + ("special character", "спецсимволы"), + ("length>=8", "8+ символов"), + ("Weak", "Слабый"), + ("Medium", "Средний"), + ("Strong", "Стойкий"), + ("Switch Sides", "Переключить стороны"), + ("Please confirm if you want to share your desktop?", "Подтверждаете, что разрешаете демонстрацию рабочего стола?"), + ("Display", "Отображение"), + ("Default View Style", "Стиль отображения по умолчанию"), + ("Default Scroll Style", "Стиль прокрутки по умолчанию"), + ("Default Image Quality", "Качество изображения по умолчанию"), + ("Default Codec", "Кодек по умолчанию"), + ("Bitrate", "Битрейт"), + ("FPS", "Частота кадров"), + ("Auto", "Авто"), + ("Other Default Options", "Другие параметры по умолчанию"), + ("Voice call", "Голосовой вызов"), + ("Text chat", "Текстовый чат"), + ("Stop voice call", "Завершить голосовой вызов"), + ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."), + ("Reconnect", "Переподключить"), + ("Codec", "Кодек"), + ("Resolution", "Разрешение"), + ("No transfers in progress", "Передача не осуществляется"), + ("Set one-time password length", "Установить длину одноразового пароля"), + ("idd_driver_tip", "Установить драйвер виртуального дисплея (используется при отсутствии физических дисплеев)"), + ("confirm_idd_driver_tip", "Включена функция установки драйвера виртуального дисплея. Обратите внимание, что для доверия к драйверу будет установлен тестовый сертификат. Этот сертификат будет использоваться только для подтверждения доверия драйверам Rustdesk."), + ("RDP Settings", "Настройки RDP"), + ("Sort by", "Сортировка"), + ("New Connection", "Новое подключение"), + ("Restore", "Восстановить"), + ("Minimize", "Свернуть"), + ("Maximize", "Развернуть"), + ("Your Device", "Ваше устройство"), + ("empty_recent_tip", "Нет последних сеансов!\nПора спланировать новый."), + ("empty_favorite_tip", "Ещё нет избранных удалённых узлов?\nДавайте найдём, кого можно добавить в избранное!"), + ("empty_lan_tip", "Не найдено удалённых узлов."), + ("empty_address_book_tip", "В адресной книге нет удалённых узлов."), + ("eg: admin", "например: admin"), + ("Empty Username", "Пустое имя пользователя"), + ("Empty Password", "Пустой пароль"), + ("Me", "Я"), + ("identical_file_tip", "Файл идентичен файлу на удалённом узле."), + ("show_monitors_tip", "Показывать мониторы на панели инструментов"), + ("View Mode", "Режим просмотра"), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 72995dd94..7d28cfb72 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Schránka je prázdna"), ("Stop service", "Zastaviť službu"), ("Change ID", "Zmeniť ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9 a _ (podčiarkovník). Prvý znak musí byť a-z, A-Z. Dĺžka musí byť medzi 6 a 16 znakmi."), ("Website", "Webová stránka"), ("About", "O RustDesk"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Stíšiť"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Zvukový vstup"), ("Enhancements", ""), ("Hardware Codec", ""), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API server"), ("invalid_http", "Musí začínať http:// alebo https://"), ("Invalid IP", "Neplatná IP adresa"), - ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9 a _ (podčiarkovník). Prvý znak musí byť a-z, A-Z. Dĺžka musí byť medzi 6 a 16 znakmi."), ("Invalid format", "Neplatný formát"), ("server_not_support", "Zatiaľ serverom nepodporované"), ("Not available", "Nie je k dispozícii"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Upozornenie"), ("Login screen using Wayland is not supported", "Prihlasovacia obrazovka prostredníctvom Wayland nie je podporovaná"), ("Reboot required", "Vyžaduje sa reštart"), - ("Unsupported display server ", "Nepodporovaný zobrazovací (display) server"), + ("Unsupported display server", "Nepodporovaný zobrazovací (display) server"), ("x11 expected", "očakáva sa x11"), ("Port", ""), ("Settings", "Nastavenia"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Manuálne ukončené opačnou stranou pripojenia"), ("Enable remote configuration modification", "Povoliť zmeny konfigurácie zo vzdialeného PC"), ("Run without install", "Spustiť bez inštalácie"), - ("Always connected via relay", "Vždy pripojené cez prepájací server"), + ("Connect via relay", ""), ("Always connect via relay", "Vždy pripájať cez prepájací server"), ("whitelist_tip", "Len vymenované IP adresy majú oprávnenie sa pripojiť k vzdialenej správe"), ("Login", "Prihlásenie"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Odhlásenie"), ("Tags", "Štítky"), ("Search ID", "Hľadať ID"), - ("Current Wayland display server is not supported", "Zobrazovací (display) server Wayland nie je podporovaný"), ("whitelist_sep", "Oddelené čiarkou, bodkočiarkou, medzerou alebo koncom riadku"), ("Add ID", "Pridať ID"), ("Add Tag", "Pridať štítok"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Poznámka"), ("Connection", "Pripojenie"), ("Share Screen", "Zdielať obrazovku"), - ("CLOSE", "ZATVORIŤ"), - ("OPEN", "OTVORIŤ"), ("Chat", "Chat"), ("Total", "Celkom"), ("items", "položiek"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Zapnutie \"Zachytávanie obsahu obrazovky\" automaticky spistí službu, čo iným zariadeniam umožní požiadať o pripojenie k tomuto zariadeniu."), ("android_stop_service_tip", "Zastavenie služby automaticky ukončí všetky naviazané spojenia."), ("android_version_audio_tip", "Vaša verzia Androidu neumožňuje zaznamenávanie zvuku. Prejdite na verziu Android 10 alebo vyššiu."), - ("android_start_service_tip", "Klepnite na [Spustiť službu] alebo OTVORTE oprávnenie [Zachytávanie obsahu obrazovky], aby sa aktivovala služba zdieľania obrazovky."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", ""), ("Overwrite", "Prepísať"), ("This file exists, skip or overwrite this file?", "Preskočiť alebo prepísať existujúci súbor?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Prispôsobivá mierka"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), + ("Light Theme", ""), ("Dark", ""), ("Light", ""), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 alebo vyššiu verziu."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs new file mode 100755 index 000000000..544e544f7 --- /dev/null +++ b/src/lang/sl.rs @@ -0,0 +1,487 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Stanje"), + ("Your Desktop", "Vaše namizje"), + ("desk_tip", "Do vašega namizja lahko dostopate s spodnjim IDjem in geslom"), + ("Password", "Geslo"), + ("Ready", "Pripravljen"), + ("Established", "Povezava vzpostavljena"), + ("connecting_status", "Vzpostavljanje povezave z omrežjem RustDesk..."), + ("Enable Service", "Omogoči storitev"), + ("Start Service", "Zaženi storitev"), + ("Service is running", "Storitev se izvaja"), + ("Service is not running", "Storitev se ne izvaja"), + ("not_ready_status", "Ni pripravljeno, preverite vašo mrežno povezavo"), + ("Control Remote Desktop", "Nadzoruj oddaljeno namizje"), + ("Transfer File", "Prenos datotek"), + ("Connect", "Poveži"), + ("Recent Sessions", "Nedavne seje"), + ("Address Book", "Adresar"), + ("Confirmation", "Potrditev"), + ("TCP Tunneling", "TCP tuneliranje"), + ("Remove", "Odstrani"), + ("Refresh random password", "Osveži naključno geslo"), + ("Set your own password", "Nastavi lastno geslo"), + ("Enable Keyboard/Mouse", "Omogoči tipkovnico in miško"), + ("Enable Clipboard", "Omogoči odložišče"), + ("Enable File Transfer", "Omogoči prenos datotek"), + ("Enable TCP Tunneling", "Omogoči TCP tuneliranje"), + ("IP Whitelisting", "Omogoči seznam dovoljenih IPjev"), + ("ID/Relay Server", "Strežnik za ID/posredovanje"), + ("Import Server Config", "Uvozi nastavitve strežnika"), + ("Export Server Config", "Izvozi nastavitve strežnika"), + ("Import server configuration successfully", "Nastavitve strežnika uspešno uvožene"), + ("Export server configuration successfully", "Nastavitve strežnika uspešno izvožene"), + ("Invalid server configuration", "Neveljavne nastavitve strežnika"), + ("Clipboard is empty", "Odložišče je prazno"), + ("Stop service", "Ustavi storitev"), + ("Change ID", "Spremeni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez šumnikov), 0-9 in _. Prvi znak mora biti črka, dolžina od 6 do 16 znakov."), + ("Website", "Spletna stran"), + ("About", "O programu"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Izklopi zvok"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), + ("Audio Input", "Avdio vhod"), + ("Enhancements", "Izboljšave"), + ("Hardware Codec", "Strojni kodek"), + ("Adaptive Bitrate", "Prilagodljiva bitna hitrost"), + ("ID Server", "ID strežnik"), + ("Relay Server", "Posredniški strežnik"), + ("API Server", "API strežnik"), + ("invalid_http", "mora se začeti s http:// ali https://"), + ("Invalid IP", "Neveljaven IP"), + ("Invalid format", "Neveljavna oblika"), + ("server_not_support", "Strežnik še ne podpira"), + ("Not available", "Ni na voljo"), + ("Too frequent", "Prepogosto"), + ("Cancel", "Prekliči"), + ("Skip", "Izpusti"), + ("Close", "Zapri"), + ("Retry", "Ponovi"), + ("OK", "V redu"), + ("Password Required", "Potrebno je geslo"), + ("Please enter your password", "Vnesite vaše geslo"), + ("Remember password", "Zapomni si geslo"), + ("Wrong Password", "Napačno geslo"), + ("Do you want to enter again?", "Želite znova vnesti?"), + ("Connection Error", "Napaka pri povezavi"), + ("Error", "Napaka"), + ("Reset by the peer", "Povezava prekinjena"), + ("Connecting...", "Povezovanje..."), + ("Connection in progress. Please wait.", "Vzpostavljanje povezave, prosim počakajte."), + ("Please try 1 minute later", "Poizkusite čez 1 minuto"), + ("Login Error", "Napaka pri prijavi"), + ("Successful", "Uspešno"), + ("Connected, waiting for image...", "Povezava vzpostavljena, čakam na sliko..."), + ("Name", "Ime"), + ("Type", "Vrsta"), + ("Modified", "Čas spremembe"), + ("Size", "Velikost"), + ("Show Hidden Files", "Prikaži skrite datoteke"), + ("Receive", "Prejmi"), + ("Send", "Pošlji"), + ("Refresh File", "Osveži datoteko"), + ("Local", "Lokalno"), + ("Remote", "Oddaljeno"), + ("Remote Computer", "Oddaljeni računalnik"), + ("Local Computer", "Lokalni računalnik"), + ("Confirm Delete", "Potrdi izbris"), + ("Delete", "Izbriši"), + ("Properties", "Lastnosti"), + ("Multi Select", "Večkratna izbira"), + ("Select All", "Izberi vse"), + ("Unselect All", "Počisti vse"), + ("Empty Directory", "Prazen imenik"), + ("Not an empty directory", "Imenik ni prazen"), + ("Are you sure you want to delete this file?", "Ali res želite izbrisati to datoteko?"), + ("Are you sure you want to delete this empty directory?", "Ali res želite izbrisati to prazno mapo?"), + ("Are you sure you want to delete the file of this directory?", "Ali res želite datoteko iz mape?"), + ("Do this for all conflicts", "Naredi to za vse"), + ("This is irreversible!", "Tega dejanja ni mogoče razveljaviti!"), + ("Deleting", "Brisanje"), + ("files", "datoteke"), + ("Waiting", "Čakanje"), + ("Finished", "Opravljeno"), + ("Speed", "Hitrost"), + ("Custom Image Quality", "Kakovost slike po meri"), + ("Privacy mode", "Zasebni način"), + ("Block user input", "Onemogoči uporabnikov vnos"), + ("Unblock user input", "Omogoči uporabnikov vnos"), + ("Adjust Window", "Prilagodi okno"), + ("Original", "Originalno"), + ("Shrink", "Skrči"), + ("Stretch", "Raztegni"), + ("Scrollbar", "Drsenje z drsniki"), + ("ScrollAuto", "Samodejno drsenje"), + ("Good image quality", "Visoka kakovost slike"), + ("Balanced", "Uravnoteženo"), + ("Optimize reaction time", "Optimiraj odzivni čas"), + ("Custom", "Po meri"), + ("Show remote cursor", "Prikaži oddaljeni kazalec miške"), + ("Show quality monitor", "Prikaži nadzornik kakovosti"), + ("Disable clipboard", "Onemogoči odložišče"), + ("Lock after session end", "Zakleni ob koncu seje"), + ("Insert", "Vstavi"), + ("Insert Lock", "Zakleni oddaljeni računalnik"), + ("Refresh", "Osveži"), + ("ID does not exist", "ID ne obstaja"), + ("Failed to connect to rendezvous server", "Ni se bilo mogoče povezati na povezovalni strežnik"), + ("Please try later", "Poizkusite znova kasneje"), + ("Remote desktop is offline", "Oddaljeno namizje ni dosegljivo"), + ("Key mismatch", "Ključ ni ustrezen"), + ("Timeout", "Časovna omejitev"), + ("Failed to connect to relay server", "Ni se bilo mogoče povezati na posredniški strežnik"), + ("Failed to connect via rendezvous server", "Ni se bilo mogoče povezati preko povezovalnega strežnika"), + ("Failed to connect via relay server", "Ni se bilo mogoče povezati preko posredniškega strežnika"), + ("Failed to make direct connection to remote desktop", "Ni bilo mogoče vzpostaviti neposredne povezave z oddaljenim namizjem"), + ("Set Password", "Nastavi geslo"), + ("OS Password", "Geslo operacijskega sistema"), + ("install_tip", "Zaradi nadzora uporabniškega računa, RustDesk v nekaterih primerih na oddaljeni strani ne deluje pravilno. Temu se lahko izognete z namestitvijo."), + ("Click to upgrade", "Klikni za nadgradnjo"), + ("Click to download", "Klikni za prenos"), + ("Click to update", "Klikni za posodobitev"), + ("Configure", "Nastavi"), + ("config_acc", "Za oddaljeni nadzor namizja morate RustDesku dodeliti pravico za dostopnost"), + ("config_screen", "Za oddaljeni dostop do namizja morate RustDesku dodeliti pravico snemanje zaslona"), + ("Installing ...", "Nameščanje..."), + ("Install", "Namesti"), + ("Installation", "Namestitev"), + ("Installation Path", "Pot za namestitev"), + ("Create start menu shortcuts", "Ustvari bližnjice v meniju Začetek"), + ("Create desktop icon", "Ustvari ikono na namizju"), + ("agreement_tip", "Z namestitvijo se strinjate z licenčno pogodbo"), + ("Accept and Install", "Sprejmi in namesti"), + ("End-user license agreement", "Licenčna pogodba za končnega uporabnika"), + ("Generating ...", "Ustvarjanje ..."), + ("Your installation is lower version.", "Vaša namestitev je starejša"), + ("not_close_tcp_tip", "Med uporabo tunela ne zaprite tega okna"), + ("Listening ...", "Poslušam ..."), + ("Remote Host", "Oddaljeni gostitelj"), + ("Remote Port", "Oddaljena vrata"), + ("Action", "Dejanje"), + ("Add", "Dodaj"), + ("Local Port", "Lokalna vrata"), + ("Local Address", "Lokalni naslov"), + ("Change Local Port", "Spremeni lokalna vrata"), + ("setup_server_tip", "Za hitrejšo povezavo uporabite lasten strežnik"), + ("Too short, at least 6 characters.", "Prekratek, mora biti najmanj 6 znakov."), + ("The confirmation is not identical.", "Potrditev ni enaka."), + ("Permissions", "Dovoljenja"), + ("Accept", "Sprejmi"), + ("Dismiss", "Opusti"), + ("Disconnect", "Prekini povezavo"), + ("Allow using keyboard and mouse", "Dovoli uporabo tipkovnice in miške"), + ("Allow using clipboard", "Dovoli uporabo odložišča"), + ("Allow hearing sound", "Dovoli prenos zvoka"), + ("Allow file copy and paste", "Dovoli kopiranje in lepljenje datotek"), + ("Connected", "Povezan"), + ("Direct and encrypted connection", "Neposredna šifrirana povezava"), + ("Relayed and encrypted connection", "Posredovana šifrirana povezava"), + ("Direct and unencrypted connection", "Neposredna nešifrirana povezava"), + ("Relayed and unencrypted connection", "Posredovana šifrirana povezava"), + ("Enter Remote ID", "Vnesi oddaljeni ID"), + ("Enter your password", "Vnesi geslo"), + ("Logging in...", "Prijavljanje..."), + ("Enable RDP session sharing", "Omogoči deljenje RDP seje"), + ("Auto Login", "Samodejna prijava"), + ("Enable Direct IP Access", "Omogoči neposredni dostop preko IP"), + ("Rename", "Preimenuj"), + ("Space", "Prazno"), + ("Create Desktop Shortcut", "Ustvari bližnjico na namizju"), + ("Change Path", "Spremeni pot"), + ("Create Folder", "Ustvari mapo"), + ("Please enter the folder name", "Vnesite ime mape"), + ("Fix it", "Popravi"), + ("Warning", "Opozorilo"), + ("Login screen using Wayland is not supported", "Prijava z Waylandom ni podprta"), + ("Reboot required", "Potreben je ponovni zagon"), + ("Unsupported display server", "Nepodprt zaslonski strežnik"), + ("x11 expected", "Pričakovan X11"), + ("Port", "Vrata"), + ("Settings", "Nastavitve"), + ("Username", "Uporabniško ime"), + ("Invalid port", "Neveljavno geslo"), + ("Closed manually by the peer", "Povezavo ročno prekinil odjemalec"), + ("Enable remote configuration modification", "Omogoči oddaljeno spreminjanje nastavitev"), + ("Run without install", "Zaženi brez namestitve"), + ("Connect via relay", ""), + ("Always connect via relay", "Vedno poveži preko posrednika"), + ("whitelist_tip", "Dostop je možen samo iz dovoljenih IPjev"), + ("Login", "Prijavi"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "Odjavi"), + ("Tags", "Oznake"), + ("Search ID", "Išči ID"), + ("whitelist_sep", "Naslovi ločeni z vejico, podpičjem, presledkom ali novo vrstico"), + ("Add ID", "Dodaj ID"), + ("Add Tag", "Dodaj oznako"), + ("Unselect all tags", ""), + ("Network error", "Omrežna napaka"), + ("Username missed", "Up. ime izpuščeno"), + ("Password missed", "Geslo izpuščeno"), + ("Wrong credentials", "Napačne poverilnice"), + ("Edit Tag", "Uredi oznako"), + ("Unremember Password", "Pozabi geslo"), + ("Favorites", "Priljubljene"), + ("Add to Favorites", "Dodaj med priljubljene"), + ("Remove from Favorites", "Odstrani iz priljubljenih"), + ("Empty", "Prazno"), + ("Invalid folder name", "Napačno ime mape"), + ("Socks5 Proxy", "Socks5 posredniški strežnik"), + ("Hostname", "Ime gostitelja"), + ("Discovered", "Odkriti"), + ("install_daemon_tip", "Za samodejni zagon ob vklopu računalnika je potrebno dodati sistemsko storitev"), + ("Remote ID", "Oddaljeni ID"), + ("Paste", "Prilepi"), + ("Paste here?", "Prilepi tu?"), + ("Are you sure to close the connection?", "Ali želite prekiniti povezavo?"), + ("Download new version", "Prenesi novo različico"), + ("Touch mode", "Način dotika"), + ("Mouse mode", "Način mišle"), + ("One-Finger Tap", "Tap z enim prstom"), + ("Left Mouse", "Leva tipka miške"), + ("One-Long Tap", "Dolg tap z enim prstom"), + ("Two-Finger Tap", "Tap z dvema prstoma"), + ("Right Mouse", "Desna tipka miške"), + ("One-Finger Move", "Premik z enim prstom"), + ("Double Tap & Move", "Dvojni tap in premik"), + ("Mouse Drag", "Vlečenje z miško"), + ("Three-Finger vertically", "Triprstno navpično"), + ("Mouse Wheel", "Miškino kolesce"), + ("Two-Finger Move", "Premik z dvema prstoma"), + ("Canvas Move", "Premik platna"), + ("Pinch to Zoom", "Povečava s približevanjem prstov"), + ("Canvas Zoom", "Povečava platna"), + ("Reset canvas", "Ponastavi platno"), + ("No permission of file transfer", "Ni pravic za prenos datotek"), + ("Note", "Opomba"), + ("Connection", "Povezava"), + ("Share Screen", "Deli zaslon"), + ("Chat", "Pogovor"), + ("Total", "Skupaj"), + ("items", "elementi"), + ("Selected", "Izbrano"), + ("Screen Capture", "Zajem zaslona"), + ("Input Control", "Nadzor vnosa"), + ("Audio Capture", "Zajem zvoka"), + ("File Connection", "Datotečna povezava"), + ("Screen Connection", "Zaslonska povezava"), + ("Do you accept?", "Ali sprejmete?"), + ("Open System Setting", "Odpri sistemske nastavitve"), + ("How to get Android input permission?", "Kako pridobiti dovoljenje za vnos na Androidu?"), + ("android_input_permission_tip1", "Za oddaljeni nadzor vaše naprave Android, je potrebno RustDesku dodeliti pravico za dostopnost."), + ("android_input_permission_tip2", "Pojdite v sistemske nastavitve, poiščite »Nameščene storitve« in vklopite storitev »RustDesk Input«."), + ("android_new_connection_tip", "Prejeta je bila zahteva za oddaljeni nadzor vaše naprave."), + ("android_service_will_start_tip", "Z vklopom zajema zaslona se bo samodejno zagnala storitev, ki omogoča da oddaljene naprave pošljejo zahtevo za povezavo na vašo napravo."), + ("android_stop_service_tip", "Z zaustavitvijo storitve bodo samodejno prekinjene vse oddaljene povezave."), + ("android_version_audio_tip", "Trenutna različica Androida ne omogoča zajema zvoka. Za zajem zvoka nadgradite na Android 10 ali novejši."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", "Račun"), + ("Overwrite", "Prepiši"), + ("This file exists, skip or overwrite this file?", "Datoteka obstaja, izpusti ali prepiši?"), + ("Quit", "Izhod"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Pomoč"), + ("Failed", "Ni uspelo"), + ("Succeeded", "Uspelo"), + ("Someone turns on privacy mode, exit", "Vklopljen je zasebni način, izhod"), + ("Unsupported", "Ni podprto"), + ("Peer denied", "Odjemalec zavrnil"), + ("Please install plugins", "Namestite vključke"), + ("Peer exit", "Odjemalec se je zaprl"), + ("Failed to turn off", "Ni bilo mogoče izklopiti"), + ("Turned off", "Izklopljeno"), + ("In privacy mode", "V zasebnem načinu"), + ("Out privacy mode", "Iz zasebnega načina"), + ("Language", "Jezik"), + ("Keep RustDesk background service", "Ohrani RustDeskovo storitev v ozadju"), + ("Ignore Battery Optimizations", "Prezri optimizacije baterije"), + ("android_open_battery_optimizations_tip", "Če želite izklopiti to možnost, pojdite v nastavitve aplikacije RustDesk, poiščite »Baterija« in izklopite »Neomejeno«"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", "Povezava ni dovoljena"), + ("Legacy mode", "Stari način"), + ("Map mode", "Način preslikave"), + ("Translate mode", "Način prevajanja"), + ("Use permanent password", "Uporabi stalno geslo"), + ("Use both passwords", "Uporabi obe gesli"), + ("Set permanent password", "Nastavi stalno geslo"), + ("Enable Remote Restart", "Omogoči oddaljeni ponovni zagon"), + ("Allow remote restart", "Dovoli oddaljeni ponovni zagon"), + ("Restart Remote Device", "Znova zaženi oddaljeno napravo"), + ("Are you sure you want to restart", "Ali ste prepričani, da želite znova zagnati"), + ("Restarting Remote Device", "Ponovni zagon oddaljene naprave"), + ("remote_restarting_tip", "Oddaljena naprava se znova zaganja, prosim zaprite to sporočilo in se čez nekaj časa povežite s stalnim geslom."), + ("Copied", "Kopirano"), + ("Exit Fullscreen", "Izhod iz celozaslonskega načina"), + ("Fullscreen", "Celozaslonski način"), + ("Mobile Actions", "Dejanja za prenosne naprave"), + ("Select Monitor", "Izberite zaslon"), + ("Control Actions", "Dejanja za nadzor"), + ("Display Settings", "Nastavitve zaslona"), + ("Ratio", "Razmerje"), + ("Image Quality", "Kakovost slike"), + ("Scroll Style", "Način drsenja"), + ("Show Menubar", "Prikaži meni"), + ("Hide Menubar", "Skrij meni"), + ("Direct Connection", "Neposredna povezava"), + ("Relay Connection", "Posredovana povezava"), + ("Secure Connection", "Zavarovana povezava"), + ("Insecure Connection", "Nezavarovana povezava"), + ("Scale original", "Originalna velikost"), + ("Scale adaptive", "Prilagojena velikost"), + ("General", "Splošno"), + ("Security", "Varnost"), + ("Theme", "Tema"), + ("Dark Theme", "Temna tema"), + ("Light Theme", ""), + ("Dark", "Temna"), + ("Light", "Svetla"), + ("Follow System", "Sistemska"), + ("Enable hardware codec", "Omogoči strojno pospeševanje"), + ("Unlock Security Settings", "Odkleni varnostne nastavitve"), + ("Enable Audio", "Omogoči zvok"), + ("Unlock Network Settings", "Odkleni mrežne nastavitve"), + ("Server", "Strežnik"), + ("Direct IP Access", "Neposredni dostop preko IPja"), + ("Proxy", "Posredniški strežnik"), + ("Apply", "Uveljavi"), + ("Disconnect all devices?", "Odklopi vse naprave?"), + ("Clear", "Počisti"), + ("Audio Input Device", "Vhodna naprava za zvok"), + ("Deny remote access", "Onemogoči oddaljeni dostop"), + ("Use IP Whitelisting", "Omogoči seznam dovoljenih IP naslovov"), + ("Network", "Mreža"), + ("Enable RDP", "Omogoči RDP"), + ("Pin menubar", "Pripni menijsko vrstico"), + ("Unpin menubar", "Odpni menijsko vrstico"), + ("Recording", "Snemanje"), + ("Directory", "Imenik"), + ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), + ("Change", "Spremeni"), + ("Start session recording", "Začni snemanje seje"), + ("Stop session recording", "Ustavi snemanje seje"), + ("Enable Recording Session", "Omogoči snemanje seje"), + ("Allow recording session", "Dovoli snemanje seje"), + ("Enable LAN Discovery", "Omogoči odkrivanje lokalnega omrežja"), + ("Deny LAN Discovery", "Onemogoči odkrivanje lokalnega omrežja"), + ("Write a message", "Napiši spoorčilo"), + ("Prompt", "Poziv"), + ("Please wait for confirmation of UAC...", "Počakajte za potrditev nadzora uporabniškega računa"), + ("elevated_foreground_window_tip", "Trenutno aktivno okno na oddaljenem računalniku zahteva višje pravice za upravljanje. Oddaljenega uporabnika lahko prosite, da okno minimizira, ali pa kliknite gumb za povzdig pravic v oknu za upravljanje povezave. Če se želite izogniti temu problemu, na oddaljenem računalniku RustDesk namestite."), + ("Disconnected", "Brez povezave"), + ("Other", "Drugo"), + ("Confirm before closing multiple tabs", "Zahtevajte potrditev pred zapiranjem večih zavihkov"), + ("Keyboard Settings", "Nastavitve tipkovnice"), + ("Full Access", "Poln dostop"), + ("Screen Share", "Deljenje zaslona"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ali novejši"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Zahtevana je novejša različica Waylanda. Posodobite vašo distribucijo ali pa uporabite X11."), + ("JumpLink", "Pogled"), + ("Please Select the screen to be shared(Operate on the peer side).", "Izberite zaslon za delitev (na oddaljeni strani)."), + ("Show RustDesk", "Prikaži RustDesk"), + ("This PC", "Ta računalnik"), + ("or", "ali"), + ("Continue with", "Nadaljuj z"), + ("Elevate", "Povzdig pravic"), + ("Zoom cursor", "Prilagodi velikost miškinega kazalca"), + ("Accept sessions via password", "Sprejmi seje z geslom"), + ("Accept sessions via click", "Sprejmi seje s potrditvijo"), + ("Accept sessions via both", "Sprejmi seje z geslom ali potrditvijo"), + ("Please wait for the remote side to accept your session request...", "Počakajte, da oddaljeni računalnik sprejme povezavo..."), + ("One-time Password", "Enkratno geslo"), + ("Use one-time password", "Uporabi enkratno geslo"), + ("One-time password length", "Dolžina enkratnega gesla"), + ("Request access to your device", "Zahtevaj dostop do svoje naprave"), + ("Hide connection management window", "Skrij okno za upravljanje povezave"), + ("hide_cm_tip", "Dovoli skrivanje samo pri sprejemanju sej z geslom"), + ("wayland_experiment_tip", "Podpora za Wayland je v preizkusni fazi. Uporabite X11, če rabite nespremljan dostop."), + ("Right click to select tabs", "Desno-kliknite za izbiro zavihkov"), + ("Skipped", "Izpuščeno"), + ("Add to Address Book", "Dodaj v adresar"), + ("Group", "Skupina"), + ("Search", "Iskanje"), + ("Closed manually by web console", "Ročno zaprto iz spletne konzole"), + ("Local keyboard type", "Lokalna vrsta tipkovnice"), + ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/sq.rs b/src/lang/sq.rs index 7a2472aa8..afd5c2c2d 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Clipboard është bosh"), ("Stop service", "Ndaloni shërbimin"), ("Change ID", "Ndryshoni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9 dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), ("Website", "Faqe ëebi"), ("About", "Rreth"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Pa zë"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Inputi zërit"), ("Enhancements", "Përmirësimet"), ("Hardware Codec", "Kodeku Harduerik"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Serveri API"), ("invalid_http", "Duhet të fillojë me http:// ose https://"), ("Invalid IP", "IP e pavlefshme"), - ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9 dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), ("Invalid format", "Format i pavlefshëm"), ("server_not_support", "Nuk suportohet akoma nga severi"), ("Not available", "I padisponueshëm"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Dicka po shkon keq"), ("Login screen using Wayland is not supported", "Hyrja në ekran duke përdorur Wayland muk suportohet"), ("Reboot required", "Kërkohet rinisja"), - ("Unsupported display server ", "Nuk supurtohet severi ekranit"), + ("Unsupported display server", "Nuk supurtohet severi ekranit"), ("x11 expected", "Pritet x11"), ("Port", "Port"), ("Settings", "Cilësimet"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "E mbyllur manualisht nga peer"), ("Enable remote configuration modification", "Aktivizoni modifikimin e konfigurimit në distancë"), ("Run without install", "Ekzekuto pa instaluar"), - ("Always connected via relay", "Gjithmonë i ldihur me transmetues"), + ("Connect via relay", ""), ("Always connect via relay", "Gjithmonë lidheni me transmetues"), ("whitelist_tip", "Vetëm IP e listës së bardhë mund të më aksesoj."), ("Login", "Hyrje"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Dalje"), ("Tags", "Tage"), ("Search ID", "Kerko ID"), - ("Current Wayland display server is not supported", "Serveri aktual i ekranit Wayland nuk mbështetet"), ("whitelist_sep", "Të ndara me presje, pikëpresje, hapësira ose rresht të ri"), ("Add ID", "Shto ID"), ("Add Tag", "Shto Tag"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Shënime"), ("Connection", "Lidhja"), ("Share Screen", "Ndaj ekranin"), - ("CLOSE", "Mbyll"), - ("OPEN", "Hap"), ("Chat", "Biseda"), ("Total", "Total"), ("items", "artikuj"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Aktivizimi i \"Regjistrimi i ekranit\" do të nisë automatikisht shërbimin, duke lejuar pajisjet e tjera të kërkojnë një lidhje me pajisjen tuaj."), ("android_stop_service_tip", "Mbyllja e shërbimit do të mbyllë automatikisht të gjitha lidhjet e vendosura."), ("android_version_audio_tip", "Versioni aktual i Android nuk mbështet regjistrimin e audios, ju lutemi përmirësoni në Android 10 ose më të lartë."), - ("android_start_service_tip", "Shtyp [Fillo Shërbimin] ose HAP lejen e [Kapjen e Ekranit] për të nisur shërbimin e ndarjes së ekranit."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Llogaria"), ("Overwrite", "Përshkruaj"), ("This file exists, skip or overwrite this file?", "Ky skedar ekziston , tejkalo ose përshkruaj këtë skedarë"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Mbaje shërbimin e sfondit të RustDesk"), ("Ignore Battery Optimizations", "Injoro optimizimet e baterisë"), ("android_open_battery_optimizations_tip", "Nëse dëshironi ta çaktivizoni këtë veçori, ju lutemi shkoni te faqja tjetër e cilësimeve të aplikacionit RustDesk, gjeni dhe shtypni [Batteri], hiqni zgjedhjen [Te pakufizuara]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Lidhja nuk lejohet"), ("Legacy mode", "Modaliteti i trashëgimisë"), ("Map mode", "Modaliteti i hartës"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", " E përsjhtatshme në shkallë"), ("General", "Gjeneral"), ("Security", "Siguria"), - ("Account", "Llogaria"), ("Theme", "Theme"), ("Dark Theme", "Theme e errët"), + ("Light Theme", ""), ("Dark", "E errët"), ("Light", "Drita"), ("Follow System", "Ndiq sistemin"), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Qasje e drejtpërdrejtë IP"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Apliko"), ("Disconnect all devices?", "Shkyç të gjitha pajisjet?"), ("Clear", "Pastro"), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Tjetër"), ("Confirm before closing multiple tabs", "Konfirmo përpara se të mbyllësh shumë skeda"), ("Keyboard Settings", "Cilësimet e tastierës"), - ("Custom", "Personalizuar"), ("Full Access", "Qasje e plotë"), ("Screen Share", "Ndarja e ekranit"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kërkon Ubuntu 21.04 ose version më të lartë"), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 0d15b199c..05204f728 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Clipboard je prazan"), ("Stop service", "Stopiraj servis"), ("Change ID", "Promeni ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), ("Website", "Web sajt"), ("About", "O programu"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Utišaj"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Audio ulaz"), ("Enhancements", "Proširenja"), ("Hardware Codec", "Hardverski kodek"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API server"), ("invalid_http", "mora početi sa http:// ili https://"), ("Invalid IP", "Nevažeća IP"), - ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), ("Invalid format", "Pogrešan format"), ("server_not_support", "Server još uvek ne podržava"), ("Not available", "Nije dostupno"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Upozorenje"), ("Login screen using Wayland is not supported", "Ekran za prijavu koji koristi Wayland nije podržan"), ("Reboot required", "Potreban je restart"), - ("Unsupported display server ", "Nepodržan server za prikaz"), + ("Unsupported display server", "Nepodržan server za prikaz"), ("x11 expected", "x11 očekivan"), ("Port", "Port"), ("Settings", "Postavke"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Klijent ručno raskinuo konekciju"), ("Enable remote configuration modification", "Dozvoli modifikaciju udaljene konfiguracije"), ("Run without install", "Pokreni bez instalacije"), - ("Always connected via relay", "Uvek spojne preko posrednika"), + ("Connect via relay", ""), ("Always connect via relay", "Uvek se spoj preko posrednika"), ("whitelist_tip", "Samo dozvoljene IP mi mogu pristupiti"), ("Login", "Prijava"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Odjava"), ("Tags", "Oznake"), ("Search ID", "Traži ID"), - ("Current Wayland display server is not supported", "Tekući Wazland server za prikaz nije podržan"), ("whitelist_sep", "Odvojeno zarezima, tačka zarezima, praznim mestima ili novim redovima"), ("Add ID", "Dodaj ID"), ("Add Tag", "Dodaj oznaku"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Primedba"), ("Connection", "Konekcija"), ("Share Screen", "Podeli ekran"), - ("CLOSE", "ZATVORI"), - ("OPEN", "OTVORI"), ("Chat", "Dopisivanje"), ("Total", "Ukupno"), ("items", "stavki"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Uključenje \"Screen Capture\" automatski će pokrenuti servis, dozvoljavajući drugim uređajima da zahtevaju spajanje na vaš uređaj."), ("android_stop_service_tip", "Zatvaranje servisa automatski će zatvoriti sve uspostavljene konekcije."), ("android_version_audio_tip", "Tekuća Android verzija ne podržava audio snimanje, molimo nadogradite na Android 10 ili veći."), - ("android_start_service_tip", "Kliknite [Start Service] ili OPEN [Screen Capture] dozvolu da pokrenete servis deljenja ekrana."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Nalog"), ("Overwrite", "Prepiši preko"), ("This file exists, skip or overwrite this file?", "Ova datoteka postoji, preskoči ili prepiši preko?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Zadrži RustDesk kao pozadinski servis"), ("Ignore Battery Optimizations", "Zanemari optimizacije baterije"), ("android_open_battery_optimizations_tip", "Ako želite da onemogućite ovu funkciju, molimo idite na sledeću stranicu za podešavanje RustDesk aplikacije, pronađite i uđite u [Battery], isključite [Unrestricted]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Konekcija nije dozvoljena"), ("Legacy mode", "Zastareli mod"), ("Map mode", "Mod mapiranja"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Adaptivno skaliranje"), ("General", "Uopšteno"), ("Security", "Bezbednost"), - ("Account", "Nalog"), ("Theme", "Tema"), ("Dark Theme", "Tamna tema"), + ("Light Theme", ""), ("Dark", "Tamno"), ("Light", "Svetlo"), ("Follow System", "Prema sistemu"), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direktan IP pristup"), ("Proxy", "Proksi"), - ("Port", "Port"), ("Apply", "Primeni"), ("Disconnect all devices?", "Otkači sve uređaju?"), ("Clear", "Obriši"), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Ostalo"), ("Confirm before closing multiple tabs", "Potvrda pre zatvaranja više kartica"), ("Keyboard Settings", "Postavke tastature"), - ("Custom", "Korisnički"), ("Full Access", "Pun pristup"), ("Screen Share", "Deljenje ekrana"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland zahteva Ubuntu 21.04 ili veću verziju"), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Dodaj u adresar"), ("Group", "Grupa"), ("Search", "Pretraga"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 9fa3c75fb..7941c2f58 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Urklippet är tomt"), ("Stop service", "Avsluta tjänsten"), ("Change ID", "Byt ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Bara a-z, A-Z, 0-9 och _ (understräck) tecken är tillåtna. Den första bokstaven måste vara a-z, A-Z. Längd mellan 6 och 16."), ("Website", "Hemsida"), ("About", "Om"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Tyst"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Ljud input"), ("Enhancements", "Förbättringar"), ("Hardware Codec", "Hårdvarucodec"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Server"), ("invalid_http", "måste börja med http:// eller https://"), ("Invalid IP", "Ogiltig IP"), - ("id_change_tip", "Bara a-z, A-Z, 0-9 och _ (understräck) tecken är tillåtna. Den första bokstaven måste vara a-z, A-Z. Längd mellan 6 och 16."), ("Invalid format", "Ogiltigt format"), ("server_not_support", "Stöds ännu inte av servern"), ("Not available", "Ej tillgänglig"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Varning"), ("Login screen using Wayland is not supported", "Login med Wayland stöds inte"), ("Reboot required", "Omstart krävs"), - ("Unsupported display server ", "Displayserver stöds inte "), + ("Unsupported display server", "Displayserver stöds inte "), ("x11 expected", "x11 förväntades"), ("Port", "Port"), ("Settings", "Inställningar"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Stängd manuellt av klienten"), ("Enable remote configuration modification", "Tillåt fjärrkonfigurering"), ("Run without install", "Kör utan installation"), - ("Always connected via relay", "Anslut alltid via relay"), + ("Connect via relay", ""), ("Always connect via relay", "Anslut alltid via relay"), ("whitelist_tip", "Bara vitlistade IPs kan koppla upp till mig"), ("Login", "Logga in"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Logga ut"), ("Tags", "Taggar"), ("Search ID", "Sök ID"), - ("Current Wayland display server is not supported", "Nuvarande Wayland displayserver stöds inte"), ("whitelist_sep", "Separerat av ett comma, semikolon, mellanslag eller ny linje"), ("Add ID", "Lägg till ID"), ("Add Tag", "Lägg till Tagg"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Notering"), ("Connection", "Anslutning"), ("Share Screen", "Dela skärm"), - ("CLOSE", "STÄNG"), - ("OPEN", "ÖPPNA"), ("Chat", "Chatt"), ("Total", "Totalt"), ("items", "föremål"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Sätter du på \"skärminspelning\" kommer tjänsten automatiskt att starta. Detta tillåter andra enheter att kontrollera din enhet."), ("android_stop_service_tip", "Genom att stänga av tjänsten kommer alla enheter att kopplas ifrån."), ("android_version_audio_tip", "Din version av Android stödjer inte ljudinspelning, Android 10 eller nyare krävs"), - ("android_start_service_tip", "Tryck på [Starta tjänsten] eller tillåt [skärminspelning] för att starta skärmdelning."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Konto"), ("Overwrite", "Skriv över"), ("This file exists, skip or overwrite this file?", "Filen finns redan, hoppa över eller skriv över filen?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Behåll RustDesk i bakgrunden"), ("Ignore Battery Optimizations", "Ignorera batterioptimering"), ("android_open_battery_optimizations_tip", "Om du vill stänga av denna funktion, gå till nästa RustDesk programs inställningar, hitta [Batteri], Checka ur [Obegränsad]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Anslutning ej tillåten"), ("Legacy mode", "Legacy mode"), ("Map mode", "Kartläge"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skala adaptivt"), ("General", "Generellt"), ("Security", "Säkerhet"), - ("Account", "Konto"), ("Theme", "Tema"), ("Dark Theme", "Mörkt tema"), + ("Light Theme", ""), ("Dark", "Mörk"), ("Light", "Ljus"), ("Follow System", "Följ system"), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direkt IP åtkomst"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Tillämpa"), ("Disconnect all devices?", "Koppla ifrån alla enheter?"), ("Clear", "Töm"), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Övrigt"), ("Confirm before closing multiple tabs", "Bekräfta innan du stänger flera flikar"), ("Keyboard Settings", "Tangentbordsinställningar"), - ("Custom", "Anpassat"), ("Full Access", "Full tillgång"), ("Screen Share", "Skärmdelning"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland kräver Ubuntu 21.04 eller högre."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 1bd9f5e98..56508d80a 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", ""), ("Stop service", ""), ("Change ID", ""), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", ""), ("Website", ""), ("About", ""), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", ""), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", ""), ("Enhancements", ""), ("Hardware Codec", ""), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", ""), ("invalid_http", ""), ("Invalid IP", ""), - ("id_change_tip", ""), ("Invalid format", ""), ("server_not_support", ""), ("Not available", ""), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", ""), ("Login screen using Wayland is not supported", ""), ("Reboot required", ""), - ("Unsupported display server ", ""), + ("Unsupported display server", ""), ("x11 expected", ""), ("Port", ""), ("Settings", ""), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", ""), ("Enable remote configuration modification", ""), ("Run without install", ""), - ("Always connected via relay", ""), + ("Connect via relay", ""), ("Always connect via relay", ""), ("whitelist_tip", ""), ("Login", ""), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", ""), ("Tags", ""), ("Search ID", ""), - ("Current Wayland display server is not supported", ""), ("whitelist_sep", ""), ("Add ID", ""), ("Add Tag", ""), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", ""), ("Connection", ""), ("Share Screen", ""), - ("CLOSE", ""), - ("OPEN", ""), ("Chat", ""), ("Total", ""), ("items", ""), @@ -280,6 +289,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", ""), ("android_version_audio_tip", ""), ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", ""), ("Overwrite", ""), ("This file exists, skip or overwrite this file?", ""), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", ""), ("Ignore Battery Optimizations", ""), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", ""), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", ""), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), + ("Light Theme", ""), ("Dark", ""), ("Light", ""), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", ""), @@ -405,8 +415,74 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), + ("login_linux_tooltip_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 186e88453..4f178bf1d 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -1,413 +1,487 @@ lazy_static::lazy_static! { - pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "สถานะ"), - ("Your Desktop", "หน้าจอของคุณ"), - ("desk_tip", "คุณสามารถเข้าถึงเดสก์ท็อปของคุณได้ด้วย ID และรหัสผ่านต่อไปนี้"), - ("Password", "รหัสผ่าน"), - ("Ready", "พร้อม"), - ("Established", "เชื่อมต่อแล้ว"), - ("connecting_status", "กำลังเชื่อมต่อไปยังเครือข่าย RustDesk..."), - ("Enable Service", "เปิดใช้การงานเซอร์วิส"), - ("Start Service", "เริ่มต้นใช้งานเซอร์วิส"), - ("Service is running", "เซอร์วิสกำลังทำงาน"), - ("Service is not running", "เซอร์วิสไม่ทำงาน"), - ("not_ready_status", "ไม่พร้อมใช้งาน กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ"), - ("Control Remote Desktop", "การควบคุมเดสก์ท็อปปลายทาง"), - ("Transfer File", "การถ่ายโอนไฟล์"), - ("Connect", "เชื่อมต่อ"), - ("Recent Sessions", "เซสชันล่าสุด"), - ("Address Book", "สมุดรายชื่อ"), - ("Confirmation", "การยืนยัน"), - ("TCP Tunneling", "อุโมงค์การเชื่อมต่อ TCP"), - ("Remove", "ลบ"), - ("Refresh random password", "รีเฟรชรหัสผ่านใหม่แบบสุ่ม"), - ("Set your own password", "ตั้งรหัสผ่านของคุณเอง"), - ("Enable Keyboard/Mouse", "เปิดการใช้งาน คีย์บอร์ด/เมาส์"), - ("Enable Clipboard", "เปิดการใช้งาน คลิปบอร์ด"), - ("Enable File Transfer", "เปิดการใช้งาน การถ่ายโอนไฟล์"), - ("Enable TCP Tunneling", "เปิดการใช้งาน อุโมงค์การเชื่อมต่อ TCP"), - ("IP Whitelisting", "IP ไวท์ลิสต์"), - ("ID/Relay Server", "เซิร์ฟเวอร์ ID/Relay"), - ("Import Server Config", "นำเข้าการตั้งค่าเซิร์ฟเวอร์"), - ("Export Server Config", "ส่งออกการตั้งค่าเซิร์ฟเวอร์"), - ("Import server configuration successfully", "นำเข้าการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), - ("Export server configuration successfully", "ส่งออกการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), - ("Invalid server configuration", "การตั้งค่าของเซิร์ฟเวอร์ไม่ถูกต้อง"), - ("Clipboard is empty", "คลิปบอร์ดว่างเปล่า"), - ("Stop service", "หยุดการใช้งานเซอร์วิส"), - ("Change ID", "เปลี่ยน ID"), - ("Website", "เว็บไซต์"), - ("About", "เกี่ยวกับ"), - ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), - ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), - ("Mute", "ปิดเสียง"), - ("Audio Input", "ออดิโออินพุท"), - ("Enhancements", "การปรับปรุง"), - ("Hardware Codec", "ฮาร์ดแวร์ codec"), - ("Adaptive Bitrate", "บิทเรทผันแปร"), - ("ID Server", "เซิร์ฟเวอร์ ID"), - ("Relay Server", "เซิร์ฟเวอร์ Relay"), - ("API Server", "เซิร์ฟเวอร์ API"), - ("invalid_http", "ต้องขึ้นต้นด้วย http:// หรือ https:// เท่านั้น"), - ("Invalid IP", "IP ไม่ถูกต้อง"), - ("id_change_tip", "อนุญาตเฉพาะตัวอักษร a-z A-Z 0-9 และ _ (ขีดล่าง) เท่านั้น โดยตัวอักษรขึ้นต้นจะต้องเป็น a-z หรือไม่ก็ A-Z และมีความยาวระหว่าง 6 ถึง 16 ตัวอักษร"), - ("Invalid format", "รูปแบบไม่ถูกต้อง"), - ("server_not_support", "ยังไม่รองรับโดยเซิร์ฟเวอร์"), - ("Not available", "ไม่พร้อมใช้งาน"), - ("Too frequent", "ดำเนินการถี่เกินไป"), - ("Cancel", "ยกเลิก"), - ("Skip", "ข้าม"), - ("Close", "ปิด"), - ("Retry", "ลองใหม่อีกครั้ง"), - ("OK", "ตกลง"), - ("Password Required", "ต้องใช้รหัสผ่าน"), - ("Please enter your password", "กรุณาใส่รหัสผ่านของคุณ"), - ("Remember password", "จดจำรหัสผ่าน"), - ("Wrong Password", "รหัสผ่านไม่ถูกต้อง"), - ("Do you want to enter again?", "ต้องการใส่ข้อมูลอีกครั้งหรือไม่?"), - ("Connection Error", "การเชื่อมต่อผิดพลาด"), - ("Error", "ข้อผิดพลาด"), - ("Reset by the peer", "รีเซ็ตโดยอีกฝั่ง"), - ("Connecting...", "กำลังเชื่อมต่อ..."), - ("Connection in progress. Please wait.", "กำลังดำเนินการเชื่อมต่อ กรุณารอซักครู่"), - ("Please try 1 minute later", "กรุณาลองใหม่อีกครั้งใน 1 นาที"), - ("Login Error", "การเข้าสู่ระบบผิดพลาด"), - ("Successful", "สำเร็จ"), - ("Connected, waiting for image...", "เชื่อมต่อสำเร็จ กำลังรับข้อมูลภาพ..."), - ("Name", "ชื่อ"), - ("Type", "ประเภท"), - ("Modified", "แก้ไขล่าสุด"), - ("Size", "ขนาด"), - ("Show Hidden Files", "แสดงไฟล์ที่ถูกซ่อน"), - ("Receive", "รับ"), - ("Send", "ส่ง"), - ("Refresh File", "รีเฟรชไฟล์"), - ("Local", "ต้นทาง"), - ("Remote", "ปลายทาง"), - ("Remote Computer", "คอมพิวเตอร์ปลายทาง"), - ("Local Computer", "คอมพิวเตอร์ต้นทาง"), - ("Confirm Delete", "ยืนยันการลบ"), - ("Delete", "ลบ"), - ("Properties", "ข้อมูล"), - ("Multi Select", "เลือกหลายรายการ"), - ("Select All", "เลือกทั้งหมด"), - ("Unselect All", "ยกเลิกการเลือกทั้งหมด"), - ("Empty Directory", "ไดเรกทอรีว่างเปล่า"), - ("Not an empty directory", "ไม่ใช่ไดเรกทอรีว่างเปล่า"), - ("Are you sure you want to delete this file?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์นี้?"), - ("Are you sure you want to delete this empty directory?", "คุณแน่ใจหรือไม่ที่จะลบไดเรอทอรีว่างเปล่านี้?"), - ("Are you sure you want to delete the file of this directory?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์ของไดเรกทอรีนี้?"), - ("Do this for all conflicts", "ดำเนินการแบบเดียวกันสำหรับรายการทั้งหมด"), - ("This is irreversible!", "การดำเนินการนี้ไม่สามารถย้อนกลับได้!"), - ("Deleting", "กำลังลบ"), - ("files", "ไฟล์"), - ("Waiting", "กำลังรอ"), - ("Finished", "เสร็จแล้ว"), - ("Speed", "ความเร็ว"), - ("Custom Image Quality", "คุณภาพของภาพแบบกำหนดเอง"), - ("Privacy mode", "โหมดความเป็นส่วนตัว"), - ("Block user input", "บล็อคอินพุทจากผู้ใช้งาน"), - ("Unblock user input", "ยกเลิกการบล็อคอินพุทจากผู้ใช้งาน"), - ("Adjust Window", "ปรับขนาดหน้าต่าง"), - ("Original", "ต้นฉบับ"), - ("Shrink", "ย่อ"), - ("Stretch", "ยืด"), - ("Scrollbar", "แถบเลื่อน"), - ("ScrollAuto", "เลื่อนอัตโนมัติ"), - ("Good image quality", "ภาพคุณภาพดี"), - ("Balanced", "สมดุล"), - ("Optimize reaction time", "เน้นการตอบสนอง"), - ("Custom", "กำหนดเอง"), - ("Show remote cursor", "แสดงเคอร์เซอร์ปลายทาง"), - ("Show quality monitor", "แสดงคุณภาพหน้าจอ"), - ("Disable clipboard", "ปิดการใช้งานคลิปบอร์ด"), - ("Lock after session end", "ล็อคหลังจากจบเซสชัน"), - ("Insert", "แทรก"), - ("Insert Lock", "แทรกล็อค"), - ("Refresh", "รีเฟรช"), - ("ID does not exist", "ไม่พอข้อมูล ID"), - ("Failed to connect to rendezvous server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์นัดพบล้มเหลว"), - ("Please try later", "กรุณาลองใหม่ในภายหลัง"), - ("Remote desktop is offline", "เดสก์ท็อปปลายทางออฟไลน์"), - ("Key mismatch", "คีย์ไม่ถูกต้อง"), - ("Timeout", "หมดเวลา"), - ("Failed to connect to relay server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์รีเลย์ล้มเหลว"), - ("Failed to connect via rendezvous server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์นัดพบล้มเหลว"), - ("Failed to connect via relay server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์รีเลย์ล้มเหลว"), - ("Failed to make direct connection to remote desktop", "การเชื่อมต่อตรงไปยังเดสก์ท็อปปลายทางล้มเหลว"), - ("Set Password", "ตั้งรหัสผ่าน"), - ("OS Password", "รหัสผ่านระบบปฏิบัติการ"), - ("install_tip", "เนื่องด้วยข้อจำกัดของการใช้งาน UAC ทำให้ RustDesk ไม่สามารถทำงานได้ปกติในฝั่งปลายทางในบางครั้ง เพื่อหลีกเลี่ยงข้อจำกัดของ UAC กรุณากดปุ่มด้านล่างเพื่อติดตั้ง RustDesk ไปยังระบบของคุณ"), - ("Click to upgrade", "คลิกเพื่ออัปเกรด"), - ("Click to download", "คลิกเพื่อดาวน์โหลด"), - ("Click to update", "คลิกเพื่ออัปเดต"), - ("Configure", "ปรับแต่งค่า"), - ("config_acc", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่ RustDesk"), - ("config_screen", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การบันทึกภาพหน้าจอ\" ให้แก่ RustDesk"), - ("Installing ...", "กำลังติดตั้ง ..."), - ("Install", "ติดตั้ง"), - ("Installation", "การติดตั้ง"), - ("Installation Path", "ตำแหน่งที่ติดตั้ง"), - ("Create start menu shortcuts", "สร้างทางลัดไปยัง Start Menu"), - ("Create desktop icon", "สร้างไอคอนบนเดสก์ท็อป"), - ("agreement_tip", "ในการเริ่มต้นการติดตั้ง ถือว่าคุณได้ยอมรับข้อตกลงใบอนุญาตแล้ว"), - ("Accept and Install", "ยอมรับและติดตั้ง"), - ("End-user license agreement", "ข้อตกลงใบอนุญาตผู้ใช้งาน"), - ("Generating ...", "กำลังสร้าง ..."), - ("Your installation is lower version.", "การติดตั้งของคุณเป็นเวอร์ชั่นที่ต่ำกว่า"), - ("not_close_tcp_tip", "อย่าปิดหน้าต่างนี้ในขณะที่คุณกำลังใช้งานอุโมงค์การเชื่อมต่อ"), - ("Listening ...", "กำลังรอรับข้อมูล ..."), - ("Remote Host", "โฮสต์ปลายทาง"), - ("Remote Port", "พอร์ทปลายทาง"), - ("Action", "การดำเนินการ"), - ("Add", "เพิ่ม"), - ("Local Port", "พอร์ทต้นทาง"), - ("Local Address", "ที่อยู่ต้นทาง"), - ("Change Local Port", "เปลี่ยนพอร์ทต้นทาง"), - ("setup_server_tip", "เพื่อการเชื่อมต่อที่เร็วขึ้น กรุณาเซ็ตอัปเซิร์ฟเวอร์ของคุณเอง"), - ("Too short, at least 6 characters.", "สั้นเกินไป ต้องไม่ต่ำกว่า 6 ตัวอักษร"), - ("The confirmation is not identical.", "การยืนยันข้อมูลไม่ถูกต้อง"), - ("Permissions", "สิทธิ์การใช้งาน"), - ("Accept", "ยอมรับ"), - ("Dismiss", "ปิด"), - ("Disconnect", "ยกเลิกการเชื่อมต่อ"), - ("Allow using keyboard and mouse", "อนุญาตให้ใช้งานคีย์บอร์ดและเมาส์"), - ("Allow using clipboard", "อนุญาตให้ใช้คลิปบอร์ด"), - ("Allow hearing sound", "อนุญาตให้ได้ยินเสียง"), - ("Allow file copy and paste", "อนุญาตให้มีการคัดลอกและวางไฟล์"), - ("Connected", "เชื่อมต่อแล้ว"), - ("Direct and encrypted connection", "การเชื่อมต่อตรงที่มีการเข้ารหัส"), - ("Relayed and encrypted connection", "การเชื่อมต่อแบบรีเลย์ที่มีการเข้ารหัส"), - ("Direct and unencrypted connection", "การเชื่อมต่อตรงที่ไม่มีการเข้ารหัส"), - ("Relayed and unencrypted connection", "การเชื่อมต่อแบบรีเลย์ที่ไม่มีการเข้ารหัส"), - ("Enter Remote ID", "กรอก ID ปลายทาง"), - ("Enter your password", "กรอกรหัสผ่าน"), - ("Logging in...", "กำลังเข้าสู่ระบบ..."), - ("Enable RDP session sharing", "เปิดการใช้งานการแชร์เซสชัน RDP"), - ("Auto Login", "เข้าสู่ระบอัตโนมัติ"), - ("Enable Direct IP Access", "เปิดการใช้งาน IP ตรง"), - ("Rename", "ปลายทาง"), - ("Space", "พื้นที่ว่าง"), - ("Create Desktop Shortcut", "สร้างทางลัดบนเดสก์ท็อป"), - ("Change Path", "เปลี่ยนตำแหน่ง"), - ("Create Folder", "สร้างโฟลเดอร์"), - ("Please enter the folder name", "กรุณาใส่ชื่อโฟลเดอร์"), - ("Fix it", "แก้ไข"), - ("Warning", "คำเตือน"), - ("Login screen using Wayland is not supported", "หน้าจอการเข้าสู่ระบบโดยใช้ Wayland ยังไม่ถูกรองรับ"), - ("Reboot required", "จำเป็นต้องเริ่มต้นระบบใหม่"), - ("Unsupported display server ", "เซิร์ฟเวอร์การแสดงผลที่ไม่รองรับ"), - ("x11 expected", "ต้องใช้งาน x11"), - ("Port", "พอร์ท"), - ("Settings", "ตั้งค่า"), - ("Username", "ชื่อผู้ใช้งาน"), - ("Invalid port", "พอร์ทไม่ถูกต้อง"), - ("Closed manually by the peer", "ถูกปิดโดยอีกฝั่งการการเชื่อมต่อ"), - ("Enable remote configuration modification", "เปิดการใช้งานการแก้ไขการตั้งค่าปลายทาง"), - ("Run without install", "ใช้งานโดยไม่ต้องติดตั้ง"), - ("Always connected via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), - ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), - ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), - ("Login", "เข้าสู่ระบบ"), - ("Logout", "ออกจากระบบ"), - ("Tags", "แท็ก"), - ("Search ID", "ค้นหา ID"), - ("Current Wayland display server is not supported", "เซิร์ฟเวอร์การแสดงผล Wayland ปัจจุบันไม่รองรับ"), - ("whitelist_sep", "คั่นโดยเครื่องหมาย comma semicolon เว้นวรรค หรือ ขึ้นบรรทัดใหม่"), - ("Add ID", "เพิ่ม ID"), - ("Add Tag", "เพิ่มแท็ก"), - ("Unselect all tags", "ยกเลิกการเลือกแท็กทั้งหมด"), - ("Network error", "ข้อผิดพลาดของเครือข่าย"), - ("Username missed", "ไม่พบข้อมูลผู้ใช้งาน"), - ("Password missed", "ไม่พบรหัสผ่าน"), - ("Wrong credentials", "ข้อมูลสำหรับเข้าสู่ระบบไม่ถูกต้อง"), - ("Edit Tag", "แก้ไขแท็ก"), - ("Unremember Password", "ยกเลิกการจดจำรหัสผ่าน"), - ("Favorites", "รายการโปรด"), - ("Add to Favorites", "เพิ่มไปยังรายการโปรด"), - ("Remove from Favorites", "ลบออกจากรายการโปรด"), - ("Empty", "ว่างเปล่า"), - ("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"), - ("Socks5 Proxy", "พรอกซี Socks5"), - ("Hostname", "ชื่อโฮสต์"), - ("Discovered", "ค้นพบ"), - ("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"), - ("Remote ID", "ID ปลายทาง"), - ("Paste", "วาง"), - ("Paste here?", "วางที่นี่หรือไม่?"), - ("Are you sure to close the connection?", "คุณแน่ใจหรือไม่ที่จะปิดการเชื่อมต่อ?"), - ("Download new version", "ดาวน์โหลดเวอร์ชั่นใหม่"), - ("Touch mode", "โหมดการสัมผัส"), - ("Mouse mode", "โหมดการใช้เมาส์"), - ("One-Finger Tap", "แตะนิ้วเดียว"), - ("Left Mouse", "เมาส์ซ้าย"), - ("One-Long Tap", "แตะยาวหนึ่งครั้ง"), - ("Two-Finger Tap", "แตะสองนิ้ว"), - ("Right Mouse", "เมาส์ขวา"), - ("One-Finger Move", "ลากนิ้วเดียว"), - ("Double Tap & Move", "แตะเบิ้ลและลาก"), - ("Mouse Drag", "ลากเมาส์"), - ("Three-Finger vertically", "สามนิ้วแนวตั้ง"), - ("Mouse Wheel", "ลูกลิ้งเมาส์"), - ("Two-Finger Move", "ลากสองนิ้ว"), - ("Canvas Move", "ลากแคนวาส"), - ("Pinch to Zoom", "ถ่างเพื่อขยาย"), - ("Canvas Zoom", "ขยายแคนวาส"), - ("Reset canvas", "รีเซ็ตแคนวาส"), - ("No permission of file transfer", "ไม่มีสิทธิ์ในการถ่ายโอนไฟล์"), - ("Note", "บันทึกข้อความ"), - ("Connection", "การเชื่อมต่อ"), - ("Share Screen", "แชร์หน้าจอ"), - ("CLOSE", "ปิด"), - ("OPEN", "เปิด"), - ("Chat", "แชท"), - ("Total", "รวม"), - ("items", "รายการ"), - ("Selected", "ถูกเลือก"), - ("Screen Capture", "แคปเจอร์หน้าจอ"), - ("Input Control", "ควบคุมอินพุท"), - ("Audio Capture", "แคปเจอร์เสียง"), - ("File Connection", "การเชื่อมต่อไฟล์"), - ("Screen Connection", "การเชื่อมต่อหน้าจอ"), - ("Do you accept?", "ยอมรับหรือไม่?"), - ("Open System Setting", "เปิดการตั้งค่าระบบ"), - ("How to get Android input permission?", "เปิดสิทธิ์การใช้งานอินพุทของแอนดรอยด์ได้อย่างไร?"), - ("android_input_permission_tip1", "ในการที่จะอนุญาตให้เครื่องปลายทางควบคุมอุปกรณ์แอนดรอยด์ของคุณโดยใช้เมาส์หรือการสัมผัส คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่เซอร์วิสของ RustDesk"), - ("android_input_permission_tip2", "กรุณาไปยังหน้าตั้งค่าถัดไป ค้นหาและเข้าไปยัง [เซอร์วิสที่ถูกติดตั้ง] และเปิดการใช้งานเซอร์วิส [อินพุท RustDesk]"), - ("android_new_connection_tip", "ได้รับคำขอควบคุมใหม่ที่ต้องการควบคุมอุปกรณ์ของคุณ"), - ("android_service_will_start_tip", "การเปิดการใช้งาน \"การบันทึกหน้าจอ\" จะเป็นการเริ่มต้นการทำงานของเซอร์วิสโดยอัตโนมัติ ที่จะอนุญาตให้อุปกรณ์อื่นๆ ส่งคำขอเข้าถึงมายังอุปกรณ์ของคุณได้"), - ("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"), - ("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"), - ("android_start_service_tip", "แตะ [เริ่มต้นใช้งานเซอร์วิส] หรือเปิดสิทธิ์ [การบันทึกหน้าจอ] เพื่อเริ่มเซอร์วิสการแชร์หน้าจอ"), - ("Account", "บัญชี"), - ("Overwrite", "เขียนทับ"), - ("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"), - ("Quit", "ออก"), - ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("Help", "ช่วยเหลือ"), - ("Failed", "ล้มเหลว"), - ("Succeeded", "สำเร็จ"), - ("Someone turns on privacy mode, exit", "มีใครบางคนเปิดใช้งานโหมดความเป็นส่วนตัว กำลังออก"), - ("Unsupported", "ไม่รองรับ"), - ("Peer denied", "ถูกปฏิเสธโดยอีกฝั่ง"), - ("Please install plugins", "กรุณาติดตั้งปลั๊กอิน"), - ("Peer exit", "อีกฝั่งออก"), - ("Failed to turn off", "การปิดล้มเหลว"), - ("Turned off", "ปิด"), - ("In privacy mode", "อยู่ในโหมดความเป็นส่วนตัว"), - ("Out privacy mode", "อยู่นอกโหมดความเป็นส่วนตัว"), - ("Language", "ภาษา"), - ("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"), - ("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"), - ("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"), - ("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), - ("Use permanent password", "ใช้รหัสผ่านถาวร"), - ("Use both passwords", "ใช้รหัสผ่านทั้งสองแบบ"), - ("Set permanent password", "ตั้งค่ารหัสผ่านถาวร"), - ("Enable Remote Restart", "เปิดการใช้งานการรีสตาร์ทระบบทางไกล"), - ("Allow remote restart", "อนุญาตการรีสตาร์ทระบบทางไกล"), - ("Restart Remote Device", "รีสตาร์ทอุปกรณ์ปลายทาง"), - ("Are you sure you want to restart", "คุณแน่ใจหรือไม่ที่จะรีสตาร์ท"), - ("Restarting Remote Device", "กำลังรีสตาร์ทระบบปลายทาง"), - ("remote_restarting_tip", "ระบบปลายทางกำลังรีสตาร์ท กรุณาปิดกล่องข้อความนี้และดำเนินการเขื่อมต่อใหม่อีกครั้งด้วยรหัสผ่านถาวรหลังจากผ่านไปซักครู่"), - ("Copied", "คัดลอกแล้ว"), - ("Exit Fullscreen", "ออกจากเต็มหน้าจอ"), - ("Fullscreen", "เต็มหน้าจอ"), - ("Mobile Actions", "การดำเนินการบนมือถือ"), - ("Select Monitor", "เลือกหน้าจอ"), - ("Control Actions", "การดำเนินการควบคุม"), - ("Display Settings", "การตั้งค่าแสดงผล"), - ("Ratio", "อัตราส่วน"), - ("Image Quality", "คุณภาพภาพ"), - ("Scroll Style", "ลักษณะการเลื่อน"), - ("Show Menubar", "แสดงแถบเมนู"), - ("Hide Menubar", "ซ่อนแถบเมนู"), - ("Direct Connection", "การเชื่อมต่อตรง"), - ("Relay Connection", "การเชื่อมต่อแบบรีเลย์"), - ("Secure Connection", "การเชื่อมต่อที่ปลอดภัย"), - ("Insecure Connection", "การเชื่อมต่อที่ไม่ปลอดภัย"), - ("Scale original", "ขนาดเดิม"), - ("Scale adaptive", "ขนาดยืดหยุ่น"), - ("General", "ทั่วไป"), - ("Security", "ความปลอดภัย"), - ("Account", "บัญชี"), - ("Theme", "ธีม"), - ("Dark Theme", "ธีมมืด"), - ("Dark", "มืด"), - ("Light", "สว่าง"), - ("Follow System", "ตามระบบ"), - ("Enable hardware codec", "เปิดการใช้งานฮาร์ดแวร์ codec"), - ("Unlock Security Settings", "ปลดล็อคการตั้งค่าความปลอดภัย"), - ("Enable Audio", "เปิดการใช้งานเสียง"), - ("Unlock Network Settings", "ปลดล็อคการตั้งค่าเครือข่าย"), - ("Server", "เซิร์ฟเวอร์"), - ("Direct IP Access", "การเข้าถึง IP ตรง"), - ("Proxy", "พรอกซี"), - ("Port", "พอร์ท"), - ("Apply", "นำไปใช้"), - ("Disconnect all devices?", "ยกเลิกการเชื่อมต่ออุปกรณ์ทั้งหมด?"), - ("Clear", "ล้างข้อมูล"), - ("Audio Input Device", "อุปกรณ์รับอินพุทข้อมูลเสียง"), - ("Deny remote access", "ปฏิเสธการเชื่อมต่อ"), - ("Use IP Whitelisting", "ใช้งาน IP ไวท์ลิสต์"), - ("Network", "เครือข่าย"), - ("Enable RDP", "เปิดการใช้งาน RDP"), - ("Pin menubar", "ปักหมุดแถบเมนู"), - ("Unpin menubar", "ยกเลิกการปักหมุดแถบเมนู"), - ("Recording", "การบันทึก"), - ("Directory", "ไดเรกทอรี่"), - ("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"), - ("Change", "เปลี่ยน"), - ("Start session recording", "เริ่มต้นการบันทึกเซสชัน"), - ("Stop session recording", "หยุดการบันทึกเซสซัน"), - ("Enable Recording Session", "เปิดใช้งานการบันทึกเซสชัน"), - ("Allow recording session", "อนุญาตการบันทึกเซสชัน"), - ("Enable LAN Discovery", "เปิดการใช้งานการค้นหาในวง LAN"), - ("Deny LAN Discovery", "ปฏิเสธการใช้งานการค้นหาในวง LAN"), - ("Write a message", "เขียนข้อความ"), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", "กรุณารอการยืนยันจาก UAC..."), - ("elevated_foreground_window_tip", "หน้าต่างปัจจุบันของเครื่องปลายทางต้องการสิทธิ์การใช้งานที่สูงขึ้นสำหรับการทำงาน ดังนั้นเมาส์และคีย์บอร์ดจะไม่สามารถใช้งานได้ชั่วคราว คุณสามารถขอผู้ใช้งานปลายทางให้ย่อหน้าต่าง หรือคลิกปุ่มให้สิทธิ์การใช้งานในหน้าต่างการจัดการการเชื่อมต่อ เพื่อหลีกเลี่ยงปัญหานี้เราแนะนำให้ดำเนินการติดตั้งซอฟท์แวร์ในเครื่องปลายทาง"), - ("Disconnected", "ยกเลิกการเชื่อมต่อ"), - ("Other", "อื่นๆ"), - ("Confirm before closing multiple tabs", "ยืนยันการปิดหลายแท็บ"), - ("Keyboard Settings", "การตั้งค่าคีย์บอร์ด"), - ("Custom", "กำหนดเอง"), - ("Full Access", "การเข้าถึงทั้งหมด"), - ("Screen Share", "การแชร์จอ"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland ต้องการ Ubuntu เวอร์ชั่น 21.04 หรือสูงกว่า"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), - ("JumpLink", "View"), - ("Please Select the screen to be shared(Operate on the peer side).", "กรุณาเลือกหน้าจอที่ต้องการแชร์ (ใช้งานในอีกฝั่งของการเชื่อมต่อ)"), - ("Show RustDesk", "แสดง RustDesk"), - ("This PC", ""), - ("or", "หรือ"), - ("Continue with", "ทำต่อด้วย"), - ("Elevate", "ยกระดับ"), - ("Zoom cursor", "ขยายเคอร์เซอร์"), - ("Accept sessions via password", "ยอมรับการเชื่อมต่อด้วยรหัสผ่าน"), - ("Accept sessions via click", "ยอมรับการเชื่อมต่อด้วยการคลิก"), - ("Accept sessions via both", "ยอมรับการเชื่อมต่อด้วยทั้งสองวิธิ"), - ("Please wait for the remote side to accept your session request...", "กรุณารอให้อีกฝั่งยอมรับการเชื่อมต่อของคุณ..."), - ("One-time Password", "รหัสผ่านครั้งเดียว"), - ("Use one-time password", "ใช้รหัสผ่านครั้งเดียว"), - ("One-time password length", "ความยาวรหัสผ่านครั้งเดียว"), - ("Request access to your device", "คำขอการเข้าถึงอุปกรณ์ของคุณ"), - ("Hide connection management window", "ซ่อนหน้าต่างการจัดการการเชื่อมต่อ"), - ("hide_cm_tip", "อนุญาตการซ่อนก็ต่อเมื่อยอมรับการเชื่อมต่อด้วยรหัสผ่าน และต้องเป็นรหัสผ่านถาวรเท่านั้น"), - ("wayland_experiment_tip", "การสนับสนุน Wayland ยังอยู่ในขั้นตอนการทดลอง กรุณาใช้ X11 หากคุณต้องการใช้งานการเข้าถึงแบบไม่มีผู้ดูแล"), - ("Right click to select tabs", "คลิกขวาเพื่อเลือกแท็บ"), - ("Skipped", "ข้าม"), - ("Add to Address Book", "เพิ่มไปยังสมุดรายชื่อ"), - ("Group", "กลุ่ม"), - ("Search", "ค้นหา"), - ("Closed manually by the web console", "ถูกปิดโดยเว็บคอนโซล"), - ("Local keyboard type", "ประเภทคีย์บอร์ด"), - ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), - ].iter().cloned().collect(); - } - \ No newline at end of file +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "สถานะ"), + ("Your Desktop", "หน้าจอของคุณ"), + ("desk_tip", "คุณสามารถเข้าถึงเดสก์ท็อปของคุณได้ด้วย ID และรหัสผ่านต่อไปนี้"), + ("Password", "รหัสผ่าน"), + ("Ready", "พร้อม"), + ("Established", "เชื่อมต่อแล้ว"), + ("connecting_status", "กำลังเชื่อมต่อไปยังเครือข่าย RustDesk..."), + ("Enable Service", "เปิดใช้การงานเซอร์วิส"), + ("Start Service", "เริ่มต้นใช้งานเซอร์วิส"), + ("Service is running", "เซอร์วิสกำลังทำงาน"), + ("Service is not running", "เซอร์วิสไม่ทำงาน"), + ("not_ready_status", "ไม่พร้อมใช้งาน กรุณาตรวจสอบการเชื่อมต่ออินเทอร์เน็ตของคุณ"), + ("Control Remote Desktop", "การควบคุมเดสก์ท็อปปลายทาง"), + ("Transfer File", "การถ่ายโอนไฟล์"), + ("Connect", "เชื่อมต่อ"), + ("Recent Sessions", "เซสชันล่าสุด"), + ("Address Book", "สมุดรายชื่อ"), + ("Confirmation", "การยืนยัน"), + ("TCP Tunneling", "อุโมงค์การเชื่อมต่อ TCP"), + ("Remove", "ลบ"), + ("Refresh random password", "รีเฟรชรหัสผ่านใหม่แบบสุ่ม"), + ("Set your own password", "ตั้งรหัสผ่านของคุณเอง"), + ("Enable Keyboard/Mouse", "เปิดการใช้งาน คีย์บอร์ด/เมาส์"), + ("Enable Clipboard", "เปิดการใช้งาน คลิปบอร์ด"), + ("Enable File Transfer", "เปิดการใช้งาน การถ่ายโอนไฟล์"), + ("Enable TCP Tunneling", "เปิดการใช้งาน อุโมงค์การเชื่อมต่อ TCP"), + ("IP Whitelisting", "IP ไวท์ลิสต์"), + ("ID/Relay Server", "เซิร์ฟเวอร์ ID/Relay"), + ("Import Server Config", "นำเข้าการตั้งค่าเซิร์ฟเวอร์"), + ("Export Server Config", "ส่งออกการตั้งค่าเซิร์ฟเวอร์"), + ("Import server configuration successfully", "นำเข้าการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), + ("Export server configuration successfully", "ส่งออกการตั้งค่าเซิร์ฟเวอร์เสร็จสมบูรณ์"), + ("Invalid server configuration", "การตั้งค่าของเซิร์ฟเวอร์ไม่ถูกต้อง"), + ("Clipboard is empty", "คลิปบอร์ดว่างเปล่า"), + ("Stop service", "หยุดการใช้งานเซอร์วิส"), + ("Change ID", "เปลี่ยน ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "อนุญาตเฉพาะตัวอักษร a-z A-Z 0-9 และ _ (ขีดล่าง) เท่านั้น โดยตัวอักษรขึ้นต้นจะต้องเป็น a-z หรือไม่ก็ A-Z และมีความยาวระหว่าง 6 ถึง 16 ตัวอักษร"), + ("Website", "เว็บไซต์"), + ("About", "เกี่ยวกับ"), + ("Slogan_tip", "ทำด้วยใจ ในโลกใบนี้ที่ยุ่งเหยิง!"), + ("Privacy Statement", "คำแถลงเกี่ยวกับความเป็นส่วนตัว"), + ("Mute", "ปิดเสียง"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), + ("Audio Input", "ออดิโออินพุท"), + ("Enhancements", "การปรับปรุง"), + ("Hardware Codec", "ฮาร์ดแวร์ codec"), + ("Adaptive Bitrate", "บิทเรทผันแปร"), + ("ID Server", "เซิร์ฟเวอร์ ID"), + ("Relay Server", "เซิร์ฟเวอร์ Relay"), + ("API Server", "เซิร์ฟเวอร์ API"), + ("invalid_http", "ต้องขึ้นต้นด้วย http:// หรือ https:// เท่านั้น"), + ("Invalid IP", "IP ไม่ถูกต้อง"), + ("Invalid format", "รูปแบบไม่ถูกต้อง"), + ("server_not_support", "ยังไม่รองรับโดยเซิร์ฟเวอร์"), + ("Not available", "ไม่พร้อมใช้งาน"), + ("Too frequent", "ดำเนินการถี่เกินไป"), + ("Cancel", "ยกเลิก"), + ("Skip", "ข้าม"), + ("Close", "ปิด"), + ("Retry", "ลองใหม่อีกครั้ง"), + ("OK", "ตกลง"), + ("Password Required", "ต้องใช้รหัสผ่าน"), + ("Please enter your password", "กรุณาใส่รหัสผ่านของคุณ"), + ("Remember password", "จดจำรหัสผ่าน"), + ("Wrong Password", "รหัสผ่านไม่ถูกต้อง"), + ("Do you want to enter again?", "ต้องการใส่ข้อมูลอีกครั้งหรือไม่?"), + ("Connection Error", "การเชื่อมต่อผิดพลาด"), + ("Error", "ข้อผิดพลาด"), + ("Reset by the peer", "รีเซ็ตโดยอีกฝั่ง"), + ("Connecting...", "กำลังเชื่อมต่อ..."), + ("Connection in progress. Please wait.", "กำลังดำเนินการเชื่อมต่อ กรุณารอซักครู่"), + ("Please try 1 minute later", "กรุณาลองใหม่อีกครั้งใน 1 นาที"), + ("Login Error", "การเข้าสู่ระบบผิดพลาด"), + ("Successful", "สำเร็จ"), + ("Connected, waiting for image...", "เชื่อมต่อสำเร็จ กำลังรับข้อมูลภาพ..."), + ("Name", "ชื่อ"), + ("Type", "ประเภท"), + ("Modified", "แก้ไขล่าสุด"), + ("Size", "ขนาด"), + ("Show Hidden Files", "แสดงไฟล์ที่ถูกซ่อน"), + ("Receive", "รับ"), + ("Send", "ส่ง"), + ("Refresh File", "รีเฟรชไฟล์"), + ("Local", "ต้นทาง"), + ("Remote", "ปลายทาง"), + ("Remote Computer", "คอมพิวเตอร์ปลายทาง"), + ("Local Computer", "คอมพิวเตอร์ต้นทาง"), + ("Confirm Delete", "ยืนยันการลบ"), + ("Delete", "ลบ"), + ("Properties", "ข้อมูล"), + ("Multi Select", "เลือกหลายรายการ"), + ("Select All", "เลือกทั้งหมด"), + ("Unselect All", "ยกเลิกการเลือกทั้งหมด"), + ("Empty Directory", "ไดเรกทอรีว่างเปล่า"), + ("Not an empty directory", "ไม่ใช่ไดเรกทอรีว่างเปล่า"), + ("Are you sure you want to delete this file?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์นี้?"), + ("Are you sure you want to delete this empty directory?", "คุณแน่ใจหรือไม่ที่จะลบไดเรอทอรีว่างเปล่านี้?"), + ("Are you sure you want to delete the file of this directory?", "คุณแน่ใจหรือไม่ที่จะลบไฟล์ของไดเรกทอรีนี้?"), + ("Do this for all conflicts", "ดำเนินการแบบเดียวกันสำหรับรายการทั้งหมด"), + ("This is irreversible!", "การดำเนินการนี้ไม่สามารถย้อนกลับได้!"), + ("Deleting", "กำลังลบ"), + ("files", "ไฟล์"), + ("Waiting", "กำลังรอ"), + ("Finished", "เสร็จแล้ว"), + ("Speed", "ความเร็ว"), + ("Custom Image Quality", "คุณภาพของภาพแบบกำหนดเอง"), + ("Privacy mode", "โหมดความเป็นส่วนตัว"), + ("Block user input", "บล็อคอินพุทจากผู้ใช้งาน"), + ("Unblock user input", "ยกเลิกการบล็อคอินพุทจากผู้ใช้งาน"), + ("Adjust Window", "ปรับขนาดหน้าต่าง"), + ("Original", "ต้นฉบับ"), + ("Shrink", "ย่อ"), + ("Stretch", "ยืด"), + ("Scrollbar", "แถบเลื่อน"), + ("ScrollAuto", "เลื่อนอัตโนมัติ"), + ("Good image quality", "ภาพคุณภาพดี"), + ("Balanced", "สมดุล"), + ("Optimize reaction time", "เน้นการตอบสนอง"), + ("Custom", "กำหนดเอง"), + ("Show remote cursor", "แสดงเคอร์เซอร์ปลายทาง"), + ("Show quality monitor", "แสดงคุณภาพหน้าจอ"), + ("Disable clipboard", "ปิดการใช้งานคลิปบอร์ด"), + ("Lock after session end", "ล็อคหลังจากจบเซสชัน"), + ("Insert", "แทรก"), + ("Insert Lock", "แทรกล็อค"), + ("Refresh", "รีเฟรช"), + ("ID does not exist", "ไม่พอข้อมูล ID"), + ("Failed to connect to rendezvous server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์นัดพบล้มเหลว"), + ("Please try later", "กรุณาลองใหม่ในภายหลัง"), + ("Remote desktop is offline", "เดสก์ท็อปปลายทางออฟไลน์"), + ("Key mismatch", "คีย์ไม่ถูกต้อง"), + ("Timeout", "หมดเวลา"), + ("Failed to connect to relay server", "การเชื่อมต่อไปยังเซิร์ฟเวอร์รีเลย์ล้มเหลว"), + ("Failed to connect via rendezvous server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์นัดพบล้มเหลว"), + ("Failed to connect via relay server", "การเชื่อมต่อผ่านเซิร์ฟเวอร์รีเลย์ล้มเหลว"), + ("Failed to make direct connection to remote desktop", "การเชื่อมต่อตรงไปยังเดสก์ท็อปปลายทางล้มเหลว"), + ("Set Password", "ตั้งรหัสผ่าน"), + ("OS Password", "รหัสผ่านระบบปฏิบัติการ"), + ("install_tip", "เนื่องด้วยข้อจำกัดของการใช้งาน UAC ทำให้ RustDesk ไม่สามารถทำงานได้ปกติในฝั่งปลายทางในบางครั้ง เพื่อหลีกเลี่ยงข้อจำกัดของ UAC กรุณากดปุ่มด้านล่างเพื่อติดตั้ง RustDesk ไปยังระบบของคุณ"), + ("Click to upgrade", "คลิกเพื่ออัปเกรด"), + ("Click to download", "คลิกเพื่อดาวน์โหลด"), + ("Click to update", "คลิกเพื่ออัปเดต"), + ("Configure", "ปรับแต่งค่า"), + ("config_acc", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่ RustDesk"), + ("config_screen", "เพื่อที่จะควบคุมเดสก์ท็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การบันทึกภาพหน้าจอ\" ให้แก่ RustDesk"), + ("Installing ...", "กำลังติดตั้ง ..."), + ("Install", "ติดตั้ง"), + ("Installation", "การติดตั้ง"), + ("Installation Path", "ตำแหน่งที่ติดตั้ง"), + ("Create start menu shortcuts", "สร้างทางลัดไปยัง Start Menu"), + ("Create desktop icon", "สร้างไอคอนบนเดสก์ท็อป"), + ("agreement_tip", "ในการเริ่มต้นการติดตั้ง ถือว่าคุณได้ยอมรับข้อตกลงใบอนุญาตแล้ว"), + ("Accept and Install", "ยอมรับและติดตั้ง"), + ("End-user license agreement", "ข้อตกลงใบอนุญาตผู้ใช้งาน"), + ("Generating ...", "กำลังสร้าง ..."), + ("Your installation is lower version.", "การติดตั้งของคุณเป็นเวอร์ชั่นที่ต่ำกว่า"), + ("not_close_tcp_tip", "อย่าปิดหน้าต่างนี้ในขณะที่คุณกำลังใช้งานอุโมงค์การเชื่อมต่อ"), + ("Listening ...", "กำลังรอรับข้อมูล ..."), + ("Remote Host", "โฮสต์ปลายทาง"), + ("Remote Port", "พอร์ทปลายทาง"), + ("Action", "การดำเนินการ"), + ("Add", "เพิ่ม"), + ("Local Port", "พอร์ทต้นทาง"), + ("Local Address", "ที่อยู่ต้นทาง"), + ("Change Local Port", "เปลี่ยนพอร์ทต้นทาง"), + ("setup_server_tip", "เพื่อการเชื่อมต่อที่เร็วขึ้น กรุณาเซ็ตอัปเซิร์ฟเวอร์ของคุณเอง"), + ("Too short, at least 6 characters.", "สั้นเกินไป ต้องไม่ต่ำกว่า 6 ตัวอักษร"), + ("The confirmation is not identical.", "การยืนยันข้อมูลไม่ถูกต้อง"), + ("Permissions", "สิทธิ์การใช้งาน"), + ("Accept", "ยอมรับ"), + ("Dismiss", "ปิด"), + ("Disconnect", "ยกเลิกการเชื่อมต่อ"), + ("Allow using keyboard and mouse", "อนุญาตให้ใช้งานคีย์บอร์ดและเมาส์"), + ("Allow using clipboard", "อนุญาตให้ใช้คลิปบอร์ด"), + ("Allow hearing sound", "อนุญาตให้ได้ยินเสียง"), + ("Allow file copy and paste", "อนุญาตให้มีการคัดลอกและวางไฟล์"), + ("Connected", "เชื่อมต่อแล้ว"), + ("Direct and encrypted connection", "การเชื่อมต่อตรงที่มีการเข้ารหัส"), + ("Relayed and encrypted connection", "การเชื่อมต่อแบบรีเลย์ที่มีการเข้ารหัส"), + ("Direct and unencrypted connection", "การเชื่อมต่อตรงที่ไม่มีการเข้ารหัส"), + ("Relayed and unencrypted connection", "การเชื่อมต่อแบบรีเลย์ที่ไม่มีการเข้ารหัส"), + ("Enter Remote ID", "กรอก ID ปลายทาง"), + ("Enter your password", "กรอกรหัสผ่าน"), + ("Logging in...", "กำลังเข้าสู่ระบบ..."), + ("Enable RDP session sharing", "เปิดการใช้งานการแชร์เซสชัน RDP"), + ("Auto Login", "เข้าสู่ระบอัตโนมัติ"), + ("Enable Direct IP Access", "เปิดการใช้งาน IP ตรง"), + ("Rename", "ปลายทาง"), + ("Space", "พื้นที่ว่าง"), + ("Create Desktop Shortcut", "สร้างทางลัดบนเดสก์ท็อป"), + ("Change Path", "เปลี่ยนตำแหน่ง"), + ("Create Folder", "สร้างโฟลเดอร์"), + ("Please enter the folder name", "กรุณาใส่ชื่อโฟลเดอร์"), + ("Fix it", "แก้ไข"), + ("Warning", "คำเตือน"), + ("Login screen using Wayland is not supported", "หน้าจอการเข้าสู่ระบบโดยใช้ Wayland ยังไม่ถูกรองรับ"), + ("Reboot required", "จำเป็นต้องเริ่มต้นระบบใหม่"), + ("Unsupported display server", "เซิร์ฟเวอร์การแสดงผลที่ไม่รองรับ"), + ("x11 expected", "ต้องใช้งาน x11"), + ("Port", "พอร์ท"), + ("Settings", "ตั้งค่า"), + ("Username", "ชื่อผู้ใช้งาน"), + ("Invalid port", "พอร์ทไม่ถูกต้อง"), + ("Closed manually by the peer", "ถูกปิดโดยอีกฝั่งการการเชื่อมต่อ"), + ("Enable remote configuration modification", "เปิดการใช้งานการแก้ไขการตั้งค่าปลายทาง"), + ("Run without install", "ใช้งานโดยไม่ต้องติดตั้ง"), + ("Connect via relay", ""), + ("Always connect via relay", "เชื่อมต่อผ่านรีเลย์เสมอ"), + ("whitelist_tip", "อนุญาตเฉพาะการเชื่อมต่อจาก IP ที่ไวท์ลิสต์"), + ("Login", "เข้าสู่ระบบ"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), + ("Logout", "ออกจากระบบ"), + ("Tags", "แท็ก"), + ("Search ID", "ค้นหา ID"), + ("whitelist_sep", "คั่นโดยเครื่องหมาย comma semicolon เว้นวรรค หรือ ขึ้นบรรทัดใหม่"), + ("Add ID", "เพิ่ม ID"), + ("Add Tag", "เพิ่มแท็ก"), + ("Unselect all tags", "ยกเลิกการเลือกแท็กทั้งหมด"), + ("Network error", "ข้อผิดพลาดของเครือข่าย"), + ("Username missed", "ไม่พบข้อมูลผู้ใช้งาน"), + ("Password missed", "ไม่พบรหัสผ่าน"), + ("Wrong credentials", "ข้อมูลสำหรับเข้าสู่ระบบไม่ถูกต้อง"), + ("Edit Tag", "แก้ไขแท็ก"), + ("Unremember Password", "ยกเลิกการจดจำรหัสผ่าน"), + ("Favorites", "รายการโปรด"), + ("Add to Favorites", "เพิ่มไปยังรายการโปรด"), + ("Remove from Favorites", "ลบออกจากรายการโปรด"), + ("Empty", "ว่างเปล่า"), + ("Invalid folder name", "ชื่อโฟลเดอร์ไม่ถูกต้อง"), + ("Socks5 Proxy", "พรอกซี Socks5"), + ("Hostname", "ชื่อโฮสต์"), + ("Discovered", "ค้นพบ"), + ("install_daemon_tip", "หากต้องการใช้งานขณะระบบเริ่มต้น คุณจำเป็นจะต้องติดตั้งเซอร์วิส"), + ("Remote ID", "ID ปลายทาง"), + ("Paste", "วาง"), + ("Paste here?", "วางที่นี่หรือไม่?"), + ("Are you sure to close the connection?", "คุณแน่ใจหรือไม่ที่จะปิดการเชื่อมต่อ?"), + ("Download new version", "ดาวน์โหลดเวอร์ชั่นใหม่"), + ("Touch mode", "โหมดการสัมผัส"), + ("Mouse mode", "โหมดการใช้เมาส์"), + ("One-Finger Tap", "แตะนิ้วเดียว"), + ("Left Mouse", "เมาส์ซ้าย"), + ("One-Long Tap", "แตะยาวหนึ่งครั้ง"), + ("Two-Finger Tap", "แตะสองนิ้ว"), + ("Right Mouse", "เมาส์ขวา"), + ("One-Finger Move", "ลากนิ้วเดียว"), + ("Double Tap & Move", "แตะเบิ้ลและลาก"), + ("Mouse Drag", "ลากเมาส์"), + ("Three-Finger vertically", "สามนิ้วแนวตั้ง"), + ("Mouse Wheel", "ลูกลิ้งเมาส์"), + ("Two-Finger Move", "ลากสองนิ้ว"), + ("Canvas Move", "ลากแคนวาส"), + ("Pinch to Zoom", "ถ่างเพื่อขยาย"), + ("Canvas Zoom", "ขยายแคนวาส"), + ("Reset canvas", "รีเซ็ตแคนวาส"), + ("No permission of file transfer", "ไม่มีสิทธิ์ในการถ่ายโอนไฟล์"), + ("Note", "บันทึกข้อความ"), + ("Connection", "การเชื่อมต่อ"), + ("Share Screen", "แชร์หน้าจอ"), + ("Chat", "แชท"), + ("Total", "รวม"), + ("items", "รายการ"), + ("Selected", "ถูกเลือก"), + ("Screen Capture", "แคปเจอร์หน้าจอ"), + ("Input Control", "ควบคุมอินพุท"), + ("Audio Capture", "แคปเจอร์เสียง"), + ("File Connection", "การเชื่อมต่อไฟล์"), + ("Screen Connection", "การเชื่อมต่อหน้าจอ"), + ("Do you accept?", "ยอมรับหรือไม่?"), + ("Open System Setting", "เปิดการตั้งค่าระบบ"), + ("How to get Android input permission?", "เปิดสิทธิ์การใช้งานอินพุทของแอนดรอยด์ได้อย่างไร?"), + ("android_input_permission_tip1", "ในการที่จะอนุญาตให้เครื่องปลายทางควบคุมอุปกรณ์แอนดรอยด์ของคุณโดยใช้เมาส์หรือการสัมผัส คุณจำเป็นจะต้องอนุญาตสิทธิ์ \"การเข้าถึง\" ให้แก่เซอร์วิสของ RustDesk"), + ("android_input_permission_tip2", "กรุณาไปยังหน้าตั้งค่าถัดไป ค้นหาและเข้าไปยัง [เซอร์วิสที่ถูกติดตั้ง] และเปิดการใช้งานเซอร์วิส [อินพุท RustDesk]"), + ("android_new_connection_tip", "ได้รับคำขอควบคุมใหม่ที่ต้องการควบคุมอุปกรณ์ของคุณ"), + ("android_service_will_start_tip", "การเปิดการใช้งาน \"การบันทึกหน้าจอ\" จะเป็นการเริ่มต้นการทำงานของเซอร์วิสโดยอัตโนมัติ ที่จะอนุญาตให้อุปกรณ์อื่นๆ ส่งคำขอเข้าถึงมายังอุปกรณ์ของคุณได้"), + ("android_stop_service_tip", "การปิดการใช้งานเซอร์วิสจะปิดการเชื่อมต่อทั้งหมดโดยอัตโนมัติ"), + ("android_version_audio_tip", "เวอร์ชั่นแอนดรอยด์ปัจจุบันของคุณไม่รองรับการบันทึกข้อมูลเสียง กรุณาอัปเกรดเป็นแอนดรอยด์เวอร์ชั่น 10 หรือสูงกว่า"), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), + ("Account", "บัญชี"), + ("Overwrite", "เขียนทับ"), + ("This file exists, skip or overwrite this file?", "พบไฟล์ที่มีอยู่แล้ว ต้องการเขียนทับหรือไม่?"), + ("Quit", "ออก"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "ช่วยเหลือ"), + ("Failed", "ล้มเหลว"), + ("Succeeded", "สำเร็จ"), + ("Someone turns on privacy mode, exit", "มีใครบางคนเปิดใช้งานโหมดความเป็นส่วนตัว กำลังออก"), + ("Unsupported", "ไม่รองรับ"), + ("Peer denied", "ถูกปฏิเสธโดยอีกฝั่ง"), + ("Please install plugins", "กรุณาติดตั้งปลั๊กอิน"), + ("Peer exit", "อีกฝั่งออก"), + ("Failed to turn off", "การปิดล้มเหลว"), + ("Turned off", "ปิด"), + ("In privacy mode", "อยู่ในโหมดความเป็นส่วนตัว"), + ("Out privacy mode", "อยู่นอกโหมดความเป็นส่วนตัว"), + ("Language", "ภาษา"), + ("Keep RustDesk background service", "คงสถานะการทำงานเบื้องหลังของเซอร์วิส RustDesk"), + ("Ignore Battery Optimizations", "เพิกเฉยการตั้งค่าการใช้งาน Battery Optimization"), + ("android_open_battery_optimizations_tip", "หากคุณต้องการปิดการใช้งานฟีเจอร์นี้ กรุณาไปยังหน้าตั้งค่าในแอปพลิเคชัน RustDesk ค้นหาหัวข้อ [Battery] และยกเลิกการเลือกรายการ [Unrestricted]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), + ("Connection not allowed", "การเชื่อมต่อไม่อนุญาต"), + ("Legacy mode", ""), + ("Map mode", ""), + ("Translate mode", ""), + ("Use permanent password", "ใช้รหัสผ่านถาวร"), + ("Use both passwords", "ใช้รหัสผ่านทั้งสองแบบ"), + ("Set permanent password", "ตั้งค่ารหัสผ่านถาวร"), + ("Enable Remote Restart", "เปิดการใช้งานการรีสตาร์ทระบบทางไกล"), + ("Allow remote restart", "อนุญาตการรีสตาร์ทระบบทางไกล"), + ("Restart Remote Device", "รีสตาร์ทอุปกรณ์ปลายทาง"), + ("Are you sure you want to restart", "คุณแน่ใจหรือไม่ที่จะรีสตาร์ท"), + ("Restarting Remote Device", "กำลังรีสตาร์ทระบบปลายทาง"), + ("remote_restarting_tip", "ระบบปลายทางกำลังรีสตาร์ท กรุณาปิดกล่องข้อความนี้และดำเนินการเขื่อมต่อใหม่อีกครั้งด้วยรหัสผ่านถาวรหลังจากผ่านไปซักครู่"), + ("Copied", "คัดลอกแล้ว"), + ("Exit Fullscreen", "ออกจากเต็มหน้าจอ"), + ("Fullscreen", "เต็มหน้าจอ"), + ("Mobile Actions", "การดำเนินการบนมือถือ"), + ("Select Monitor", "เลือกหน้าจอ"), + ("Control Actions", "การดำเนินการควบคุม"), + ("Display Settings", "การตั้งค่าแสดงผล"), + ("Ratio", "อัตราส่วน"), + ("Image Quality", "คุณภาพภาพ"), + ("Scroll Style", "ลักษณะการเลื่อน"), + ("Show Menubar", "แสดงแถบเมนู"), + ("Hide Menubar", "ซ่อนแถบเมนู"), + ("Direct Connection", "การเชื่อมต่อตรง"), + ("Relay Connection", "การเชื่อมต่อแบบรีเลย์"), + ("Secure Connection", "การเชื่อมต่อที่ปลอดภัย"), + ("Insecure Connection", "การเชื่อมต่อที่ไม่ปลอดภัย"), + ("Scale original", "ขนาดเดิม"), + ("Scale adaptive", "ขนาดยืดหยุ่น"), + ("General", "ทั่วไป"), + ("Security", "ความปลอดภัย"), + ("Theme", "ธีม"), + ("Dark Theme", "ธีมมืด"), + ("Light Theme", ""), + ("Dark", "มืด"), + ("Light", "สว่าง"), + ("Follow System", "ตามระบบ"), + ("Enable hardware codec", "เปิดการใช้งานฮาร์ดแวร์ codec"), + ("Unlock Security Settings", "ปลดล็อคการตั้งค่าความปลอดภัย"), + ("Enable Audio", "เปิดการใช้งานเสียง"), + ("Unlock Network Settings", "ปลดล็อคการตั้งค่าเครือข่าย"), + ("Server", "เซิร์ฟเวอร์"), + ("Direct IP Access", "การเข้าถึง IP ตรง"), + ("Proxy", "พรอกซี"), + ("Apply", "นำไปใช้"), + ("Disconnect all devices?", "ยกเลิกการเชื่อมต่ออุปกรณ์ทั้งหมด?"), + ("Clear", "ล้างข้อมูล"), + ("Audio Input Device", "อุปกรณ์รับอินพุทข้อมูลเสียง"), + ("Deny remote access", "ปฏิเสธการเชื่อมต่อ"), + ("Use IP Whitelisting", "ใช้งาน IP ไวท์ลิสต์"), + ("Network", "เครือข่าย"), + ("Enable RDP", "เปิดการใช้งาน RDP"), + ("Pin menubar", "ปักหมุดแถบเมนู"), + ("Unpin menubar", "ยกเลิกการปักหมุดแถบเมนู"), + ("Recording", "การบันทึก"), + ("Directory", "ไดเรกทอรี่"), + ("Automatically record incoming sessions", "บันทึกเซสชันขาเข้าโดยอัตโนมัติ"), + ("Change", "เปลี่ยน"), + ("Start session recording", "เริ่มต้นการบันทึกเซสชัน"), + ("Stop session recording", "หยุดการบันทึกเซสซัน"), + ("Enable Recording Session", "เปิดใช้งานการบันทึกเซสชัน"), + ("Allow recording session", "อนุญาตการบันทึกเซสชัน"), + ("Enable LAN Discovery", "เปิดการใช้งานการค้นหาในวง LAN"), + ("Deny LAN Discovery", "ปฏิเสธการใช้งานการค้นหาในวง LAN"), + ("Write a message", "เขียนข้อความ"), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", "กรุณารอการยืนยันจาก UAC..."), + ("elevated_foreground_window_tip", "หน้าต่างปัจจุบันของเครื่องปลายทางต้องการสิทธิ์การใช้งานที่สูงขึ้นสำหรับการทำงาน ดังนั้นเมาส์และคีย์บอร์ดจะไม่สามารถใช้งานได้ชั่วคราว คุณสามารถขอผู้ใช้งานปลายทางให้ย่อหน้าต่าง หรือคลิกปุ่มให้สิทธิ์การใช้งานในหน้าต่างการจัดการการเชื่อมต่อ เพื่อหลีกเลี่ยงปัญหานี้เราแนะนำให้ดำเนินการติดตั้งซอฟท์แวร์ในเครื่องปลายทาง"), + ("Disconnected", "ยกเลิกการเชื่อมต่อ"), + ("Other", "อื่นๆ"), + ("Confirm before closing multiple tabs", "ยืนยันการปิดหลายแท็บ"), + ("Keyboard Settings", "การตั้งค่าคีย์บอร์ด"), + ("Full Access", "การเข้าถึงทั้งหมด"), + ("Screen Share", "การแชร์จอ"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland ต้องการ Ubuntu เวอร์ชั่น 21.04 หรือสูงกว่า"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland ต้องการลินุกซ์เวอร์ชันที่สูงกว่านี้ กรุณาเปลี่ยนไปใช้เดสก์ท็อป X11 หรือเปลี่ยนระบบปฏิบัติการของคุณ"), + ("JumpLink", "View"), + ("Please Select the screen to be shared(Operate on the peer side).", "กรุณาเลือกหน้าจอที่ต้องการแชร์ (ใช้งานในอีกฝั่งของการเชื่อมต่อ)"), + ("Show RustDesk", "แสดง RustDesk"), + ("This PC", ""), + ("or", "หรือ"), + ("Continue with", "ทำต่อด้วย"), + ("Elevate", "ยกระดับ"), + ("Zoom cursor", "ขยายเคอร์เซอร์"), + ("Accept sessions via password", "ยอมรับการเชื่อมต่อด้วยรหัสผ่าน"), + ("Accept sessions via click", "ยอมรับการเชื่อมต่อด้วยการคลิก"), + ("Accept sessions via both", "ยอมรับการเชื่อมต่อด้วยทั้งสองวิธิ"), + ("Please wait for the remote side to accept your session request...", "กรุณารอให้อีกฝั่งยอมรับการเชื่อมต่อของคุณ..."), + ("One-time Password", "รหัสผ่านครั้งเดียว"), + ("Use one-time password", "ใช้รหัสผ่านครั้งเดียว"), + ("One-time password length", "ความยาวรหัสผ่านครั้งเดียว"), + ("Request access to your device", "คำขอการเข้าถึงอุปกรณ์ของคุณ"), + ("Hide connection management window", "ซ่อนหน้าต่างการจัดการการเชื่อมต่อ"), + ("hide_cm_tip", "อนุญาตการซ่อนก็ต่อเมื่อยอมรับการเชื่อมต่อด้วยรหัสผ่าน และต้องเป็นรหัสผ่านถาวรเท่านั้น"), + ("wayland_experiment_tip", "การสนับสนุน Wayland ยังอยู่ในขั้นตอนการทดลอง กรุณาใช้ X11 หากคุณต้องการใช้งานการเข้าถึงแบบไม่มีผู้ดูแล"), + ("Right click to select tabs", "คลิกขวาเพื่อเลือกแท็บ"), + ("Skipped", "ข้าม"), + ("Add to Address Book", "เพิ่มไปยังสมุดรายชื่อ"), + ("Group", "กลุ่ม"), + ("Search", "ค้นหา"), + ("Closed manually by web console", "ถูกปิดโดยเว็บคอนโซล"), + ("Local keyboard type", "ประเภทคีย์บอร์ด"), + ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/tr.rs b/src/lang/tr.rs index f74d0b435..f5209129b 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Kopyalanan geçici veri boş"), ("Stop service", "Servisi Durdur"), ("Change ID", "ID Değiştir"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Yalnızca a-z, A-Z, 0-9 ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), ("Website", "Website"), ("About", "Hakkında"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Sustur"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), ("Hardware Codec", "Donanımsal Codec"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API Sunucu"), ("invalid_http", "http:// veya https:// ile başlamalıdır"), ("Invalid IP", "Geçersiz IP adresi"), - ("id_change_tip", "Yalnızca a-z, A-Z, 0-9 ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), ("Invalid format", "Hatalı Format"), ("server_not_support", "Henüz sunucu tarafından desteklenmiyor"), ("Not available", "Erişilebilir değil"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Uyarı"), ("Login screen using Wayland is not supported", "Wayland kullanan giriş ekranı desteklenmiyor"), ("Reboot required", "Yeniden başlatma gerekli"), - ("Unsupported display server ", "Desteklenmeyen görüntü sunucusu"), + ("Unsupported display server", "Desteklenmeyen görüntü sunucusu"), ("x11 expected", "x11 bekleniyor"), ("Port", "Port"), ("Settings", "Ayarlar"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Eş tarafından manuel olarak kapatıldı"), ("Enable remote configuration modification", "Uzaktan yapılandırma değişikliğini etkinleştir"), ("Run without install", "Yüklemeden çalıştır"), - ("Always connected via relay", "Her zaman röle ile bağlı"), + ("Connect via relay", ""), ("Always connect via relay", "Always connect via relay"), ("whitelist_tip", "Bu masaüstüne yalnızca yetkili IP adresleri bağlanabilir"), ("Login", "Giriş yap"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Çıkış yap"), ("Tags", "Etiketler"), ("Search ID", "ID Arama"), - ("Current Wayland display server is not supported", "Mevcut Wayland görüntüleme sunucusu desteklenmiyor"), ("whitelist_sep", "Virgül, noktalı virgül, boşluk veya yeni satır ile ayrılmış"), ("Add ID", "ID Ekle"), ("Add Tag", "Etiket Ekle"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Not"), ("Connection", "Bağlantı"), ("Share Screen", "Ekranı Paylaş"), - ("CLOSE", "KAPAT"), - ("OPEN", "AÇ"), ("Chat", "Mesajlaş"), ("Total", "Toplam"), ("items", "öğeler"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Ekran Yakalamanın etkinleştirilmesi, hizmeti otomatik olarak başlatacak ve diğer cihazların bu cihazdan bağlantı talep etmesine izin verecektir."), ("android_stop_service_tip", "Hizmetin kapatılması, kurulan tüm bağlantıları otomatik olarak kapatacaktır."), ("android_version_audio_tip", "Mevcut Android sürümü ses yakalamayı desteklemiyor, lütfen Android 10 veya sonraki bir sürüme yükseltin."), - ("android_start_service_tip", "Ekran paylaşım hizmetini başlatmak için [Hizmeti Başlat] veya AÇ [Ekran Yakalama] iznine dokunun."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Hesap"), ("Overwrite", "üzerine yaz"), ("This file exists, skip or overwrite this file?", "Bu dosya var, bu dosya atlansın veya üzerine yazılsın mı?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "RustDesk arka plan hizmetini sürdürün"), ("Ignore Battery Optimizations", "Pil Optimizasyonlarını Yoksay"), ("android_open_battery_optimizations_tip", ""), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "bağlantıya izin verilmedi"), ("Legacy mode", "Eski mod"), ("Map mode", "Haritalama modu"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Ölçek uyarlanabilir"), ("General", "Genel"), ("Security", "Güvenlik"), - ("Account", "Hesap"), ("Theme", "Tema"), ("Dark Theme", "Koyu Tema"), + ("Light Theme", ""), ("Dark", "Koyu"), ("Light", "Açık"), ("Follow System", "Sisteme Uy"), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Sunucu"), ("Direct IP Access", "Direk IP Erişimi"), ("Proxy", "Vekil"), - ("Port", "Port"), ("Apply", "Uygula"), ("Disconnect all devices?", "Tüm cihazların bağlantısını kes?"), ("Clear", "Temizle"), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Diğer"), ("Confirm before closing multiple tabs", "Çoklu sekmeleri kapatmadan önce onayla"), ("Keyboard Settings", "Klavye Ayarları"), - ("Custom", "Özel"), ("Full Access", "Tam Erişim"), ("Screen Share", "Ekran Paylaşımı"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland, Ubuntu 21.04 veya daha yüksek bir sürüm gerektirir."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index a3eb9691d..2158db175 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -1,412 +1,487 @@ -lazy_static::lazy_static! { -pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "狀態"), - ("Your Desktop", "您的桌面"), - ("desk_tip", "您可以透過此 ID 及密碼存取您的桌面"), - ("Password", "密碼"), - ("Ready", "就緒"), - ("Established", "已建立"), - ("connecting_status", "正在連接至 RustDesk 網路..."), - ("Enable Service", "啟用服務"), - ("Start Service", "啟動服務"), - ("Service is running", "服務正在運行"), - ("Service is not running", "服務尚未執行"), - ("not_ready_status", "尚未就緒。請檢查您的網路連線"), - ("Control Remote Desktop", "控制遠端桌面"), - ("Transfer File", "傳輸檔案"), - ("Connect", "連接"), - ("Recent Sessions", "近期的工作階段"), - ("Address Book", "通訊錄"), - ("Confirmation", "確認"), - ("TCP Tunneling", "TCP 通道"), - ("Remove", "移除"), - ("Refresh random password", "重新產生隨機密碼"), - ("Set your own password", "自行設置密碼"), - ("Enable Keyboard/Mouse", "啟用鍵盤/滑鼠"), - ("Enable Clipboard", "啟用剪貼簿"), - ("Enable File Transfer", "啟用檔案傳輸"), - ("Enable TCP Tunneling", "啟用 TCP 通道"), - ("IP Whitelisting", "IP 白名單"), - ("ID/Relay Server", "ID/轉送伺服器"), - ("Import Server Config", "匯入伺服器設定"), - ("Export Server Config", "導出服務器配置"), - ("Import server configuration successfully", "匯入伺服器設定成功"), - ("Export server configuration successfully", "導出服務器配置信息成功"), - ("Invalid server configuration", "無效的伺服器設定"), - ("Clipboard is empty", "剪貼簿是空的"), - ("Stop service", "停止服務"), - ("Change ID", "更改 ID"), - ("Website", "網站"), - ("About", "關於"), - ("Slogan_tip", ""), - ("Privacy Statement", ""), - ("Mute", "靜音"), - ("Audio Input", "音訊輸入"), - ("Enhancements", "增強功能"), - ("Hardware Codec", "硬件編解碼"), - ("Adaptive Bitrate", "自適應碼率"), - ("ID Server", "ID 伺服器"), - ("Relay Server", "轉送伺服器"), - ("API Server", "API 伺服器"), - ("invalid_http", "開頭必須為 http:// 或 https://"), - ("Invalid IP", "IP 無效"), - ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), - ("Invalid format", "格式無效"), - ("server_not_support", "服務器暫不支持"), - ("Not available", "無法使用"), - ("Too frequent", "修改過於頻繁,請稍後再試。"), - ("Cancel", "取消"), - ("Skip", "跳過"), - ("Close", "關閉"), - ("Retry", "重試"), - ("OK", "確定"), - ("Password Required", "需要密碼"), - ("Please enter your password", "請輸入您的密碼"), - ("Remember password", "記住密碼"), - ("Wrong Password", "密碼錯誤"), - ("Do you want to enter again?", "您要重新輸入嗎?"), - ("Connection Error", "連線錯誤"), - ("Error", "錯誤"), - ("Reset by the peer", "對方重置了連線"), - ("Connecting...", "正在連接..."), - ("Connection in progress. Please wait.", "正在連接,請稍候。"), - ("Please try 1 minute later", "請於 1 分鐘後再試"), - ("Login Error", "登入錯誤"), - ("Successful", "成功"), - ("Connected, waiting for image...", "已連線,等待畫面傳輸..."), - ("Name", "名稱"), - ("Type", "類型"), - ("Modified", "修改時間"), - ("Size", "大小"), - ("Show Hidden Files", "顯示隱藏檔案"), - ("Receive", "接收"), - ("Send", "傳送"), - ("Refresh File", "刷新文件"), - ("Local", "本地"), - ("Remote", "遠端"), - ("Remote Computer", "遠端電腦"), - ("Local Computer", "本地電腦"), - ("Confirm Delete", "確認刪除"), - ("Delete", "刪除"), - ("Properties", "屬性"), - ("Multi Select", "多選"), - ("Select All", "全選"), - ("Unselect All", "取消全選"), - ("Empty Directory", "空文件夾"), - ("Not an empty directory", "不是一個空文件夾"), - ("Are you sure you want to delete this file?", "您確定要刪除此檔案嗎?"), - ("Are you sure you want to delete this empty directory?", "您確定要刪除此空目錄嗎?"), - ("Are you sure you want to delete the file of this directory?", "您確定要刪除此目錄中的檔案嗎?"), - ("Do this for all conflicts", "套用到其他衝突"), - ("This is irreversible!", "此操作不可逆!"), - ("Deleting", "正在刪除"), - ("files", "檔案"), - ("Waiting", "正在等候..."), - ("Finished", "已完成"), - ("Speed", "速度"), - ("Custom Image Quality", "自訂圖片品質"), - ("Privacy mode", "隱私模式"), - ("Block user input", "封鎖使用者輸入"), - ("Unblock user input", "取消封鎖使用者輸入"), - ("Adjust Window", "調整視窗"), - ("Original", "原始"), - ("Shrink", "縮減"), - ("Stretch", "延展"), - ("Scrollbar", "滾動條"), - ("ScrollAuto", "自動滾動"), - ("Good image quality", "畫面品質良好"), - ("Balanced", "平衡"), - ("Optimize reaction time", "回應速度最佳化"), - ("Custom", "自定義"), - ("Show remote cursor", "顯示遠端游標"), - ("Show quality monitor", "顯示質量監測"), - ("Disable clipboard", "停用剪貼簿"), - ("Lock after session end", "工作階段結束後鎖定電腦"), - ("Insert", "插入"), - ("Insert Lock", "鎖定遠端電腦"), - ("Refresh", "重新載入"), - ("ID does not exist", "ID 不存在"), - ("Failed to connect to rendezvous server", "無法連接至 rendezvous 伺服器"), - ("Please try later", "請稍候再試"), - ("Remote desktop is offline", "遠端電腦離線"), - ("Key mismatch", "金鑰不符"), - ("Timeout", "逾時"), - ("Failed to connect to relay server", "無法連接至轉送伺服器"), - ("Failed to connect via rendezvous server", "無法透過 rendezvous 伺服器連接"), - ("Failed to connect via relay server", "無法透過轉送伺服器連接"), - ("Failed to make direct connection to remote desktop", "無法直接連線至遠端電腦"), - ("Set Password", "設置密碼"), - ("OS Password", "作業系統密碼"), - ("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常以遠端電腦運作。若要避開 UAC,請點擊下方按鈕將 RustDesk 安裝到系統中。"), - ("Click to upgrade", "點擊以升級"), - ("Click to download", "點擊以下載"), - ("Click to update", "點擊以更新"), - ("Configure", "設定"), - ("config_acc", "您需要授予 RustDesk 「協助工具」 權限才能遠端存取電腦。"), - ("config_screen", "您需要授予 RustDesk 「畫面錄製」 權限才能遠端存取電腦。"), - ("Installing ...", "正在安裝..."), - ("Install", "安裝"), - ("Installation", "安裝"), - ("Installation Path", "安裝路徑"), - ("Create start menu shortcuts", "建立開始選單捷徑"), - ("Create desktop icon", "建立桌面圖示"), - ("agreement_tip", "開始安裝即表示接受許可協議"), - ("Accept and Install", "接受並安裝"), - ("End-user license agreement", "使用者授權合約"), - ("Generating ...", "正在產生 ..."), - ("Your installation is lower version.", "您的安裝版本過舊。"), - ("not_close_tcp_tip", "使用通道時請不要關閉此視窗"), - ("Listening ...", "正在等待通道連接..."), - ("Remote Host", "遠端主機"), - ("Remote Port", "遠端連接埠"), - ("Action", "操作"), - ("Add", "新增"), - ("Local Port", "本機連接埠"), - ("Local Address", "本機地址"), - ("Change Local Port", "修改本機連接埠"), - ("setup_server_tip", "若您需要更快的連接速度,可以選擇自行建立伺服器"), - ("Too short, at least 6 characters.", "過短,至少需 6 個字元。"), - ("The confirmation is not identical.", "兩次輸入不相符"), - ("Permissions", "權限"), - ("Accept", "接受"), - ("Dismiss", "關閉"), - ("Disconnect", "斷開連線"), - ("Allow using keyboard and mouse", "允許使用鍵盤和滑鼠"), - ("Allow using clipboard", "允許使用剪貼簿"), - ("Allow hearing sound", "允許分享音訊"), - ("Allow file copy and paste", "允許文件複製和粘貼"), - ("Connected", "已連接"), - ("Direct and encrypted connection", "加密直接連線"), - ("Relayed and encrypted connection", "加密轉送連線"), - ("Direct and unencrypted connection", "未加密直接連線"), - ("Relayed and unencrypted connection", "未加密轉送連線"), - ("Enter Remote ID", "輸入遠端 ID"), - ("Enter your password", "輸入您的密碼"), - ("Logging in...", "正在登入..."), - ("Enable RDP session sharing", "啟用 RDP 工作階段共享"), - ("Auto Login", "自動登入 (鎖定將在設定關閉後套用)"), - ("Enable Direct IP Access", "允許 IP 直接存取"), - ("Rename", "重新命名"), - ("Space", "空白"), - ("Create Desktop Shortcut", "建立桌面捷徑"), - ("Change Path", "更改路徑"), - ("Create Folder", "建立資料夾"), - ("Please enter the folder name", "請輸入資料夾名稱"), - ("Fix it", "修復"), - ("Warning", "警告"), - ("Login screen using Wayland is not supported", "不支援使用 Wayland 的登入畫面"), - ("Reboot required", "需要重新啟動"), - ("Unsupported display server ", "不支援顯示伺服器"), - ("x11 expected", "預期 x11"), - ("Port", "端口"), - ("Settings", "設定"), - ("Username", "使用者名稱"), - ("Invalid port", "連接埠無效"), - ("Closed manually by the peer", "由對方手動關閉"), - ("Enable remote configuration modification", "啟用遠端更改設定"), - ("Run without install", "跳過安裝直接執行"), - ("Always connected via relay", "一律透過轉送連線"), - ("Always connect via relay", "一律透過轉送連線"), - ("whitelist_tip", "只有白名單中的 IP 可以存取"), - ("Login", "登入"), - ("Logout", "登出"), - ("Tags", "標籤"), - ("Search ID", "搜尋 ID"), - ("Current Wayland display server is not supported", "目前不支援 Wayland 顯示伺服器"), - ("whitelist_sep", "使用逗號、分號、空白,或是換行來分隔"), - ("Add ID", "新增 ID"), - ("Add Tag", "新增標籤"), - ("Unselect all tags", "取消選取所有標籤"), - ("Network error", "網路錯誤"), - ("Username missed", "缺少使用者名稱"), - ("Password missed", "缺少密碼"), - ("Wrong credentials", "提供的登入資訊有誤"), - ("Edit Tag", "編輯標籤"), - ("Unremember Password", "忘掉密碼"), - ("Favorites", "收藏"), - ("Add to Favorites", "加入到收藏"), - ("Remove from Favorites", "從收藏中刪除"), - ("Empty", "空空如也"), - ("Invalid folder name", "資料夾名稱無效"), - ("Socks5 Proxy", "Socks5 代理"), - ("Hostname", "主機名稱"), - ("Discovered", "已發現"), - ("install_daemon_tip", "為了開機啟動,請安裝系統服務。"), - ("Remote ID", "遠端 ID"), - ("Paste", "貼上"), - ("Paste here?", "貼上到這裡?"), - ("Are you sure to close the connection?", "您確定要關閉連線嗎?"), - ("Download new version", "下載新版本"), - ("Touch mode", "觸控模式"), - ("Mouse mode", "滑鼠模式"), - ("One-Finger Tap", "單指輕觸"), - ("Left Mouse", "滑鼠左鍵"), - ("One-Long Tap", "單指長按"), - ("Two-Finger Tap", "雙指輕觸"), - ("Right Mouse", "滑鼠右鍵"), - ("One-Finger Move", "單指移動"), - ("Double Tap & Move", "雙擊並移動"), - ("Mouse Drag", "滑鼠選中拖動"), - ("Three-Finger vertically", "三指垂直滑動"), - ("Mouse Wheel", "滑鼠滾輪"), - ("Two-Finger Move", "雙指移動"), - ("Canvas Move", "移動畫布"), - ("Pinch to Zoom", "雙指縮放"), - ("Canvas Zoom", "縮放畫布"), - ("Reset canvas", "重置畫布"), - ("No permission of file transfer", "無文件傳輸權限"), - ("Note", "備註"), - ("Connection", "連接"), - ("Share Screen", "共享畫面"), - ("CLOSE", "關閉"), - ("OPEN", "開啟"), - ("Chat", "聊天消息"), - ("Total", "總計"), - ("items", "個項目"), - ("Selected", "已選擇"), - ("Screen Capture", "畫面錄製"), - ("Input Control", "輸入控制"), - ("Audio Capture", "音訊錄製"), - ("File Connection", "檔案連線"), - ("Screen Connection", "畫面連線"), - ("Do you accept?", "是否接受?"), - ("Open System Setting", "打開系統設定"), - ("How to get Android input permission?", "如何獲取 Android 的輸入權限?"), - ("android_input_permission_tip1", "取得輸入權限後可以讓遠端裝置通過滑鼠控制此 Android 裝置"), - ("android_input_permission_tip2", "請在接下來的系統設定頁面中,找到並進入 「已安裝的服務」 頁面,並將 「RustDesk Input」 服務開啟"), - ("android_new_connection_tip", "收到新的連接控制請求,對方想要控制您目前的設備"), - ("android_service_will_start_tip", "開啟畫面錄製權限將自動開啟服務,允許其他裝置向此裝置請求建立連接。"), - ("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連接。"), - ("android_version_audio_tip", "目前的 Android 版本不支持音訊錄製,請升級至 Android 10 或以上版本。"), - ("android_start_service_tip", "點擊 「啟動服務」 或啟用 「畫面錄製」 權限以開啟手機畫面共享服務。"), - ("Account", "賬戶"), - ("Overwrite", "覆寫"), - ("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要跳過或是覆寫此檔案嗎?"), - ("Quit", "退出"), - ("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"), - ("Help", "幫助"), - ("Failed", "失敗"), - ("Succeeded", "成功"), - ("Someone turns on privacy mode, exit", "其他用戶開啟隱私模式,退出"), - ("Unsupported", "不支持"), - ("Peer denied", "被控端拒絕"), - ("Please install plugins", "請安裝插件"), - ("Peer exit", "被控端退出"), - ("Failed to turn off", "退出失敗"), - ("Turned off", "退出"), - ("In privacy mode", "開啟隱私模式"), - ("Out privacy mode", "退出隱私模式"), - ("Language", "語言"), - ("Keep RustDesk background service", "保持RustDesk後台服務"), - ("Ignore Battery Optimizations", "忽略電池優化"), - ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的RustDesk應用設置頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"), - ("Connection not allowed", "對方不允許連接"), - ("Legacy mode", "傳統模式"), - ("Map mode", "1:1傳輸"), - ("Translate mode", "翻譯模式"), - ("Use permanent password", "使用固定密碼"), - ("Use both passwords", "同時使用兩種密碼"), - ("Set permanent password", "設定固定密碼"), - ("Enable Remote Restart", "允許遠程重啓"), - ("Allow remote restart", "允許遠程重啓"), - ("Restart Remote Device", "重啓遠程電腦"), - ("Are you sure you want to restart", "确定要重启"), - ("Restarting Remote Device", "正在重啓遠程設備"), - ("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"), - ("Copied", "已複製"), - ("Exit Fullscreen", "退出全屏"), - ("Fullscreen", "全屏"), - ("Mobile Actions", "移動端操作"), - ("Select Monitor", "選擇監視器"), - ("Control Actions", "控制操作"), - ("Display Settings", "顯示設置"), - ("Ratio", "比例"), - ("Image Quality", "畫質"), - ("Scroll Style", "滾動樣式"), - ("Show Menubar", "顯示菜單欄"), - ("Hide Menubar", "隱藏菜單欄"), - ("Direct Connection", "直接連接"), - ("Relay Connection", "中繼連接"), - ("Secure Connection", "安全連接"), - ("Insecure Connection", "非安全連接"), - ("Scale original", "原始尺寸"), - ("Scale adaptive", "適應窗口"), - ("General", "常規"), - ("Security", "安全"), - ("Account", "賬戶"), - ("Theme", "主題"), - ("Dark Theme", "暗黑主題"), - ("Dark", "黑暗"), - ("Light", "明亮"), - ("Follow System", "跟隨系統"), - ("Enable hardware codec", "使用硬件編解碼"), - ("Unlock Security Settings", "解鎖安全設置"), - ("Enable Audio", "允許傳輸音頻"), - ("Unlock Network Settings", "解鎖網絡設置"), - ("Server", "服務器"), - ("Direct IP Access", "IP直接訪問"), - ("Proxy", "代理"), - ("Port", "端口"), - ("Apply", "應用"), - ("Disconnect all devices?", "斷開所有遠程連接?"), - ("Clear", "清空"), - ("Audio Input Device", "音頻輸入設備"), - ("Deny remote access", "拒絕遠程訪問"), - ("Use IP Whitelisting", "只允許白名單上的IP訪問"), - ("Network", "網絡"), - ("Enable RDP", "允許RDP訪問"), - ("Pin menubar", "固定菜單欄"), - ("Unpin menubar", "取消固定菜單欄"), - ("Recording", "錄屏"), - ("Directory", "目錄"), - ("Automatically record incoming sessions", "自動錄製來訪會話"), - ("Change", "變更"), - ("Start session recording", "開始錄屏"), - ("Stop session recording", "結束錄屏"), - ("Enable Recording Session", "允許錄製會話"), - ("Allow recording session", "允許錄製會話"), - ("Enable LAN Discovery", "允許局域網發現"), - ("Deny LAN Discovery", "拒絕局域網發現"), - ("Write a message", "輸入聊天消息"), - ("Prompt", "提示"), - ("Please wait for confirmation of UAC...", "請等待對方確認UAC"), - ("elevated_foreground_window_tip", "遠端桌面的當前窗口需要更高的權限才能操作, 暫時無法使用鼠標鍵盤, 可以請求對方最小化當前窗口, 或者在連接管理窗口點擊提升。為避免這個問題,建議在遠端設備上安裝本軟件。"), - ("Disconnected", "會話已結束"), - ("Other", "其他"), - ("Confirm before closing multiple tabs", "關閉多個分頁前跟我確認"), - ("Keyboard Settings", "鍵盤設置"), - ("Custom", "自定義"), - ("Full Access", "完全訪問"), - ("Screen Share", "僅共享屏幕"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 linux 發行版。 請嘗試 X11 桌面或更改您的操作系統。"), - ("JumpLink", "查看"), - ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的畫面(在對端操作)。"), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), - ("Elevate", "提權"), - ("Zoom cursor", ""), - ("Accept sessions via password", "只允許密碼訪問"), - ("Accept sessions via click", "只允許點擊訪問"), - ("Accept sessions via both", "允許密碼或點擊訪問"), - ("Please wait for the remote side to accept your session request...", "請等待對方接受你的連接..."), - ("One-time Password", "一次性密碼"), - ("Use one-time password", "使用一次性密碼"), - ("One-time password length", "一次性密碼長度"), - ("Request access to your device", "請求訪問你的設備"), - ("Hide connection management window", "隱藏連接管理窗口"), - ("hide_cm_tip", "在只允許密碼連接並且只用固定密碼的情況下才允許隱藏"), - ("wayland_experiment_tip", ""), - ("Right click to select tabs", "右鍵選擇選項卡"), - ("Skipped", ""), - ("Add to Address Book", "添加到地址簿"), - ("Group", "小組"), - ("Search", "搜索"), - ("Closed manually by the web console", "被web控制台手動關閉"), - ("Local keyboard type", "本地鍵盤類型"), - ("Select local keyboard type", "請選擇本地鍵盤類型"), - ].iter().cloned().collect(); -} +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "狀態"), + ("Your Desktop", "您的桌面"), + ("desk_tip", "您可以透過此 ID 及密碼存取您的桌面"), + ("Password", "密碼"), + ("Ready", "就緒"), + ("Established", "已建立"), + ("connecting_status", "正在連線到 RustDesk 網路 ..."), + ("Enable Service", "啟用服務"), + ("Start Service", "啟動服務"), + ("Service is running", "服務正在執行"), + ("Service is not running", "服務尚未執行"), + ("not_ready_status", "尚未就緒,請檢查您的網路連線。"), + ("Control Remote Desktop", "控制遠端桌面"), + ("Transfer File", "傳輸檔案"), + ("Connect", "連線"), + ("Recent Sessions", "近期的工作階段"), + ("Address Book", "通訊錄"), + ("Confirmation", "確認"), + ("TCP Tunneling", "TCP 通道"), + ("Remove", "移除"), + ("Refresh random password", "重新產生隨機密碼"), + ("Set your own password", "自行設定密碼"), + ("Enable Keyboard/Mouse", "啟用鍵盤和滑鼠"), + ("Enable Clipboard", "啟用剪貼簿"), + ("Enable File Transfer", "啟用檔案傳輸"), + ("Enable TCP Tunneling", "啟用 TCP 通道"), + ("IP Whitelisting", "IP 白名單"), + ("ID/Relay Server", "ID / 轉送伺服器"), + ("Import Server Config", "匯入伺服器設定"), + ("Export Server Config", "匯出伺服器設定"), + ("Import server configuration successfully", "匯入伺服器設定成功"), + ("Export server configuration successfully", "匯出伺服器設定成功"), + ("Invalid server configuration", "無效的伺服器設定"), + ("Clipboard is empty", "剪貼簿是空的"), + ("Stop service", "停止服務"), + ("Change ID", "更改 ID"), + ("Your new ID", "您的新 ID"), + ("length %min% to %max%", "長度在 %min% 與 %max% 之間"), + ("starts with a letter", "以字母開頭"), + ("allowed characters", "使用允許的字元"), + ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), + ("Website", "網站"), + ("About", "關於"), + ("Slogan_tip", ""), + ("Privacy Statement", "隱私權聲明"), + ("Mute", "靜音"), + ("Build Date", "構建日期"), + ("Version", "版本"), + ("Home", "首頁"), + ("Audio Input", "音訊輸入"), + ("Enhancements", "增強功能"), + ("Hardware Codec", "硬體編解碼器"), + ("Adaptive Bitrate", "自適應位元速率"), + ("ID Server", "ID 伺服器"), + ("Relay Server", "轉送伺服器"), + ("API Server", "API 伺服器"), + ("invalid_http", "開頭必須為 http:// 或 https://"), + ("Invalid IP", "IP 無效"), + ("Invalid format", "格式無效"), + ("server_not_support", "伺服器暫不支持"), + ("Not available", "無法使用"), + ("Too frequent", "修改過於頻繁,請稍後再試。"), + ("Cancel", "取消"), + ("Skip", "跳過"), + ("Close", "關閉"), + ("Retry", "重試"), + ("OK", "確定"), + ("Password Required", "需要密碼"), + ("Please enter your password", "請輸入您的密碼"), + ("Remember password", "記住密碼"), + ("Wrong Password", "密碼錯誤"), + ("Do you want to enter again?", "您要重新輸入嗎?"), + ("Connection Error", "連線錯誤"), + ("Error", "錯誤"), + ("Reset by the peer", "對方重設了連線"), + ("Connecting...", "正在連線 ..."), + ("Connection in progress. Please wait.", "正在連線,請稍候。"), + ("Please try 1 minute later", "請於 1 分鐘後再試"), + ("Login Error", "登入錯誤"), + ("Successful", "成功"), + ("Connected, waiting for image...", "已連線,等待畫面傳輸 ..."), + ("Name", "名稱"), + ("Type", "類型"), + ("Modified", "修改時間"), + ("Size", "大小"), + ("Show Hidden Files", "顯示隱藏檔案"), + ("Receive", "接收"), + ("Send", "傳送"), + ("Refresh File", "重新整理檔案"), + ("Local", "本地"), + ("Remote", "遠端"), + ("Remote Computer", "遠端電腦"), + ("Local Computer", "本地電腦"), + ("Confirm Delete", "確認刪除"), + ("Delete", "刪除"), + ("Properties", "屬性"), + ("Multi Select", "多選"), + ("Select All", "全選"), + ("Unselect All", "取消全選"), + ("Empty Directory", "空資料夾"), + ("Not an empty directory", "不是一個空資料夾"), + ("Are you sure you want to delete this file?", "您確定要刪除此檔案嗎?"), + ("Are you sure you want to delete this empty directory?", "您確定要刪除此空資料夾嗎?"), + ("Are you sure you want to delete the file of this directory?", "您確定要刪除此資料夾中的檔案嗎?"), + ("Do this for all conflicts", "套用到其他衝突"), + ("This is irreversible!", "此操作不可逆!"), + ("Deleting", "正在刪除 ..."), + ("files", "檔案"), + ("Waiting", "正在等候 ..."), + ("Finished", "已完成"), + ("Speed", "速度"), + ("Custom Image Quality", "自訂畫面品質"), + ("Privacy mode", "隱私模式"), + ("Block user input", "封鎖使用者輸入"), + ("Unblock user input", "取消封鎖使用者輸入"), + ("Adjust Window", "調整視窗"), + ("Original", "原始"), + ("Shrink", "縮減"), + ("Stretch", "延展"), + ("Scrollbar", "滾動條"), + ("ScrollAuto", "自動滾動"), + ("Good image quality", "最佳化畫面品質"), + ("Balanced", "平衡"), + ("Optimize reaction time", "最佳化反應時間"), + ("Custom", "自訂"), + ("Show remote cursor", "顯示遠端游標"), + ("Show quality monitor", "顯示質量監測"), + ("Disable clipboard", "停用剪貼簿"), + ("Lock after session end", "工作階段結束後鎖定電腦"), + ("Insert", "插入"), + ("Insert Lock", "鎖定遠端電腦"), + ("Refresh", "重新載入"), + ("ID does not exist", "ID 不存在"), + ("Failed to connect to rendezvous server", "無法連線到 rendezvous 伺服器"), + ("Please try later", "請稍候再試"), + ("Remote desktop is offline", "遠端桌面已離線"), + ("Key mismatch", "金鑰不符"), + ("Timeout", "逾時"), + ("Failed to connect to relay server", "無法連線到轉送伺服器"), + ("Failed to connect via rendezvous server", "無法透過 rendezvous 伺服器連線"), + ("Failed to connect via relay server", "無法透過轉送伺服器連線"), + ("Failed to make direct connection to remote desktop", "無法直接連線到遠端桌面"), + ("Set Password", "設定密碼"), + ("OS Password", "作業系統密碼"), + ("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常以遠端電腦執行。若要避開 UAC,請點擊下方按鈕將 RustDesk 安裝到系統中。"), + ("Click to upgrade", "點擊以升級"), + ("Click to download", "點擊以下載"), + ("Click to update", "點擊以更新"), + ("Configure", "設定"), + ("config_acc", "您需要授予 RustDesk「協助工具」權限才能存取遠端電腦。"), + ("config_screen", "您需要授予 RustDesk「畫面錄製」權限才能存取遠端電腦。"), + ("Installing ...", "正在安裝 ..."), + ("Install", "安裝"), + ("Installation", "安裝"), + ("Installation Path", "安裝路徑"), + ("Create start menu shortcuts", "新增開始功能表捷徑"), + ("Create desktop icon", "新增桌面捷徑"), + ("agreement_tip", "開始安裝即表示接受許可協議"), + ("Accept and Install", "接受並安裝"), + ("End-user license agreement", "使用者授權合約"), + ("Generating ...", "正在產生 ..."), + ("Your installation is lower version.", "您安裝的版本過舊。"), + ("not_close_tcp_tip", "使用通道時請不要關閉此視窗"), + ("Listening ...", "正在等待通道連線 ..."), + ("Remote Host", "遠端主機"), + ("Remote Port", "遠端連線端口"), + ("Action", "操作"), + ("Add", "新增"), + ("Local Port", "本機連線端口"), + ("Local Address", "本機地址"), + ("Change Local Port", "修改本機連線端口"), + ("setup_server_tip", "若您需要更快的連線速度,可以選擇自行建立伺服器"), + ("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"), + ("The confirmation is not identical.", "兩次輸入不相符"), + ("Permissions", "權限"), + ("Accept", "接受"), + ("Dismiss", "關閉"), + ("Disconnect", "中斷連線"), + ("Allow using keyboard and mouse", "允許使用鍵盤和滑鼠"), + ("Allow using clipboard", "允許使用剪貼簿"), + ("Allow hearing sound", "允許分享音訊"), + ("Allow file copy and paste", "允許檔案複製和貼上"), + ("Connected", "已連線"), + ("Direct and encrypted connection", "加密直接連線"), + ("Relayed and encrypted connection", "加密轉送連線"), + ("Direct and unencrypted connection", "未加密直接連線"), + ("Relayed and unencrypted connection", "未加密轉送連線"), + ("Enter Remote ID", "輸入遠端 ID"), + ("Enter your password", "輸入您的密碼"), + ("Logging in...", "正在登入 ..."), + ("Enable RDP session sharing", "啟用 RDP 工作階段共享"), + ("Auto Login", "自動登入 (鎖定將在設定關閉後套用)"), + ("Enable Direct IP Access", "允許 IP 直接存取"), + ("Rename", "重新命名"), + ("Space", "空白"), + ("Create Desktop Shortcut", "新增桌面捷徑"), + ("Change Path", "更改路徑"), + ("Create Folder", "新增資料夾"), + ("Please enter the folder name", "請輸入資料夾名稱"), + ("Fix it", "修復"), + ("Warning", "警告"), + ("Login screen using Wayland is not supported", "不支援使用 Wayland 的登入畫面"), + ("Reboot required", "需要重新啟動"), + ("Unsupported display server", "不支援顯示伺服器"), + ("x11 expected", "預期 x11"), + ("Port", "端口"), + ("Settings", "設定"), + ("Username", "使用者名稱"), + ("Invalid port", "連線端口無效"), + ("Closed manually by the peer", "遠端使用者關閉了工作階段"), + ("Enable remote configuration modification", "允許遠端使用者更改設定"), + ("Run without install", "跳過安裝直接執行"), + ("Connect via relay", "中繼連線"), + ("Always connect via relay", "一律透過轉送連線"), + ("whitelist_tip", "只有白名單中的 IP 可以存取"), + ("Login", "登入"), + ("Verify", "驗證"), + ("Remember me", "記住我"), + ("Trust this device", "信任此裝置"), + ("Verification code", "驗證碼"), + ("verification_tip", "檢測到新裝置登入,已向註冊電子信箱發送了登入驗證碼,請輸入驗證碼以繼續登入"), + ("Logout", "登出"), + ("Tags", "標籤"), + ("Search ID", "搜尋 ID"), + ("whitelist_sep", "使用逗號、分號、空白,或是換行來分隔"), + ("Add ID", "新增 ID"), + ("Add Tag", "新增標籤"), + ("Unselect all tags", "取消選取所有標籤"), + ("Network error", "網路錯誤"), + ("Username missed", "缺少使用者名稱"), + ("Password missed", "缺少密碼"), + ("Wrong credentials", "提供的登入資訊有誤"), + ("Edit Tag", "編輯標籤"), + ("Unremember Password", "忘掉密碼"), + ("Favorites", "我的最愛"), + ("Add to Favorites", "新增到我的最愛"), + ("Remove from Favorites", "從我的最愛中刪除"), + ("Empty", "空空如也"), + ("Invalid folder name", "資料夾名稱無效"), + ("Socks5 Proxy", "Socks5 代理"), + ("Hostname", "主機名稱"), + ("Discovered", "已探索"), + ("install_daemon_tip", "為了能夠開機時自動啟動,請先安裝系統服務。"), + ("Remote ID", "遠端 ID"), + ("Paste", "貼上"), + ("Paste here?", "貼上到這裡?"), + ("Are you sure to close the connection?", "您確定要關閉連線嗎?"), + ("Download new version", "下載新版本"), + ("Touch mode", "觸控模式"), + ("Mouse mode", "滑鼠模式"), + ("One-Finger Tap", "單指輕觸"), + ("Left Mouse", "滑鼠左鍵"), + ("One-Long Tap", "單指長按"), + ("Two-Finger Tap", "雙指輕觸"), + ("Right Mouse", "滑鼠右鍵"), + ("One-Finger Move", "單指移動"), + ("Double Tap & Move", "雙擊並移動"), + ("Mouse Drag", "滑鼠選中拖動"), + ("Three-Finger vertically", "三指垂直滑動"), + ("Mouse Wheel", "滑鼠滾輪"), + ("Two-Finger Move", "雙指移動"), + ("Canvas Move", "移動畫布"), + ("Pinch to Zoom", "雙指縮放"), + ("Canvas Zoom", "縮放畫布"), + ("Reset canvas", "重設畫布"), + ("No permission of file transfer", "沒有檔案傳輸權限"), + ("Note", "備註"), + ("Connection", "連線"), + ("Share Screen", "共享螢幕畫面"), + ("Chat", "聊天訊息"), + ("Total", "總計"), + ("items", "個項目"), + ("Selected", "已選擇"), + ("Screen Capture", "畫面錄製"), + ("Input Control", "輸入控制"), + ("Audio Capture", "音訊錄製"), + ("File Connection", "檔案連線"), + ("Screen Connection", "畫面連線"), + ("Do you accept?", "是否接受?"), + ("Open System Setting", "開啟系統設定"), + ("How to get Android input permission?", "如何獲取 Android 的輸入權限?"), + ("android_input_permission_tip1", "取得輸入權限後可以讓遠端裝置透過滑鼠控制此 Android 裝置"), + ("android_input_permission_tip2", "請在接下來的系統設定頁面中,找到並進入「已安裝的服務」頁面,並將「RustDesk Input」服務開啟"), + ("android_new_connection_tip", "收到新的連線控制請求,對方想要控制您目前的裝置"), + ("android_service_will_start_tip", "開啟畫面錄製權限將自動開啟服務,允許其他裝置向此裝置請求建立連線。"), + ("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連線。"), + ("android_version_audio_tip", "目前的 Android 版本不支援音訊錄製,請升級到 Android 10 或以上版本。"), + ("android_start_service_tip", "點擊「啟動服務」或啟用「螢幕錄製」權限,以啟動螢幕共享服務。"), + ("android_permission_may_not_change_tip", "對於已經建立的連線,權限可能不會立即發生改變,除非重新建立連線。"), + ("Account", "帳號"), + ("Overwrite", "取代"), + ("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要略過或是取代此檔案嗎?"), + ("Quit", "退出"), + ("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"), + ("Help", "幫助"), + ("Failed", "失敗"), + ("Succeeded", "成功"), + ("Someone turns on privacy mode, exit", "其他使用者開啟隱私模式,退出"), + ("Unsupported", "不支援"), + ("Peer denied", "被控端拒絕"), + ("Please install plugins", "請安裝插件"), + ("Peer exit", "被控端退出"), + ("Failed to turn off", "退出失敗"), + ("Turned off", "退出"), + ("In privacy mode", "開啟隱私模式"), + ("Out privacy mode", "退出隱私模式"), + ("Language", "語言"), + ("Keep RustDesk background service", "保持 RustDesk 後台服務"), + ("Ignore Battery Optimizations", "忽略電池最佳化"), + ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的 RustDesk 應用設定頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"), + ("Start on Boot", "開機自動啟動"), + ("Start the screen sharing service on boot, requires special permissions", "開機自動啟動螢幕共享服務,此功能需要一些特殊權限。"), + ("Connection not allowed", "對方不允許連線"), + ("Legacy mode", "傳統模式"), + ("Map mode", "1:1 傳輸模式"), + ("Translate mode", "翻譯模式"), + ("Use permanent password", "使用固定密碼"), + ("Use both passwords", "同時使用兩種密碼"), + ("Set permanent password", "設定固定密碼"), + ("Enable Remote Restart", "啟用遠端重新啟動"), + ("Allow remote restart", "允許遠端重新啟動"), + ("Restart Remote Device", "重新啟動遠端電腦"), + ("Are you sure you want to restart", "確定要重新啟動"), + ("Restarting Remote Device", "正在重新啟動遠端裝置"), + ("remote_restarting_tip", "遠端裝置正在重新啟動,請關閉當前提示框,並在一段時間後使用永久密碼重新連線"), + ("Copied", "已複製"), + ("Exit Fullscreen", "退出全螢幕"), + ("Fullscreen", "全螢幕"), + ("Mobile Actions", "手機操作"), + ("Select Monitor", "選擇顯示器"), + ("Control Actions", "控制操作"), + ("Display Settings", "顯示設定"), + ("Ratio", "比例"), + ("Image Quality", "畫質"), + ("Scroll Style", "滾動樣式"), + ("Show Menubar", "顯示選單欄"), + ("Hide Menubar", "隱藏選單欄"), + ("Direct Connection", "直接連線"), + ("Relay Connection", "中繼連線"), + ("Secure Connection", "安全連線"), + ("Insecure Connection", "非安全連線"), + ("Scale original", "原始尺寸"), + ("Scale adaptive", "適應視窗"), + ("General", "通用"), + ("Security", "安全"), + ("Theme", "主題"), + ("Dark Theme", "黑暗主題"), + ("Light Theme", "明亮主題"), + ("Dark", "黑暗"), + ("Light", "明亮"), + ("Follow System", "跟隨系統"), + ("Enable hardware codec", "使用硬體編解碼器"), + ("Unlock Security Settings", "解鎖安全設定"), + ("Enable Audio", "允許傳輸音訊"), + ("Unlock Network Settings", "解鎖網路設定"), + ("Server", "伺服器"), + ("Direct IP Access", "IP 直接連線"), + ("Proxy", "代理"), + ("Apply", "應用"), + ("Disconnect all devices?", "中斷所有遠端連線?"), + ("Clear", "清空"), + ("Audio Input Device", "音訊輸入裝置"), + ("Deny remote access", "拒絕遠端存取"), + ("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"), + ("Network", "網路"), + ("Enable RDP", "允許 RDP 訪問"), + ("Pin menubar", "固定選單欄"), + ("Unpin menubar", "取消固定選單欄"), + ("Recording", "錄製"), + ("Directory", "路徑"), + ("Automatically record incoming sessions", "自動錄製連入的工作階段"), + ("Change", "變更"), + ("Start session recording", "開始錄影"), + ("Stop session recording", "停止錄影"), + ("Enable Recording Session", "啟用錄製工作階段"), + ("Allow recording session", "允許錄製工作階段"), + ("Enable LAN Discovery", "允許區域網路探索"), + ("Deny LAN Discovery", "拒絕區域網路探索"), + ("Write a message", "輸入聊天訊息"), + ("Prompt", "提示"), + ("Please wait for confirmation of UAC...", "請等待對方確認 UAC ..."), + ("elevated_foreground_window_tip", "目前的遠端桌面視窗需要更高的權限才能繼續操作,暫時無法使用滑鼠、鍵盤,可以請求對方最小化目前視窗,或者在連線管理視窗點擊提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"), + ("Disconnected", "斷開連線"), + ("Other", "其他"), + ("Confirm before closing multiple tabs", "關閉多個分頁前詢問我"), + ("Keyboard Settings", "鍵盤設定"), + ("Full Access", "完全訪問"), + ("Screen Share", "僅分享螢幕畫面"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"), + ("JumpLink", "查看"), + ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的螢幕畫面(在對端操作)。"), + ("Show RustDesk", "顯示 RustDesk"), + ("This PC", "此電腦"), + ("or", "或"), + ("Continue with", "繼續"), + ("Elevate", "提升權限"), + ("Zoom cursor", "縮放游標"), + ("Accept sessions via password", "只允許透過輸入密碼進行連線"), + ("Accept sessions via click", "只允許透過點擊接受進行連線"), + ("Accept sessions via both", "允許輸入密碼或點擊接受進行連線"), + ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求 ..."), + ("One-time Password", "一次性密碼"), + ("Use one-time password", "使用一次性密碼"), + ("One-time password length", "一次性密碼長度"), + ("Request access to your device", "請求訪問您的裝置"), + ("Hide connection management window", "隱藏連線管理視窗"), + ("hide_cm_tip", "在只允許密碼連線並且只用固定密碼的情況下才允許隱藏"), + ("wayland_experiment_tip", "Wayland 支援處於實驗階段,如果您需要使用無人值守訪問,請使用 X11。"), + ("Right click to select tabs", "右鍵選擇分頁"), + ("Skipped", "已略過"), + ("Add to Address Book", "新增到通訊錄"), + ("Group", "群組"), + ("Search", "搜尋"), + ("Closed manually by web console", "被 Web 控制台手動關閉"), + ("Local keyboard type", "本地鍵盤類型"), + ("Select local keyboard type", "請選擇本地鍵盤類型"), + ("software_render_tip", "如果您使用 NVIDIA 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"), + ("Always use software rendering", "使用軟體渲染"), + ("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk \"輸入監控\" 權限。"), + ("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"), + ("request_elevation_tip", "如果遠端使用者可以操作電腦,也可以請求提升權限。"), + ("Wait", "等待"), + ("Elevation Error", "權限提升失敗"), + ("Ask the remote user for authentication", "請求遠端使用者進行身分驗證"), + ("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"), + ("Transmit the username and password of administrator", "發送管理員的使用者名稱和密碼"), + ("still_click_uac_tip", "依然需要遠端使用者在 UAC 視窗點擊確認。"), + ("Request Elevation", "請求權限提升"), + ("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"), + ("Elevate successfully", "權限提升成功"), + ("uppercase", "大寫字母"), + ("lowercase", "小寫字母"), + ("digit", "數字"), + ("special character", "特殊字元"), + ("length>=8", "長度不能小於 8"), + ("Weak", "弱"), + ("Medium", "中"), + ("Strong", "強"), + ("Switch Sides", "反轉存取方向"), + ("Please confirm if you want to share your desktop?", "請確認是否要讓對方存取您的桌面?"), + ("Display", "顯示"), + ("Default View Style", "預設顯示方式"), + ("Default Scroll Style", "預設滾動方式"), + ("Default Image Quality", "預設圖像質量"), + ("Default Codec", "預設編解碼器"), + ("Bitrate", "位元速率"), + ("FPS", "幀率"), + ("Auto", "自動"), + ("Other Default Options", "其他預設選項"), + ("Voice call", "語音通話"), + ("Text chat", "文字聊天"), + ("Stop voice call", "停止語音通話"), + ("relay_hint_tip", "可能無法直接連線,可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,可以在 ID 後面新增/r,或者在卡片選項裡選擇強制走中繼連線。"), + ("Reconnect", "重新連線"), + ("Codec", "編解碼器"), + ("Resolution", "解析度"), + ("No transfers in progress", "沒有正在進行的傳輸"), + ("Set one-time password length", "設定一次性密碼長度"), + ("idd_driver_tip", "安裝虛擬顯示器驅動程式,以便在沒有連接顯示器的情況下啟動虛擬顯示器進行控制。"), + ("confirm_idd_driver_tip", "安裝虛擬顯示器驅動程式的選項已勾選。請注意,測試證書將被安裝以信任虛擬顯示器驅動。測試證書僅會用於信任 RustDesk 的驅動程式。"), + ("RDP Settings", "RDP 設定"), + ("Sort by", "排序方式"), + ("New Connection", "新連線"), + ("Restore", "還原"), + ("Minimize", "最小化"), + ("Maximize", "最大化"), + ("Your Device", "您的裝置"), + ("empty_recent_tip", "空空如也"), + ("empty_favorite_tip", "空空如也"), + ("empty_lan_tip", "空空如也"), + ("empty_address_book_tip", "空空如也"), + ("eg: admin", "例如:admin"), + ("Empty Username", "空使用者帳號"), + ("Empty Password", "空密碼"), + ("Me", "我"), + ("identical_file_tip", "此檔案與對方的檔案一致"), + ("show_monitors_tip", "在工具列中顯示顯示器"), + ("View Mode", "瀏覽模式"), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 09ed272d5..e21cd88df 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Буфер обміну порожній"), ("Stop service", "Зупинити службу"), ("Change ID", "Змінити ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Допускаються тільки символи a-z, A-Z, 0-9 і _ (підкреслення). Перша буква повинна бути a-z, A-Z. Довжина від 6 до 16"), ("Website", "Веб-сайт"), ("About", "Про RustDesk"), ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), ("Privacy Statement", "Декларація про конфіденційність"), ("Mute", "Вимкнути звук"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), ("Hardware Codec", "Апаратний кодек"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "API-сервер"), ("invalid_http", "Повинен починатися з http:// або https://"), ("Invalid IP", "Невірна IP-адреса"), - ("id_change_tip", "Допускаються тільки символи a-z, A-Z, 0-9 і _ (підкреслення). Перша буква повинна бути a-z, A-Z. Довжина від 6 до 16"), ("Invalid format", "Невірний формат"), ("server_not_support", "Поки не підтримується сервером"), ("Not available", "Недоступно"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Попередження"), ("Login screen using Wayland is not supported", "Вхід у систему з використанням Wayland не підтримується"), ("Reboot required", "Потрібне перезавантаження"), - ("Unsupported display server ", "Графічний сервер не підтримується"), + ("Unsupported display server", "Графічний сервер не підтримується"), ("x11 expected", "Очікується X11"), ("Port", "Порт"), ("Settings", "Налаштування"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Закрито вузлом вручну"), ("Enable remote configuration modification", "Дозволити віддалену зміну конфігурації"), ("Run without install", "Запустити без установки"), - ("Always connected via relay", "Завжди підключений через ретрансляційний сервер"), + ("Connect via relay", ""), ("Always connect via relay", "Завжди підключатися через ретрансляційний сервер"), ("whitelist_tip", "Тільки IP-адреси з білого списку можуть отримати доступ до мене"), ("Login", "Увійти"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Вийти"), ("Tags", "Ключові слова"), ("Search ID", "Пошук за ID"), - ("Current Wayland display server is not supported", "Поточний графічний сервер Wayland не підтримується"), ("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"), ("Add ID", "Додати ID"), ("Add Tag", "Додати ключове слово"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Примітка"), ("Connection", "З'єднання"), ("Share Screen", "Поділитися екраном"), - ("CLOSE", "ЗАКРИТИ"), - ("OPEN", "ВІДКРИТИ"), ("Chat", "Чат"), ("Total", "Всього"), ("items", "елементи"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Увімкнення захоплення екрана автоматично запускає службу, дозволяючи іншим пристроям запитувати з'єднання з цього пристрою."), ("android_stop_service_tip", "Закриття служби автоматично закриє всі встановлені з'єднання."), ("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, оновіть її до Android 10 або вище."), - ("android_start_service_tip", "Натисніть [Запуск проміжного сервера] або ВІДКРИТИ роздільну здатність [Захоплення екрана], щоб запустити службу демонстрації екрана."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", "Акаунт"), ("Overwrite", "Перезаписати"), ("This file exists, skip or overwrite this file?", "Цей файл існує, пропустити або перезаписати файл?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Зберегти фонову службу RustDesk"), ("Ignore Battery Optimizations", "Ігнорувати оптимізацію батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Підключення не дозволено"), ("Legacy mode", "Застарілий режим"), ("Map mode", "Режим карти"), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Масштаб адаптивний"), ("General", "Загальні"), ("Security", "Безпека"), - ("Account", "Акаунт"), ("Theme", "Тема"), ("Dark Theme", "Темна тема"), + ("Light Theme", ""), ("Dark", "Темна"), ("Light", "Світла"), ("Follow System", "Як у системі"), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Сервер"), ("Direct IP Access", "Прямий IP доступ"), ("Proxy", "Проксі"), - ("Port", "Порт"), ("Apply", "Застосувати"), ("Disconnect all devices?", "Відключити всі прилади?"), ("Clear", "Очистити"), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", "Інше"), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", "Налаштування клавіатури"), - ("Custom", "Користувацькі"), ("Full Access", "Повний доступ"), ("Screen Share", "Демонстрація екрану"), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland потребує Ubuntu 21.04 або новішої версії."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", "Додати IP до Адресної книги"), ("Group", "Група"), ("Search", "Пошук"), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index c95d4fe63..f716bd536 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -37,11 +37,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is empty", "Khay nhớ tạm trống"), ("Stop service", "Dừng dịch vụ"), ("Change ID", "Thay đổi ID"), + ("Your new ID", ""), + ("length %min% to %max%", ""), + ("starts with a letter", ""), + ("allowed characters", ""), + ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9 và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"), ("Website", "Trang web"), ("About", "About"), ("Slogan_tip", ""), ("Privacy Statement", ""), ("Mute", "Tắt tiếng"), + ("Build Date", ""), + ("Version", ""), + ("Home", ""), ("Audio Input", "Đầu vào âm thanh"), ("Enhancements", "Các tiện itchs"), ("Hardware Codec", "Codec phần cứng"), @@ -51,7 +59,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("API Server", "Máy chủ API"), ("invalid_http", "phải bắt đầu bằng http:// hoặc https://"), ("Invalid IP", "IP không hợp lệ"), - ("id_change_tip", "Các kí tự đuợc phép là: từ a-z, A-Z, 0-9 và _ (dấu gạch dưới). Kí tự đầu tiên phải bắt đầu từ a-z, A-Z. Độ dài kí tự từ 6 đến 16"), ("Invalid format", "Định dạng không hợp lệnh"), ("server_not_support", "Chưa đuợc hỗ trợ bới server"), ("Not available", "Chưa có mặt"), @@ -197,7 +204,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Warning", "Cảnh báo"), ("Login screen using Wayland is not supported", "Màn hình đăng nhập sử dụng Wayland không đựoc hỗ trợ"), ("Reboot required", "Yêu cầu khởi động lại"), - ("Unsupported display server ", "Máy chủ hiển thị không đuợc hỗ trọ"), + ("Unsupported display server", "Máy chủ hiển thị không đuợc hỗ trọ"), ("x11 expected", "Cần x11"), ("Port", ""), ("Settings", "Cài đặt"), @@ -206,14 +213,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Closed manually by the peer", "Đóng thủ công bởi peer"), ("Enable remote configuration modification", "Cho phép thay đổi cấu hình bên từ xa"), ("Run without install", "Chạy mà không cần cài"), - ("Always connected via relay", "Luôn đuợc kết nối qua relay"), + ("Connect via relay", ""), ("Always connect via relay", "Luôn kết nối qua relay"), ("whitelist_tip", "Chỉ có những IP đựoc cho phép mới có thể truy cập"), ("Login", "Đăng nhập"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Đăng xuất"), ("Tags", "Tags"), ("Search ID", "Tìm ID"), - ("Current Wayland display server is not supported", "Máy chủ hình ảnh Wayland hiện không đuợc hỗ trợ"), ("whitelist_sep", "Đuợc cách nhau bởi dấu phẩy, dấu chấm phẩy, dấu cách hay dòng mới"), ("Add ID", "Thêm ID"), ("Add Tag", "Thêm Tag"), @@ -259,8 +270,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Note", "Ghi nhớ"), ("Connection", "Kết nối"), ("Share Screen", "Chia sẻ màn hình"), - ("CLOSE", "ĐÓNG"), - ("OPEN", "MỞ"), ("Chat", "Chat"), ("Total", "Tổng"), ("items", "items"), @@ -279,7 +288,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_service_will_start_tip", "Bật \"Ghi màn hình\" sẽ tự động khởi động dịch vụ, cho phép các thiết bị khác yêu cầu kết nối với thiết bị của bạn."), ("android_stop_service_tip", "Đóng dịch vụ sẽ tự động đóng tất cả các kết nối đã thiết lập."), ("android_version_audio_tip", "Phiên bản Android hiện tại không hỗ trợ ghi âm, vui lòng nâng cấp lên Android 10 trở lên."), - ("android_start_service_tip", "Nhấn vào [Bắt đầu dịch vụ] hoặc MỞ quyền [Ghi màn hình] để bắt đầu dịch vụ chia sẻ màn hình."), + ("android_start_service_tip", ""), + ("android_permission_may_not_change_tip", ""), ("Account", ""), ("Overwrite", "Ghi đè"), ("This file exists, skip or overwrite this file?", "Tệp tin này đã tồn tại, bạn có muốn bỏ qua hay ghi đè lên tệp tin này?"), @@ -301,6 +311,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Keep RustDesk background service", "Giữ dịch vụ nền RustDesk"), ("Ignore Battery Optimizations", "Bỏ qua các tối ưu pin"), ("android_open_battery_optimizations_tip", "Nếu bạn muốn tắt tính năng này, vui lòng chuyển đến trang cài đặt ứng dụng RustDesk tiếp theo, tìm và nhập [Pin], Bỏ chọn [Không hạn chế]"), + ("Start on Boot", ""), + ("Start the screen sharing service on boot, requires special permissions", ""), ("Connection not allowed", "Kết nối không đuợc phép"), ("Legacy mode", ""), ("Map mode", ""), @@ -334,9 +346,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Quy mô thích ứng"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), + ("Light Theme", ""), ("Dark", ""), ("Light", ""), ("Follow System", ""), @@ -347,7 +359,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -376,7 +387,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Other", ""), ("Confirm before closing multiple tabs", ""), ("Keyboard Settings", ""), - ("Custom", ""), ("Full Access", ""), ("Screen Share", ""), ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland yêu cầu phiên bản Ubuntu 21.04 trở lên."), @@ -405,8 +415,73 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add to Address Book", ""), ("Group", ""), ("Search", ""), - ("Closed manually by the web console", ""), + ("Closed manually by web console", ""), ("Local keyboard type", ""), ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("config_microphone", ""), + ("request_elevation_tip", ""), + ("Wait", ""), + ("Elevation Error", ""), + ("Ask the remote user for authentication", ""), + ("Choose this if the remote account is administrator", ""), + ("Transmit the username and password of administrator", ""), + ("still_click_uac_tip", ""), + ("Request Elevation", ""), + ("wait_accept_uac_tip", ""), + ("Elevate successfully", ""), + ("uppercase", ""), + ("lowercase", ""), + ("digit", ""), + ("special character", ""), + ("length>=8", ""), + ("Weak", ""), + ("Medium", ""), + ("Strong", ""), + ("Switch Sides", ""), + ("Please confirm if you want to share your desktop?", ""), + ("Display", ""), + ("Default View Style", ""), + ("Default Scroll Style", ""), + ("Default Image Quality", ""), + ("Default Codec", ""), + ("Bitrate", ""), + ("FPS", ""), + ("Auto", ""), + ("Other Default Options", ""), + ("Voice call", ""), + ("Text chat", ""), + ("Stop voice call", ""), + ("relay_hint_tip", ""), + ("Reconnect", ""), + ("Codec", ""), + ("Resolution", ""), + ("No transfers in progress", ""), + ("Set one-time password length", ""), + ("idd_driver_tip", ""), + ("confirm_idd_driver_tip", ""), + ("RDP Settings", ""), + ("Sort by", ""), + ("New Connection", ""), + ("Restore", ""), + ("Minimize", ""), + ("Maximize", ""), + ("Your Device", ""), + ("empty_recent_tip", ""), + ("empty_favorite_tip", ""), + ("empty_lan_tip", ""), + ("empty_address_book_tip", ""), + ("eg: admin", ""), + ("Empty Username", ""), + ("Empty Password", ""), + ("Me", ""), + ("identical_file_tip", ""), + ("show_monitors_tip", ""), + ("View Mode", ""), + ("enter_rustdesk_passwd_tip", ""), + ("remember_rustdesk_passwd_tip", ""), + ("login_linux_tip", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 7b94c8a2c..5dcd6389c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ pub use self::rendezvous_mediator::*; pub mod common; #[cfg(not(any(target_os = "ios")))] pub mod ipc; -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli", feature = "flutter")))] pub mod ui; mod version; pub use version::*; @@ -56,3 +56,5 @@ pub mod clipboard_file; #[cfg(all(windows, feature = "with_rc"))] pub mod rc; +#[cfg(target_os = "windows")] +pub mod win_privacy; diff --git a/src/license.rs b/src/license.rs index f8f5d27d5..8875d2b64 100644 --- a/src/license.rs +++ b/src/license.rs @@ -1,3 +1,4 @@ +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use hbb_common::{bail, sodiumoxide::crypto::sign, ResultType}; use serde_derive::{Deserialize, Serialize}; @@ -18,7 +19,7 @@ fn get_license_from_string_(s: &str) -> ResultType { 12, 46, 129, 83, 17, 84, 193, 119, 197, 130, 103, ]; let pk = sign::PublicKey(*PK); - let data = base64::decode_config(tmp, base64::URL_SAFE_NO_PAD)?; + let data = URL_SAFE_NO_PAD.decode(tmp)?; if let Ok(lic) = serde_json::from_slice::(&data) { return Ok(lic); } diff --git a/src/main.rs b/src/main.rs index 67ddb875f..3759f6056 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ -// Specify the Windows subsystem to eliminate console window. -// Requires Rust 1.18. -//#![windows_subsystem = "windows"] + #![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" + )] use librustdesk::*; -#[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] fn main() { if !common::global_init() { return; @@ -16,7 +17,12 @@ fn main() { common::global_clean(); } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any( + target_os = "android", + target_os = "ios", + feature = "cli", + feature = "flutter" +)))] fn main() { if !common::global_init() { return; @@ -91,6 +97,7 @@ fn main() { let token = LocalConfig::get_option("access_token"); cli::connect_test(p, key, token); } else if let Some(p) = matches.value_of("server") { + log::info!("id={}", hbb_common::config::Config::get_id()); crate::start_server(true); } common::global_clean(); diff --git a/src/naming.rs b/src/naming.rs index 38b514f86..53e675d92 100644 --- a/src/naming.rs +++ b/src/naming.rs @@ -1,10 +1,11 @@ mod license; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use hbb_common::ResultType; use license::*; fn gen_name(lic: &License) -> ResultType { let tmp = serde_json::to_vec::(lic)?; - let tmp = base64::encode_config(tmp, base64::URL_SAFE_NO_PAD); + let tmp = URL_SAFE_NO_PAD.encode(&tmp); let tmp: String = tmp.chars().rev().collect(); Ok(tmp) } diff --git a/src/ui/macos.rs b/src/platform/delegate.rs similarity index 83% rename from src/ui/macos.rs rename to src/platform/delegate.rs index ab3fb9079..01855536e 100644 --- a/src/ui/macos.rs +++ b/src/platform/delegate.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"; @@ -22,7 +26,7 @@ const SHOW_SETTINGS_TAG: u32 = 2; const RUN_ME_TAG: u32 = 3; const AWAKE: u32 = 4; -trait AppHandler { +pub trait AppHandler { fn command(&mut self, cmd: u32); } @@ -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) { @@ -59,9 +63,12 @@ impl AppHandler for Rc { } // https://github.com/xi-editor/druid/blob/master/druid-shell/src/platform/mac/application.rs -unsafe fn set_delegate(handler: Option>) { - let mut decl = - ClassDecl::new("AppDelegate", class!(NSObject)).expect("App Delegate definition failed"); +pub unsafe fn set_delegate(handler: Option>) { + let decl = ClassDecl::new("AppDelegate", class!(NSObject)); + if decl.is_none() { + return; + } + let mut decl = decl.unwrap(); decl.add_ivar::<*mut c_void>(APP_HANDLER_IVAR); decl.add_method( @@ -98,18 +105,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 +141,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 +183,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,30 +250,3 @@ pub fn show_dock() { NSApp().setActivationPolicy_(NSApplicationActivationPolicyRegular); } } - -pub fn make_tray() { - unsafe { - set_delegate(None); - } - crate::tray::make_tray(); -} - -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/platform/linux.rs b/src/platform/linux.rs index ab436ed30..6980385c8 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -1,14 +1,21 @@ use super::{CursorData, ResultType}; pub use hbb_common::platform::linux::*; -use hbb_common::{allow_err, bail, log}; -use libc::{c_char, c_int, c_void}; +use hbb_common::{ + allow_err, bail, + libc::{c_char, c_int, c_long, c_void}, + log, + message_proto::Resolution, + regex::{Captures, Regex}, +}; use std::{ cell::RefCell, - path::PathBuf, + path::{Path, PathBuf}, + process::{Child, Command}, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, + time::{Duration, Instant}, }; type Xdo = *const c_void; @@ -53,8 +60,8 @@ pub struct xcb_xfixes_get_cursor_image { pub height: u16, pub xhot: u16, pub yhot: u16, - pub cursor_serial: libc::c_long, - pub pixels: *const libc::c_long, + pub cursor_serial: c_long, + pub pixels: *const c_long, } pub fn get_cursor_pos() -> Option<(i32, i32)> { @@ -161,10 +168,29 @@ fn start_uinput_service() { }); } -fn stop_server(server: &mut Option) { +#[inline] +fn try_start_server_(user: Option<(String, String)>) -> ResultType> { + if user.is_some() { + run_as_user(vec!["--server"], user) + } else { + Ok(Some(crate::run_me(vec!["--server"])?)) + } +} + +#[inline] +fn start_server(user: Option<(String, String)>, server: &mut Option) { + match try_start_server_(user) { + Ok(ps) => *server = ps, + Err(err) => { + log::error!("Failed to start server: {}", err); + } + } +} + +fn stop_server(server: &mut Option) { if let Some(mut ps) = server.take() { allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); + std::thread::sleep(Duration::from_millis(30)); match ps.try_wait() { Ok(Some(_status)) => {} Ok(None) => { @@ -181,7 +207,7 @@ fn set_x11_env(uid: &str) { let mut auth = get_env_tries("XAUTHORITY", uid, 10); // auth is another user's when uid = 0, https://github.com/rustdesk/rustdesk/issues/2468 if auth.is_empty() || uid == "0" { - auth = if std::path::Path::new(&gdm).exists() { + auth = if Path::new(&gdm).exists() { gdm } else { let username = get_active_username(); @@ -189,7 +215,7 @@ fn set_x11_env(uid: &str) { format!("/{}/.Xauthority", username) } else { let tmp = format!("/home/{}/.Xauthority", username); - if std::path::Path::new(&tmp).exists() { + if Path::new(&tmp).exists() { tmp } else { format!("/var/lib/{}/.Xauthority", username) @@ -222,8 +248,8 @@ fn should_start_server( uid: &mut String, cur_uid: String, cm0: &mut bool, - last_restart: &mut std::time::Instant, - server: &mut Option, + last_restart: &mut Instant, + server: &mut Option, ) -> bool { let cm = get_cm(); let mut start_new = false; @@ -234,8 +260,8 @@ fn should_start_server( } if let Some(ps) = server.as_mut() { allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - *last_restart = std::time::Instant::now(); + std::thread::sleep(Duration::from_millis(30)); + *last_restart = Instant::now(); } } else if !cm && ((*cm0 && last_restart.elapsed().as_secs() > 60) @@ -246,8 +272,8 @@ fn should_start_server( // and x server get displays failure issue if let Some(ps) = server.as_mut() { allow_err!(ps.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - *last_restart = std::time::Instant::now(); + std::thread::sleep(Duration::from_millis(30)); + *last_restart = Instant::now(); log::info!("restart server"); } } @@ -266,6 +292,13 @@ fn should_start_server( start_new } +// to-do: stop_server(&mut user_server); may not stop child correctly +// stop_rustdesk_servers() is just a temp solution here. +fn force_stop_server() { + stop_rustdesk_servers(); + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); +} + pub fn start_os_service() { stop_rustdesk_servers(); start_uinput_service(); @@ -273,8 +306,8 @@ pub fn start_os_service() { let running = Arc::new(AtomicBool::new(true)); let r = running.clone(); let mut uid = "".to_owned(); - let mut server: Option = None; - let mut user_server: Option = None; + let mut server: Option = None; + let mut user_server: Option = None; if let Err(err) = ctrlc::set_handler(move || { r.store(false, Ordering::SeqCst); }) { @@ -282,12 +315,27 @@ pub fn start_os_service() { } let mut cm0 = false; - let mut last_restart = std::time::Instant::now(); + let mut last_restart = Instant::now(); while running.load(Ordering::SeqCst) { let (cur_uid, cur_user) = get_active_user_id_name(); + + // for fixing https://github.com/rustdesk/rustdesk/issues/3129 to avoid too much dbus calling, + // though duplicate logic here with should_start_server + if !(cur_uid != *uid && !cur_uid.is_empty()) { + let cm = get_cm(); + if !(!cm + && ((cm0 && last_restart.elapsed().as_secs() > 60) + || last_restart.elapsed().as_secs() > 3600)) + { + std::thread::sleep(Duration::from_millis(500)); + continue; + } + } + let is_wayland = current_is_wayland(); if cur_user == "root" || !is_wayland { + // try kill subprocess "--server" stop_server(&mut user_server); // try start subprocess "--server" if should_start_server( @@ -298,16 +346,8 @@ pub fn start_os_service() { &mut last_restart, &mut server, ) { - // to-do: stop_server(&mut user_server); may not stop child correctly - // stop_rustdesk_servers() is just a temp solution here. - stop_rustdesk_servers(); - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); - match crate::run_me(vec!["--server"]) { - Ok(ps) => server = Some(ps), - Err(err) => { - log::error!("Failed to start server: {}", err); - } - } + force_stop_server(); + start_server(None, &mut server); } } else if cur_user != "" { if cur_user != "gdm" { @@ -323,23 +363,16 @@ pub fn start_os_service() { &mut last_restart, &mut user_server, ) { - stop_rustdesk_servers(); - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); - match run_as_user(vec!["--server"], Some((cur_uid, cur_user))) { - Ok(ps) => user_server = ps, - Err(err) => { - log::error!("Failed to start server: {}", err); - } - } + force_stop_server(); + start_server(Some((cur_uid, cur_user)), &mut user_server); } } } else { - stop_rustdesk_servers(); - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + force_stop_server(); stop_server(&mut user_server); stop_server(&mut server); } - std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); + std::thread::sleep(Duration::from_millis(super::SERVICE_INTERVAL)); } if let Some(ps) = user_server.take().as_mut() { @@ -361,7 +394,7 @@ pub fn get_active_userid() -> String { } fn get_cm() -> bool { - if let Ok(output) = std::process::Command::new("ps").args(vec!["aux"]).output() { + if let Ok(output) = Command::new("ps").args(vec!["aux"]).output() { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains(&format!( "{} --cm", @@ -379,7 +412,7 @@ fn get_cm() -> bool { fn get_display() -> String { let user = get_active_username(); log::debug!("w {}", &user); - if let Ok(output) = std::process::Command::new("w").arg(&user).output() { + if let Ok(output) = Command::new("w").arg(&user).output() { for line in String::from_utf8_lossy(&output.stdout).lines() { log::debug!(" {}", line); let mut iter = line.split_whitespace(); @@ -394,7 +427,7 @@ fn get_display() -> String { // above not work for gdm user log::debug!("ls -l /tmp/.X11-unix/"); let mut last = "".to_owned(); - if let Ok(output) = std::process::Command::new("ls") + if let Ok(output) = Command::new("ls") .args(vec!["-l", "/tmp/.X11-unix/"]) .output() { @@ -425,104 +458,11 @@ pub fn is_login_wayland() -> bool { } } -pub fn fix_login_wayland() { - let mut file = "/etc/gdm3/custom.conf".to_owned(); - if !std::path::Path::new(&file).exists() { - file = "/etc/gdm/custom.conf".to_owned(); - } - match std::process::Command::new("pkexec") - .args(vec![ - "sed", - "-i", - "s/#WaylandEnable=false/WaylandEnable=false/g", - &file, - ]) - .output() - { - Ok(x) => { - let x = String::from_utf8_lossy(&x.stderr); - if !x.is_empty() { - log::error!("fix_login_wayland failed: {}", x); - } - } - Err(err) => { - log::error!("fix_login_wayland failed: {}", err); - } - } -} - pub fn current_is_wayland() -> bool { let dtype = get_display_server(); return "wayland" == dtype && unsafe { UNMODIFIED }; } -pub fn modify_default_login() -> String { - let dsession = std::env::var("DESKTOP_SESSION").unwrap(); - let user_name = std::env::var("USERNAME").unwrap(); - if let Ok(x) = run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION}-xorg.desktop".to_owned()) { - if x.trim_end().to_string() != "" { - match std::process::Command::new("pkexec") - .args(vec![ - "sed", - "-i", - &format!("s/={0}$/={0}-xorg/g", &dsession), - &format!("/var/lib/AccountsService/users/{}", &user_name), - ]) - .output() - { - Ok(x) => { - let x = String::from_utf8_lossy(&x.stderr); - if !x.is_empty() { - log::error!("modify_default_login failed: {}", x); - return "Fix failed! Please re-login with X server manually".to_owned(); - } else { - unsafe { - UNMODIFIED = false; - } - return "".to_owned(); - } - } - Err(err) => { - log::error!("modify_default_login failed: {}", err); - return "Fix failed! Please re-login with X server manually".to_owned(); - } - } - } else if let Ok(z) = - run_cmds("ls /usr/share/* | grep ${DESKTOP_SESSION:0:-8}.desktop".to_owned()) - { - if z.trim_end().to_string() != "" { - match std::process::Command::new("pkexec") - .args(vec![ - "sed", - "-i", - &format!("s/={}$/={}/g", &dsession, &dsession[..dsession.len() - 8]), - &format!("/var/lib/AccountsService/users/{}", &user_name), - ]) - .output() - { - Ok(x) => { - let x = String::from_utf8_lossy(&x.stderr); - if !x.is_empty() { - log::error!("modify_default_login failed: {}", x); - return "Fix failed! Please re-login with X server manually".to_owned(); - } else { - unsafe { - UNMODIFIED = false; - } - return "".to_owned(); - } - } - Err(err) => { - log::error!("modify_default_login failed: {}", err); - return "Fix failed! Please re-login with X server manually".to_owned(); - } - } - } - } - } - return "Fix failed! Please re-login with X server manually".to_owned(); -} - // to-do: test the other display manager fn _get_display_manager() -> String { if let Ok(x) = std::fs::read_to_string("/etc/X11/default-display-manager") { @@ -566,10 +506,7 @@ fn is_opensuse() -> bool { false } -pub fn run_as_user( - arg: Vec<&str>, - user: Option<(String, String)>, -) -> ResultType> { +pub fn run_as_user(arg: Vec<&str>, user: Option<(String, String)>) -> ResultType> { let (uid, username) = match user { Some(id_name) => id_name, None => get_active_user_id_name(), @@ -583,7 +520,7 @@ pub fn run_as_user( args.insert(0, "-E"); } - let task = std::process::Command::new("sudo").args(args).spawn()?; + let task = Command::new("sudo").args(args).spawn()?; Ok(Some(task)) } @@ -626,11 +563,26 @@ pub fn get_pa_sources() -> Vec<(String, String)> { out } +pub fn get_default_pa_source() -> Option<(String, String)> { + use pulsectl::controllers::*; + match SourceController::create() { + Ok(mut handler) => { + if let Ok(dev) = handler.get_default_device() { + return Some(( + dev.name.unwrap_or("".to_owned()), + dev.description.unwrap_or("".to_owned()), + )); + } + } + Err(err) => { + log::error!("Failed to get_pa_source: {:?}", err); + } + } + None +} + pub fn lock_screen() { - std::process::Command::new("xdg-screensaver") - .arg("lock") - .spawn() - .ok(); + Command::new("xdg-screensaver").arg("lock").spawn().ok(); } pub fn toggle_blank_screen(_v: bool) { @@ -651,7 +603,7 @@ fn get_env_tries(name: &str, uid: &str, n: usize) -> String { if !x.is_empty() { return x; } - std::thread::sleep(std::time::Duration::from_millis(300)); + std::thread::sleep(Duration::from_millis(300)); } "".to_owned() } @@ -678,12 +630,12 @@ pub fn quit_gui() { pub fn check_super_user_permission() -> ResultType { let file = "/usr/share/rustdesk/files/polkit"; let arg; - if std::path::Path::new(file).is_file() { + if Path::new(file).is_file() { arg = file; } else { arg = "echo"; } - let status = std::process::Command::new("pkexec").arg(arg).status()?; + let status = Command::new("pkexec").arg(arg).status()?; Ok(status.success() && status.code() == Some(0)) } @@ -706,14 +658,120 @@ pub fn get_double_click_time() -> u32 { unsafe { let mut double_click_time = 0u32; let property = std::ffi::CString::new("gtk-double-click-time").unwrap(); - let setings = gtk_settings_get_default(); + let settings = gtk_settings_get_default(); g_object_get( - setings, + settings, property.as_ptr(), &mut double_click_time as *mut u32, - 0 as *const libc::c_void, + 0 as *const c_void, ); double_click_time } } +#[inline] +fn get_width_height_from_captures<'t>(caps: &Captures<'t>) -> Option<(i32, i32)> { + match (caps.name("width"), caps.name("height")) { + (Some(width), Some(height)) => { + match ( + width.as_str().parse::(), + height.as_str().parse::(), + ) { + (Ok(width), Ok(height)) => { + return Some((width, height)); + } + _ => {} + } + } + _ => {} + } + None +} + +#[inline] +fn get_xrandr_conn_pat(name: &str) -> String { + format!( + r"{}\s+connected.+?(?P\d+)x(?P\d+)\+(?P\d+)\+(?P\d+).*?\n", + name + ) +} + +pub fn resolutions(name: &str) -> Vec { + let resolutions_pat = r"(?P(\s*\d+x\d+\s+\d+.*\n)+)"; + let connected_pat = get_xrandr_conn_pat(name); + let mut v = vec![]; + if let Ok(re) = Regex::new(&format!("{}{}", connected_pat, resolutions_pat)) { + match run_cmds("xrandr --query | tr -s ' '".to_owned()) { + Ok(xrandr_output) => { + // There'are different kinds of xrandr output. + /* + 1. + Screen 0: minimum 320 x 175, current 1920 x 1080, maximum 1920 x 1080 + default connected 1920x1080+0+0 0mm x 0mm + 1920x1080 10.00* + 1280x720 25.00 + 1680x1050 60.00 + Virtual2 disconnected (normal left inverted right x axis y axis) + Virtual3 disconnected (normal left inverted right x axis y axis) + + XWAYLAND0 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm + Virtual1 connected primary 1920x984+0+0 (normal left inverted right x axis y axis) 0mm x 0mm + HDMI-0 connected (normal left inverted right x axis y axis) + + rdp0 connected primary 1920x1080+0+0 0mm x 0mm + */ + if let Some(caps) = re.captures(&xrandr_output) { + if let Some(resolutions) = caps.name("resolutions") { + let resolution_pat = + r"\s*(?P\d+)x(?P\d+)\s+(?P(\d+\.\d+[* ]*)+)\s*\n"; + let resolution_re = Regex::new(&format!(r"{}", resolution_pat)).unwrap(); + for resolution_caps in resolution_re.captures_iter(resolutions.as_str()) { + if let Some((width, height)) = + get_width_height_from_captures(&resolution_caps) + { + let resolution = Resolution { + width, + height, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } + } + } + } + } + } + Err(e) => log::error!("Failed to run xrandr query, {}", e), + } + } + + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let xrandr_output = run_cmds("xrandr --query | tr -s ' '".to_owned())?; + let re = Regex::new(&get_xrandr_conn_pat(name))?; + if let Some(caps) = re.captures(&xrandr_output) { + if let Some((width, height)) = get_width_height_from_captures(&caps) { + return Ok(Resolution { + width, + height, + ..Default::default() + }); + } + } + bail!("Failed to find current resolution for {}", name); +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + Command::new("xrandr") + .args(vec![ + "--output", + name, + "--mode", + &format!("{}x{}", width, height), + ]) + .spawn()?; + Ok(()) +} diff --git a/src/platform/macos.mm b/src/platform/macos.mm new file mode 100644 index 000000000..a252a9a8f --- /dev/null +++ b/src/platform/macos.mm @@ -0,0 +1,201 @@ +#import +#import +#import +#include +#include + + +// https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm + +extern "C" bool InputMonitoringAuthStatus(bool prompt) { + #ifdef NO_InputMonitoringAuthStatus + return true; + #else + if (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_15) { + IOHIDAccessType theType = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent); + NSLog(@"IOHIDCheckAccess = %d, kIOHIDAccessTypeGranted = %d", theType, kIOHIDAccessTypeGranted); + switch (theType) { + case kIOHIDAccessTypeGranted: + return true; + break; + case kIOHIDAccessTypeDenied: { + if (prompt) { + NSString *urlString = @"x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent"; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:urlString]]; + } + break; + } + case kIOHIDAccessTypeUnknown: { + if (prompt) { + bool result = IOHIDRequestAccess(kIOHIDRequestTypeListenEvent); + NSLog(@"IOHIDRequestAccess result = %d", result); + } + break; + } + default: + break; + } + } else { + return true; + } + return false; + #endif +} + +extern "C" bool MacCheckAdminAuthorization() { + AuthorizationRef authRef; + OSStatus status; + + status = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, + kAuthorizationFlagDefaults, &authRef); + if (status != errAuthorizationSuccess) { + printf("Failed to create AuthorizationRef\n"); + return false; + } + + AuthorizationItem authItem = {kAuthorizationRightExecute, 0, NULL, 0}; + AuthorizationRights authRights = {1, &authItem}; + AuthorizationFlags flags = kAuthorizationFlagDefaults | + kAuthorizationFlagInteractionAllowed | + kAuthorizationFlagPreAuthorize | + kAuthorizationFlagExtendRights; + status = AuthorizationCopyRights(authRef, &authRights, kAuthorizationEmptyEnvironment, flags, NULL); + if (status != errAuthorizationSuccess) { + printf("Failed to authorize\n"); + return false; + } + + AuthorizationFree(authRef, kAuthorizationFlagDefaults); + return true; +} + +extern "C" float BackingScaleFactor() { + NSScreen* s = [NSScreen mainScreen]; + if (s) return [s backingScaleFactor]; + return 1; +} + +// https://github.com/jhford/screenresolution/blob/master/cg_utils.c +// https://github.com/jdoupe/screenres/blob/master/setgetscreen.m + +size_t bitDepth(CGDisplayModeRef mode) { + size_t depth = 0; + // Deprecated, same display same bpp? + // https://stackoverflow.com/questions/8210824/how-to-avoid-cgdisplaymodecopypixelencoding-to-get-bpp + // https://github.com/libsdl-org/SDL/pull/6628 + CFStringRef pixelEncoding = CGDisplayModeCopyPixelEncoding(mode); + // my numerical representation for kIO16BitFloatPixels and kIO32bitFloatPixels + // are made up and possibly non-sensical + if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO32BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 96; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO64BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 64; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO16BitFloatPixels), kCFCompareCaseInsensitive)) { + depth = 48; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO32BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 32; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(kIO30BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 30; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO16BitDirectPixels), kCFCompareCaseInsensitive)) { + depth = 16; + } else if (kCFCompareEqualTo == CFStringCompare(pixelEncoding, CFSTR(IO8BitIndexedPixels), kCFCompareCaseInsensitive)) { + depth = 8; + } + CFRelease(pixelEncoding); + return depth; +} + +extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + return false; + } + *numModes = CFArrayGetCount(allModes); + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, uint32_t max, uint32_t *numModes) { + CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); + if (currentMode == NULL) { + return false; + } + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + CGDisplayModeRelease(currentMode); + return false; + } + uint32_t allModeCount = CFArrayGetCount(allModes); + uint32_t realNum = 0; + for (uint32_t i = 0; i < allModeCount && realNum < max; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if (CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) && + bitDepth(currentMode) == bitDepth(mode)) { + widths[realNum] = (uint32_t)CGDisplayModeGetWidth(mode); + heights[realNum] = (uint32_t)CGDisplayModeGetHeight(mode); + realNum++; + } + } + *numModes = realNum; + CGDisplayModeRelease(currentMode); + CFRelease(allModes); + return true; +} + +extern "C" bool MacGetMode(CGDirectDisplayID display, uint32_t *width, uint32_t *height) { + CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display); + if (mode == NULL) { + return false; + } + *width = (uint32_t)CGDisplayModeGetWidth(mode); + *height = (uint32_t)CGDisplayModeGetHeight(mode); + CGDisplayModeRelease(mode); + return true; +} + + +static bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { + CGError rc; + CGDisplayConfigRef config; + rc = CGBeginDisplayConfiguration(&config); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGConfigureDisplayWithDisplayMode(config, display, mode, NULL); + if (rc != kCGErrorSuccess) { + return false; + } + rc = CGCompleteDisplayConfiguration(config, kCGConfigureForSession); + if (rc != kCGErrorSuccess) { + return false; + } + return true; +} + +extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height) +{ + bool ret = false; + CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); + if (currentMode == NULL) { + return ret; + } + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + if (allModes == NULL) { + CGDisplayModeRelease(currentMode); + return ret; + } + int numModes = CFArrayGetCount(allModes); + for (int i = 0; i < numModes; i++) { + CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); + if (width == CGDisplayModeGetWidth(mode) && + height == CGDisplayModeGetHeight(mode) && + CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) && + bitDepth(currentMode) == bitDepth(mode)) { + ret = setDisplayToMode(display, mode); + break; + } + } + CGDisplayModeRelease(currentMode); + CFRelease(allModes); + return ret; +} \ No newline at end of file diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 0bbec399c..251211c29 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -4,6 +4,7 @@ use super::{CursorData, ResultType}; use cocoa::{ + appkit::{NSApp, NSApplication, NSApplicationActivationPolicy::*}, base::{id, nil, BOOL, NO, YES}, foundation::{NSDictionary, NSPoint, NSSize, NSString}, }; @@ -16,7 +17,7 @@ use core_graphics::{ display::{kCGNullWindowID, kCGWindowListOptionOnScreenOnly, CGWindowListCopyWindowInfo}, window::{kCGWindowName, kCGWindowOwnerPID}, }; -use hbb_common::{bail, log}; +use hbb_common::{allow_err, anyhow::anyhow, bail, log, message_proto::Resolution}; use include_dir::{include_dir, Dir}; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; @@ -32,6 +33,18 @@ extern "C" { fn CGEventGetLocation(e: *const c_void) -> CGPoint; static kAXTrustedCheckOptionPrompt: CFStringRef; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; + fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; + fn MacCheckAdminAuthorization() -> BOOL; + fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL; + fn MacGetModes( + display: u32, + widths: *mut u32, + heights: *mut u32, + max: u32, + numModes: *mut u32, + ) -> BOOL; + fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL; + fn MacSetMode(display: u32, width: u32, height: u32) -> BOOL; } pub fn is_process_trusted(prompt: bool) -> bool { @@ -47,6 +60,13 @@ pub fn is_process_trusted(prompt: bool) -> bool { } } +pub fn is_can_input_monitoring(prompt: bool) -> bool { + unsafe { + let value = if prompt { YES } else { NO }; + InputMonitoringAuthStatus(value) == YES + } +} + // macOS >= 10.15 // https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/ // remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk @@ -162,7 +182,7 @@ pub fn is_installed_daemon(prompt: bool) -> bool { false } -pub fn uninstall() -> bool { +pub fn uninstall(show_new_window: bool) -> bool { // to-do: do together with win/linux about refactory start/stop service if !is_installed_daemon(false) { return false; @@ -197,14 +217,21 @@ pub fn uninstall() -> bool { .args(&["remove", &format!("{}_server", crate::get_full_name())]) .status() .ok(); - std::process::Command::new("sh") - .arg("-c") - .arg(&format!( - "sleep 0.5; open /Applications/{}.app", - crate::get_app_name(), - )) - .spawn() - .ok(); + if show_new_window { + std::process::Command::new("sh") + .arg("-c") + .arg(&format!( + "sleep 0.5; open /Applications/{}.app", + crate::get_app_name(), + )) + .spawn() + .ok(); + } else { + std::process::Command::new("pkill") + .arg(crate::get_app_name()) + .status() + .ok(); + } quit_gui(); } } @@ -322,7 +349,7 @@ pub fn get_cursor_data(hcursor: u64) -> ResultType { */ let mut colors: Vec = Vec::new(); colors.reserve((size.height * size.width) as usize * 4); - // TIFF is rgb colrspace, no need to convert + // TIFF is rgb colorspace, no need to convert // let cs: id = msg_send![class!(NSColorSpace), sRGBColorSpace]; for y in 0..(size.height as _) { for x in 0..(size.width as _) { @@ -431,7 +458,7 @@ pub fn start_os_service() { .status() .ok(); println!("The others killed"); - // launchctl load/unload/start agent not work in daemon, show not priviledged. + // launchctl load/unload/start agent not work in daemon, show not privileged. // sudo launchctl asuser 501 open -n also not allowed. std::process::Command::new("launchctl") .args(&[ @@ -532,7 +559,6 @@ pub fn is_installed() -> bool { } pub fn quit_gui() { - use cocoa::appkit::NSApp; unsafe { let () = msg_send!(NSApp(), terminate: nil); }; @@ -542,3 +568,105 @@ pub fn get_double_click_time() -> u32 { // to-do: https://github.com/servo/core-foundation-rs/blob/786895643140fa0ee4f913d7b4aeb0c4626b2085/cocoa/src/appkit.rs#L2823 500 as _ } + +pub fn hide_dock() { + unsafe { + NSApp().setActivationPolicy_(NSApplicationActivationPolicyAccessory); + } +} + +fn check_main_window() -> bool { + use hbb_common::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 usize).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 true; + } + } + std::process::Command::new("open") + .args(["-n", &app]) + .status() + .ok(); + false +} + +pub fn handle_application_should_open_untitled_file() { + hbb_common::log::debug!("icon clicked on finder"); + let x = std::env::args().nth(1).unwrap_or_default(); + if x == "--server" || x == "--cm" || x == "--tray" { + if crate::platform::macos::check_main_window() { + allow_err!(crate::ipc::send_url_scheme("rustdesk:".into())); + } + } +} + +pub fn resolutions(name: &str) -> Vec { + let mut v = vec![]; + if let Ok(display) = name.parse::() { + let mut num = 0; + unsafe { + if YES == MacGetModeNum(display, &mut num) { + let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]); + let mut real_num = 0; + if YES + == MacGetModes( + display, + widths.as_mut_ptr(), + heights.as_mut_ptr(), + num, + &mut real_num, + ) + { + if real_num <= num { + for i in 0..real_num { + let resolution = Resolution { + width: widths[i as usize] as _, + height: heights[i as usize] as _, + ..Default::default() + }; + if !v.contains(&resolution) { + v.push(resolution); + } + } + } + } + } + } + } + v +} + +pub fn current_resolution(name: &str) -> ResultType { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + let (mut width, mut height) = (0, 0); + if NO == MacGetMode(display, &mut width, &mut height) { + bail!("MacGetMode failed"); + } + Ok(Resolution { + width: width as _, + height: height as _, + ..Default::default() + }) + } +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + let display = name.parse::().map_err(|e| anyhow!(e))?; + unsafe { + if NO == MacSetMode(display, width as _, height as _) { + bail!("MacSetMode failed"); + } + } + Ok(()) +} + +pub fn check_super_user_permission() -> ResultType { + unsafe { Ok(MacCheckAdminAuthorization() == YES) } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index ed5fcfaa1..2ef39e92a 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -11,6 +11,9 @@ pub mod windows; #[cfg(target_os = "macos")] pub mod macos; +#[cfg(target_os = "macos")] +pub mod delegate; + #[cfg(target_os = "linux")] pub mod linux; @@ -19,14 +22,6 @@ use hbb_common::{message_proto::CursorData, ResultType}; #[cfg(not(target_os = "macos"))] const SERVICE_INTERVAL: u64 = 300; -pub fn get_license_key() -> String { - #[cfg(windows)] - if let Some(lic) = windows::get_license() { - return lic.key; - } - Default::default() -} - pub fn is_xfce() -> bool { #[cfg(target_os = "linux")] { @@ -38,6 +33,11 @@ pub fn is_xfce() -> bool { } } +pub fn breakdown_callback() { + #[cfg(target_os = "linux")] + crate::input_service::clear_remapped_keycode() +} + // Android #[cfg(target_os = "android")] pub fn get_active_username() -> String { @@ -74,5 +74,13 @@ mod tests { assert!(!get_cursor_pos().is_none()); } } -} + #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[test] + fn test_resolution() { + let name = r"\\.\DISPLAY1"; + println!("current:{:?}", current_resolution(name)); + println!("change:{:?}", change_resolution(name, 2880, 1800)); + println!("resolutions:{:?}", resolutions(name)); + } +} diff --git a/src/windows.cc b/src/platform/windows.cc similarity index 100% rename from src/windows.cc rename to src/platform/windows.cc diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a2a99800f..379cd29fb 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -5,26 +5,31 @@ use crate::license::*; use hbb_common::{ allow_err, bail, config::{self, Config}, - log, sleep, timeout, tokio, + log, + message_proto::Resolution, + sleep, timeout, tokio, }; use std::io::prelude::*; +use std::ptr::null_mut; use std::{ ffi::OsString, fs, io, mem, + os::windows::process::CommandExt, path::PathBuf, sync::{Arc, Mutex}, time::{Duration, Instant}, + collections::HashMap }; use winapi::{ ctypes::c_void, - shared::{minwindef::*, ntdef::NULL, windef::*}, + shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, - OpenProcessToken, + OpenProcessToken, PROCESS_INFORMATION, STARTUPINFOW, }, securitybaseapi::GetTokenInformation, shellapi::ShellExecuteW, @@ -49,6 +54,7 @@ use winreg::RegKey; pub fn get_cursor_pos() -> Option<(i32, i32)> { unsafe { + #[allow(invalid_value)] let mut out = mem::MaybeUninit::uninit().assume_init(); if GetCursorPos(&mut out) == FALSE { return None; @@ -61,6 +67,7 @@ pub fn reset_input_cache() {} pub fn get_cursor() -> ResultType> { unsafe { + #[allow(invalid_value)] let mut ci: CURSORINFO = mem::MaybeUninit::uninit().assume_init(); ci.cbSize = std::mem::size_of::() as _; if crate::portable_service::client::get_cursor_info(&mut ci) == FALSE { @@ -79,6 +86,7 @@ struct IconInfo(ICONINFO); impl IconInfo { fn new(icon: HICON) -> ResultType { unsafe { + #[allow(invalid_value)] let mut ii = mem::MaybeUninit::uninit().assume_init(); if GetIconInfo(icon, &mut ii) == FALSE { Err(io::Error::last_os_error().into()) @@ -829,8 +837,8 @@ fn get_default_install_path() -> String { pub fn check_update_broker_process() -> ResultType<()> { // let (_, path, _, _) = get_install_info(); - let process_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE; - let origin_process_exe = crate::ui::win_privacy::ORIGIN_PROCESS_EXE; + let process_exe = crate::win_privacy::INJECTED_PROCESS_EXE; + let origin_process_exe = crate::win_privacy::ORIGIN_PROCESS_EXE; let exe_file = std::env::current_exe()?; if exe_file.parent().is_none() { @@ -839,6 +847,11 @@ pub fn check_update_broker_process() -> ResultType<()> { let cur_dir = exe_file.parent().unwrap(); let cur_exe = cur_dir.join(process_exe); + if !std::path::Path::new(&cur_exe).exists() { + std::fs::copy(origin_process_exe, cur_exe)?; + return Ok(()); + } + let ori_modified = fs::metadata(origin_process_exe)?.modified()?; if let Ok(metadata) = fs::metadata(&cur_exe) { if let Ok(cur_modified) = metadata.modified() { @@ -910,8 +923,8 @@ pub fn copy_exe_cmd(src_exe: &str, _exe: &str, path: &str) -> String { ", main_exe = main_exe, path = path, - ORIGIN_PROCESS_EXE = crate::ui::win_privacy::ORIGIN_PROCESS_EXE, - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + ORIGIN_PROCESS_EXE = crate::win_privacy::ORIGIN_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ); } @@ -929,7 +942,7 @@ pub fn update_me() -> ResultType<()> { {lic} ", copy_exe = copy_exe_cmd(&src_exe, &exe, &path), - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, app_name = crate::get_app_name(), lic = register_licence(), cur_pid = get_current_pid(), @@ -965,7 +978,7 @@ fn get_after_install(exe: &str) -> String { } pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> ResultType<()> { - let uninstall_str = get_uninstall(); + let uninstall_str = get_uninstall(false); let mut path = path.trim_end_matches('\\').to_owned(); let (subkey, _path, start_menu, exe) = get_default_install_info(); let mut exe = exe; @@ -1097,6 +1110,12 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name} ); let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_string(); + let install_cert = if options.contains("driverCert") { + format!("\"{}\" --install-cert \"RustDeskIddDriver.cer\"", src_exe) + } else { + "".to_owned() + }; + let cmds = format!( " {uninstall_str} @@ -1128,6 +1147,7 @@ sc create {app_name} binpath= \"\\\"{exe}\\\" --import-config \\\"{config_path}\ sc start {app_name} sc stop {app_name} sc delete {app_name} +{install_cert} {after_install} {sleep} ", @@ -1148,6 +1168,7 @@ sc delete {app_name} shortcuts=shortcuts, config_path=Config::file().to_str().unwrap_or(""), lic=register_licence(), + install_cert=install_cert, after_install=get_after_install(&exe), sleep=if debug { "timeout 300" @@ -1177,30 +1198,35 @@ pub fn run_after_install() -> ResultType<()> { } pub fn run_before_uninstall() -> ResultType<()> { - run_cmds(get_before_uninstall(), true, "before_install") + run_cmds(get_before_uninstall(true), true, "before_install") } -fn get_before_uninstall() -> String { +fn get_before_uninstall(kill_self: bool) -> String { let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); + let filter = if kill_self { + "".to_string() + } else { + format!(" /FI \"PID ne {}\"", get_current_pid()) + }; format!( " chcp 65001 sc stop {app_name} sc delete {app_name} taskkill /F /IM {broker_exe} - taskkill /F /IM {app_name}.exe /FI \"PID ne {cur_pid}\" + taskkill /F /IM {app_name}.exe{filter} reg delete HKEY_CLASSES_ROOT\\.{ext} /f netsh advfirewall firewall delete rule name=\"{app_name} Service\" ", app_name = app_name, - broker_exe = crate::ui::win_privacy::INJECTED_PROCESS_EXE, + broker_exe = crate::win_privacy::INJECTED_PROCESS_EXE, ext = ext, - cur_pid = get_current_pid(), + filter = filter, ) } -fn get_uninstall() -> String { +fn get_uninstall(kill_self: bool) -> String { let (subkey, path, start_menu, _) = get_install_info(); format!( " @@ -1211,7 +1237,7 @@ fn get_uninstall() -> String { if exist \"%PUBLIC%\\Desktop\\{app_name}.lnk\" del /f /q \"%PUBLIC%\\Desktop\\{app_name}.lnk\" if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" ", - before_uninstall=get_before_uninstall(), + before_uninstall=get_before_uninstall(kill_self), subkey=subkey, app_name = crate::get_app_name(), path = path, @@ -1219,12 +1245,22 @@ fn get_uninstall() -> String { ) } -pub fn uninstall_me() -> ResultType<()> { - run_cmds(get_uninstall(), true, "uninstall") +pub fn uninstall_me(kill_self: bool) -> ResultType<()> { + allow_err!(cert::uninstall_certs()); + run_cmds(get_uninstall(kill_self), true, "uninstall") } fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType { let mut tmp = std::env::temp_dir(); + // When dir contains these characters, the bat file will not execute in elevated mode. + if vec!["&", "@", "^"] + .drain(..) + .any(|s| tmp.to_string_lossy().to_string().contains(s)) + { + if let Ok(dir) = user_accessible_folder() { + tmp = dir; + } + } tmp.push(format!("{}_{}.{}", crate::get_app_name(), tip, ext)); let mut file = std::fs::File::create(&tmp)?; // in case cmds mixed with \r\n and \n, make sure all ending with \r\n @@ -1368,22 +1404,6 @@ pub fn get_license() -> Option { pub fn bootstrap() { if let Some(lic) = get_license() { *config::PROD_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); - #[cfg(feature = "hbbs")] - { - if !is_win_server() { - return; - } - crate::hbbs::bootstrap(&lic.key, &lic.host); - std::thread::spawn(move || loop { - let tmp = Config::get_option("stop-rendezvous-service"); - if tmp.is_empty() { - crate::hbbs::start(); - } else { - crate::hbbs::stop(); - } - std::thread::sleep(std::time::Duration::from_millis(100)); - }); - } } } @@ -1652,6 +1672,29 @@ pub fn is_elevated(process_id: Option) -> ResultType { } } +#[inline] +fn filter_foreground_window(process_id: DWORD) -> ResultType { + if let Ok(output) = std::process::Command::new("tasklist") + .args(vec![ + "/SVC", + "/NH", + "/FI", + &format!("PID eq {}", process_id), + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + { + let s = String::from_utf8_lossy(&output.stdout) + .to_string() + .to_lowercase(); + Ok(["Taskmgr", "mmc", "regedit"] + .iter() + .any(|name| s.contains(&name.to_string().to_lowercase()))) + } else { + bail!("run tasklist failed"); + } +} + pub fn is_foreground_window_elevated() -> ResultType { unsafe { let mut process_id: DWORD = 0; @@ -1659,7 +1702,12 @@ pub fn is_foreground_window_elevated() -> ResultType { if process_id == 0 { bail!("Failed to get processId, errno {}", GetLastError()) } - is_elevated(Some(process_id)) + let elevated = is_elevated(Some(process_id))?; + if elevated { + filter_foreground_window(process_id) + } else { + Ok(false) + } } } @@ -1714,3 +1762,399 @@ pub fn send_message_to_hnwd( } return true; } + +pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> { + let last_error_table = HashMap::from([ + (ERROR_LOGON_FAILURE, "The user name or password is incorrect."), + (ERROR_ACCESS_DENIED, "Access is denied.") + ]); + + unsafe { + let user_split = user.split("\\").collect::>(); + let wuser = wide_string(user_split.get(1).unwrap_or(&user)); + let wpc = wide_string(user_split.get(0).unwrap_or(&"")); + let wpwd = wide_string(pwd); + let cmd = if arg.is_empty() { + format!("\"{}\"", exe) + } else { + format!("\"{}\" {}", exe, arg) + }; + let mut wcmd = wide_string(&cmd); + let mut si: STARTUPINFOW = mem::zeroed(); + si.wShowWindow = SW_HIDE as _; + si.lpDesktop = NULL as _; + si.cb = std::mem::size_of::() as _; + si.dwFlags = STARTF_USESHOWWINDOW; + let mut pi: PROCESS_INFORMATION = mem::zeroed(); + let wexe = wide_string(exe); + if FALSE + == CreateProcessWithLogonW( + wuser.as_ptr(), + wpc.as_ptr(), + wpwd.as_ptr(), + LOGON_WITH_PROFILE, + wexe.as_ptr(), + wcmd.as_mut_ptr(), + CREATE_UNICODE_ENVIRONMENT, + NULL, + NULL as _, + &mut si as *mut STARTUPINFOW, + &mut pi as *mut PROCESS_INFORMATION, + ) + { + let last_error = GetLastError(); + bail!( + "CreateProcessWithLogonW failed : \"{}\", errno={}", + last_error_table + .get(&last_error) + .unwrap_or(&"Unknown error"), + last_error + ); + } + } + return Ok(()); +} + +pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> { + std::process::Command::new("icacls") + .arg(dir.as_os_str()) + .arg("/grant") + .arg(format!("*S-1-1-0:(OI)(CI){}", permission)) + .arg("/T") + .spawn()?; + Ok(()) +} + +pub fn resolutions(name: &str) -> Vec { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + let wname = wide_string(name); + let len = if wname.len() <= dm.dmDeviceName.len() { + wname.len() + } else { + dm.dmDeviceName.len() + }; + std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); + dm.dmSize = std::mem::size_of::() as _; + let mut v = vec![]; + let mut num = 0; + loop { + if EnumDisplaySettingsW(NULL as _, num, &mut dm) == 0 { + break; + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + if !v.contains(&r) { + v.push(r); + } + num += 1; + } + v + } +} + +pub fn current_resolution(name: &str) -> ResultType { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + dm.dmSize = std::mem::size_of::() as _; + let wname = wide_string(name); + if EnumDisplaySettingsW(wname.as_ptr(), ENUM_CURRENT_SETTINGS, &mut dm) == 0 { + bail!( + "failed to get currrent resolution, errno={}", + GetLastError() + ); + } + let r = Resolution { + width: dm.dmPelsWidth as _, + height: dm.dmPelsHeight as _, + ..Default::default() + }; + Ok(r) + } +} + +pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> { + unsafe { + let mut dm: DEVMODEW = std::mem::zeroed(); + if FALSE == EnumDisplaySettingsW(NULL as _, ENUM_CURRENT_SETTINGS, &mut dm) { + bail!("EnumDisplaySettingsW failed, errno={}", GetLastError()); + } + let wname = wide_string(name); + let len = if wname.len() <= dm.dmDeviceName.len() { + wname.len() + } else { + dm.dmDeviceName.len() + }; + std::ptr::copy_nonoverlapping(wname.as_ptr(), dm.dmDeviceName.as_mut_ptr(), len); + dm.dmSize = std::mem::size_of::() as _; + dm.dmPelsWidth = width as _; + dm.dmPelsHeight = height as _; + dm.dmFields = DM_PELSHEIGHT | DM_PELSWIDTH; + let res = ChangeDisplaySettingsExW( + wname.as_ptr(), + &mut dm, + NULL as _, + CDS_UPDATEREGISTRY | CDS_GLOBAL | CDS_RESET, + NULL, + ); + if res != DISP_CHANGE_SUCCESSFUL { + bail!( + "ChangeDisplaySettingsExW failed, res={}, errno={}", + res, + GetLastError() + ); + } + Ok(()) + } +} + +pub fn user_accessible_folder() -> ResultType { + let disk = std::env::var("SystemDrive").unwrap_or("C:".to_string()); + let dir1 = PathBuf::from(format!("{}\\ProgramData", disk)); + // NOTICE: "C:\Windows\Temp" requires permanent authorization. + let dir2 = PathBuf::from(format!("{}\\Windows\\Temp", disk)); + let dir; + if dir1.exists() { + dir = dir1; + } else if dir2.exists() { + dir = dir2; + } else { + bail!("no vaild user accessible folder"); + } + Ok(dir) +} + +#[inline] +pub fn install_cert(cert_file: &str) -> ResultType<()> { + let exe_file = std::env::current_exe()?; + if let Some(cur_dir) = exe_file.parent() { + allow_err!(cert::install_cert(cur_dir.join(cert_file))); + } else { + bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ); + } + Ok(()) +} + +mod cert { + use hbb_common::{allow_err, bail, log, ResultType}; + use std::{path::Path, str::from_utf8}; + use winapi::shared::{ + minwindef::{BYTE, DWORD, TRUE}, + ntdef::NULL, + }; + use winapi::um::{ + errhandlingapi::GetLastError, + wincrypt::{ + CertCloseStore, CertEnumCertificatesInStore, CertNameToStrA, CertOpenSystemStoreA, + CryptHashCertificate, ALG_ID, CALG_SHA1, CERT_ID_SHA1_HASH, CERT_X500_NAME_STR, + PCCERT_CONTEXT, + }, + winreg::HKEY_LOCAL_MACHINE, + }; + use winreg::{ + enums::{KEY_WRITE, REG_BINARY}, + RegKey, + }; + + const ROOT_CERT_STORE_PATH: &str = + "SOFTWARE\\Microsoft\\SystemCertificates\\ROOT\\Certificates\\"; + const THUMBPRINT_ALG: ALG_ID = CALG_SHA1; + const THUMBPRINT_LEN: DWORD = 20; + + #[inline] + unsafe fn compute_thumbprint(pb_encoded: *const BYTE, cb_encoded: DWORD) -> (Vec, String) { + let mut size = THUMBPRINT_LEN; + let mut thumbprint = [0u8; THUMBPRINT_LEN as usize]; + if CryptHashCertificate( + 0, + THUMBPRINT_ALG, + 0, + pb_encoded, + cb_encoded, + thumbprint.as_mut_ptr(), + &mut size, + ) == TRUE + { + ( + thumbprint.to_vec(), + hex::encode(thumbprint).to_ascii_uppercase(), + ) + } else { + (thumbprint.to_vec(), "".to_owned()) + } + } + + #[inline] + unsafe fn open_reg_cert_store() -> ResultType { + let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); + Ok(hklm.open_subkey_with_flags(ROOT_CERT_STORE_PATH, KEY_WRITE)?) + } + + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gpef/6a9e35fa-2ac7-4c10-81e1-eabe8d2472f1 + fn create_cert_blob(thumbprint: Vec, encoded: Vec) -> Vec { + let mut blob = Vec::new(); + + let mut property_id = (CERT_ID_SHA1_HASH as u32).to_le_bytes().to_vec(); + let mut pro_reserved = [0x01, 0x00, 0x00, 0x00].to_vec(); + let mut pro_length = (THUMBPRINT_LEN as u32).to_le_bytes().to_vec(); + let mut pro_val = thumbprint; + blob.append(&mut property_id); + blob.append(&mut pro_reserved); + blob.append(&mut pro_length); + blob.append(&mut pro_val); + + let mut blob_reserved = [0x20, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00].to_vec(); + let mut blob_length = (encoded.len() as u32).to_le_bytes().to_vec(); + let mut blob_val = encoded; + blob.append(&mut blob_reserved); + blob.append(&mut blob_length); + blob.append(&mut blob_val); + + blob + } + + pub fn install_cert>(path: P) -> ResultType<()> { + let mut cert_bytes = std::fs::read(path)?; + unsafe { + let thumbprint = compute_thumbprint(cert_bytes.as_mut_ptr(), cert_bytes.len() as _); + log::debug!("Thumbprint of cert {}", &thumbprint.1); + + let reg_cert_key = open_reg_cert_store()?; + let (cert_key, _) = reg_cert_key.create_subkey(&thumbprint.1)?; + let data = winreg::RegValue { + vtype: REG_BINARY, + bytes: create_cert_blob(thumbprint.0, cert_bytes), + }; + cert_key.set_raw_value("Blob", &data)?; + } + Ok(()) + } + + fn get_thumbprints_to_rm() -> ResultType> { + let issuers_to_rm = ["CN=\"WDKTestCert admin,133225435702113567\""]; + + let mut thumbprints = Vec::new(); + let mut buf = [0u8; 1024]; + + unsafe { + let store_handle = CertOpenSystemStoreA(0 as _, "ROOT\0".as_ptr() as _); + if store_handle.is_null() { + bail!("Error opening certificate store: {}", GetLastError()); + } + + let mut cert_ctx: PCCERT_CONTEXT = CertEnumCertificatesInStore(store_handle, NULL as _); + while !cert_ctx.is_null() { + // https://stackoverflow.com/a/66432736 + let cb_size = CertNameToStrA( + (*cert_ctx).dwCertEncodingType, + &mut ((*(*cert_ctx).pCertInfo).Issuer) as _, + CERT_X500_NAME_STR, + buf.as_mut_ptr() as _, + buf.len() as _, + ); + if cb_size != 1 { + if let Ok(issuer) = from_utf8(&buf[..cb_size as _]) { + for iss in issuers_to_rm.iter() { + if issuer.contains(iss) { + let (_, thumbprint) = compute_thumbprint( + (*cert_ctx).pbCertEncoded, + (*cert_ctx).cbCertEncoded, + ); + if !thumbprint.is_empty() { + thumbprints.push(thumbprint); + } + } + } + } + } + cert_ctx = CertEnumCertificatesInStore(store_handle, cert_ctx); + } + CertCloseStore(store_handle, 0); + } + + Ok(thumbprints) + } + + pub fn uninstall_certs() -> ResultType<()> { + let thumbprints = get_thumbprints_to_rm()?; + let reg_cert_key = unsafe { open_reg_cert_store()? }; + for thumbprint in thumbprints.iter() { + allow_err!(reg_cert_key.delete_subkey(thumbprint)); + } + Ok(()) + } +} + +pub fn get_char_by_vk(vk: u32) -> Option { + const BUF_LEN: i32 = 32; + let mut buff = [0_u16; BUF_LEN as usize]; + let buff_ptr = buff.as_mut_ptr(); + let len = unsafe { + let current_window_thread_id = GetWindowThreadProcessId(GetForegroundWindow(), null_mut()); + let layout = GetKeyboardLayout(current_window_thread_id); + + // refs: https://github.com/fufesou/rdev/blob/25a99ce71ab42843ad253dd51e6a35e83e87a8a4/src/windows/keyboard.rs#L115 + let press_state = 129; + let mut state: [BYTE; 256] = [0; 256]; + let shift_left = rdev::get_modifier(rdev::Key::ShiftLeft); + let shift_right = rdev::get_modifier(rdev::Key::ShiftRight); + if shift_left { + state[VK_LSHIFT as usize] = press_state; + } + if shift_right { + state[VK_RSHIFT as usize] = press_state; + } + if shift_left || shift_right { + state[VK_SHIFT as usize] = press_state; + } + ToUnicodeEx(vk, 0x00, &state as _, buff_ptr, BUF_LEN, 0, layout) + }; + if len == 1 { + if let Some(chr) = String::from_utf16(&buff[..len as usize]) + .ok()? + .chars() + .next() + { + if chr.is_control() { + return None; + } else { + Some(chr) + } + } else { + None + } + } else { + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_install_cert() { + println!( + "install driver cert: {:?}", + cert::install_cert("RustDeskIddDriver.cer") + ); + } + + #[test] + fn test_uninstall_cert() { + println!("uninstall driver certs: {:?}", cert::uninstall_certs()); + } + + #[test] + fn test_get_char_by_vk() { + let chr = get_char_by_vk(0x41); // VK_A + assert_eq!(chr, Some('a')); + let chr = get_char_by_vk(VK_ESCAPE as u32); // VK_ESC + assert_eq!(chr, None) + } +} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index ec70bdf84..8b7dae1ba 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -469,21 +469,13 @@ impl RendezvousMediator { Ok(()) } - fn get_relay_server(&self, provided_by_rendzvous_server: String) -> String { + fn get_relay_server(&self, provided_by_rendezvous_server: String) -> String { let mut relay_server = Config::get_option("relay-server"); if relay_server.is_empty() { - relay_server = provided_by_rendzvous_server; + relay_server = provided_by_rendezvous_server; } if relay_server.is_empty() { - if self.host.contains(":") { - let tmp: Vec<&str> = self.host.split(":").collect(); - if tmp.len() == 2 { - let port: u16 = tmp[1].parse().unwrap_or(0); - relay_server = format!("{}:{}", tmp[0], port + 1); - } - } else { - relay_server = self.host.clone(); - } + relay_server = crate::increase_port(&self.host, 1); } relay_server } @@ -506,8 +498,7 @@ async fn direct_server(server: ServerPtr) { let disabled = Config::get_option("direct-server").is_empty(); if !disabled && listener.is_none() { port = get_direct_port(); - let addr = format!("0.0.0.0:{}", port); - match hbb_common::tcp::new_listener(&addr, false).await { + match hbb_common::tcp::listen_any(port as _).await { Ok(l) => { listener = Some(l); log::info!( @@ -518,8 +509,8 @@ async fn direct_server(server: ServerPtr) { Err(err) => { // to-do: pass to ui log::error!( - "Failed to start direct server on : {}, error: {}", - addr, + "Failed to start direct server on port: {}, error: {}", + port, err ); loop { diff --git a/src/server.rs b/src/server.rs index 5c020261f..1f174ef03 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,8 +1,16 @@ -use crate::ipc::Data; +use std::{ + collections::HashMap, + net::SocketAddr, + sync::{Arc, Mutex, RwLock, Weak}, + time::Duration, +}; + use bytes::Bytes; + pub use connection::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::config::Config2; +use hbb_common::tcp::new_listener; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, @@ -19,12 +27,8 @@ use hbb_common::{ #[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; use service::{GenericService, Service, Subscriber}; -use std::{ - collections::HashMap, - net::SocketAddr, - sync::{Arc, Mutex, RwLock, Weak}, - time::Duration, -}; + +use crate::ipc::Data; pub mod audio_service; cfg_if::cfg_if! { @@ -55,14 +59,19 @@ mod service; mod video_qos; pub mod video_service; -use hbb_common::tcp::new_listener; - pub type Childs = Arc>>; type ConnMap = HashMap; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); pub static ref CONN_COUNT: Arc> = Default::default(); + // A client server used to provide local services(audio, video, clipboard, etc.) + // for all initiative connections. + // + // [Note] + // Now we use this [`CLIENT_SERVER`] to do following operations: + // - record local audio, and send to remote + pub static ref CLIENT_SERVER: ServerPtr = new(); } pub struct Server { @@ -194,9 +203,14 @@ pub async fn create_tcp_connection( } } - #[cfg(target_os = "macos")]{ + #[cfg(target_os = "macos")] + { use std::process::Command; - Command::new("/usr/bin/caffeinate").arg("-u").arg("-t 5").spawn().ok(); + Command::new("/usr/bin/caffeinate") + .arg("-u") + .arg("-t 5") + .spawn() + .ok(); log::info!("wake up macos"); } Connection::start(addr, stream, id, Arc::downgrade(&server)).await; @@ -248,10 +262,7 @@ async fn create_relay_connection_( ) .await?; let mut msg_out = RendezvousMessage::new(); - let mut licence_key = Config::get_option("key"); - if licence_key.is_empty() { - licence_key = crate::platform::get_license_key(); - } + let licence_key = crate::get_key(true).await; msg_out.set_request_relay(RequestRelay { licence_key, uuid, @@ -309,6 +320,13 @@ impl Server { } } } + + // get a new unique id + pub fn get_new_id(&mut self) -> i32 { + let new_id = self.id_count; + self.id_count += 1; + new_id + } } impl Drop for Server { @@ -385,6 +403,7 @@ pub async fn start_server(is_server: bool) { #[cfg(windows)] crate::platform::windows::bootstrap(); input_service::fix_key_down_timeout_loop(); + crate::hbbs_http::sync::start(); #[cfg(target_os = "linux")] if crate::platform::current_is_wayland() { allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await); @@ -398,7 +417,8 @@ pub async fn start_server(is_server: bool) { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; if Config::set(config) { log::info!("config synced"); } @@ -419,6 +439,48 @@ pub async fn start_server(is_server: bool) { } } +#[cfg(target_os = "macos")] +#[tokio::main(flavor = "current_thread")] +pub async fn start_ipc_url_server() { + log::debug!("Start an ipc server for listening to url schemes"); + match crate::ipc::new_listener("_url").await { + Ok(mut incoming) => { + while let Some(Ok(conn)) = incoming.next().await { + let mut conn = crate::ipc::Connection::new(conn); + match conn.next_timeout(1000).await { + Ok(Some(data)) => match data { + #[cfg(feature = "flutter")] + Data::UrlLink(url) => { + if let Some(stream) = crate::flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(crate::flutter::APP_TYPE_MAIN) + { + let mut m = HashMap::new(); + m.insert("name", "on_url_scheme_received"); + m.insert("url", url.as_str()); + stream.add(serde_json::to_string(&m).unwrap()); + } else { + log::warn!("No main window app found!"); + } + } + _ => { + log::warn!("An unexpected data was sent to the ipc url server.") + } + }, + Err(err) => { + log::error!("{}", err); + } + _ => {} + } + } + } + Err(err) => { + log::error!("{}", err); + } + } +} + #[cfg(target_os = "macos")] async fn sync_and_watch_config_dir() { if crate::platform::is_root() { @@ -443,7 +505,8 @@ async fn sync_and_watch_config_dir() { if conn.send(&Data::SyncConfig(None)).await.is_ok() { if let Ok(Some(data)) = conn.next_timeout(1000).await { match data { - Data::SyncConfig(Some((config, config2))) => { + Data::SyncConfig(Some(configs)) => { + let (config, config2) = *configs; let _chk = crate::ipc::CheckIfRestart::new(); if cfg0.0 != config { cfg0.0 = config.clone(); @@ -468,7 +531,7 @@ async fn sync_and_watch_config_dir() { let cfg = (Config::get(), Config2::get()); if cfg != cfg0 { log::info!("config updated, sync to root"); - match conn.send(&Data::SyncConfig(Some(cfg.clone()))).await { + match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await { Err(e) => { log::error!("sync config to root failed: {}", e); break; diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs index c7a720ded..bef1ac1f3 100644 --- a/src/server/audio_service.rs +++ b/src/server/audio_service.rs @@ -13,7 +13,6 @@ // https://github.com/krruzic/pulsectl use super::*; -use hbb_common::get_time; use magnum_opus::{Application::*, Channels::*, Encoder}; use std::sync::atomic::{AtomicBool, Ordering}; @@ -349,7 +348,6 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { let mut msg_out = Message::new(); msg_out.set_audio_frame(AudioFrame { data: data.into(), - timestamp: get_time(), ..Default::default() }); sp.send(msg_out); @@ -369,7 +367,6 @@ fn send_f32(data: &[f32], encoder: &mut Encoder, sp: &GenericService) { let mut msg_out = Message::new(); msg_out.set_audio_frame(AudioFrame { data: data.into(), - timestamp: get_time(), ..Default::default() }); sp.send(msg_out); diff --git a/src/server/connection.rs b/src/server/connection.rs index 919aeae99..1f89d905f 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -3,7 +3,15 @@ use super::{input_service::*, *}; use crate::clipboard_file::*; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::common::update_clipboard; -use crate::video_service; +#[cfg(windows)] +use crate::portable_service::client as portable_client; +use crate::{ + client::{ + new_voice_call_request, new_voice_call_response, start_audio_thread, MediaData, MediaSender, + }, + common::{get_default_sound_input, set_sound_input}, + video_service, +}; #[cfg(any(target_os = "android", target_os = "ios"))] use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; use crate::{ipc, VERSION}; @@ -26,12 +34,14 @@ use hbb_common::{ }; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::call_main_service_mouse_input; -use serde::Deserialize; use serde_json::{json, value::Value}; use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; -use std::sync::{atomic::AtomicI64, mpsc as std_mpsc}; +use std::{ + num::NonZeroI64, + sync::{atomic::AtomicI64, mpsc as std_mpsc}, +}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; @@ -40,6 +50,8 @@ pub type Sender = mpsc::UnboundedSender<(Instant, Arc)>; lazy_static::lazy_static! { static ref LOGIN_FAILURES: Arc::>> = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); + static ref ALIVE_CONNS: Arc::>> = Default::default(); + static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); } pub static CLICK_TIME: AtomicI64 = AtomicI64::new(0); pub static MOUSE_MOVE_TIME: AtomicI64 = AtomicI64::new(0); @@ -74,7 +86,6 @@ pub struct Connection { read_jobs: Vec, timer: Interval, file_timer: Interval, - http_timer: Interval, file_transfer: Option<(String, bool)>, port_forward_socket: Option>, port_forward_address: String, @@ -88,12 +99,22 @@ pub struct Connection { recording: bool, last_test_delay: i64, lock_after_session_end: bool, - show_remote_cursor: bool, // by peer + show_remote_cursor: bool, + // by peer ip: String, - disable_clipboard: bool, // by peer - disable_audio: bool, // by peer - enable_file_transfer: bool, // by peer - tx_input: std_mpsc::Sender, // handle input messages + // by peer + disable_keyboard: bool, + // by peer + disable_clipboard: bool, + // by peer + disable_audio: bool, + // by peer + enable_file_transfer: bool, + // by peer + audio_sender: Option, + // audio by the remote peer/client + tx_input: std_mpsc::Sender, + // handle input messages video_ack_required: bool, peer_info: (String, String), server_audit_conn: String, @@ -101,7 +122,19 @@ pub struct Connection { lr: LoginRequest, last_recv_time: Arc>, chat_unanswered: bool, - close_manually: bool, + #[cfg(windows)] + portable: PortableState, + from_switch: bool, + origin_resolution: HashMap, + voice_call_request_timestamp: Option, + audio_input_device_before_voice_call: Option, + options_in_login: Option, +} + +impl ConnInner { + pub fn new(id: i32, tx: Option, tx_video: Option) -> Self { + Self { id, tx, tx_video } + } } impl Subscriber for ConnInner { @@ -147,13 +180,15 @@ impl Connection { challenge: Config::get_auto_password(6), ..Default::default() }; + ALIVE_CONNS.lock().unwrap().push(id); let (tx_from_cm_holder, mut rx_from_cm) = mpsc::unbounded_channel::(); - // holding tx_from_cm_holde to avoid cpu burning of rx_from_cm.recv when all sender closed + // holding tx_from_cm_holder to avoid cpu burning of rx_from_cm.recv when all sender closed let tx_from_cm = tx_from_cm_holder.clone(); let (tx_to_cm, rx_to_cm) = mpsc::unbounded_channel::(); let (tx, mut rx) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_video, mut rx_video) = mpsc::unbounded_channel::<(Instant, Arc)>(); let (tx_input, rx_input) = std_mpsc::channel(); + let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); let tx_cloned = tx.clone(); let mut conn = Self { @@ -168,7 +203,6 @@ impl Connection { read_jobs: Vec::new(), timer: time::interval(SEC30), file_timer: time::interval(SEC30), - http_timer: time::interval(Duration::from_secs(3)), file_transfer: None, port_forward_socket: None, port_forward_address: "".to_owned(), @@ -187,6 +221,7 @@ impl Connection { disable_audio: false, enable_file_transfer: false, disable_clipboard: false, + disable_keyboard: false, tx_input, video_ack_required: false, peer_info: Default::default(), @@ -195,7 +230,14 @@ impl Connection { lr: Default::default(), last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, - close_manually: false, + #[cfg(windows)] + portable: Default::default(), + from_switch: false, + origin_resolution: Default::default(), + audio_sender: None, + voice_call_request_timestamp: None, + audio_input_device_before_voice_call: None, + options_in_login: None, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -242,12 +284,6 @@ impl Connection { #[cfg(not(any(target_os = "android", target_os = "ios")))] std::thread::spawn(move || Self::handle_input(rx_input, tx_cloned)); let mut second_timer = time::interval(Duration::from_secs(1)); - #[cfg(windows)] - let mut last_uac = false; - #[cfg(windows)] - let mut last_foreground_window_elevated = false; - #[cfg(windows)] - let is_installed = crate::platform::is_installed(); loop { tokio::select! { @@ -262,7 +298,9 @@ impl Connection { } } ipc::Data::Close => { - conn.on_close_manually("connection manager", "peer").await; + conn.chat_unanswered = false; // seen + conn.send_close_reason_no_retry("").await; + conn.on_close("connection manager", true).await; break; } ipc::Data::ChatMessage{text} => { @@ -292,7 +330,7 @@ impl Connection { if let Some(s) = conn.server.upgrade() { s.write().unwrap().subscribe( super::clipboard_service::NAME, - conn.inner.clone(), conn.clipboard_enabled() && conn.keyboard); + conn.inner.clone(), conn.clipboard_enabled() && conn.peer_keyboard_enabled()); } } else if &name == "audio" { conn.audio = enabled; @@ -318,7 +356,7 @@ impl Connection { allow_err!(conn.stream.send_raw(bytes).await); } #[cfg(windows)] - ipc::Data::ClipbaordFile(_clip) => { + ipc::Data::ClipboardFile(_clip) => { if conn.file_transfer_enabled() { allow_err!(conn.stream.send(&clip_2_msg(_clip)).await); } @@ -353,10 +391,27 @@ impl Connection { } #[cfg(windows)] ipc::Data::DataPortableService(ipc::DataPortableService::RequestStart) => { - if let Err(e) = crate::portable_service::client::start_portable_service() { + if let Err(e) = portable_client::start_portable_service(portable_client::StartPara::Direct) { log::error!("Failed to start portable service from cm:{:?}", e); } } + ipc::Data::SwitchSidesBack => { + let mut misc = Misc::new(); + misc.set_switch_back(SwitchBack::default()); + let mut msg = Message::new(); + msg.set_misc(misc); + conn.send(msg).await; + } + ipc::Data::VoiceCallResponse(accepted) => { + conn.handle_voice_call(accepted).await; + } + ipc::Data::CloseVoiceCall(_reason) => { + log::debug!("Close the voice call from the ipc."); + conn.close_voice_call().await; + // Notify the peer that we closed the voice call. + let msg = new_voice_call_request(false); + conn.send(msg).await; + } _ => {} } }, @@ -392,12 +447,13 @@ impl Connection { conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30); } } - _ = conn.http_timer.tick() => { - if let Err(_) = Connection::post_heartbeat(conn.server_audit_conn.clone(), conn.inner.id).await { - conn.on_close_manually("web console", "web console").await; + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&id) { + conn.send_close_reason_no_retry("Closed manually by web console").await; + conn.on_close("web console", true).await; break; } - }, + } Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { video_service::notify_video_frame_fetched(id, Some(instant.into())); @@ -424,7 +480,8 @@ impl Connection { Some(message::Union::Misc(m)) => { match &m.union { Some(misc::Union::StopService(_)) => { - conn.on_close_manually("stop service", "peer").await; + conn.send_close_reason_no_retry("").await; + conn.on_close("stop service", true).await; break; } _ => {}, @@ -439,36 +496,7 @@ impl Connection { }, _ = second_timer.tick() => { #[cfg(windows)] - { - if !is_installed { - let portable_service_running = crate::portable_service::client::PORTABLE_SERVICE_RUNNING.lock().unwrap().clone(); - let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); - if last_uac != uac { - last_uac = uac; - if !uac || !portable_service_running{ - let mut misc = Misc::new(); - misc.set_uac(uac); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } - let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap().clone(); - if last_foreground_window_elevated != foreground_window_elevated { - last_foreground_window_elevated = foreground_window_elevated; - if !foreground_window_elevated || !portable_service_running { - let mut misc = Misc::new(); - misc.set_foreground_window_elevated(foreground_window_elevated); - let mut msg = Message::new(); - msg.set_misc(misc); - conn.inner.send(msg.into()); - } - } - let show_elevation = !portable_service_running; - conn.send_to_cm(ipc::Data::DataPortableService(ipc::DataPortableService::CmShowElevation(show_elevation))); - - } - } + conn.portable_check(); } _ = test_delay_timer.tick() => { if last_recv_time.elapsed() >= SEC30 { @@ -512,6 +540,15 @@ impl Connection { conn.post_conn_audit(json!({ "action": "close", })); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + conn.reset_resolution(); + ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); + if let Some(s) = conn.server.upgrade() { + let mut s = s.write().unwrap(); + s.remove_connection(&conn.inner); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + try_stop_record_cursor_pos(); + } log::info!("#{} connection loop exited", id); } @@ -574,6 +611,8 @@ impl Connection { } } } + #[cfg(target_os = "linux")] + clear_remapped_keycode(); log::info!("Input thread exited"); } @@ -585,12 +624,13 @@ impl Connection { if let Some(mut forward) = self.port_forward_socket.take() { log::info!("Running port forwarding loop"); self.stream.set_raw(); + let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); loop { tokio::select! { Some(data) = rx_from_cm.recv() => { match data { ipc::Data::Close => { - bail!("Close requested from selfection manager"); + bail!("Close requested from selection manager"); } _ => {} } @@ -615,7 +655,12 @@ impl Connection { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } - Connection::post_heartbeat(self.server_audit_conn.clone(), self.inner.id).await?; + } + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&self.inner.id) { + // todo: check reconnect + bail!("Closed manually by the web console"); + } } } } @@ -657,7 +702,7 @@ impl Connection { self.send_login_error("Your ip is blocked by the peer") .await; Self::post_alarm_audit( - AlarmAuditType::IpWhiltelist, //"ip whiltelist", + AlarmAuditType::IpWhitelist, //"ip whitelist", true, json!({ "ip":addr.ip(), @@ -698,32 +743,13 @@ impl Connection { let url = self.server_audit_conn.clone(); let mut v = v; v["id"] = json!(Config::get_id()); - v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); + v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); v["conn_id"] = json!(self.inner.id); tokio::spawn(async move { allow_err!(Self::post_audit_async(url, v).await); }); } - async fn post_heartbeat(server_audit_conn: String, conn_id: i32) -> ResultType<()> { - if server_audit_conn.is_empty() { - return Ok(()); - } - let url = server_audit_conn.clone(); - let mut v = Value::default(); - v["id"] = json!(Config::get_id()); - v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); - v["conn_id"] = json!(conn_id); - if let Ok(rsp) = Self::post_audit_async(url, v).await { - if let Ok(rsp) = serde_json::from_str::(&rsp) { - if rsp.action == "disconnect" { - bail!("disconnect by server"); - } - } - } - return Ok(()); - } - fn post_file_audit( &self, r#type: FileAuditType, @@ -747,7 +773,7 @@ impl Connection { info["files"] = json!(files); let v = json!({ "id":json!(Config::get_id()), - "uuid":json!(base64::encode(hbb_common::get_uuid())), + "uuid":json!(crate::encode64(hbb_common::get_uuid())), "peer_id":json!(self.lr.my_id), "type": r#type as i8, "path":path, @@ -770,7 +796,7 @@ impl Connection { } let mut v = Value::default(); v["id"] = json!(Config::get_id()); - v["uuid"] = json!(base64::encode(hbb_common::get_uuid())); + v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); v["typ"] = json!(typ as i8); v["from_remote"] = json!(from_remote); v["info"] = serde_json::Value::String(info.to_string()); @@ -816,6 +842,18 @@ impl Connection { pi.hostname = DEVICE_NAME.lock().unwrap().clone(); pi.platform = "Android".into(); } + #[cfg(target_os = "linux")] + { + let mut platform_additions = serde_json::Map::new(); + if crate::platform::current_is_wayland() { + platform_additions.insert("is_wayland".into(), json!(true)); + } + if !platform_additions.is_empty() { + pi.platform_additions = + serde_json::to_string(&platform_additions).unwrap_or("".into()); + } + } + #[cfg(feature = "hwcodec")] { let (h264, h265) = scrap::codec::Encoder::supported_encoding(); @@ -869,6 +907,16 @@ impl Connection { ..Default::default() }) .into(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: video_service::get_current_display_name() + .map(|name| crate::platform::resolutions(&name)) + .unwrap_or(vec![]), + ..Default::default() + }) + .into(); + } let mut sub_service = false; if self.file_transfer.is_some() { @@ -884,16 +932,20 @@ impl Connection { res.set_error(format!("{}", err)); } Ok((current, displays)) => { - pi.displays = displays.into(); + pi.displays = displays.clone(); pi.current_display = current as _; res.set_peer_info(pi); sub_service = true; + *super::video_service::LAST_SYNC_DISPLAYS.write().unwrap() = displays; } } } let mut msg_out = Message::new(); msg_out.set_login_response(res); self.send(msg_out).await; + if let Some(o) = self.options_in_login.take() { + self.update_options(&o).await; + } if let Some((dir, show_hidden)) = self.file_transfer.clone() { let dir = if !dir.is_empty() && std::path::Path::new(&dir).is_dir() { &dir @@ -904,25 +956,30 @@ impl Connection { } else if sub_service { if let Some(s) = self.server.upgrade() { let mut noperms = Vec::new(); - if !self.keyboard && !self.show_remote_cursor { + if !self.peer_keyboard_enabled() && !self.show_remote_cursor { noperms.push(NAME_CURSOR); } if !self.show_remote_cursor { noperms.push(NAME_POS); } - if !self.clipboard_enabled() || !self.keyboard { + if !self.clipboard_enabled() || !self.peer_keyboard_enabled() { noperms.push(super::clipboard_service::NAME); } if !self.audio_enabled() { noperms.push(super::audio_service::NAME); } - s.write() - .unwrap() - .add_connection(self.inner.clone(), &noperms); + let mut s = s.write().unwrap(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + try_start_record_cursor_pos(); + s.add_connection(self.inner.clone(), &noperms); } } } + fn peer_keyboard_enabled(&self) -> bool { + self.keyboard && !self.disable_keyboard + } + fn clipboard_enabled(&self) -> bool { self.clipboard && !self.disable_clipboard } @@ -951,6 +1008,7 @@ impl Connection { file_transfer_enabled: self.file_transfer_enabled(), restart: self.restart, recording: self.recording, + from_switch: self.from_switch, }); } @@ -1030,18 +1088,21 @@ impl Connection { false } - fn is_of_recent_session(&mut self) -> bool { + fn is_recent_session(&mut self) -> bool { let session = SESSIONS .lock() .unwrap() .get(&self.lr.my_id) .map(|s| s.to_owned()); + SESSIONS + .lock() + .unwrap() + .retain(|_, s| s.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT); if let Some(session) = session { if session.name == self.lr.my_name && session.session_id == self.lr.session_id && !self.lr.password.is_empty() && self.validate_one_password(session.random_password.clone()) - && session.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT { SESSIONS.lock().unwrap().insert( self.lr.my_id.clone(), @@ -1072,29 +1133,33 @@ impl Connection { return Config::get_option(enable_prefix_option).is_empty(); } - async fn on_message(&mut self, msg: Message) -> bool { - if let Some(message::Union::LoginRequest(lr)) = msg.union { - self.lr = lr.clone(); - if let Some(o) = lr.option.as_ref() { - self.update_option(o).await; - if let Some(q) = o.video_codec_state.clone().take() { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::State(q), - ); - } else { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::DisableHwIfNotExist, - ); - } + async fn handle_login_request_without_validation(&mut self, lr: &LoginRequest) { + self.lr = lr.clone(); + if let Some(o) = lr.option.as_ref() { + self.options_in_login = Some(o.clone()); + if let Some(q) = o.video_codec_state.clone().take() { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::State(q), + ); } else { scrap::codec::Encoder::update_video_encoder( self.inner.id(), scrap::codec::EncoderUpdate::DisableHwIfNotExist, ); } - self.video_ack_required = lr.video_ack_required; + } else { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::DisableHwIfNotExist, + ); + } + self.video_ack_required = lr.video_ack_required; + } + + async fn on_message(&mut self, msg: Message) -> bool { + if let Some(message::Union::LoginRequest(lr)) = msg.union { + self.handle_login_request_without_validation(&lr).await; if self.authorized { return true; } @@ -1167,7 +1232,7 @@ impl Connection { { self.send_login_error("Connection not allowed").await; return false; - } else if self.is_of_recent_session() { + } else if self.is_recent_session() { self.try_start_cm(lr.my_id, lr.my_name, true); self.send_logon_response().await; if self.port_forward_socket.is_some() { @@ -1241,6 +1306,25 @@ impl Connection { .unwrap() .update_network_delay(new_delay); } + } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union { + #[cfg(feature = "flutter")] + if let Some(lr) = _s.lr.clone().take() { + self.handle_login_request_without_validation(&lr).await; + SWITCH_SIDES_UUID + .lock() + .unwrap() + .retain(|_, v| v.0.elapsed() < Duration::from_secs(10)); + let uuid_old = SWITCH_SIDES_UUID.lock().unwrap().remove(&lr.my_id); + if let Ok(uuid) = uuid::Uuid::from_slice(_s.uuid.to_vec().as_ref()) { + if let Some((_instant, uuid_old)) = uuid_old { + if uuid == uuid_old { + self.from_switch = true; + self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true); + self.send_logon_response().await; + } + } + } + } } else if self.authorized { match msg.union { Some(message::Union::MouseEvent(me)) => { @@ -1249,7 +1333,7 @@ impl Connection { log::debug!("call_main_service_mouse_input fail:{}", e); } #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.keyboard { + if self.peer_keyboard_enabled() { if is_left_up(&me) { CLICK_TIME.store(get_time(), Ordering::SeqCst); } else { @@ -1260,7 +1344,7 @@ impl Connection { } Some(message::Union::KeyEvent(me)) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.keyboard { + if self.peer_keyboard_enabled() { if is_enter(&me) { CLICK_TIME.store(get_time(), Ordering::SeqCst); } @@ -1298,7 +1382,7 @@ impl Connection { if self.file_transfer_enabled() { #[cfg(windows)] if let Some(clip) = msg_2_clip(_clip) { - self.send_to_cm(ipc::Data::ClipbaordFile(clip)) + self.send_to_cm(ipc::Data::ClipboardFile(clip)) } } } @@ -1456,7 +1540,7 @@ impl Connection { self.chat_unanswered = true; } Some(misc::Union::Option(o)) => { - self.update_option(&o).await; + self.update_options(&o).await; } Some(misc::Union::RefreshVideo(r)) => { if r { @@ -1485,15 +1569,160 @@ impl Connection { } } } + Some(misc::Union::ElevationRequest(r)) => match r.union { + Some(elevation_request::Union::Direct(_)) => { + #[cfg(windows)] + { + let mut err = "No need to elevate".to_string(); + if !crate::platform::is_installed() && !portable_client::running() { + err = portable_client::start_portable_service( + portable_client::StartPara::Direct, + ) + .err() + .map_or("".to_string(), |e| e.to_string()); + } + let mut misc = Misc::new(); + misc.set_elevation_response(err); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(msg).await; + } + } + Some(elevation_request::Union::Logon(_r)) => { + #[cfg(windows)] + { + let mut err = "No need to elevate".to_string(); + if !crate::platform::is_installed() && !portable_client::running() { + err = portable_client::start_portable_service( + portable_client::StartPara::Logon(_r.username, _r.password), + ) + .err() + .map_or("".to_string(), |e| e.to_string()); + } + let mut misc = Misc::new(); + misc.set_elevation_response(err); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(msg).await; + } + } + _ => {} + }, + Some(misc::Union::AudioFormat(format)) => { + if !self.disable_audio { + // Drop the audio sender previously. + drop(std::mem::replace(&mut self.audio_sender, None)); + self.audio_sender = Some(start_audio_thread()); + allow_err!(self + .audio_sender + .as_ref() + .unwrap() + .send(MediaData::AudioFormat(format))); + } + } + #[cfg(feature = "flutter")] + Some(misc::Union::SwitchSidesRequest(s)) => { + if let Ok(uuid) = uuid::Uuid::from_slice(&s.uuid.to_vec()[..]) { + crate::run_me(vec![ + "--connect", + &self.lr.my_id, + "--switch_uuid", + uuid.to_string().as_ref(), + ]) + .ok(); + self.on_close("switch sides", false).await; + return false; + } + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + Some(misc::Union::ChangeResolution(r)) => { + if self.keyboard { + if let Ok(name) = video_service::get_current_display_name() { + if let Ok(current) = crate::platform::current_resolution(&name) { + if let Err(e) = crate::platform::change_resolution( + &name, + r.width as _, + r.height as _, + ) { + log::error!("change resolution failed:{:?}", e); + } else { + if !self.origin_resolution.contains_key(&name) { + self.origin_resolution.insert(name, current); + } + } + } + } + } + } _ => {} }, + Some(message::Union::AudioFrame(frame)) => { + if !self.disable_audio { + if let Some(sender) = &self.audio_sender { + allow_err!(sender.send(MediaData::AudioFrame(frame))); + } else { + log::warn!( + "Processing audio frame without the voice call audio sender." + ); + } + } + } + Some(message::Union::VoiceCallRequest(request)) => { + if request.is_connect { + self.voice_call_request_timestamp = Some( + NonZeroI64::new(request.req_timestamp) + .unwrap_or(NonZeroI64::new(get_time()).unwrap()), + ); + // Notify the connection manager. + self.send_to_cm(Data::VoiceCallIncoming); + } else { + self.close_voice_call().await; + } + } + Some(message::Union::VoiceCallResponse(_response)) => { + // TODO: Maybe we can do a voice call from cm directly. + } _ => {} } } true } - async fn update_option(&mut self, o: &OptionMessage) { + pub async fn handle_voice_call(&mut self, accepted: bool) { + if let Some(ts) = self.voice_call_request_timestamp.take() { + let msg = new_voice_call_response(ts.get(), accepted); + if accepted { + // Backup the default input device. + let audio_input_device = Config::get_option("audio-input"); + log::debug!("Backup the sound input device {}", audio_input_device); + self.audio_input_device_before_voice_call = Some(audio_input_device); + // Switch to default input device + let default_sound_device = get_default_sound_input(); + if let Some(device) = default_sound_device { + set_sound_input(device); + } + self.send_to_cm(Data::StartVoiceCall); + } else { + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); + } + self.send(msg).await; + } else { + log::warn!("Possible a voice call attack."); + } + } + + pub async fn close_voice_call(&mut self) { + // Restore to the prior audio device. + if let Some(sound_input) = + std::mem::replace(&mut self.audio_input_device_before_voice_call, None) + { + set_sound_input(sound_input); + } + // Notify the connection manager that the voice call has been closed. + self.send_to_cm(Data::CloseVoiceCall("".to_owned())); + } + + async fn update_options(&mut self, o: &OptionMessage) { log::info!("Option update: {:?}", o); if let Ok(q) = o.image_quality.enum_value() { let image_quality; @@ -1519,12 +1748,18 @@ impl Connection { .unwrap() .update_user_fps(o.custom_fps as _); } - + if let Some(q) = o.video_codec_state.clone().take() { + scrap::codec::Encoder::update_video_encoder( + self.inner.id(), + scrap::codec::EncoderUpdate::State(q), + ); + } if let Ok(q) = o.lock_after_session_end.enum_value() { if q != BoolOption::NotSet { self.lock_after_session_end = q == BoolOption::Yes; } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] if let Ok(q) = o.show_remote_cursor.enum_value() { if q != BoolOption::NotSet { self.show_remote_cursor = q == BoolOption::Yes; @@ -1532,7 +1767,7 @@ impl Connection { s.write().unwrap().subscribe( NAME_CURSOR, self.inner.clone(), - self.keyboard || self.show_remote_cursor, + self.peer_keyboard_enabled() || self.show_remote_cursor, ); s.write().unwrap().subscribe( NAME_POS, @@ -1570,7 +1805,24 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, self.inner.clone(), - self.clipboard_enabled() && self.keyboard, + self.clipboard_enabled() && self.peer_keyboard_enabled(), + ); + } + } + } + if let Ok(q) = o.disable_keyboard.enum_value() { + if q != BoolOption::NotSet { + self.disable_keyboard = q == BoolOption::Yes; + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::NAME, + self.inner.clone(), + self.clipboard_enabled() && self.peer_keyboard_enabled(), + ); + s.write().unwrap().subscribe( + NAME_CURSOR, + self.inner.clone(), + self.peer_keyboard_enabled() || self.show_remote_cursor, ); } } @@ -1646,25 +1898,16 @@ impl Connection { } } } - if let Some(q) = o.video_codec_state.clone().take() { - scrap::codec::Encoder::update_video_encoder( - self.inner.id(), - scrap::codec::EncoderUpdate::State(q), - ); - } } async fn on_close(&mut self, reason: &str, lock: bool) { - if let Some(s) = self.server.upgrade() { - s.write().unwrap().remove_connection(&self.inner); - } log::info!("#{} Connection closed: {}", self.inner.id(), reason); if lock && self.lock_after_session_end && self.keyboard { #[cfg(not(any(target_os = "android", target_os = "ios")))] lock_screen().await; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - let data = if self.chat_unanswered && !self.close_manually { + let data = if self.chat_unanswered { ipc::Data::Disconnected } else { ipc::Data::Close @@ -1675,15 +1918,17 @@ impl Connection { self.port_forward_socket.take(); } - async fn on_close_manually(&mut self, close_from: &str, close_by: &str) { - self.close_manually = true; + // The `reason` should be consistent with `check_if_retry` if not empty + async fn send_close_reason_no_retry(&mut self, reason: &str) { let mut misc = Misc::new(); - misc.set_close_reason(format!("Closed manually by the {}", close_by)); + if reason.is_empty() { + misc.set_close_reason("Closed manually by the peer".to_string()); + } else { + misc.set_close_reason(reason.to_string()); + } let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; - self.on_close(&format!("Close requested from {}", close_from), false) - .await; SESSIONS.lock().unwrap().remove(&self.lr.my_id); } @@ -1699,6 +1944,82 @@ impl Connection { async fn send(&mut self, msg: Message) { allow_err!(self.stream.send(&msg).await); } + + pub fn alive_conns() -> Vec { + ALIVE_CONNS.lock().unwrap().clone() + } + + #[cfg(windows)] + fn portable_check(&mut self) { + if self.portable.is_installed + || self.file_transfer.is_some() + || self.port_forward_socket.is_some() + { + return; + } + let running = portable_client::running(); + let show_elevation = !running; + self.send_to_cm(ipc::Data::DataPortableService( + ipc::DataPortableService::CmShowElevation(show_elevation), + )); + if self.authorized { + let p = &mut self.portable; + if running != p.last_running { + p.last_running = running; + let mut misc = Misc::new(); + misc.set_portable_service_running(running); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); + if p.last_uac != uac { + p.last_uac = uac; + if !uac || !running { + let mut misc = Misc::new(); + misc.set_uac(uac); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + let foreground_window_elevated = crate::video_service::IS_FOREGROUND_WINDOW_ELEVATED + .lock() + .unwrap() + .clone(); + if p.last_foreground_window_elevated != foreground_window_elevated { + p.last_foreground_window_elevated = foreground_window_elevated; + if !foreground_window_elevated || !running { + let mut misc = Misc::new(); + misc.set_foreground_window_elevated(foreground_window_elevated); + let mut msg = Message::new(); + msg.set_misc(misc); + self.inner.send(msg.into()); + } + } + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn reset_resolution(&self) { + self.origin_resolution + .iter() + .map(|(name, r)| { + if let Err(e) = + crate::platform::change_resolution(&name, r.width as _, r.height as _) + { + log::error!("change resolution failed:{:?}", e); + } + }) + .count(); + } +} + +pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { + SWITCH_SIDES_UUID + .lock() + .unwrap() + .insert(id, (tokio::time::Instant::now(), uuid)); } #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1726,10 +2047,12 @@ async fn start_ipc( for _ in 0..10 { #[cfg(not(target_os = "linux"))] { + log::debug!("Start cm"); res = crate::platform::run_as_user(args.clone()); } #[cfg(target_os = "linux")] { + log::debug!("Start cm"); res = crate::platform::run_as_user(args.clone(), None); } if res.is_ok() { @@ -1745,12 +2068,13 @@ async fn start_ipc( run_done = false; } if !run_done { + log::debug!("Start cm"); super::CHILD_PROCESS .lock() .unwrap() .push(crate::run_me(args)?); } - for _ in 0..10 { + for _ in 0..20 { sleep(0.3).await; if let Ok(s) = crate::ipc::connect(1000, "_cm").await { stream = Some(s); @@ -1822,7 +2146,7 @@ mod privacy_mode { pub(super) fn turn_off_privacy(_conn_id: i32) -> Message { #[cfg(windows)] { - use crate::ui::win_privacy::*; + use crate::win_privacy::*; let res = turn_off_privacy(_conn_id, None); match res { @@ -1846,8 +2170,8 @@ mod privacy_mode { pub(super) fn turn_on_privacy(_conn_id: i32) -> ResultType { #[cfg(windows)] { - let plugin_exitst = crate::ui::win_privacy::turn_on_privacy(_conn_id)?; - Ok(plugin_exitst) + let plugin_exist = crate::win_privacy::turn_on_privacy(_conn_id)?; + Ok(plugin_exist) } #[cfg(not(windows))] { @@ -1856,15 +2180,8 @@ mod privacy_mode { } } -#[derive(Debug, Deserialize)] -struct ConnAuditResponse { - #[allow(dead_code)] - ret: bool, - action: String, -} - pub enum AlarmAuditType { - IpWhiltelist = 0, + IpWhitelist = 0, ManyWrongPassword = 1, FrequentAttempt = 2, } @@ -1873,3 +2190,23 @@ pub enum FileAuditType { RemoteSend = 0, RemoteReceive = 1, } + +#[cfg(windows)] +pub struct PortableState { + pub last_uac: bool, + pub last_foreground_window_elevated: bool, + pub last_running: bool, + pub is_installed: bool, +} + +#[cfg(windows)] +impl Default for PortableState { + fn default() -> Self { + Self { + is_installed: crate::platform::is_installed(), + last_uac: Default::default(), + last_foreground_window_elevated: Default::default(), + last_running: Default::default(), + } + } +} diff --git a/src/server/dbus.rs b/src/server/dbus.rs index 5a38fe7cb..081db3e8f 100644 --- a/src/server/dbus.rs +++ b/src/server/dbus.rs @@ -5,10 +5,10 @@ /// [Flutter]: handle uni links for linux use dbus::blocking::Connection; use dbus_crossroads::{Crossroads, IfaceBuilder}; -use hbb_common::{log}; -use std::{error::Error, fmt, time::Duration}; +use hbb_common::log; #[cfg(feature = "flutter")] use std::collections::HashMap; +use std::{error::Error, fmt, time::Duration}; const DBUS_NAME: &str = "org.rustdesk.rustdesk"; const DBUS_PREFIX: &str = "/dbus"; @@ -30,15 +30,16 @@ impl fmt::Display for DbusError { impl Error for DbusError {} /// invoke new connection from dbus -/// +/// /// [Tips]: /// How to test by CLI: /// - use dbus-send command: /// `dbus-send --session --print-reply --dest=org.rustdesk.rustdesk /dbus org.rustdesk.rustdesk.NewConnection string:'PEER_ID'` -pub fn invoke_new_connection(peer_id: String) -> Result<(), Box> { +pub fn invoke_new_connection(uni_links: String) -> Result<(), Box> { let conn = Connection::new_session()?; let proxy = conn.with_proxy(DBUS_NAME, DBUS_PREFIX, DBUS_TIMEOUT); - let (ret,): (String,) = proxy.method_call(DBUS_NAME, DBUS_METHOD_NEW_CONNECTION, (peer_id,))?; + let (ret,): (String,) = + proxy.method_call(DBUS_NAME, DBUS_METHOD_NEW_CONNECTION, (uni_links,))?; if ret != DBUS_METHOD_RETURN_SUCCESS { log::error!("error on call new connection to dbus server"); return Err(Box::new(DbusError("not success".to_string()))); @@ -67,7 +68,7 @@ fn handle_client_message(builder: &mut IfaceBuilder<()>) { DBUS_METHOD_NEW_CONNECTION, (DBUS_METHOD_NEW_CONNECTION_ID,), (DBUS_METHOD_RETURN,), - move |_, _, (_peer_id,): (String,)| { + move |_, _, (_uni_links,): (String,)| { #[cfg(feature = "flutter")] { use crate::flutter::{self, APP_TYPE_MAIN}; @@ -79,7 +80,7 @@ fn handle_client_message(builder: &mut IfaceBuilder<()>) { { let data = HashMap::from([ ("name", "new_connection"), - ("peer_id", _peer_id.as_str()) + ("uni_links", _uni_links.as_str()), ]); if !stream.add(serde_json::ser::to_string(&data).unwrap_or("".to_string())) { log::error!("failed to add dbus message to flutter global dbus stream."); diff --git a/src/server/input_service.rs b/src/server/input_service.rs index bd2ad9a16..bb9ba167c 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -13,7 +13,8 @@ use std::{ convert::TryFrom, ops::Sub, sync::atomic::{AtomicBool, Ordering}, - time::Instant, + thread, + time::{self, Instant}, }; const INVALID_CURSOR_POS: i32 = i32::MIN; @@ -71,7 +72,6 @@ struct Input { y: i32, } -const KEY_RDEV_START: u64 = 999; const KEY_CHAR_START: u64 = 9999; #[derive(Clone, Default)] @@ -129,6 +129,7 @@ pub fn new_pos() -> GenericService { sp } +#[inline] fn update_last_cursor_pos(x: i32, y: i32) { let mut lock = LATEST_SYS_CURSOR_POS.lock().unwrap(); if lock.1 .0 != x || lock.1 .1 != y { @@ -137,29 +138,31 @@ fn update_last_cursor_pos(x: i32, y: i32) { } fn run_pos(sp: GenericService, state: &mut StatePos) -> ResultType<()> { - if let Some((x, y)) = crate::get_cursor_pos() { - update_last_cursor_pos(x, y); - if state.is_moved(x, y) { - let mut msg_out = Message::new(); - msg_out.set_cursor_position(CursorPosition { - x, - y, - ..Default::default() - }); - let exclude = { - let now = get_time(); - let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap(); - if now - lock.time < 300 { - lock.conn - } else { - 0 - } - }; - sp.send_without(msg_out, exclude); - } - state.cursor_pos = (x, y); + let (_, (x, y)) = *LATEST_SYS_CURSOR_POS.lock().unwrap(); + if x == INVALID_CURSOR_POS || y == INVALID_CURSOR_POS { + return Ok(()); } + if state.is_moved(x, y) { + let mut msg_out = Message::new(); + msg_out.set_cursor_position(CursorPosition { + x, + y, + ..Default::default() + }); + let exclude = { + let now = get_time(); + let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap(); + if now - lock.time < 300 { + lock.conn + } else { + 0 + } + }; + sp.send_without(msg_out, exclude); + } + state.cursor_pos = (x, y); + sp.snapshot(|sps| { let mut msg_out = Message::new(); msg_out.set_cursor_position(CursorPosition { @@ -202,13 +205,19 @@ fn run_cursor(sp: MouseCursorService, state: &mut StateCursor) -> ResultType<()> Ok(()) } +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +enum KeysDown { + RdevKey(RawKey), + EnigoKey(u64), +} + lazy_static::lazy_static! { static ref ENIGO: Arc> = { Arc::new(Mutex::new(Enigo::new())) }; - static ref KEYS_DOWN: Arc>> = Default::default(); + static ref KEYS_DOWN: Arc>> = Default::default(); static ref LATEST_PEER_INPUT_CURSOR: Arc> = Default::default(); - static ref LATEST_SYS_CURSOR_POS: Arc> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (0, 0)))); + static ref LATEST_SYS_CURSOR_POS: Arc> = Arc::new(Mutex::new((Instant::now().sub(MOUSE_MOVE_PROTECTION_TIMEOUT), (INVALID_CURSOR_POS, INVALID_CURSOR_POS)))); } static EXITING: AtomicBool = AtomicBool::new(false); @@ -216,6 +225,42 @@ const MOUSE_MOVE_PROTECTION_TIMEOUT: Duration = Duration::from_millis(1_000); // Actual diff of (x,y) is (1,1) here. But 5 may be tolerant. const MOUSE_ACTIVE_DISTANCE: i32 = 5; +static RECORD_CURSOR_POS_RUNNING: AtomicBool = AtomicBool::new(false); + +pub fn try_start_record_cursor_pos() { + if RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) { + return; + } + + RECORD_CURSOR_POS_RUNNING.store(true, Ordering::SeqCst); + thread::spawn(|| { + let interval = time::Duration::from_millis(33); + loop { + if !RECORD_CURSOR_POS_RUNNING.load(Ordering::SeqCst) { + break; + } + + let now = time::Instant::now(); + if let Some((x, y)) = crate::get_cursor_pos() { + update_last_cursor_pos(x, y); + } + let elapsed = now.elapsed(); + if elapsed < interval { + thread::sleep(interval - elapsed); + } + } + update_last_cursor_pos(INVALID_CURSOR_POS, INVALID_CURSOR_POS); + }); +} + +pub fn try_stop_record_cursor_pos() { + let count_lock = CONN_COUNT.lock().unwrap(); + if *count_lock > 0 { + return; + } + RECORD_CURSOR_POS_RUNNING.store(false, Ordering::SeqCst); +} + // mac key input must be run in main thread, otherwise crash on >= osx 10.15 #[cfg(target_os = "macos")] lazy_static::lazy_static! { @@ -368,6 +413,11 @@ pub fn fix_key_down_timeout_at_exit() { log::info!("fix_key_down_timeout_at_exit"); } +#[cfg(target_os = "linux")] +pub fn clear_remapped_keycode() { + ENIGO.lock().unwrap().tfc_clear_remapped(); +} + #[inline] fn record_key_is_control_key(record_key: u64) -> bool { record_key < KEY_CHAR_START @@ -375,12 +425,7 @@ fn record_key_is_control_key(record_key: u64) -> bool { #[inline] fn record_key_is_chr(record_key: u64) -> bool { - KEY_RDEV_START <= record_key && record_key < KEY_CHAR_START -} - -#[inline] -fn record_key_is_rdev_layout(record_key: u64) -> bool { - KEY_CHAR_START <= record_key + record_key < KEY_CHAR_START } #[inline] @@ -396,15 +441,16 @@ fn record_key_to_key(record_key: u64) -> Option { } #[inline] -fn release_record_key(record_key: u64) { - let func = move || { - if record_key_is_rdev_layout(record_key) { - simulate_(&EventType::KeyRelease(RdevKey::Unknown( - (record_key - KEY_RDEV_START) as _, - ))); - } else if let Some(key) = record_key_to_key(record_key) { - ENIGO.lock().unwrap().key_up(key); - log::debug!("Fixed {:?} timeout", key); +fn release_record_key(record_key: KeysDown) { + let func = move || match record_key { + KeysDown::RdevKey(raw_key) => { + simulate_(&EventType::KeyRelease(RdevKey::RawKey(raw_key))); + } + KeysDown::EnigoKey(key) => { + if let Some(key) = record_key_to_key(key) { + ENIGO.lock().unwrap().key_up(key); + log::debug!("Fixed {:?} timeout", key); + } } }; @@ -487,7 +533,7 @@ fn active_mouse_(conn: i32) -> bool { return false; } - let in_actived_dist = |a: i32, b: i32| -> bool { (a - b).abs() < MOUSE_ACTIVE_DISTANCE }; + let in_active_dist = |a: i32, b: i32| -> bool { (a - b).abs() < MOUSE_ACTIVE_DISTANCE }; // Check if input is in valid range match crate::get_cursor_pos() { @@ -496,7 +542,7 @@ fn active_mouse_(conn: i32) -> bool { let lock = LATEST_PEER_INPUT_CURSOR.lock().unwrap(); (lock.x, lock.y) }; - let mut can_active = in_actived_dist(last_in_x, x) && in_actived_dist(last_in_y, y); + let mut can_active = in_active_dist(last_in_x, x) && in_active_dist(last_in_y, y); // The cursor may not have been moved to last input position if system is busy now. // While this is not a common case, we check it again after some time later. if !can_active { @@ -505,7 +551,7 @@ fn active_mouse_(conn: i32) -> bool { std::thread::sleep(std::time::Duration::from_micros(10)); // Sleep here can also somehow suppress delay accumulation. if let Some((x2, y2)) = crate::get_cursor_pos() { - can_active = in_actived_dist(last_in_x, x2) && in_actived_dist(last_in_y, y2); + can_active = in_active_dist(last_in_x, x2) && in_active_dist(last_in_y, y2); } } if !can_active { @@ -556,27 +602,39 @@ pub fn handle_mouse_(evt: &MouseEvent) { en.mouse_move_to(evt.x, evt.y); } 1 => match buttons { - 1 => { + 0x01 => { allow_err!(en.mouse_down(MouseButton::Left)); } - 2 => { + 0x02 => { allow_err!(en.mouse_down(MouseButton::Right)); } - 4 => { + 0x04 => { allow_err!(en.mouse_down(MouseButton::Middle)); } + 0x08 => { + allow_err!(en.mouse_down(MouseButton::Back)); + } + 0x10 => { + allow_err!(en.mouse_down(MouseButton::Forward)); + } _ => {} }, 2 => match buttons { - 1 => { + 0x01 => { en.mouse_up(MouseButton::Left); } - 2 => { + 0x02 => { en.mouse_up(MouseButton::Right); } - 4 => { + 0x04 => { en.mouse_up(MouseButton::Middle); } + 0x08 => { + en.mouse_up(MouseButton::Back); + } + 0x10 => { + en.mouse_up(MouseButton::Forward); + } _ => {} }, 3 | 4 => { @@ -704,11 +762,8 @@ pub fn handle_key(evt: &KeyEvent) { fn reset_input() { unsafe { let _lock = VIRTUAL_INPUT_MTX.lock(); - VIRTUAL_INPUT = VirtualInput::new( - CGEventSourceStateID::Private, - CGEventTapLocation::AnnotatedSession, - ) - .ok(); + VIRTUAL_INPUT = + VirtualInput::new(CGEventSourceStateID::Private, CGEventTapLocation::Session).ok(); } } @@ -721,7 +776,7 @@ pub fn reset_input_ondisconn() { } } -fn sim_rdev_rawkey(code: u32, keydown: bool) { +fn sim_rdev_rawkey_position(code: u32, keydown: bool) { #[cfg(target_os = "windows")] let rawkey = RawKey::ScanCode(code); #[cfg(target_os = "linux")] @@ -732,6 +787,21 @@ fn sim_rdev_rawkey(code: u32, keydown: bool) { #[cfg(target_os = "macos")] let rawkey = RawKey::MacVirtualKeycode(code); + // map mode(1): Send keycode according to the peer platform. + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); + + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); +} + +#[cfg(target_os = "windows")] +fn sim_rdev_rawkey_virtual(code: u32, keydown: bool) { + let rawkey = RawKey::WinVirtualKeycode(code); + record_pressed_key(KeysDown::RdevKey(rawkey), keydown); let event_type = if keydown { EventType::KeyPress(RdevKey::RawKey(rawkey)) } else { @@ -862,15 +932,12 @@ fn sync_numlock_capslock_status(key_event: &KeyEvent) { } fn map_keyboard_mode(evt: &KeyEvent) { - // map mode(1): Send keycode according to the peer platform. - record_pressed_key(evt.chr() as u64 + KEY_CHAR_START, evt.down); - #[cfg(windows)] crate::platform::windows::try_change_desktop(); // Wayland #[cfg(target_os = "linux")] - if !*IS_X11.lock().unwrap() { + if !*IS_X11 { let mut en = ENIGO.lock().unwrap(); let code = evt.chr() as u16; @@ -882,7 +949,7 @@ fn map_keyboard_mode(evt: &KeyEvent) { return; } - sim_rdev_rawkey(evt.chr(), evt.down); + sim_rdev_rawkey_position(evt.chr(), evt.down); } #[cfg(target_os = "macos")] @@ -912,10 +979,11 @@ fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { #[cfg(target_os = "linux")] fn is_altgr_pressed() -> bool { + let altgr_rawkey = RawKey::LinuxXorgKeycode(ControlKey::RAlt.value() as _); KEYS_DOWN .lock() .unwrap() - .get(&(ControlKey::RAlt.value() as _)) + .get(&KeysDown::RdevKey(altgr_rawkey)) .is_some() } @@ -999,7 +1067,7 @@ fn release_keys(en: &mut Enigo, to_release: &Vec) { } } -fn record_pressed_key(record_key: u64, down: bool) { +fn record_pressed_key(record_key: KeysDown, down: bool) { let mut key_down = KEYS_DOWN.lock().unwrap(); if down { key_down.insert(record_key, Instant::now()); @@ -1038,12 +1106,12 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { return; } let record_key = ck.value() as u64; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_control_key(&mut en, &ck, down) } Some(key_event::Union::Chr(chr)) => { let record_key = chr as u64 + KEY_CHAR_START; - record_pressed_key(record_key, down); + record_pressed_key(KeysDown::EnigoKey(record_key), down); process_chr(&mut en, chr, down) } Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), @@ -1055,6 +1123,52 @@ fn legacy_keyboard_mode(evt: &KeyEvent) { release_keys(&mut en, &to_release); } +#[cfg(target_os = "windows")] +fn translate_process_code(code: u32, down: bool) { + crate::platform::windows::try_change_desktop(); + match code >> 16 { + 0 => sim_rdev_rawkey_position(code, down), + vk_code => sim_rdev_rawkey_virtual(vk_code, down), + }; +} + +fn translate_keyboard_mode(evt: &KeyEvent) { + match &evt.union { + Some(key_event::Union::Seq(seq)) => { + // Fr -> US + // client: Shift + & => 1(send to remote) + // remote: Shift + 1 => ! + // + // Try to release shift first. + // remote: Shift + 1 => 1 + let mut en = ENIGO.lock().unwrap(); + + #[cfg(target_os = "linux")] + { + simulate_(&EventType::KeyRelease(RdevKey::ShiftLeft)); + simulate_(&EventType::KeyRelease(RdevKey::ShiftRight)); + for chr in seq.chars() { + en.key_click(Key::Layout(chr)); + } + } + #[cfg(not(target_os = "linux"))] + en.key_sequence(seq); + } + Some(key_event::Union::Chr(..)) => { + #[cfg(target_os = "windows")] + translate_process_code(evt.chr(), evt.down); + #[cfg(not(target_os = "windows"))] + sim_rdev_rawkey_position(evt.chr(), evt.down); + } + Some(key_event::Union::Unicode(..)) => { + // Do not handle unicode for now. + } + _ => { + log::debug!("Unreachable. Unexpected key event {:?}", &evt); + } + } +} + pub fn handle_key_(evt: &KeyEvent) { if EXITING.load(Ordering::SeqCst) { return; @@ -1068,7 +1182,7 @@ pub fn handle_key_(evt: &KeyEvent) { map_keyboard_mode(evt); } KeyboardMode::Translate => { - legacy_keyboard_mode(evt); + translate_keyboard_mode(evt); } _ => { legacy_keyboard_mode(evt); diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index 6d2e92ae3..d9acc4152 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -2,9 +2,7 @@ use core::slice; use hbb_common::{ allow_err, anyhow::anyhow, - bail, - config::Config, - log, + bail, libc, log, message_proto::{KeyEvent, MouseEvent}, protobuf::Message, tokio::{self, sync::mpsc}, @@ -15,6 +13,7 @@ use shared_memory::*; use std::{ mem::size_of, ops::{Deref, DerefMut}, + path::PathBuf, sync::{Arc, Mutex}, time::Duration, }; @@ -25,6 +24,7 @@ use winapi::{ use crate::{ ipc::{self, new_listener, Connection, Data, DataPortableService}, + platform::set_path_permission, video_service::get_current_display, }; @@ -72,7 +72,7 @@ impl DerefMut for SharedMemory { impl SharedMemory { pub fn create(name: &str, size: usize) -> ResultType { - let flink = Self::flink(name.to_string()); + let flink = Self::flink(name.to_string())?; let shmem = match ShmemConf::new() .size(size) .flink(&flink) @@ -91,12 +91,12 @@ impl SharedMemory { } }; log::info!("Create shared memory, size:{}, flink:{}", size, flink); - Self::set_all_perm(&flink); + set_path_permission(&PathBuf::from(flink), "F").ok(); Ok(SharedMemory { inner: shmem }) } pub fn open_existing(name: &str) -> ResultType { - let flink = Self::flink(name.to_string()); + let flink = Self::flink(name.to_string())?; let shmem = match ShmemConf::new().flink(&flink).allow_raw(true).open() { Ok(m) => m, Err(e) => { @@ -116,30 +116,17 @@ impl SharedMemory { } } - fn flink(name: String) -> String { - let mut shmem_flink = format!("shared_memory{}", name); - if cfg!(windows) { - let df = "C:\\ProgramData"; - let df = if std::path::Path::new(df).exists() { - df.to_owned() - } else { - std::env::var("TEMP").unwrap_or("C:\\Windows\\TEMP".to_owned()) - }; - let df = format!("{}\\{}", df, *hbb_common::config::APP_NAME.read().unwrap()); - std::fs::create_dir(&df).ok(); - shmem_flink = format!("{}\\{}", df, shmem_flink); - } else { - shmem_flink = Config::ipc_path("").replace("ipc", "") + &shmem_flink; - } - return shmem_flink; - } - - fn set_all_perm(_p: &str) { - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(_p, std::fs::Permissions::from_mode(0o0777)).ok(); + fn flink(name: String) -> ResultType { + let mut dir = crate::platform::user_accessible_folder()?; + dir = dir.join(hbb_common::config::APP_NAME.read().unwrap().clone()); + if !dir.exists() { + std::fs::create_dir(&dir)?; + set_path_permission(&dir, "F").ok(); } + Ok(dir + .join(format!("shared_memory{}", name)) + .to_string_lossy() + .to_string()) } } @@ -203,7 +190,7 @@ mod utils { } } -// functions called in seperate SYSTEM user process. +// functions called in separate SYSTEM user process. pub mod server { use super::*; @@ -407,7 +394,7 @@ pub mod server { } ConnCount(Some(n)) => { if n == 0 { - log::info!("Connnection count equals 0, exit"); + log::info!("Connection count equals 0, exit"); stream.send(&Data::DataPortableService(WillClose)).await.ok(); break; } @@ -455,14 +442,20 @@ pub mod client { use super::*; lazy_static::lazy_static! { - pub static ref PORTABLE_SERVICE_RUNNING: Arc> = Default::default(); + static ref RUNNING: Arc> = Default::default(); static ref SHMEM: Arc>> = Default::default(); static ref SENDER : Mutex> = Mutex::new(client::start_ipc_server()); + static ref QUICK_SUPPORT: Arc> = Default::default(); } - pub(crate) fn start_portable_service() -> ResultType<()> { + pub enum StartPara { + Direct, + Logon(String, String), + } + + pub(crate) fn start_portable_service(para: StartPara) -> ResultType<()> { log::info!("start portable service"); - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { bail!("already running"); } if SHMEM.lock().unwrap().is_none() { @@ -491,14 +484,60 @@ pub mod client { unsafe { libc::memset(shmem.as_ptr() as _, 0, shmem.len() as _); } - if crate::platform::run_background( - &std::env::current_exe()?.to_string_lossy().to_string(), - "--portable-service", - ) - .is_err() - { - *SHMEM.lock().unwrap() = None; - bail!("Failed to run portable service process"); + drop(option); + match para { + StartPara::Direct => { + if let Err(e) = crate::platform::run_background( + &std::env::current_exe()?.to_string_lossy().to_string(), + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process: {}", e); + } + } + StartPara::Logon(username, password) => { + #[allow(unused_mut)] + let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); + #[cfg(feature = "flutter")] + { + if let Some(dir) = PathBuf::from(&exe).parent() { + if set_path_permission(&PathBuf::from(dir), "RX").is_err() { + *SHMEM.lock().unwrap() = None; + bail!("Failed to set permission of {:?}", dir); + } + } + } + #[cfg(not(feature = "flutter"))] + match hbb_common::directories_next::UserDirs::new() { + Some(user_dir) => { + let dir = user_dir + .home_dir() + .join("AppData") + .join("Local") + .join("rustdesk-sciter"); + if std::fs::create_dir_all(&dir).is_ok() { + let dst = dir.join("rustdesk.exe"); + if std::fs::copy(&exe, &dst).is_ok() { + if dst.exists() { + if set_path_permission(&dir, "RX").is_ok() { + exe = dst.to_string_lossy().to_string(); + } + } + } + } + } + None => {} + } + if let Err(e) = crate::platform::windows::create_process_with_logon( + username.as_str(), + password.as_str(), + &exe, + "--portable-service", + ) { + *SHMEM.lock().unwrap() = None; + bail!("Failed to run portable service process: {}", e); + } + } } let _sender = SENDER.lock().unwrap(); Ok(()) @@ -509,6 +548,10 @@ pub mod client { *SHMEM.lock().unwrap() = None; } + pub fn set_quick_support(v: bool) { + *QUICK_SUPPORT.lock().unwrap() = v; + } + pub struct CapturerPortable; impl CapturerPortable { @@ -623,17 +666,7 @@ pub mod client { use DataPortableService::*; let rx = Arc::new(tokio::sync::Mutex::new(rx)); let postfix = IPC_SUFFIX; - #[cfg(feature = "flutter")] - let quick_support = { - let args: Vec<_> = std::env::args().collect(); - args.contains(&"--quick_support".to_string()) - }; - #[cfg(not(feature = "flutter"))] - let quick_support = std::env::current_exe() - .unwrap_or("".into()) - .to_string_lossy() - .to_lowercase() - .ends_with("qs.exe"); + let quick_support = QUICK_SUPPORT.lock().unwrap().clone(); match new_listener(postfix).await { Ok(mut incoming) => loop { @@ -668,7 +701,7 @@ pub mod client { } Pong => { nack = 0; - *PORTABLE_SERVICE_RUNNING.lock().unwrap() = true; + *RUNNING.lock().unwrap() = true; }, ConnCount(None) => { if !quick_support { @@ -699,7 +732,7 @@ pub mod client { } } } - *PORTABLE_SERVICE_RUNNING.lock().unwrap() = false; + *RUNNING.lock().unwrap() = false; }); } Err(err) => { @@ -752,11 +785,11 @@ pub mod client { use_yuv: bool, portable_service_running: bool, ) -> ResultType> { - if portable_service_running != PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if portable_service_running != RUNNING.lock().unwrap().clone() { log::info!("portable service status mismatch"); } if portable_service_running { - log::info!("Create shared memeory capturer"); + log::info!("Create shared memory capturer"); return Ok(Box::new(CapturerPortable::new(current_display, use_yuv))); } else { log::debug!("Create capturer dxgi|gdi"); @@ -767,7 +800,7 @@ pub mod client { } pub fn get_cursor_info(pci: PCURSORINFO) -> BOOL { - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { get_cursor_info_(&mut SHMEM.lock().unwrap().as_mut().unwrap(), pci) } else { unsafe { winuser::GetCursorInfo(pci) } @@ -775,7 +808,7 @@ pub mod client { } pub fn handle_mouse(evt: &MouseEvent) { - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { handle_mouse_(evt).ok(); } else { crate::input_service::handle_mouse_(evt); @@ -783,12 +816,16 @@ pub mod client { } pub fn handle_key(evt: &KeyEvent) { - if PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if RUNNING.lock().unwrap().clone() { handle_key_(evt).ok(); } else { crate::input_service::handle_key_(evt); } } + + pub fn running() -> bool { + RUNNING.lock().unwrap().clone() + } } #[repr(C)] diff --git a/src/server/video_service.rs b/src/server/video_service.rs index b986c785c..3b9db0697 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -20,14 +20,10 @@ use super::{video_qos::VideoQoS, *}; #[cfg(windows)] -use crate::portable_service::client::PORTABLE_SERVICE_RUNNING; -#[cfg(windows)] use hbb_common::get_version_number; -use hbb_common::{ - tokio::sync::{ - mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - Mutex as TokioMutex, - }, +use hbb_common::tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + Mutex as TokioMutex, }; #[cfg(not(windows))] use scrap::Capturer; @@ -69,6 +65,7 @@ lazy_static::lazy_static! { pub static ref VIDEO_QOS: Arc> = Default::default(); pub static ref IS_UAC_RUNNING: Arc> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); + pub static ref LAST_SYNC_DISPLAYS: Arc>> = Default::default(); } fn is_capturer_mag_supported() -> bool { @@ -211,7 +208,7 @@ fn create_capturer( if privacy_mode_id > 0 { #[cfg(windows)] { - use crate::ui::win_privacy::*; + use crate::win_privacy::*; match scrap::CapturerMag::new( display.origin(), @@ -309,14 +306,14 @@ pub fn test_create_capturer(privacy_mode_id: i32, timeout_millis: u64) -> bool { } #[cfg(windows)] -fn check_uac_switch(privacy_mode_id: i32, captuerer_privacy_mode_id: i32) -> ResultType<()> { - if captuerer_privacy_mode_id != 0 { - if privacy_mode_id != captuerer_privacy_mode_id { - if !crate::ui::win_privacy::is_process_consent_running()? { +fn check_uac_switch(privacy_mode_id: i32, capturer_privacy_mode_id: i32) -> ResultType<()> { + if capturer_privacy_mode_id != 0 { + if privacy_mode_id != capturer_privacy_mode_id { + if !crate::win_privacy::is_process_consent_running()? { bail!("consent.exe is running"); } } - if crate::ui::win_privacy::is_process_consent_running()? { + if crate::win_privacy::is_process_consent_running()? { bail!("consent.exe is running"); } } @@ -330,7 +327,7 @@ pub(super) struct CapturerInfo { pub ndisplay: usize, pub current: usize, pub privacy_mode_id: i32, - pub _captuerer_privacy_mode_id: i32, + pub _capturer_privacy_mode_id: i32, pub capturer: Box, } @@ -359,7 +356,7 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType ResultType Option> { + let displays = try_get_displays().ok()?; + let last_sync_displays = &*LAST_SYNC_DISPLAYS.read().unwrap(); + + if displays.len() != last_sync_displays.len() { + Some(displays) + } else { + for i in 0..displays.len() { + if displays[i].height() != (last_sync_displays[i].height as usize) { + return Some(displays); + } + if displays[i].width() != (last_sync_displays[i].width as usize) { + return Some(displays); + } + if displays[i].origin() != (last_sync_displays[i].x, last_sync_displays[i].y) { + return Some(displays); + } + } + None + } +} + +fn check_get_displays_changed_msg() -> Option { + let displays = check_displays_new()?; + let (current, displays) = get_displays_2(&displays); + let mut pi = PeerInfo { + conn_id: crate::SYNC_PEER_INFO_DISPLAYS, + ..Default::default() + }; + pi.displays = displays.clone(); + pi.current_display = current as _; + let mut msg_out = Message::new(); + msg_out.set_peer_info(pi); + *LAST_SYNC_DISPLAYS.write().unwrap() = displays; + Some(msg_out) +} + fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] ensure_close_virtual_device()?; @@ -419,7 +454,7 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(target_os = "linux")] super::wayland::ensure_inited()?; #[cfg(windows)] - let last_portable_service_running = PORTABLE_SERVICE_RUNNING.lock().unwrap().clone(); + let last_portable_service_running = crate::portable_service::client::running(); #[cfg(not(windows))] let last_portable_service_running = false; @@ -467,6 +502,14 @@ fn run(sp: GenericService) -> ResultType<()> { width: c.width as _, height: c.height as _, cursor_embedded: capture_cursor_embedded(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + resolutions: Some(SupportedResolutions { + resolutions: get_current_display_name() + .map(|name| crate::platform::resolutions(&name)) + .unwrap_or(vec![]), + ..SupportedResolutions::default() + }) + .into(), ..Default::default() }); let mut msg_out = Message::new(); @@ -493,7 +536,7 @@ fn run(sp: GenericService) -> ResultType<()> { while sp.ok() { #[cfg(windows)] - check_uac_switch(c.privacy_mode_id, c._captuerer_privacy_mode_id)?; + check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; let mut video_qos = VIDEO_QOS.lock().unwrap(); if video_qos.check_if_updated() { @@ -502,7 +545,7 @@ fn run(sp: GenericService) -> ResultType<()> { video_qos.target_bitrate, video_qos.fps ); - encoder.set_bitrate(video_qos.target_bitrate).unwrap(); + allow_err!(encoder.set_bitrate(video_qos.target_bitrate)); spf = video_qos.spf(); } drop(video_qos); @@ -518,14 +561,14 @@ fn run(sp: GenericService) -> ResultType<()> { bail!("SWITCH"); } #[cfg(windows)] - if last_portable_service_running != PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() { + if last_portable_service_running != crate::portable_service::client::running() { bail!("SWITCH"); } check_privacy_mode_changed(&sp, c.privacy_mode_id)?; #[cfg(windows)] { if crate::platform::windows::desktop_changed() - && !PORTABLE_SERVICE_RUNNING.lock().unwrap().clone() + && !crate::portable_service::client::running() { bail!("Desktop changed"); } @@ -533,7 +576,17 @@ fn run(sp: GenericService) -> ResultType<()> { let now = time::Instant::now(); if last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; - if c.ndisplay != get_display_num() { + + // Capturer on macos does not return Err event the solution is changed. + #[cfg(target_os = "macos")] + if check_display_changed(c.ndisplay, c.current, c.width, c.height) { + log::info!("Displays changed"); + *SWITCH.lock().unwrap() = true; + bail!("SWITCH"); + } + + if let Some(msg_out) = check_get_displays_changed_msg() { + sp.send(msg_out); log::info!("Displays changed"); *SWITCH.lock().unwrap() = true; bail!("SWITCH"); @@ -602,7 +655,7 @@ fn run(sp: GenericService) -> ResultType<()> { if !scrap::is_x11() { if would_block_count >= 100 { super::wayland::release_resource(); - bail!("Wayland capturer none 100 times, try restart captuere"); + bail!("Wayland capturer none 100 times, try restart capture"); } } } @@ -637,7 +690,7 @@ fn run(sp: GenericService) -> ResultType<()> { while wait_begin.elapsed().as_millis() < timeout_millis as _ { check_privacy_mode_changed(&sp, c.privacy_mode_id)?; #[cfg(windows)] - check_uac_switch(c.privacy_mode_id, c._captuerer_privacy_mode_id)?; + check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; frame_controller.try_wait_next(&mut fetched_conn_ids, 300); // break if all connections have received current frame if fetched_conn_ids.len() >= frame_controller.send_conn_ids.len() { @@ -731,7 +784,6 @@ fn create_msg(vp9s: Vec) -> Message { frames: vp9s.into(), ..Default::default() }); - vf.timestamp = hbb_common::get_time(); msg_out.set_video_frame(vf); msg_out } @@ -790,25 +842,6 @@ pub fn handle_one_frame_encoded( Ok(send_conn_ids) } -fn get_display_num() -> usize { - #[cfg(target_os = "linux")] - { - if !scrap::is_x11() { - return if let Ok(n) = super::wayland::get_display_num() { - n - } else { - 0 - }; - } - } - - if let Ok(d) = try_get_displays() { - d.len() - } else { - 0 - } -} - pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { let mut displays = Vec::new(); let mut primary = 0; @@ -865,6 +898,7 @@ pub async fn switch_display(i: i32) { } } +#[inline] pub fn refresh() { #[cfg(target_os = "android")] Display::refresh_size(); @@ -892,10 +926,12 @@ fn get_primary() -> usize { 0 } +#[inline] pub async fn switch_to_primary() { switch_display(get_primary() as _).await; } +#[inline] #[cfg(not(windows))] fn try_get_displays() -> ResultType> { Ok(Display::all()?) @@ -954,21 +990,24 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> { get_current_display_2(try_get_displays()?) } +pub fn get_current_display_name() -> ResultType { + Ok(get_current_display_2(try_get_displays()?)?.2.name()) +} + #[cfg(windows)] fn start_uac_elevation_check() { static START: Once = Once::new(); START.call_once(|| { - if !crate::platform::is_installed() - && !crate::platform::is_root() - && !crate::platform::is_elevated(None).map_or(false, |b| b) - { + if !crate::platform::is_installed() && !crate::platform::is_root() { std::thread::spawn(|| loop { std::thread::sleep(std::time::Duration::from_secs(1)); - if let Ok(uac) = crate::ui::win_privacy::is_process_consent_running() { + if let Ok(uac) = crate::win_privacy::is_process_consent_running() { *IS_UAC_RUNNING.lock().unwrap() = uac; } - if let Ok(elevated) = crate::platform::is_foreground_window_elevated() { - *IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap() = elevated; + if !crate::platform::is_elevated(None).unwrap_or(false) { + if let Ok(elevated) = crate::platform::is_foreground_window_elevated() { + *IS_FOREGROUND_WINDOW_ELEVATED.lock().unwrap() = elevated; + } } }); } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 24b3be110..73a022871 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,6 +1,6 @@ use super::*; use hbb_common::{allow_err, platform::linux::DISTRO}; -use scrap::{set_map_err, Capturer, Display, Frame, TraitCapturer}; +use scrap::{is_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; use super::video_service::{ @@ -12,7 +12,7 @@ lazy_static::lazy_static! { static ref LOG_SCRAP_COUNT: Mutex = Mutex::new(0); } -pub fn set_wayland_scrap_map_err() { +pub fn init() { set_map_err(map_err_scrap); } @@ -129,7 +129,7 @@ pub(super) async fn check_init() -> ResultType<()> { let num = all.len(); let (primary, mut displays) = super::video_service::get_displays_2(&all); for display in displays.iter_mut() { - display.cursor_embedded = true; + display.cursor_embedded = is_cursor_embedded(); } let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); @@ -230,19 +230,6 @@ pub(super) fn get_primary() -> ResultType { } } -pub(super) fn get_display_num() -> ResultType { - let addr = *CAP_DISPLAY_INFO.read().unwrap(); - if addr != 0 { - let cap_display_info: *const CapDisplayInfo = addr as _; - unsafe { - let cap_display_info = &*cap_display_info; - Ok(cap_display_info.num) - } - } else { - bail!("Failed to get capturer display info"); - } -} - #[allow(dead_code)] pub(super) fn release_resource() { if scrap::is_x11() { @@ -276,7 +263,7 @@ pub(super) fn get_capturer() -> ResultType { ndisplay: cap_display_info.num, current: cap_display_info.current, privacy_mode_id: 0, - _captuerer_privacy_mode_id: 0, + _capturer_privacy_mode_id: 0, capturer: Box::new(cap_display_info.capturer.clone()), }) } diff --git a/src/tray.rs b/src/tray.rs index 98a4127a3..38ed9b0cb 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,11 +1,5 @@ -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(target_os = "windows")] use super::ui_interface::get_option_opt; -#[cfg(target_os = "linux")] -use hbb_common::log::{debug, error, info}; -#[cfg(target_os = "linux")] -use libappindicator::AppIndicator; -#[cfg(target_os = "linux")] -use std::env::temp_dir; #[cfg(target_os = "windows")] use std::sync::{Arc, Mutex}; #[cfg(target_os = "windows")] @@ -83,119 +77,10 @@ pub fn start_tray() { }); } -/// Start a tray icon in Linux -/// -/// [Block] -/// This function will block current execution, show the tray icon and handle events. -#[cfg(target_os = "linux")] -pub fn start_tray() { - use std::time::Duration; - - use glib::{clone, Continue}; - use gtk::traits::{GtkMenuItemExt, MenuShellExt, WidgetExt}; - - info!("configuring tray"); - // init gtk context - if let Err(err) = gtk::init() { - error!("Error when starting the tray: {}", err); - return; - } - if let Some(mut appindicator) = get_default_app_indicator() { - let mut menu = gtk::Menu::new(); - let stoped = is_service_stopped(); - // start/stop service - let label = if stoped { - crate::client::translate("Start Service".to_owned()) - } else { - crate::client::translate("Stop service".to_owned()) - }; - let menu_item_service = gtk::MenuItem::with_label(label.as_str()); - menu_item_service.connect_activate(move |_| { - let _lock = crate::ui_interface::SENDER.lock().unwrap(); - change_service_state(); - }); - menu.append(&menu_item_service); - // show tray item - menu.show_all(); - appindicator.set_menu(&mut menu); - // start event loop - info!("Setting tray event loop"); - // check the connection status for every second - glib::timeout_add_local( - Duration::from_secs(1), - clone!(@strong menu_item_service as item => move || { - let _lock = crate::ui_interface::SENDER.lock().unwrap(); - update_tray_service_item(&item); - // continue to trigger the next status check - Continue(true) - }), - ); - gtk::main(); - } else { - error!("Tray process exit now"); - } -} - -#[cfg(target_os = "linux")] -fn change_service_state() { - if is_service_stopped() { - debug!("Now try to start service"); - crate::ipc::set_option("stop-service", ""); - } else { - debug!("Now try to stop service"); - crate::ipc::set_option("stop-service", "Y"); - } -} - -#[cfg(target_os = "linux")] -#[inline] -fn update_tray_service_item(item: >k::MenuItem) { - use gtk::traits::GtkMenuItemExt; - - if is_service_stopped() { - item.set_label(&crate::client::translate("Start Service".to_owned())); - } else { - item.set_label(&crate::client::translate("Stop service".to_owned())); - } -} - -#[cfg(target_os = "linux")] -fn get_default_app_indicator() -> Option { - use libappindicator::AppIndicatorStatus; - use std::io::Write; - - let icon = include_bytes!("../res/icon.png"); - // appindicator does not support icon buffer, so we write it to tmp folder - let mut icon_path = temp_dir(); - icon_path.push("RustDesk"); - icon_path.push("rustdesk.png"); - match std::fs::File::create(icon_path.clone()) { - Ok(mut f) => { - f.write_all(icon).unwrap(); - // set .png icon file to be writable - // this ensures successful file rewrite when switching between x11 and wayland. - let mut perm = f.metadata().unwrap().permissions(); - if perm.readonly() { - perm.set_readonly(false); - f.set_permissions(perm).unwrap(); - } - } - Err(err) => { - error!("Error when writing icon to {:?}: {}", icon_path, err); - return None; - } - } - debug!("write temp icon complete"); - let mut appindicator = AppIndicator::new("RustDesk", icon_path.to_str().unwrap_or("rustdesk")); - appindicator.set_label("RustDesk", "A remote control software."); - appindicator.set_status(AppIndicatorStatus::Active); - Some(appindicator) -} - /// Check if service is stoped. /// Return [`true`] if service is stoped, [`false`] otherwise. #[inline] -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(target_os = "windows")] fn is_service_stopped() -> bool { if let Some(v) = get_option_opt("stop-service") { v == "Y" @@ -204,33 +89,90 @@ fn is_service_stopped() -> bool { } } -#[cfg(target_os = "macos")] -pub fn make_tray() { - use tray_item::TrayItem; - let mode = dark_light::detect(); - let icon_path; - match mode { - dark_light::Mode::Dark => { - icon_path = "mac-tray-light.png"; - } - dark_light::Mode::Light => { - icon_path = "mac-tray-dark.png"; - } - } - if let Ok(mut tray) = TrayItem::new(&crate::get_app_name(), icon_path) { - tray.add_label(&format!( - "{} {}", - crate::get_app_name(), - crate::lang::translate("Service is running".to_owned()) - )) - .ok(); +/// Start a tray icon in Linux +/// +/// [Block] +/// This function will block current execution, show the tray icon and handle events. +#[cfg(target_os = "linux")] +pub fn start_tray() {} - 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)); - } - } +#[cfg(target_os = "macos")] +pub fn start_tray() { + use hbb_common::{allow_err, log}; + allow_err!(make_tray()); +} + +#[cfg(target_os = "macos")] +pub fn make_tray() -> hbb_common::ResultType<()> { + // https://github.com/tauri-apps/tray-icon/blob/dev/examples/tao.rs + use hbb_common::anyhow::Context; + use tao::event_loop::{ControlFlow, EventLoopBuilder}; + use tray_icon::{ + menu::{Menu, MenuEvent, MenuItem}, + ClickEvent, TrayEvent, TrayIconBuilder, + }; + let mode = dark_light::detect(); + const LIGHT: &[u8] = include_bytes!("../res/mac-tray-light-x2.png"); + const DARK: &[u8] = include_bytes!("../res/mac-tray-dark-x2.png"); + let icon = match mode { + dark_light::Mode::Dark => LIGHT, + _ => DARK, + }; + let (icon_rgba, icon_width, icon_height) = { + let image = image::load_from_memory(icon) + .context("Failed to open icon path")? + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + let icon = tray_icon::icon::Icon::from_rgba(icon_rgba, icon_width, icon_height) + .context("Failed to open icon")?; + + let event_loop = EventLoopBuilder::new().build(); + + unsafe { + crate::platform::delegate::set_delegate(None); + } + + let tray_menu = Menu::new(); + let quit_i = MenuItem::new(crate::client::translate("Exit".to_owned()), true, None); + tray_menu.append_items(&[&quit_i]); + + let _tray_icon = Some( + TrayIconBuilder::new() + .with_menu(Box::new(tray_menu)) + .with_tooltip(format!( + "{} {}", + crate::get_app_name(), + crate::lang::translate("Service is running".to_owned()) + )) + .with_icon(icon) + .build()?, + ); + + let menu_channel = MenuEvent::receiver(); + let tray_channel = TrayEvent::receiver(); + let mut docker_hiden = false; + + event_loop.run(move |_event, _, control_flow| { + if !docker_hiden { + crate::platform::macos::hide_dock(); + docker_hiden = true; + } + *control_flow = ControlFlow::Wait; + + if let Ok(event) = menu_channel.try_recv() { + if event.id == quit_i.id() { + crate::platform::macos::uninstall(false); + } + println!("{event:?}"); + } + + if let Ok(event) = tray_channel.try_recv() { + if event.event == ClickEvent::Double { + crate::platform::macos::handle_application_should_open_untitled_file(); + } + } + }); } diff --git a/src/ui.rs b/src/ui.rs index d45a64298..f7419cd34 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,22 +9,18 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, PeerConfig}, + config::{LocalConfig, PeerConfig}, log, }; -use crate::common::get_app_name; -use crate::ipc; -use crate::ui_interface::*; +#[cfg(not(any(feature = "flutter", feature = "cli")))] +use crate::ui_session_interface::Session; +use crate::{common::get_app_name, ipc, ui_interface::*}; mod cm; #[cfg(feature = "inline")] pub mod inline; -#[cfg(target_os = "macos")] -mod macos; pub mod remote; -#[cfg(target_os = "windows")] -pub mod win_privacy; pub type Children = Arc)>>; #[allow(dead_code)] @@ -35,11 +31,17 @@ lazy_static::lazy_static! { static ref STUPID_VALUES: Mutex>>> = Default::default(); } +#[cfg(not(any(feature = "flutter", feature = "cli")))] +lazy_static::lazy_static! { + pub static ref CUR_SESSION: Arc>>> = Default::default(); + static ref CHILDREN : Children = Default::default(); +} + struct UIHostHandler; pub fn start(args: &mut [String]) { #[cfg(target_os = "macos")] - macos::show_dock(); + crate::platform::delegate::show_dock(); #[cfg(all(target_os = "linux", feature = "inline"))] { #[cfg(feature = "appimage")] @@ -71,7 +73,7 @@ pub fn start(args: &mut [String]) { allow_err!(sciter::set_options(sciter::RuntimeOptions::UxTheming(true))); frame.set_title(&crate::get_app_name()); #[cfg(target_os = "macos")] - macos::make_menubar(frame.get_host(), args.is_empty()); + crate::platform::delegate::make_menubar(frame.get_host(), args.is_empty()); let page; if args.len() > 1 && args[0] == "--play" { args[0] = "--connect".to_owned(); @@ -90,6 +92,9 @@ pub fn start(args: &mut [String]) { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); page = "index.html"; + // Start pulse audio local server. + #[cfg(target_os = "linux")] + std::thread::spawn(crate::ipc::start_pa); } else if args[0] == "--install" { frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); @@ -119,9 +124,10 @@ pub fn start(args: &mut [String]) { frame.register_behavior("native-remote", move || { let handler = remote::SciterSession::new(cmd.clone(), id.clone(), pass.clone(), args.clone()); - #[cfg(not(feature = "flutter"))] - crate::keyboard::set_cur_session(handler.inner()); - + #[cfg(not(any(feature = "flutter", feature = "cli")))] + { + *CUR_SESSION.lock().unwrap() = Some(handler.inner()); + } Box::new(handler) }); page = "remote.html"; @@ -181,11 +187,11 @@ impl UI { } fn get_remote_id(&mut self) -> String { - get_remote_id() + LocalConfig::get_remote_id() } fn set_remote_id(&mut self, id: String) { - set_remote_id(id); + LocalConfig::set_remote_id(&id); } fn goto_install(&mut self) { @@ -208,10 +214,6 @@ impl UI { show_run_without_install() } - fn has_rendezvous_service(&self) -> bool { - has_rendezvous_service() - } - fn get_license(&self) -> String { get_license() } @@ -304,7 +306,10 @@ impl UI { } fn is_release(&self) -> bool { - is_release() + #[cfg(not(debug_assertions))] + return true; + #[cfg(debug_assertions)] + return false; } fn is_rdp_service_open(&self) -> bool { @@ -324,11 +329,18 @@ impl UI { } fn closing(&mut self, x: i32, y: i32, w: i32, h: i32) { - closing(x, y, w, h) + crate::server::input_service::fix_key_down_timeout_at_exit(); + LocalConfig::set_size(x, y, w, h); } fn get_size(&mut self) -> Value { - Value::from_iter(get_size()) + let s = LocalConfig::get_size(); + let mut v = Vec::new(); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + Value::from_iter(v) } fn get_mouse_time(&self) -> f64 { @@ -383,7 +395,7 @@ impl UI { fn get_recent_sessions(&mut self) -> Value { // to-do: limit number of recent sessions, and remove old peer file - let peers: Vec = get_recent_sessions() + let peers: Vec = PeerConfig::peers() .drain(..) .map(|p| Self::get_peer_value(p.0, p.2)) .collect(); @@ -395,21 +407,19 @@ impl UI { } fn remove_peer(&mut self, id: String) { - remove_peer(id) + PeerConfig::remove(&id); } fn remove_discovered(&mut self, id: String) { - let mut peers = config::LanPeers::load().peers; - peers.retain(|x| x.id != id); - config::LanPeers::store(&peers); + remove_discovered(id); } fn send_wol(&mut self, id: String) { crate::lan::send_wol(id) } - fn new_remote(&mut self, id: String, remote_type: String) { - new_remote(id, remote_type) + fn new_remote(&mut self, id: String, remote_type: String, force_relay: bool) { + new_remote(id, remote_type, force_relay) } fn is_process_trusted(&mut self, _prompt: bool) -> bool { @@ -432,20 +442,12 @@ impl UI { is_login_wayland() } - fn fix_login_wayland(&mut self) { - fix_login_wayland() - } - fn current_is_wayland(&mut self) -> bool { current_is_wayland() } - fn modify_default_login(&mut self) -> String { - modify_default_login() - } - fn get_software_update_url(&self) -> String { - get_software_update_url() + crate::SOFTWARE_UPDATE_URL.lock().unwrap().clone() } fn get_new_version(&self) -> String { @@ -461,14 +463,30 @@ impl UI { } fn get_software_ext(&self) -> String { - get_software_ext() + #[cfg(windows)] + let p = "exe"; + #[cfg(target_os = "macos")] + let p = "dmg"; + #[cfg(target_os = "linux")] + let p = "deb"; + p.to_owned() } fn get_software_store_path(&self) -> String { - get_software_store_path() + let mut p = std::env::temp_dir(); + let name = crate::SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .split("/") + .last() + .map(|x| x.to_owned()) + .unwrap_or(crate::get_app_name()); + p.push(name); + format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) } fn create_shortcut(&self, _id: String) { + #[cfg(windows)] create_shortcut(_id) } @@ -498,7 +516,17 @@ impl UI { } fn open_url(&self, url: String) { - open_url(url) + #[cfg(windows)] + let p = "explorer"; + #[cfg(target_os = "macos")] + let p = "open"; + #[cfg(target_os = "linux")] + let p = if std::path::Path::new("/usr/bin/firefox").exists() { + "firefox" + } else { + "xdg-open" + }; + allow_err!(std::process::Command::new(p).arg(url).spawn()); } fn change_id(&self, id: String) { @@ -511,7 +539,7 @@ impl UI { } fn is_ok_change_id(&self) -> bool { - is_ok_change_id() + machine_uid::get().is_ok() } fn get_async_job_status(&self) -> String { @@ -519,11 +547,11 @@ impl UI { } fn t(&self, name: String) -> String { - t(name) + crate::client::translate(name) } fn is_xfce(&self) -> bool { - is_xfce() + crate::platform::is_xfce() } fn get_api_server(&self) -> String { @@ -541,6 +569,10 @@ impl UI { fn default_video_save_directory(&self) -> String { default_video_save_directory() } + + fn handle_relay_id(&self, id: String) -> String { + handle_relay_id(id) + } } impl sciter::EventHandler for UI { @@ -558,7 +590,7 @@ impl sciter::EventHandler for UI { fn set_remote_id(String); fn closing(i32, i32, i32, i32); fn get_size(); - fn new_remote(String, bool); + fn new_remote(String, String, bool); fn send_wol(String); fn remove_peer(String); fn remove_discovered(String); @@ -588,9 +620,7 @@ impl sciter::EventHandler for UI { fn is_installed_daemon(bool); fn get_error(); fn is_login_wayland(); - fn fix_login_wayland(); fn current_is_wayland(); - fn modify_default_login(); fn get_options(); fn get_option(String); fn get_local_option(String); @@ -599,7 +629,6 @@ impl sciter::EventHandler for UI { fn peer_has_password(String); fn forget_password(String); fn set_peer_option(String, String, String); - fn has_rendezvous_service(); fn get_license(); fn test_if_valid_server(String); fn get_sound_inputs(); @@ -626,6 +655,7 @@ impl sciter::EventHandler for UI { fn has_hwcodec(); fn get_langs(); fn default_video_save_directory(); + fn handle_relay_id(String); } } @@ -689,3 +719,60 @@ pub fn value_crash_workaround(values: &[Value]) -> Arc> { STUPID_VALUES.lock().unwrap().push(persist.clone()); persist } + +#[inline] +pub fn new_remote(id: String, remote_type: String, force_relay: bool) { + let mut lock = CHILDREN.lock().unwrap(); + let mut args = vec![format!("--{}", remote_type), id.clone()]; + if force_relay { + args.push("".to_string()); // password + args.push("--relay".to_string()); + } + let key = (id.clone(), remote_type.clone()); + if let Some(c) = lock.1.get_mut(&key) { + if let Ok(Some(_)) = c.try_wait() { + lock.1.remove(&key); + } else { + if remote_type == "rdp" { + allow_err!(c.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + c.try_wait().ok(); + lock.1.remove(&key); + } else { + return; + } + } + } + match crate::run_me(args) { + Ok(child) => { + lock.1.insert(key, child); + } + Err(err) => { + log::error!("Failed to spawn remote: {}", err); + } + } +} + +#[inline] +pub fn recent_sessions_updated() -> bool { + let mut children = CHILDREN.lock().unwrap(); + if children.0 { + children.0 = false; + true + } else { + false + } +} + +pub fn get_icon() -> String { + // 128x128 + #[cfg(target_os = "macos")] + // 128x128 on 160x160 canvas, then shrink to 128, mac looks better with padding + { + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAABhGlDQ1BJQ0MgcHJvZmlsZQAAeJx9kT1Iw0AYht+mSkUqHewg4pChOlkQFXHUVihChVArtOpgcukfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfE1cVJ0UVK/C4ptIjxjuMe3vvel7vvAKFZZZrVMwFoum1mUgkxl18VQ68QEEKYZkRmljEvSWn4jq97BPh+F+dZ/nV/jgG1YDEgIBLPMcO0iTeIZzZtg/M+cZSVZZX4nHjcpAsSP3Jd8fiNc8llgWdGzWwmSRwlFktdrHQxK5sa8TRxTNV0yhdyHquctzhr1Tpr35O/MFzQV5a5TmsEKSxiCRJEKKijgipsxGnXSbGQofOEj3/Y9UvkUshVASPHAmrQILt+8D/43VurODXpJYUTQO+L43yMAqFdoNVwnO9jx2mdAMFn4Erv+GtNYPaT9EZHix0BkW3g4rqjKXvA5Q4w9GTIpuxKQVpCsQi8n9E35YHBW6B/zetb+xynD0CWepW+AQ4OgbESZa/7vLuvu2//1rT79wPpl3Jwc6WkiQAAE5pJREFUeAHtXQt0VNW5/s5kkskkEyCEZwgQSIAEg6CgYBGKiFolwQDRlWW5BatiqiIWiYV6l4uq10fN9fq4rahYwAILXNAlGlAUgV5oSXiqDRggQIBAgJAEwmQeycycu//JDAwQyJzHPpPTmW+tk8yc2fucs//v23v/+3mMiCCsYQz1A0QQWkQEEOaICCDMERFAmCMigDBHRABhjogAwhwRAYQ5IgIIc0QEEOaICCDMobkAhg8f3m/cuHHjR40adXtGRkZmampqX4vFksR+MrPDoPXzhAgedtitVmttVVXVibKysn0lJSU7tm3btrm0tPSIlg+iiQDS0tK6FBQUzMjPz/+PlJSUIeyUoMV92zFI6PFM+PEsE/Rhx+i8vLyZ7JzIBFG2cuXKZQsXLlx8+PDhGt4PwlUAjPjuRUVFL2ZnZz9uNBrNPO/1bwKBMsjcuXPfZMeCzz///BP2/1UmhDO8bshFACaTybBgwYJZ7OFfZsR34HGPMIA5Nzf3GZZ5fsUy0UvMnu87nU6P2jdRXQCDBg3quXr16hVZWVnj1L52OIIy0Lx5895hQshl1cQjBw4cqFb1+mpe7L777hvOyP+C1W3Jal43AoAy1C4GJoJJGzZs2K3WdVUTwNSpU8cw56U4UuTzA2Ws4uLiTcyZzl6zZs1WNa6pigAo50fI1wZkY7I1qxLGq1ESKBaAr87/IkK+diBbk81HMCj1CRQJgLx9cvj0Uue7RRFnmSNd3+xBg0tEk0f0no82CLAYBSRGG9A9xuD93t5BNifbMw3craR1oEgA1NRrj96+yIiuaHRje10z9l5oRlmDCxU2N6ocLriIcy+/Yst/P9dCy3eBHT1MBgyIN2KwxYhhCdEY1SkGWZZoRAntSxhke+Jg/vz578q9hmwBUCcPtfPlxlcbF1mu/vpME76sdmLj2SZUOzw+glty+RVke78LpJTLv4nePyQLb9xqZxP+r9556ffEaAHjk2IxsUssctjRJSZKq6TdEMTBokWLVsrtLJItAOrhC3W972EEfnu6GUsqHVh7ygG7vyD05WYvm95sLbbyGdcVQWtx65tFrDljZ4cNRgNwLxPDjJ7xyO1qDmmVQRwQF5MnT35WVnw5kahvn7p35cRVA42sHF98xIF3Dtpw2OoJKMbRJpFKROAP72K+w/pzDqyvdaAnqy5+08uCp1Ms6BwdmlKBuGCcvMxKgXNS48oSQEFBwa9D0bfvcIv480EH3txvY86ceLl4J0giUrkI/OGrmf/10pEG/PH4RTzb24LCPh3QyajtoCZxwTh5tLCw8C3JceXcMD8//5dy4skFOXWrjzfhhT02VDLn7nJdroRI9URAP1lZqfRaZQM+PGXFK/064slkCwwaOo2Mk2maCGDkyJH9fEO6muCY1Y0nSxqx4VSzj3hpxGgpAgpf2+TBUwfr8c8LTnyamcSCaCMC4oS4KS0tPSolnmQB0GQOaDCeT2ZdesiJ2TttaGgOLOohixgtRUA/LmPO4rQe8bivs2Y1pUDcMAF8IiWSZAGMGDHidqlxpKKREV7wTxuWHbncDFOLGC1F8E2dQ0sBEDe3sX98BZCRkTFYahwpOMa8+ge/teKHOneLYTkQo5UIojSe+CSHG8kCSE1N7SM1TrDYe86FBzY04rTdoxKpwYQHt3tNTIpVxzBBguZXSo0jWQC+CZyqY9tpFyZ+3eir79XM2W2F53Mv6hf4eaK2ApDDjZxmoOqV2ncnXZjEyLe5fIblSEzr4dW91xOM/PcGdVLTRMFCMjdyBKBqL0fJGRce/IrIB+c6vq3w6tzriV7xWJjZSdM+gABI5iakC0MqLniQs97OvP6AkzoWwRO9GfmDQ0a+LIRMAA1NInLW2XDO7qvz/d263q/6E8HMPnH4QGfkE0IiAOrafXSjA+V1/iFbXGt4HYlgJsv5H9zUUXfkE0IigA/KmvG3w662SVOJVBqkG5FkxPDORmR2jELfeAO6mgyIMwreYDa36O3CPW7z4IDVhT3nm7Gjvtl7vq17eXN+lj7JJ2gugEPnPSjc2hR8zpUpAjNL2eQ+MXiorwkTekTDEi2NICcjf2ttE9accuKzk3bUNQVUVb57FaTG409DOsgin0rB4loHNtU7QI+W08WMMZ20bTYSNBUAJXrmRids5PRdIhCqiqCbWcCcwWY8MdCEzib5DRZTlIAJ3Uze4+0hCVhVZcefjtrwk9WN9PgoPJcWh+m9zbIGe5weEY+U1eJvNXZfmkS8deIi5vROwH+nJ8p+ZjnQVAB//cmFLVVu3zeJdXgbv8cywl64ORaFWbGSc3tbMLNrz+gb5z2UgsjP+6EWxefs1/g/bzMRjOloQm5X5fcJFpoJwNosYv62Zh+ZkOfIXef3O7pHYcnYeAzs2D7m6V0PNKFlKiOfZhNdLy3PV5zH/UlmmDSaZqaZAN7b04xT1gD2VRLB80Ni8fptse1+KjeRP+X7WnxF5PvRSlqP2F1YeNKK2aw60AKaCIDa/EU7XQG5X7kIWKmMD8fG4rFBJi2SoAhE/uQ9tfj6nBPBjHC+cawBM5PjWdXDf2qZJgL46AcX6gOEr1QERP6K8WY8nBajxeMrgp3I312HDV7yEVRaTzs9WFzdiKdS+JcC3AXgZk7P+7tdrRbfckXw0Vj9kP/grjp8S+RLrPreOWFFQS/+8wq5C2DdEQ+ONwScUCiCwmEm/Dqj/ZNPxf6kHXXY6M/5EtN6yObCxjqnd/0BT3AXwJJ/tZb75YlgdM8ovDay/df5hJcPWrGxpkmR4JewakDXAjjvELGuwnOd3CzNMGbWtl9ytxnGdu7tE6jD66NKW/BO7XVEsLbGDqvbAwtHZ5CrAIj8JteNivTgDTP/1hikd9THLnK0LLHWGZgOyBIBTZD5mjUb87rz6xjiLAB3EPV624bpGS/g+Vvaf73vB/UcDk4wYv9Fl7TmbSt2+lKvAvAu3DzqS4lCETx/azTiVO7e5Y1Z/ePwm+/J+5XYx3FV+G+ZAKhK4bXAhJsAys+JONeIAA8YkCOCeJbxH78pmtdjcsO03rF4oewiLvo3JJApAlp7WGF3YUAcHxtwE0DJSX/ul9LMu9YwU9ON6GjSV+4nWIwGTEmOxdLjdskdXVeH336+SX8C2Hval1jJbf0rDfPwgPY9wHMjTOlpwtJjdskdXVeH39vQjF9x2oSHmwD2nQ1MKGSJIJZxP76PfgUwvlsMjLSfgBhsutGqncqsLm7PyE0Ah2p92V92r5+A23sYYDbqr/j3g6qBYR2N2FVPBMoXwaFGnQmAdtCovggo7f8f3l0f7f4b4ZZO0S0CUDD4VWV3e3c447FJFRcBnG2kQaCAEzJFkJmkfwEMshhl+kKXw9McqpomD3qY1K8OuQigjqa6icravxS+bwf9Fv9+9DYbrkqrPBHUNetIAFanKClx1zNGV7P+BZAU4yvFFIqgpT9BfXARQJN/3qdCEXBq+moKasm0XgVIE4F/V1O1wakVIAQk2vddhgj0n/8pmcINmsPBi4AP/ZwE4N1EU4WlXLZm6B5Wf1ewwmVoMXoaC0jwD9wpFEHLwlF9o8bpCaI53LadLJz6Q7gIIJG2KVDY9KHPJy7oXwCVVneQgr+xnWgncx7gIoBuFoAm7ngUiqC8Vv8C2H/B5xErEAFR3z1GRwKgaVsprA1//Lz0zp/A8Lur9S+AnbW+XkAFS9OTYw3cpsJxGwtI7wwmAGnt/qsNU3pSZE1K5gBF6bM9cKLRjcMXL21hLlsE6fH8Jm5xu3JWdwGbDouSO38Cw1ubgH+cEHFXqj4FsO6kkrWQlz/flKBDAQzrGZg4+SJYU+5mAtDnmMCqSqfCllDLZxpR5AVuV77Dv52kxM6fq8Ov3OdB0QQRsTobFj7U4Mbfz/iGcRWK4I7O/CbEchPAoK4CulsEnLFK6/y52jC1jSJWMRFMH6qviSHv/uSASNW/AEUtoSSTgMwEfmnnJgBKz4R0YPleKWr3nbwq/J936UsAVY0efHLQtx5Q4VrIu7uauK4P5LouICdTwPI9Pi9IgQjKzuqrOfife+xweDe+hCL/h37K7sl3KRxXAdw/CKzuRosxFIigfyf91P9bqpvxaUVTyxeF/g91/mX35LsghqsAOsQKmDQY+OxHMegirzXDzB6pj1bA+SYRj261+ZKkvOp7oEcMEjn1APrBfXXwjBFMAD9ApgcMFNwWhcduaf8CoJVQM/5uQ2XDVZtfKhDB9FT+28ZxF8C9AwX07wwcqZPuAT/Fcv7/TjRwWxalJn5X6sDayubW0yJDBL3MBuQk818PyV0AtLJ59p3sWCvN+Xmakf++Tsh/ebcDRT86L59QQQSzBmizFF6TPYIeGwm8+h1QYw1OBLPuEPCuDsinYr9wuwNv/+jbCKItkoMUQcdoAU+ma7NrqCYCiI8R8LtxIuYWo816b/ZoA/7HS74WTyYf9U4R07+z48tjzdKqtiB2RZ+TYUYnzs6fH5rtE/jUaOD9bcCx87iuCJ4bLeBtHZC/8YQLj2224ziHfQ97xBrw2wzt3jSmmQBoi5e3ckQ8/ClaNcScMQKKFJBPxTGNHiaw0oaXgI4xD//3251YcShgqZeMzp0bieDVYXFI0HAvBE33Cs67WcC88SLe3OyzjUhkiXjxbgEv3yuPOIdLxB+2uPHhHo93L8L+icAztxswY2gUEmPVMeT+Wg/e+b4JS8td3vkJavTwtSaC0V2j8GiatptgaSoAssHrEwXk3yLim4Mtaf9FhoCsHvKIsjWLmLTCje+O+iZdsMscqWelyQY3XtzsRs5AA6YMMmBCfwOSJCwyIZ4qznuw/qgbqw66sP20+9L1LxMMVUVA6wc+/pm27xsmhOSFEUOTBXYouwaRn7PcjU1HxFY9cHuTiM/2efDZfo/358FdgVuY0AYlGZCSICApDt53ChAfVubH1dhFbxG/v1bEzjMenGz1tfS+LxzeVPL6rXHel1lojZC+NEoubPS+oeUeH/lo09D0d99ZdtQQqZdLi0se+TWfA26mRvHe1oBPSgyezQzN/oe6E4CX/GU+8pV64FeE55Oz2wqf3sGAT8fGheyVM7oSgJf8v3p8cw3BgRhtRZBoMuCLeyze/6GCbgTQyMiftJRyPjgTo40IzKy6//yeeGR2Cu1EFzkCoEpUU8kS+TlLRGw+EnBSxyKgae6rJ8RhbE/V85+n7SBXQs4T0PYP8TLiyQJtN5O7lJFfgVa9fb2JgFoeq++NwwN9uKx9t0uNIFkAVqu11mKxaCaAFXuAjQfBzQPXUgSJMQLW3h+HMcl8al7iRmocyU9SWVl5PCsrq0/bIdXBxkPg5oEHF16dew3oyBy+iWZkJPKr8xk3x6TGkSyA8vLy/UwAd0qNJxdGv7ehYxHk9DNi6T1m5u0LqtmlNRA3UuNIFsCuXbt25OXlzZQaTy5yBgOLd4ADqVLDS49rZtX86z+LwbNDozWZ21BSUrJDahzJAtiyZcsmtCSRf4oYcrMETB8hYuku6EoEdyYb8PGEWFbka9ZgErdt27ZJaiTJAigtLT1aVVX1r5SUlJulxpUDsvHifAETBoqYtw44STuwt2MR9Igz4LU7ozF9sFHT3j3ihHFTKTWeLHd05cqVy+bOnftHOXHlgOw4bbiAKUNEvLcNeGsLUGdrXyLoZALmjDDit7dGwxKjHfF+ECdy4skSwMKFCxc/99xzfzAajdpNXWGIi6H5BMDTo0V8XAK89w8Bx+pDK4LeCQJm3WrEzKGh29be5XLZiBM5cWUJ4PDhw+eKi4sX5ebmzpITXykSmKHn/ByYPUbEV+UCFjP/YF25CKfCFUjBho8xinggzYAZQ4yYmMZv945gwbj4hDiRE1d2jwSrAv4rOzt7OisFOsi9hlJEMcNns1YCHQ0OZohyYP1PIr6pEFDTqK4I6IXe4/sJyEmPwgPpBtVmGykFy/0NxIXc+LIFwBR3pqio6KV58+a9I/caaoKWoT0yDOwQvNyV14goOQ58Xy16F5dW1ArMgRTh9rdfrrchE/vXqwNtcWPATd0E7ySSkb0EZHYRQjZkeyMQB8SF3PiK+iQXLFjwPisFcrOyssYpuY7aIJ4yGXmZ3bzfLp2ncYWzVnjnDl50tmxpS3MSaREmVSu0vV23eIS8SA8WZWVlW4gDJddQJACn0+nJy8t7ZBeDxWLh9FIT9UDEJrPcnXxFpaUPsq+G1Wo9RbYnDpRcR/GoxIEDB6rZg+QwR2RzKP2BcALV+8zmk8j2Sq+lyrDUhg0b9uTn52eztmhxRAR8QeSTrZnNd6txPdXGJdesWbOV+QN3rV69+ks9VAd6hK/Yn6QW+QRVB6apJBjBwESwnDmGd6l57XAHOXxU56tR7AdC9ZkJ9IBMAxOYd/oMa5++EqkSlIGKfGrqkbev1OFrDVymptCDzp8//71FixateuONN36fm5v7OBMCvzcg/xuCEW+n3lbq5FHSzm8LXGcF04M/9NBDs9PS0l4pKCiYwZyXab5RRH22vfhDrKqqKqOBHerbZ/ar4X1DTaaFUz91YWFhER3Dhw9PHTdu3PhRo0bdnpGRMTg1NbUvcxqTWDAaWGr/mwGpAyrK7TSHj6bYlZeX7yspKdlJ4/k03K7lg2i+LmD37t2V7PgL+/gXre8dwbXQzcKQCPggIoAwR0QAYY6IAMIcEQGEOSICCHNEBBDmiAggzBERQJgjIoAwR0QAYY7/B1LDyJ6QBLUVAAAAAElFTkSuQmCC".into() + } + #[cfg(not(target_os = "macos"))] // 128x128 no padding + { + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAEiuAABIrgHwmhA7AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAEx9JREFUeJztnXmYHMV5h9+vZnZ0rHYRum8J4/AErQlgAQbMsRIWBEFCjK2AgwTisGILMBFCIMug1QLiPgIYE/QY2QQwiMVYjoSlODxEAgLEHMY8YuUEbEsOp3Z1X7vanf7yR8/MztEz0zPTPTO7M78/tnurvqn6uuqdr6q7a7pFVelrkpaPhhAMTEaYjJHDUWsEARkODANGAfWgINEPxLb7QNtBPkdoR7Ud0T8iphUTbtXp4z8pyQH5KOntAEhL2yCCnALW6aAnIDQAI+3MqFHkGJM73BkCO93JXnQnsAl4C8MGuoIv69mj2rw9ouKq1wEgzRiO2noSlp6DoRHleISgnQkJnRpLw0sI4v9X4H2E9Yj172zf+2udOflgYUdYXPUaAOTpzxoImJkIsxG+YCfG+Z7cecWDIN5+J8hqjNXCIW3rdMqULvdHWBqVNQDS8tlwNPCPKJcjOslOjGZGt2UHQTStHZGnMPxQG8d9mOk4S6myBEBWbj0aZR7ILISBPRlZOiMlr+QQgGAhvITqg0ybsEZjhZWHygoA+VnbaSBLEaY6dgb0Vgii+h2GO2gcv7JcQCgLAOSp7ZNBlyI6sycR+igEILoRdJFOnfgCJVZJAZCf7pxETfhmlIsQjHNH9VkIAF0H1iKdetjvKJFKAoC0EODA9msQvQUYmL2j8uwMJ/uygwAL0dvZMHGJNmFRZBUdAHlix5dQfQw4IbeO6tMQgOgybZx4I0VW0QCQ5dQQ2v4DhO8Dofw6qk9DEIZwg0497H8ookwxKpEV7WOo2fES0IQSAnrmwBrXEhq/lcR5cnJasm1KWq5lx9knl5NvvW7877EPIMFZFFm+AyA/2Xk6EngbOCVtA1chsO1V/4oiyzcABERW7FiI6osoo2IZVQicy7HtwxRZQT8KlWaCjNm5AiOzY+Oe0jPuqdjjXjQttpWe8TMhT0Djxs/ktGRbCi07g4/kWW/C8afxX/htAc2elzyPAPIQ/Ri7cyXCbBfjXjUS9Nh2IeEnKLI8BUB+1DaI/jvXoJwfS6xC4FxOcr2i12vjpM0UWZ6dBsry/aOh61fAMfmfCyfllfoU0Y2P+dab6P/d+rVx11MCeQKALN8zDA1vAJlc+AWRpLw+D4Hcp9PHLqBEKngIkBXtdVjWWlQmA4XMgBPTymU4cONj3vXKvaXsfCgQAGkhRGfoOZDjgHwnP3F5FQXBvTp97HWUWHkDIM0Y2nY/C5zpwQw4Lq8SINC79azSdz4UEgGG7l4CnOfJDDglr09DcK/+dWkmfE7KaxIoD++aDmYtaMCDGbBtXxETQ7lXzx5dFt/8qHIGQB7eORENvI0w1E4pZAacZN+XIUDu1XPKq/MhRwDkp/Rn7+7XQY6xE6I5ZQ/BbrB+j8gWkC2g7cBeAtJFdA2GyqGIDkUYA0xAtAEYkrFstxAY7tIZY26gDJXbvYDd+5qRuM7XyBbBt+vjONgnl0NKvZtRXYewAfRtvjX8Q00cwV1JWraNRbqPRbURkTOAoxGRnHzE3KUzRpVl50MOEUAe2H88Yr0GBEu/esapHPkjWE+CPKOzh25ydVA5Sp5vHw3hbwIXInoSEvEgnY/C7Xru6MV++AIgL245FmMuQmhArQ7EvInK4zpt3Meuy3ADgDQT4tC9b6EclbbzSgOBgq5B9T7mDNuQz7c8X8kv2o9Auq8C5gB1ST5uQ/VKPW/MSl/qbmkNMbTun1G+69A2BxDma+OER12V5QqA+/c2Y1jSk5BQYSkgUGAlAb3Zr2+7W8na7fV0dH0To18G3YOwkfrOn2vjpA5f6mtpDTGk7jmUv8n4BYFLdOqEf81aXjYA5L49R2DMRtCa1A6iFBC8glgLdM7QNzM63gclaz/sR03/51DOdREld9PV9Rd65uFbM5WZ/UKQBG5DqbEnenHp6S7yuL8gkrmceHs7bT8Wi/jzoY0V2fktrSHMgGdRzgXcXKSqpya0hCzKGAHkngNfwVivJ052nM6z8TsSvALM1ssHb8l2QH1Rsn5zfzprnkf0bDshPhMyRIIuAqZBTxv3QbqyM0eAgHUbINkvu+JjJNDlhAefUbGd39Ia4kBNC3B2HpfUa+i2bstYfroIIPftn4HyQgnX1nchXKFXDM46kemrkvWb+9MRWgV6lp0Qzchp0qyY8MnaOOkNpzrSRwAL+1cqpVlC1YnFhRXd+Ws/7Mf+fs+hkc6HXOZL8XmCFfxB2nqcIoDcc+AroG9EPh61jDOI33oeCQ6gOkO/M3h9Oqf7uqTlowHUml8C03Nq49h+ShtbqDlSzxj7v8l1OUcAteanHZsT0iI1eBcJurBkZkV3/ppPBzLQ/BvKdCC3Nnayt7cGY33Psb7kCCD3HRhPN39AtIZIWYlb3yKBAhfrd+ufdHK0EiRrPh0IuhqYljZK5h8J9hHS8XrKhB3xdaZGgG6uBGq8WZRBLpHg/oru/OXUoKwCmZYxSuYfCWrpNN9OrjcBAGnGoPT8QLFoEOgGttaX7R2zomjUpw8C010NlflCIFyaXG1iBAh1nAqMdbiq5CcEuyA8W5voTnauUiS/+PgIYG5O86V8IFD9S/mPj4+Jrzt5CLggzQUFByfwBgJlgc4b8n9UsgKBuajYfeE3BAG9IL7qGADSTBD4RoarSg5OUCgEL3FV3QoqXSpHRbaR/0ncegmBpRdI3HSxJwLUdE4FRqQ5jXAuuDAILLrNAk20qEypdvbs+w7BYfz6oxOiSSYu88wkQ58h4An9p9p3qQqEl121sVcQBJgR/bcHAGFaltOI7A66hyBMWG+lKlsHeRyho2gQWDRGdw2ANDMY5egUQ/8geF7n15ft83OLLZ05qo0wz9j/xGf4BsGJ9kWnaAQIHjwdCBTtFzzGuo+qkqQP5dTGhUEQop91EkQBsLTR9WmEWwfTQaDSqlfXO96arGTp+aPfAXm/aBCIPQxE5wDHpjVMKMQTCCr2cm9WKc/k3Mb5QmDpCdADQEPazvMaAhN4mqqcFQ635NXG+UHQYFss2zuScM1nsdyUu1BJ6bF9dbjD52CfWM4mvbZ2MlWllTz/+WZgYl5t7GSfXE58XqBzsKEr0BCjJWKbuPUwEgjrqCqzVP7T3oLvkaCr35EG4h/t4jMEYdlAVZkl1oa0nec1BCINBmRiiqFTwV5AYOQdqsqscMC+OloMCNDDDcoIR0OngguDYKteO6Cy7/q5UlsrYL9tzHcIdIQhdgPIwdCp4HwhsPT3VJVVOnPyQZQ/9CTEb72GQIYbkBEZDZ0KzgcCkc0pR1tVGsnHRXlmkTLcoDIiq6FTwTlDwBaqcifFfkex/xAMN6B1rmhxKjgnCGQ7VblVW0obgx8QDDEoxoUhBUMgupeq3EnFfraA/xCY3NehOdm7gSAs+6jKpbQjbRsnpEGhEBhUxI1hQoVO9tkgMFKU9xP1DUWaqggQGGwIshoWDEGY/lTlTsqgrG2ckpcfBAaNrMf3GwKRAVTlUjrIVRun5OUMgRqQbWk7z0sILB1BVe6UcHXWVwh2GFTbHQv2GgLDWKpyKZ2QUxun5LmGoN0A7amF+ACBMp6q3Ellgr2N/g8+QdBuEGlPnbSlGHoBQQNVZZU8/ekwkFF5tbGTfSYILN1qCOvWrOvHvIFgjDTvGUZVmaWBKWk7z3sI2g1iPkgxdCrYCwhqQsdSVRbJ8UD6zvMSAsyfDJa1ydEwXp5BoI0OpVcVL5VpPfvgKwQW7xtM8H1XtHgDwdeoKq3kic9rUU5OjcQ+QdBNq9Hb2AZsLQ4EMkVu3zucqpwlwekg/QCH4dhzCNp05qi26PX51gyGXkIQoLvmG1SVThcBqW0c2/cUglaI3nVQeSODoYMzBUAgXEhVKZKWHYegnJN28h3b9woC3oTYbSdrfVGWINn7p8qtnYdTVaIOWBcD9v2SYkCAvUTfBmBA8L+AriJBYFCuoqqYpIUAcE1qR+MXBGGk36sQAUCb2Av6joNh5gqdHHQHwWVyF3VUZWvf9vNROdz1tZjYfp4QiLyrfzd4J8Q/IcSSDWloyVyhk4PZIains6M6GYTow7mWAqltHEvDWwgsa320iB4AjFntWKFTwV5AoIHjqArG77gCmJy2jWNpeAcBsja61wPAAF5D+cixQqeCC4cg/pMVKfnZrkMRWercbr5B8Dk6cn30ozEAtAkLaHF/GlEgBEL1d4Kd4ftBRwJp2s0HCJSf60zC0Y8lLtRUszL1w/gAgbZRV/MMFSz58Y4ZqFySvd08hgBJeJdhIgD38BuI/ITLLwhEFORanc8BKlTy4+3jMPIT9+3mGQSfsGn4q/G+JACgimLJY/6uQ5Ol2hSq2OcESQshCLRg4fybTPAPAovHI0N9TKlr9UM8itLhCwSit2pT8OaUOitEAsKOnf8CeiKQz5enEAi6CQd+lOxTCgB6G22gT2U8jcgHAtE7dWnopuT6KkrLd92JcKmrbyt4C4HynF405KNkl9L8Wsc8mFBAihPkCkGzNocWOddVGZLluxYDCz150ko+EIg+5OSXIwB6N++hvJRQQIoTuIWgSW8JLnWqpxIkIPLIrrtRluU1bjvZ5w7BW3rhiNec/AtmcL0ZVfvlRQpIZEftunu2QuyxZQl5ApbepLcFK/ah0PIQ/ajZ/SjCJWnbLfo/9LSbaqItDvbJtmQoW0g778r87uDrdDVE31QddUbj9uO3ceXYTizR280taQvv45KHto8jGGwBTnTVbhL/4Yh9sq2TfbJtctnKqzpr2Knp/Mz8i11LFgHhlNAT2yc19Nj7iyu68x/ecx6B4DsoibP92D6p7ebbcGBlfBlXxggAIAusxxC5jLhjyEw0N+rtZlnGQvuo5JFdh2KZO4C5jt/g4keCVTpr6Ncz+Zz9N/tB04RiP9whWyQQrq/EzpdmQvLD3dcQNh+gzI2kOnzbI+kpafgRCboQSfvO4Jjv2SIAgCxgDugKJOK9E9GGhXqHuSdrYXlKbjnYgCWXYfQIIIRar6Os0Kb+f/arzqw+NRNi8L4LMXoT6BftxGhm1KpEkcDoLTpr2JKsx+AGAABZwCzQBxCGJFW4Hax5eldgZfpP5y9pJoR2PoDId5LqBTQMrAJ9iJv6v6yJ3xHfJA/sG4lYl6DyPWBs2s4rFQTQyu7tX9arv9hJFrkGAEAWcQjd/C1qNSAEEfMu+1mlD+PLA6BkIbXUdq0BGjM2ov3/FuBZxDxLd807yde8C/bl3j3DCJizUP4B4UzQYNqZd4qPCX76DYGFcIpePOR1V8eVCwDFlCykloFdLwCnu2rEhMaQbaDrgZdB36W74z1tstfAua7/no7DEJ0CHI9YU4EpgHF9+pXiYxb/nezzgUB5UC8dco2bY7Q/UoYARDr/Vyin5dSImTvjE+Aj0M8w8jkW3QR0N4ogMhi0FiPDUGsCMAmJLNFOd53Dfb3u/XeyzwUC5T26O07SuaP341JlB4A0M5Cu7jUIUz17MUIujeimM/Kt118I9iDWCTpnaE7PZC6rR7cldD6kOdUBcDg1ynpBBIe8DOU41evm3ke8ivH0NY38F5Y5uXY+lBEA0sxADnavAaZmP9+FsoagUP8z1evs/x16xeDnyUNlAYA0M4jO8DqQqZ41YqVAYPEC9Yfmvc6i5ADIQmrpCK8GTvW8Efs8BPIG/TsviF/lm6tKOgmUhdQSDEfO80k/sUo+1UmxTWNfLhPDQv13tt9IwJyul9cX9BT2kgEgC6kloGtAG4vSiH0Lgj9BzVd17sBPKVAlGQKkmUGY8LrYM4OKEU77znCwGZjuRedDCQAQQdinT6JyClDcRuz9EGykq+urOveQnncKFaiiDwFyPeeCri5pOO2dw8F/Y8k5emXdNjxU8YcAy5pV8m9Sb4sEsIbAvmledz6UZA4gRwKlD6e9AwIFvYut9V/P5fp+LsqwKtg3daHYbaeQ12pj16tmsf8k2yeXg0O9CWWnqddf/3cizNF5h/yykMbOphIMAfo2UD4Tq3KMBOi7qHWcXlnna+dDKQBQ8yjRh0NUIUiuw0LlAbrqT9arvZvpZ1JJLgTJtSxDdHGZzK7L5exgI8b6tl5d3/PMxiKoNPcC7udGVK5HsdesVXYk6ASa2DloSrE7H0oUAWKVX8dE1FqGyLdwWm4V2yeXb1JviQSK6CosXawL6kr2Yu2yWBEk19KA0TuBcyoDAl5Dwot0ft0rlFhlAUBUch1ngd5AdEVQX4NA+A1Gm3R+7TrKRGUFQFSygKMJWPNQuRihfy+HoAt0FaLL9braFx0PuIQqSwCikvmMpsaaBzILdJKdGM2MbssWgo8RXUE3j+hib+7c+aGyBiBesogGwtZsDBcDo+3EaGaZQKC0Y1iLWC10DFyrTZG3spaxeg0AUcnfE+Cw7tNQcyZGp4JMAYIlgqAb0d+isoGgrqaj/6te/yLJb/U6AJIlN1CHhE9DZSpGjwUagJE+QdCG8D6qbxCQlwn2e1WvZ4/Xx1RM9XoAnCSLGQrdX0LNkYh1GCIjEB2GMhzRUYjU9xgnQLAdQztoO8o2hK0gH2BkE8Fgq34fz2/Hllr/D1DoAB9bI40ZAAAAAElFTkSuQmCC".into() + } +} diff --git a/src/ui/cm.rs b/src/ui/cm.rs index 2bd8824db..a574b5e88 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -55,6 +55,17 @@ impl InvokeUiCM for SciterHandler { fn show_elevation(&self, show: bool) { self.call("showElevation", &make_args!(show)); } + + fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) { + self.call( + "updateVoiceCallState", + &make_args!( + client.id, + client.in_voice_call, + client.incoming_voice_call + ), + ); + } } impl SciterHandler { @@ -89,7 +100,7 @@ impl SciterConnectionManager { } fn get_icon(&mut self) -> String { - crate::get_icon() + super::get_icon() } fn check_click_time(&mut self, id: i32) { diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 716f2c6dd..4e46e217f 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -31,7 +31,7 @@ class Body: Reactor.Component var disconnected = c.disconnected; var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; - // below size:* is work around for Linux, it already set in css, but not work, shit sciter + // below size:* is a workaround for Linux, it already set in css, but not work, shit sciter return

    @@ -42,7 +42,7 @@ class Body: Reactor.Component
    {c.name}
    ({c.peer_id})
    {auth - ? {disconnected ? translate('Disconnected') : translate('Connected')}{" "}{getElaspsed(c.time, c.now)} + ? {disconnected ? translate('Disconnected') : translate('Connected')}{" "}{getElapsed(c.time, c.now)} : {translate('Request access to your device')}{"..."}}
    @@ -442,7 +442,7 @@ function self.ready() { view.move(sw - w, 0, w, h); } -function getElaspsed(time, now) { +function getElapsed(time, now) { var seconds = Date.diff(time, now, #seconds); var hours = seconds / 3600; var days = hours / 24; @@ -482,7 +482,7 @@ function updateTime() { if (el) { var c = connections[body.cur]; if (c && c.authorized && !c.disconnected) { - el.text = getElaspsed(c.time, c.now); + el.text = getElapsed(c.time, c.now); } } updateTime(); diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index f69f6d323..6c741b31f 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -778,12 +778,14 @@ handler.confirmDeleteFiles = function(id, i, name) { }); } -handler.overrideFileConfirm = function(id, file_num, to, is_upload) { +handler.overrideFileConfirm = function(id, file_num, to, is_upload, is_identical) { var jt = file_transfer.job_table; + var identical_msg = is_identical ? translate("identical_file_tip"): ""; msgbox("custom-skip", "Confirm Write Strategy", "
    \ -
    " + translate('Overwrite') + translate('files') + ".
    \ +
    " + translate('Overwrite') + " " + translate('files') + ".
    \
    " + translate('This file exists, skip or overwrite this file?') + "
    \ " + to + "
    \ +
    " + identical_msg + "
    \
    " + translate('Do this for all conflicts') + "
    \
    ", "", function(res=null) { if (!res) { diff --git a/src/ui/header.tis b/src/ui/header.tis index dd0b35541..257ba417e 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -198,6 +198,7 @@ class Header: Reactor.Component { {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} + {keyboard_enabled && ((is_osx && pi.platform != "Mac OS") || (!is_osx && pi.platform == "Mac OS")) ?
  • {svg_checkmark}{translate('Swap control-command key')}
  • : ""} ; } @@ -378,7 +379,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")) { @@ -440,7 +441,7 @@ function toggleMenuState() { 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"]) { + for (var id in ["show-remote-cursor", "show-quality-monitor", "disable-audio", "enable-file-transfer", "disable-clipboard", "lock-after-session-end", "allow_swap_key"]) { var el = self.select('#' + id); if (el) { var value = handler.get_toggle_option(id); @@ -480,6 +481,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) { diff --git a/src/ui/index.tis b/src/ui/index.tis index c141d0efe..0e2247070 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]; @@ -63,12 +62,15 @@ function createNewConnect(id, type) { id = id.replace(/\s/g, ""); app.remote_id.value = formatId(id); if (!id) return; + var old_id = id; + id = handler.handle_relay_id(id); + var force_relay = old_id != id; if (id == my_id) { msgbox("custom-error", "Error", "You cannot connect to your own computer"); return; } handler.set_remote_id(id); - handler.new_remote(id, type); + handler.new_remote(id, type, force_relay); } class ShareRdp: Reactor.Component { @@ -310,7 +312,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})
  • : @@ -468,8 +469,6 @@ 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') + "
    \ @@ -759,11 +758,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")); } @@ -773,19 +767,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")); } @@ -1121,11 +1107,6 @@ function checkConnectStatus() { 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; 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/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 1f3d5f7ec..68decf955 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, ops::{Deref, DerefMut}, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, RwLock}, }; use sciter::{ @@ -53,6 +53,20 @@ impl SciterHandler { allow_err!(e.call_method(func, &super::value_crash_workaround(args)[..])); } } + + 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 InvokeUiSession for SciterHandler { @@ -183,10 +197,10 @@ impl InvokeUiSession for SciterHandler { self.call("confirmDeleteFiles", &make_args!(id, i, name)); } - fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool) { + fn override_file_confirm(&self, id: i32, file_num: i32, to: String, is_upload: bool, is_identical: bool) { self.call( "overrideFileConfirm", - &make_args!(id, file_num, to, is_upload), + &make_args!(id, file_num, to, is_upload, is_identical), ); } @@ -201,7 +215,7 @@ impl InvokeUiSession for SciterHandler { self.call("adaptSize", &make_args!()); } - fn on_rgba(&self, data: &[u8]) { + fn on_rgba(&self, data: &mut Vec) { VIDEO .lock() .unwrap() @@ -215,22 +229,18 @@ impl InvokeUiSession for SciterHandler { 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); - - 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); - display.set_item("cursor_embedded", d.cursor_embedded); - displays.push(display); - } - pi_sciter.set_item("displays", displays); + 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 => {} @@ -264,6 +274,33 @@ impl InvokeUiSession for SciterHandler { 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); @@ -321,7 +358,7 @@ impl sciter::EventHandler for SciterSession { 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 => { @@ -370,7 +407,7 @@ impl sciter::EventHandler for SciterSession { fn transfer_file(); fn tunnel(); fn lock_screen(); - fn reconnect(); + fn reconnect(bool); fn get_chatbox(); fn get_icon(); fn get_home_dir(); @@ -418,15 +455,21 @@ impl sciter::EventHandler for SciterSession { fn supported_hwcodec(); fn change_prefer_codec(); fn restart_remote_device(); + fn request_voice_call(); + fn close_voice_call(); } } impl SciterSession { pub fn new(cmd: String, id: String, password: String, args: Vec) -> Self { + let force_relay = args.contains(&"--relay".to_string()); 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() }; @@ -440,7 +483,11 @@ impl SciterSession { ConnType::DEFAULT_CONN }; - session.lc.write().unwrap().initialize(id, conn_type); + session + .lc + .write() + .unwrap() + .initialize(id, conn_type, None, force_relay); Self(session) } @@ -466,7 +513,7 @@ impl SciterSession { } pub fn get_icon(&self) -> String { - crate::get_icon() + super::get_icon() } fn supported_hwcodec(&self) -> Value { diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 63df0cb09..5c828689d 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -120,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; diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 695d60417..bd6eab3bf 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -48,6 +48,9 @@ pub struct Client { 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, } @@ -87,6 +90,8 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized { fn change_language(&self); fn show_elevation(&self, show: bool); + + fn update_voice_call_state(&self, client: &Client); } impl Deref for ConnectionManager { @@ -118,6 +123,7 @@ impl ConnectionManager { file: bool, restart: bool, recording: bool, + from_switch: bool, tx: mpsc::UnboundedSender, ) { let client = Client { @@ -134,7 +140,10 @@ impl ConnectionManager { file, restart, recording, + from_switch, tx, + in_voice_call: false, + incoming_voice_call: false }; CLIENTS .write() @@ -177,6 +186,30 @@ impl ConnectionManager { 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] @@ -241,6 +274,14 @@ pub fn get_clients_length() -> usize { 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) { @@ -253,7 +294,7 @@ impl IpcTaskRunner { if !pre_enabled && ContextSend::is_enabled() { allow_err!( self.stream - .send(&Data::ClipbaordFile(clipboard::ClipbaordFile::MonitorReady)) + .send(&Data::ClipboardFile(clipboard::ClipboardFile::MonitorReady)) .await ); } @@ -288,7 +329,7 @@ impl IpcTaskRunner { rx_clip = rx_clip1.lock().await; } else { let rx_clip2; - (_tx_clip, rx_clip2) = unbounded_channel::(); + (_tx_clip, rx_clip2) = unbounded_channel::(); rx_clip1 = Arc::new(TokioMutex::new(rx_clip2)); rx_clip = rx_clip1.lock().await; } @@ -308,9 +349,9 @@ impl IpcTaskRunner { } 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} => { + 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, self.tx.clone()); + 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)] @@ -354,7 +395,7 @@ impl IpcTaskRunner { } } #[cfg(windows)] - Data::ClipbaordFile(_clip) => { + Data::ClipboardFile(_clip) => { #[cfg(windows)] { let conn_id = self.conn_id; @@ -378,6 +419,15 @@ impl IpcTaskRunner { 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()); + } _ => { } @@ -394,7 +444,7 @@ impl IpcTaskRunner { clip_file = rx_clip.recv() => match clip_file { Some(_clip) => { #[cfg(windows)] - allow_err!(self.tx.send(Data::ClipbaordFile(_clip))); + allow_err!(self.tx.send(Data::ClipboardFile(_clip))); } None => { // @@ -438,16 +488,13 @@ pub async fn start_ipc(cm: ConnectionManager) { #[cfg(windows)] std::thread::spawn(move || { log::info!("try create privacy mode window"); - #[cfg(windows)] - { - 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 - ); - } + 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::ui::win_privacy::start()); + allow_err!(crate::win_privacy::start()); }); match ipc::new_listener("_cm").await { @@ -498,6 +545,7 @@ pub async fn start_listen( file, restart, recording, + from_switch, .. }) => { current_id = id; @@ -514,6 +562,7 @@ pub async fn start_listen( file, restart, recording, + from_switch, tx.clone(), ); } @@ -779,13 +828,7 @@ fn cm_inner_send(id: i32, data: Data) { pub fn can_elevate() -> bool { #[cfg(windows)] - { - return !crate::platform::is_installed() - && !crate::portable_service::client::PORTABLE_SERVICE_RUNNING - .lock() - .unwrap() - .clone(); - } + return !crate::platform::is_installed(); #[cfg(not(windows))] return false; } @@ -801,3 +844,19 @@ pub fn elevate_portable(_id: i32) { } } } + +#[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.read().unwrap().get(&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.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned()))); + }; +} diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 2e4ca4ea3..17b4fafca 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -2,7 +2,6 @@ use std::{ collections::HashMap, process::Child, sync::{Arc, Mutex}, - time::SystemTime, }; #[cfg(any(target_os = "android", target_os = "ios"))] @@ -31,7 +30,6 @@ pub type Children = Arc)>>; type Status = (i32, bool, i64, String); // (status_num, key_confirmed, mouse_time, id) lazy_static::lazy_static! { - static ref CHILDREN : Children = Default::default(); 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(); @@ -44,17 +42,6 @@ lazy_static::lazy_static! { pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); } -#[inline] -pub fn recent_sessions_updated() -> bool { - let mut children = CHILDREN.lock().unwrap(); - if children.0 { - children.0 = false; - true - } else { - false - } -} - #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[inline] pub fn get_id() -> String { @@ -64,16 +51,6 @@ pub fn get_id() -> String { return ipc::get_id(); } -#[inline] -pub fn get_remote_id() -> String { - LocalConfig::get_remote_id() -} - -#[inline] -pub fn set_remote_id(id: String) { - LocalConfig::set_remote_id(&id); -} - #[inline] pub fn goto_install() { allow_err!(crate::run_me(vec!["--install"])); @@ -134,13 +111,6 @@ pub fn show_run_without_install() -> bool { false } -#[inline] -pub fn has_rendezvous_service() -> bool { - #[cfg(all(windows, feature = "hbbs"))] - return crate::platform::is_win_server() && crate::platform::windows::get_license().is_some(); - return false; -} - #[inline] pub fn get_license() -> String { #[cfg(windows)] @@ -158,7 +128,7 @@ pub fn get_license() -> String { } #[inline] -#[cfg(any(target_os = "linux", target_os = "windows"))] +#[cfg(target_os = "windows")] pub fn get_option_opt(key: &str) -> Option { OPTIONS.lock().unwrap().get(key).map(|x| x.clone()) } @@ -243,7 +213,8 @@ pub fn set_peer_option(id: String, name: String, value: String) { #[inline] pub fn using_public_server() -> bool { - crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() + option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() + && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } #[inline] @@ -324,7 +295,7 @@ pub fn set_option(key: String, value: String) { #[cfg(target_os = "macos")] if &key == "stop-service" { let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { + if is_stop && crate::platform::macos::uninstall(true) { return; } } @@ -425,24 +396,6 @@ pub fn is_installed_lower_version() -> bool { } } -#[inline] -pub fn closing(x: i32, y: i32, w: i32, h: i32) { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - crate::server::input_service::fix_key_down_timeout_at_exit(); - LocalConfig::set_size(x, y, w, h); -} - -#[inline] -pub fn get_size() -> Vec { - let s = LocalConfig::get_size(); - let mut v = Vec::new(); - v.push(s.0); - v.push(s.1); - v.push(s.2); - v.push(s.3); - v -} - #[inline] pub fn get_mouse_time() -> f64 { let ui_status = UI_STATUS.lock().unwrap(); @@ -513,51 +466,6 @@ pub fn store_fav(fav: Vec) { LocalConfig::set_fav(fav); } -#[inline] -pub fn get_recent_sessions() -> Vec<(String, SystemTime, PeerConfig)> { - PeerConfig::peers() -} - -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn get_icon() -> String { - crate::get_icon() -} - -#[inline] -pub fn remove_peer(id: String) { - PeerConfig::remove(&id); -} - -#[inline] -pub fn new_remote(id: String, remote_type: String) { - let mut lock = CHILDREN.lock().unwrap(); - let args = vec![format!("--{}", remote_type), id.clone()]; - let key = (id.clone(), remote_type.clone()); - if let Some(c) = lock.1.get_mut(&key) { - if let Ok(Some(_)) = c.try_wait() { - lock.1.remove(&key); - } else { - if remote_type == "rdp" { - allow_err!(c.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - c.try_wait().ok(); - lock.1.remove(&key); - } else { - return; - } - } - } - match crate::run_me(args) { - Ok(child) => { - lock.1.insert(key, child); - } - Err(err) => { - log::error!("Failed to spawn remote: {}", err); - } - } -} - #[inline] pub fn is_process_trusted(_prompt: bool) -> bool { #[cfg(target_os = "macos")] @@ -582,6 +490,15 @@ pub fn is_installed_daemon(_prompt: bool) -> bool { 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")))] @@ -594,9 +511,9 @@ pub fn get_error() -> String { if dtype != "x11" { return format!( "{} {}, {}", - t("Unsupported display server ".to_owned()), + crate::client::translate("Unsupported display server".to_owned()), dtype, - t("x11 expected".to_owned()), + crate::client::translate("x11 expected".to_owned()), ); } } @@ -611,12 +528,6 @@ pub fn is_login_wayland() -> bool { return false; } -#[inline] -pub fn fix_login_wayland() { - #[cfg(target_os = "linux")] - crate::platform::linux::fix_login_wayland(); -} - #[inline] pub fn current_is_wayland() -> bool { #[cfg(target_os = "linux")] @@ -625,19 +536,6 @@ pub fn current_is_wayland() -> bool { return false; } -#[inline] -pub fn modify_default_login() -> String { - #[cfg(target_os = "linux")] - return crate::platform::linux::modify_default_login(); - #[cfg(not(target_os = "linux"))] - return "".to_owned(); -} - -#[inline] -pub fn get_software_update_url() -> String { - SOFTWARE_UPDATE_URL.lock().unwrap().clone() -} - #[inline] pub fn get_new_version() -> String { hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) @@ -654,36 +552,9 @@ pub fn get_app_name() -> String { crate::get_app_name() } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_software_ext() -> String { - #[cfg(windows)] - let p = "exe"; - #[cfg(target_os = "macos")] - let p = "dmg"; - #[cfg(target_os = "linux")] - let p = "deb"; - p.to_owned() -} - -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn get_software_store_path() -> String { - let mut p = std::env::temp_dir(); - let name = SOFTWARE_UPDATE_URL - .lock() - .unwrap() - .split("/") - .last() - .map(|x| x.to_owned()) - .unwrap_or(crate::get_app_name()); - p.push(name); - format!("{}.{}", p.to_string_lossy(), get_software_ext()) -} - +#[cfg(windows)] #[inline] pub fn create_shortcut(_id: String) { - #[cfg(windows)] crate::platform::windows::create_shortcut(&_id).ok(); } @@ -726,24 +597,15 @@ pub fn get_lan_peers() -> Vec> { } #[inline] -pub fn get_uuid() -> String { - base64::encode(hbb_common::get_uuid()) +pub fn remove_discovered(id: String) { + let mut peers = config::LanPeers::load().peers; + peers.retain(|x| x.id != id); + config::LanPeers::store(&peers); } #[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn open_url(url: String) { - #[cfg(windows)] - let p = "explorer"; - #[cfg(target_os = "macos")] - let p = "open"; - #[cfg(target_os = "linux")] - let p = if std::path::Path::new("/usr/bin/firefox").exists() { - "firefox" - } else { - "xdg-open" - }; - allow_err!(std::process::Command::new(p).arg(url).spawn()); +pub fn get_uuid() -> String { + crate::encode64(hbb_common::get_uuid()) } #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] @@ -767,45 +629,69 @@ pub fn post_request(url: String, body: String, header: String) { }); } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn is_ok_change_id() -> bool { - machine_uid::get().is_ok() -} - #[inline] pub fn get_async_job_status() -> String { ASYNC_JOB_STATUS.lock().unwrap().clone() } -#[inline] -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] -pub fn t(name: String) -> String { - crate::client::translate(name) -} - #[inline] pub fn get_langs() -> String { - crate::lang::LANGS.to_string() + use serde_json::json; + let mut x: Vec<(&str, String)> = crate::lang::LANGS + .iter() + .map(|a| (a.0, format!("{} ({})", a.1, a.0))) + .collect(); + x.sort_by(|a, b| a.0.cmp(b.0)); + json!(x).to_string() } #[inline] pub fn default_video_save_directory() -> String { let appname = crate::get_app_name(); + // ui process can show it correctly Once vidoe process created it. + let try_create = |path: &std::path::Path| { + if !path.exists() { + std::fs::create_dir_all(path).ok(); + } + if path.exists() { + path.to_string_lossy().to_string() + } else { + "".to_string() + } + }; #[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; + let dir = try_create(&std::path::Path::new(&path)); + if !dir.is_empty() { + return dir; + } } 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(); + let dir = try_create(&video_dir.join(&appname)); + if !dir.is_empty() { + return dir; + } + if video_dir.exists() { + return video_dir.to_string_lossy().to_string(); + } + } + if let Some(desktop_dir) = user.desktop_dir() { + if desktop_dir.exists() { + return desktop_dir.to_string_lossy().to_string(); + } + } + let home = user.home_dir(); + if home.exists() { + return home.to_string_lossy().to_string(); } } + // same order as above #[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") { @@ -813,22 +699,36 @@ pub fn default_video_save_directory() -> String { } else { "Videos" }; - return home.join(name).join(appname).to_string_lossy().to_string(); + let video_dir = home.join(name); + let dir = try_create(&video_dir.join(&appname)); + if !dir.is_empty() { + return dir; + } + if video_dir.exists() { + return video_dir.to_string_lossy().to_string(); + } + let desktop_dir = home.join("Desktop"); + if desktop_dir.exists() { + return desktop_dir.to_string_lossy().to_string(); + } + if home.exists() { + return home.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(); + if let Some(parent) = exe.parent() { + let dir = try_create(&parent.join("videos")); + if !dir.is_empty() { + return dir; + } + // basically exist + return parent.to_string_lossy().to_string(); } } "".to_owned() } -#[inline] -pub fn is_xfce() -> bool { - crate::platform::is_xfce() -} - #[inline] pub fn get_api_server() -> String { crate::get_api_server( @@ -845,14 +745,6 @@ pub fn has_hwcodec() -> bool { return true; } -#[inline] -pub fn is_release() -> bool { - #[cfg(not(debug_assertions))] - return true; - #[cfg(debug_assertions)] - return false; -} - #[cfg(not(any(target_os = "android", target_os = "ios")))] #[inline] pub fn is_root() -> bool { @@ -870,10 +762,10 @@ pub fn is_root() -> bool { pub fn check_super_user_permission() -> bool { #[cfg(feature = "flatpak")] return true; - #[cfg(any(windows, target_os = "linux"))] + #[cfg(any(windows, target_os = "linux", target_os = "macos"))] return crate::platform::check_super_user_permission().unwrap_or(false); - #[cfg(not(any(windows, target_os = "linux")))] - true + #[cfg(not(any(windows, target_os = "linux", target_os = "macos")))] + return true; } #[allow(dead_code)] @@ -928,7 +820,19 @@ pub fn account_auth_result() -> String { serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } -// notice: avoiding create ipc connecton repeatly, +#[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) { @@ -1021,7 +925,7 @@ pub async fn change_id_shared(id: String, old_id: String) -> &'static str { #[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()); + let uuid = crate::encode64(hbb_common::get_uuid()); if uuid.is_empty() { log::error!("Failed to change id, uuid is_empty"); @@ -1121,3 +1025,12 @@ async fn check_id( } "" } + +// if it's relay id, return id processed, otherwise return original id +pub fn handle_relay_id(id: String) -> String { + if id.ends_with(r"\r") || id.ends_with(r"/r") { + id[0..id.len() - 2].to_string() + } else { + id + } +} diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index c66e1fa3b..8a1da6b8f 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,23 +1,34 @@ +#[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}; +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::GrabState; +use crate::common::{self, GrabState}; use crate::keyboard; use crate::{client::Data, client::Interface}; -use async_trait::async_trait; -use hbb_common::config::{Config, LocalConfig, PeerConfig}; -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 rdev::{Event, EventType::*}; -use std::collections::HashMap; -use std::ops::{Deref, DerefMut}; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; + pub static IS_IN: AtomicBool = AtomicBool::new(false); #[derive(Clone, Default)] @@ -29,9 +40,37 @@ pub struct Session { 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() @@ -74,6 +113,10 @@ impl Session { 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() } @@ -116,6 +159,12 @@ impl Session { 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())); } @@ -225,6 +274,11 @@ impl Session { 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 @@ -319,10 +373,88 @@ impl Session { return "".to_owned(); } + pub fn swab_modifier_key(&self, msg: &mut KeyEvent) { + let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string()); + if allow_swap_key { + if let Some(key_event::Union::ControlKey(ck)) = msg.union { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + msg.set_control_key(ck); + } + msg.modifiers = msg + .modifiers + .iter() + .map(|ck| { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + hbb_common::protobuf::EnumOrUnknown::new(ck) + }) + .collect(); + + let code = msg.chr(); + if code != 0 { + let mut peer = self.peer_platform().to_lowercase(); + peer.retain(|c| !c.is_whitespace()); + + let key = match peer.as_str() { + "windows" => { + let key = rdev::win_key_from_scancode(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + rdev::win_scancode_from_key(key).unwrap_or_default() + } + "macos" => { + let key = rdev::macos_key_from_code(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + rdev::macos_keycode_from_key(key).unwrap_or_default() + } + _ => { + let key = rdev::linux_key_from_code(code); + let key = match key { + rdev::Key::ControlLeft => rdev::Key::MetaLeft, + rdev::Key::MetaLeft => rdev::Key::ControlLeft, + rdev::Key::ControlRight => rdev::Key::MetaLeft, + rdev::Key::MetaRight => rdev::Key::ControlLeft, + _ => key, + }; + rdev::linux_keycode_from_key(key).unwrap_or_default() + } + }; + msg.set_chr(key); + } + } + } + pub fn send_key_event(&self, evt: &KeyEvent) { // mode: legacy(0), map(1), translate(2), auto(3) + + let mut msg = evt.clone(); + self.swab_modifier_key(&mut msg); let mut msg_out = Message::new(); - msg_out.set_key_event(evt.clone()); + msg_out.set_key_event(msg); self.send(Data::Message(msg_out)); } @@ -349,11 +481,24 @@ impl Session { } 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); } @@ -391,9 +536,10 @@ impl Session { pub fn handle_flutter_key_event( &self, - name: &str, + _name: &str, keycode: i32, scancode: i32, + lock_modes: i32, down_or_up: bool, ) { if scancode < 0 || keycode < 0 { @@ -414,13 +560,13 @@ impl Session { KeyRelease(key) }; let event = Event { - time: std::time::SystemTime::now(), - name: Option::Some(name.to_owned()), - code: keycode as _, - scan_code: scancode as _, + time: SystemTime::now(), + unicode: None, + platform_code: keycode as _, + position_code: scancode as _, event_type: event_type, }; - keyboard::client::process_event(&event); + keyboard::client::process_event(&event, Some(lock_modes)); } // flutter only TODO new input @@ -501,9 +647,13 @@ impl Session { } } - pub fn reconnect(&self) { + 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 || { @@ -598,6 +748,100 @@ impl Session { } 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 { @@ -607,6 +851,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { 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); @@ -628,15 +873,30 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { 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 override_file_confirm( + &self, + id: i32, + file_num: i32, + to: String, + is_upload: bool, + is_identical: 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: &[u8]); + 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 { @@ -726,6 +986,7 @@ impl Interface for Session { "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)] @@ -758,14 +1019,35 @@ impl Interface for Session { handle_test_delay(t, peer).await; } } + + fn swap_modifier_mouse(&self, msg: &mut hbb_common::protos::message::MouseEvent) { + let allow_swap_key = self.get_toggle_option("allow_swap_key".to_string()); + if allow_swap_key { + msg.modifiers = msg + .modifiers + .iter() + .map(|ck| { + let ck = ck.enum_value_or_default(); + let ck = match ck { + ControlKey::Control => ControlKey::Meta, + ControlKey::Meta => ControlKey::Control, + ControlKey::RControl => ControlKey::Meta, + ControlKey::RWin => ControlKey::Control, + _ => ck, + }; + hbb_common::protobuf::EnumOrUnknown::new(ck) + }) + .collect(); + }; + } } impl Session { pub fn lock_screen(&self) { - crate::keyboard::client::lock_screen(); + self.send_key_event(&crate::keyboard::client::event_lock_screen()); } pub fn ctrl_alt_del(&self) { - crate::keyboard::client::ctrl_alt_del(); + self.send_key_event(&crate::keyboard::client::event_ctrl_alt_del()); } } @@ -773,12 +1055,8 @@ impl Session { 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(); - } + let key = crate::get_key(false).await; #[cfg(not(any(target_os = "android", target_os = "ios")))] if handler.is_port_forward() { if handler.is_rdp() { @@ -868,7 +1146,7 @@ pub async fn io_loop(handler: Session) { 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: &[u8]| { + 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); }); 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 diff --git a/vdi/README.md b/vdi/README.md new file mode 100644 index 000000000..85e6ff194 --- /dev/null +++ b/vdi/README.md @@ -0,0 +1 @@ +# WIP diff --git a/vdi/host/.cargo/config.toml b/vdi/host/.cargo/config.toml new file mode 100644 index 000000000..70f9eaeb2 --- /dev/null +++ b/vdi/host/.cargo/config.toml @@ -0,0 +1,2 @@ +[registries.crates-io] +protocol = "sparse" diff --git a/vdi/host/.gitignore b/vdi/host/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/vdi/host/.gitignore @@ -0,0 +1 @@ +/target diff --git a/vdi/host/Cargo.lock b/vdi/host/Cargo.lock new file mode 100644 index 000000000..0b2e8ca2b --- /dev/null +++ b/vdi/host/Cargo.lock @@ -0,0 +1,2543 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[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 = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + +[[package]] +name = "async-broadcast" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b" +dependencies = [ + "easy-parallel", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-broadcast" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot", +] + +[[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", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2 0.4.7", + "waker-fn", + "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-recursion" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + +[[package]] +name = "async-trait" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clap" +version = "4.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098" +dependencies = [ + "bitflags 2.0.2", + "clap_derive", + "clap_lex", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" +dependencies = [ + "os_str_bytes", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "confy" +version = "0.4.0" +source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" +dependencies = [ + "directories-next", + "serde", + "thiserror", + "toml", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.7.1", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "cxx" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "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", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "easy-parallel" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[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 = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "exr" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.45.0", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flexi_logger" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eae57842a8221ef13f1f207632d786a175dd13bd8fbdc8be9d852f7c9cf1046" +dependencies = [ + "chrono", + "crossbeam-channel", + "crossbeam-queue", + "glob", + "is-terminal", + "lazy_static", + "log", + "nu-ansi-term", + "regex", + "thiserror", +] + +[[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", +] + +[[package]] +name = "futures" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" + +[[package]] +name = "futures-executor" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" + +[[package]] +name = "futures-task" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" + +[[package]] +name = "futures-util" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hbb_common" +version = "0.1.0" +dependencies = [ + "anyhow", + "backtrace", + "bytes", + "chrono", + "confy", + "directories-next", + "dirs-next", + "env_logger", + "filetime", + "flexi_logger", + "futures", + "futures-util", + "lazy_static", + "libc", + "log", + "mac_address", + "machine-uid", + "osascript", + "protobuf", + "protobuf-codegen", + "rand", + "regex", + "serde", + "serde_derive", + "socket2 0.3.19", + "sodiumoxide", + "sysinfo", + "tokio", + "tokio-socks", + "tokio-util", + "winapi", + "zstd", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[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", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[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 = "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", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + +[[package]] +name = "libusb1-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mac_address" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" +dependencies = [ + "nix 0.23.2", + "winapi", +] + +[[package]] +name = "machine-uid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212" +dependencies = [ + "winreg", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.45.0", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.6.5", +] + +[[package]] +name = "nom8" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" +dependencies = [ + "memchr", +] + +[[package]] +name = "ntapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "object" +version = "0.30.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "ordered-stream" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_str_bytes" +version = "6.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" + +[[package]] +name = "osascript" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" +dependencies = [ + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "png" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "windows-sys 0.42.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro-crate" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" +dependencies = [ + "bytes", + "once_cell", + "protobuf-support", + "thiserror", +] + +[[package]] +name = "protobuf-codegen" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +dependencies = [ + "thiserror", +] + +[[package]] +name = "qemu-display" +version = "0.1.0" +source = "git+https://github.com/rustdesk/qemu-display#e8a0925c2e804aa1eb07ee3027deaf8dd1c71b1d" +dependencies = [ + "async-broadcast 0.3.4", + "async-lock", + "async-trait", + "cfg-if", + "derivative", + "enumflags2", + "futures", + "futures-util", + "libc", + "log", + "once_cell", + "serde", + "serde_bytes", + "serde_repr", + "uds_windows", + "usbredirhost", + "windows", + "zbus", + "zvariant", +] + +[[package]] +name = "qemu-rustdesk" +version = "0.1.0" +dependencies = [ + "async-trait", + "clap", + "hbb_common", + "image", + "qemu-display", + "zbus", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rusb" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703aa035c21c589b34fb5136b12e68fc8dcf7ea46486861381361dd8ebf5cee0" +dependencies = [ + "libc", + "libusb1-sys", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustix" +version = "0.36.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-xml-rs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + +[[package]] +name = "serde_bytes" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha1" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "simd-adler32" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "socket2" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "sodiumoxide" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" +dependencies = [ + "ed25519", + "libc", + "libsodium-sys", + "serde", +] + +[[package]] +name = "spin" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d6e0250b93c8427a177b849d144a96d5acc57006149479403d7861ab721e34" +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 = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysinfo" +version = "0.28.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f69e0d827cce279e61c2f3399eb789271a8f136d8245edef70f06e3c9601a670" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "winapi", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder", + "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", +] + +[[package]] +name = "tokio" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.4.7", + "tokio-macros", + "windows-sys 0.42.0", +] + +[[package]] +name = "tokio-macros" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1-1" +source = "git+https://github.com/open-trade/tokio-socks#7034e79263ce25c348be072808d7601d82cd892d" +dependencies = [ + "bytes", + "either", + "futures-core", + "futures-sink", + "futures-util", + "pin-project", + "thiserror", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "futures-util", + "hashbrown", + "pin-project-lite", + "slab", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" + +[[package]] +name = "toml_edit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" +dependencies = [ + "indexmap", + "nom8", + "toml_datetime", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "usbredirhost" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87485e4dfeb0176203afd1086f11ed2ead837053143b12b6eed55c598e9393d5" +dependencies = [ + "libc", + "rusb", + "usbredirhost-sys", + "usbredirparser", +] + +[[package]] +name = "usbredirhost-sys" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b27c305da1f7601b665d68948bcfaf9909d443bec94510ab776118ab8afc2c7d" +dependencies = [ + "libusb1-sys", + "pkg-config", + "usbredirparser-sys", +] + +[[package]] +name = "usbredirparser" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0f8b5241d7cbb3e08b4677212a9ac001f116f50731c2737d16129a84ecf6a56" +dependencies = [ + "libc", + "usbredirparser-sys", +] + +[[package]] +name = "usbredirparser-sys" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b0e834e187916fc762bccdc9d64e454a0ee58b134f8f7adab321141e8e0d91" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[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.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[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.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[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.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi", +] + +[[package]] +name = "xml-rs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" + +[[package]] +name = "zbus" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ce2de393c874ba871292e881bf3c13a0d5eb38170ebab2e50b4c410eaa222b" +dependencies = [ + "async-broadcast 0.4.1", + "async-channel", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.24.3", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde-xml-rs", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13d08f5dc6cf725b693cb6ceacd43cd430ec0664a879188f29e7d7dcd98f96d" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zstd" +version = "0.9.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.3+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.2+zstd.1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "zune-inflate" +version = "0.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01728b79fb9b7e28a8c11f715e1cd8dc2cda7416a007d66cac55cebb3a8ac6b" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zvariant" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903169c05b9ab948ee93fefc9127d08930df4ce031d46c980784274439803e51" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "serde_bytes", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce76636e8fab7911be67211cf378c252b115ee7f2bae14b18b84821b39260b5" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] diff --git a/vdi/host/Cargo.toml b/vdi/host/Cargo.toml new file mode 100644 index 000000000..42b6fc83a --- /dev/null +++ b/vdi/host/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "qemu-rustdesk" +version = "0.1.0" +authors = ["rustdesk "] +edition = "2021" + +[dependencies] +qemu-display = { git = "https://github.com/rustdesk/qemu-display" } +hbb_common = { path = "../../libs/hbb_common" } +clap = { version = "4.1", features = ["derive"] } +zbus = { version = "3.0" } +image = "0.24" +async-trait = "0.1" diff --git a/vdi/host/README.md b/vdi/host/README.md new file mode 100644 index 000000000..0283266bf --- /dev/null +++ b/vdi/host/README.md @@ -0,0 +1,5 @@ +# RustDesk protocol on QEMU D-Bus display + +``` +sudo apt install libusbredirparser-dev libusbredirhost-dev libusb-1.0-0-dev +``` diff --git a/vdi/host/src/connection.rs b/vdi/host/src/connection.rs new file mode 100644 index 000000000..9f856fa2e --- /dev/null +++ b/vdi/host/src/connection.rs @@ -0,0 +1,11 @@ +use hbb_common::{message_proto::*, tokio, ResultType}; +pub use tokio::sync::{mpsc, Mutex}; +pub struct Connection { + pub tx: mpsc::UnboundedSender, +} + +impl Connection { + pub async fn on_message(&mut self, message: Message) -> ResultType { + Ok(true) + } +} diff --git a/vdi/host/src/console.rs b/vdi/host/src/console.rs new file mode 100644 index 000000000..a342f1a9a --- /dev/null +++ b/vdi/host/src/console.rs @@ -0,0 +1,119 @@ +use hbb_common::{tokio, ResultType}; +use image::GenericImage; +use qemu_display::{Console, ConsoleListenerHandler, MouseButton}; +use std::{collections::HashSet, sync::Arc}; +pub use tokio::sync::{mpsc, Mutex}; + +#[derive(Debug)] +pub enum Event { + ConsoleUpdate((i32, i32, i32, i32)), + Disconnected, +} + +const PIXMAN_X8R8G8B8: u32 = 0x20020888; +pub type BgraImage = image::ImageBuffer, Vec>; +#[derive(Debug)] +pub struct ConsoleListener { + pub image: Arc>, + pub tx: mpsc::UnboundedSender, +} + +#[async_trait::async_trait] +impl ConsoleListenerHandler for ConsoleListener { + async fn scanout(&mut self, s: qemu_display::Scanout) { + *self.image.lock().await = image_from_vec(s.format, s.width, s.height, s.stride, s.data); + } + + async fn update(&mut self, u: qemu_display::Update) { + let update = image_from_vec(u.format, u.w as _, u.h as _, u.stride, u.data); + let mut image = self.image.lock().await; + if (u.x, u.y) == (0, 0) && update.dimensions() == image.dimensions() { + *image = update; + } else { + image.copy_from(&update, u.x as _, u.y as _).unwrap(); + } + self.tx + .send(Event::ConsoleUpdate((u.x, u.y, u.w, u.h))) + .ok(); + } + + async fn scanout_dmabuf(&mut self, _scanout: qemu_display::ScanoutDMABUF) { + unimplemented!() + } + + async fn update_dmabuf(&mut self, _update: qemu_display::UpdateDMABUF) { + unimplemented!() + } + + async fn mouse_set(&mut self, set: qemu_display::MouseSet) { + dbg!(set); + } + + async fn cursor_define(&mut self, cursor: qemu_display::Cursor) { + dbg!(cursor); + } + + fn disconnected(&mut self) { + self.tx.send(Event::Disconnected).ok(); + } +} + +pub async fn key_event(console: &mut Console, qnum: u32, down: bool) -> ResultType<()> { + if down { + console.keyboard.press(qnum).await?; + } else { + console.keyboard.release(qnum).await?; + } + Ok(()) +} + +fn image_from_vec(format: u32, width: u32, height: u32, stride: u32, data: Vec) -> BgraImage { + if format != PIXMAN_X8R8G8B8 { + todo!("unhandled pixman format: {}", format) + } + if cfg!(target_endian = "big") { + todo!("pixman/image in big endian") + } + let layout = image::flat::SampleLayout { + channels: 4, + channel_stride: 1, + width, + width_stride: 4, + height, + height_stride: stride as _, + }; + let samples = image::flat::FlatSamples { + samples: data, + layout, + color_hint: None, + }; + samples + .try_into_buffer::>() + .or_else::<&str, _>(|(_err, samples)| { + let view = samples.as_view::>().unwrap(); + let mut img = BgraImage::new(width, height); + img.copy_from(&view, 0, 0).unwrap(); + Ok(img) + }) + .unwrap() +} + +fn button_mask_to_set(mask: u8) -> HashSet { + let mut set = HashSet::new(); + if mask & 0b0000_0001 != 0 { + set.insert(MouseButton::Left); + } + if mask & 0b0000_0010 != 0 { + set.insert(MouseButton::Middle); + } + if mask & 0b0000_0100 != 0 { + set.insert(MouseButton::Right); + } + if mask & 0b0000_1000 != 0 { + set.insert(MouseButton::WheelUp); + } + if mask & 0b0001_0000 != 0 { + set.insert(MouseButton::WheelDown); + } + set +} diff --git a/vdi/host/src/lib.rs b/vdi/host/src/lib.rs new file mode 100644 index 000000000..e9f8d7ed3 --- /dev/null +++ b/vdi/host/src/lib.rs @@ -0,0 +1,3 @@ +pub mod server; +mod console; +mod connection; diff --git a/vdi/host/src/main.rs b/vdi/host/src/main.rs new file mode 100644 index 000000000..ea32a028a --- /dev/null +++ b/vdi/host/src/main.rs @@ -0,0 +1,6 @@ +fn main() { + hbb_common::init_log(false, ""); + if let Err(err) = qemu_rustdesk::server::run() { + hbb_common::log::error!("{err}"); + } +} diff --git a/vdi/host/src/server.rs b/vdi/host/src/server.rs new file mode 100644 index 000000000..b43bd364f --- /dev/null +++ b/vdi/host/src/server.rs @@ -0,0 +1,172 @@ +use clap::Parser; +use hbb_common::{ + allow_err, + anyhow::{bail, Context}, + log, + message_proto::*, + protobuf::Message as _, + tokio, + tokio::net::TcpListener, + ResultType, Stream, +}; +use qemu_display::{Console, VMProxy}; +use std::{borrow::Borrow, sync::Arc}; + +use crate::connection::*; +use crate::console::*; + +#[derive(Parser, Debug)] +pub struct SocketAddrArgs { + /// IP address + #[clap(short, long, default_value = "0.0.0.0")] + address: std::net::IpAddr, + /// IP port number + #[clap(short, long, default_value = "21116")] + port: u16, +} + +impl From for std::net::SocketAddr { + fn from(args: SocketAddrArgs) -> Self { + (args.address, args.port).into() + } +} + +#[derive(Parser, Debug)] +struct Cli { + #[clap(flatten)] + address: SocketAddrArgs, + #[clap(short, long)] + dbus_address: Option, +} + +#[derive(Debug)] +struct Server { + vm_name: String, + rx_console: mpsc::UnboundedReceiver, + tx_console: mpsc::UnboundedSender, + rx_conn: mpsc::UnboundedReceiver, + tx_conn: mpsc::UnboundedSender, + image: Arc>, + console: Arc>, +} + +impl Server { + async fn new(vm_name: String, console: Console) -> ResultType { + let width = console.width().await?; + let height = console.height().await?; + let image = BgraImage::new(width as _, height as _); + let (tx_console, rx_console) = mpsc::unbounded_channel(); + let (tx_conn, rx_conn) = mpsc::unbounded_channel(); + Ok(Self { + vm_name, + rx_console, + tx_console, + rx_conn, + tx_conn, + image: Arc::new(Mutex::new(image)), + console: Arc::new(Mutex::new(console)), + }) + } + + async fn stop_console(&self) -> ResultType<()> { + self.console.lock().await.unregister_listener(); + Ok(()) + } + + async fn run_console(&self) -> ResultType<()> { + self.console + .lock() + .await + .register_listener(ConsoleListener { + image: self.image.clone(), + tx: self.tx_console.clone(), + }) + .await?; + Ok(()) + } + + async fn dimensions(&self) -> (u16, u16) { + let image = self.image.lock().await; + (image.width() as u16, image.height() as u16) + } + + async fn handle_connection(&mut self, stream: Stream) -> ResultType<()> { + let mut stream = stream; + self.run_console().await?; + let mut conn = Connection { + tx: self.tx_conn.clone(), + }; + + loop { + tokio::select! { + Some(evt) = self.rx_console.recv() => { + match evt { + _ => {} + } + } + Some(msg) = self.rx_conn.recv() => { + allow_err!(stream.send(&msg).await); + } + res = stream.next() => { + if let Some(res) = res { + match res { + Err(err) => { + bail!(err); + } + Ok(bytes) => { + if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { + match conn.on_message(msg_in).await { + Ok(false) => { + break; + } + Err(err) => { + log::error!("{err}"); + } + _ => {} + } + } + } + } + } else { + bail!("Reset by the peer"); + } + } + } + } + + self.stop_console().await?; + Ok(()) + } +} + +#[tokio::main] +pub async fn run() -> ResultType<()> { + let args = Cli::parse(); + + let listener = TcpListener::bind::(args.address.into()) + .await + .unwrap(); + let dbus = if let Some(addr) = args.dbus_address { + zbus::ConnectionBuilder::address(addr.borrow())? + .build() + .await + } else { + zbus::Connection::session().await + } + .context("Failed to connect to DBus")?; + + let vm_name = VMProxy::new(&dbus).await?.name().await?; + let console = Console::new(&dbus.into(), 0) + .await + .context("Failed to get the console")?; + let mut server = Server::new(format!("qemu-rustdesk ({})", vm_name), console).await?; + loop { + let (stream, addr) = listener.accept().await?; + stream.set_nodelay(true).ok(); + let laddr = stream.local_addr()?; + let stream = Stream::from(stream, laddr); + if let Err(err) = server.handle_connection(stream).await { + log::error!("Connection from {addr} closed: {err}"); + } + } +}