diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml index 7825286bd..4e98f311d 100644 --- a/.github/workflows/flutter-ci.yml +++ b/.github/workflows/flutter-ci.yml @@ -1,4 +1,4 @@ -name: Flutter CI +name: Full Flutter CI on: workflow_dispatch: @@ -11,42 +11,108 @@ on: paths-ignore: - ".github/**" +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" + # vcpkg version: 2022.05.10 + # for multiarch gcc compatibility + VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" + VERSION: "1.2.0" + jobs: - build: + build-for-windows: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) runs-on: ${{ matrix.job.os }} strategy: - fail-fast: false + fail-fast: true matrix: job: - # - { target: aarch64-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true } - # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true } # - { target: i686-pc-windows-msvc , os: windows-2019 } - # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } - # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } - # - { target: x86_64-apple-darwin , os: macos-10.15 } # - { target: x86_64-pc-windows-gnu , os: windows-2019 } - # - { target: x86_64-pc-windows-msvc , os: windows-2019 } - - { target: x86_64-unknown-linux-gnu , os: ubuntu-20.04 } - # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: - name: Checkout source code uses: actions/checkout@v3 - - name: Install prerequisites - shell: bash - run: | - case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; - # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; - # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; - esac + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.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/ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: "1.62" + target: ${{ matrix.job.target }} + override: true + components: rustfmt + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Install flutter rust bridge deps + run: | + cargo install flutter_rust_bridge_codegen + Push-Location flutter ; flutter pub get ; Pop-Location + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Restore from cache and install vcpkg + uses: lukka/run-vcpkg@v7 + with: + setupOnly: true + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static + shell: bash + + - name: Build rustdesk + run: python3 .\build.py --portable --hwcodec --flutter + + build-for-macOS: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-latest, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm yasm cmake gcc wget ninja + + - 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 +122,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 +137,845 @@ jobs: uses: lukka/run-vcpkg@v7 with: setupOnly: true - vcpkgGitCommitId: '1d4128f08e30cec31b94500840c7eca8ebc579cb' + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - shell: bash - - name: Show version information (Rust, cargo, GCC) + - name: Show version information (Rust, cargo, Clang) shell: bash run: | - gcc --version || true + clang --version || true rustup -V rustup toolchain list rustup default cargo -V rustc -V - - name: Build rustdesk ffi lib - run: cargo build --features flutter --lib - - - name: Build Flutter + - name: Build rustdesk run: | + # --hwcodec not supported on macos yet + ./build.py --flutter ${{ matrix.job.extra-build-args }} + + build-vcpkg-deps-linux: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + # - { arch: armv7, os: ubuntu-20.04 } + - { arch: x86_64, os: ubuntu-20.04 } + - { arch: aarch64, os: ubuntu-20.04 } + steps: + - name: Create vcpkg artifacts folder + run: mkdir -p /opt/artifacts + + - name: Cache Vcpkg + id: cache-vcpkg + uses: actions/cache@v3 + with: + path: /opt/artifacts + key: vcpkg-${{ matrix.job.arch }} + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Run vcpkg install on ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "/opt/artifacts" + dockerRunArgs: | + --volume "/opt/artifacts:/artifacts" + shell: /bin/bash + install: | + apt update -y + case "${{ matrix.job.arch }}" in + x86_64) + # CMake 3.15+ + apt install -y gpg wget ca-certificates + echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' | tee /etc/apt/sources.list.d/kitware.list >/dev/null + wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null + apt update -y + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev + ;; + aarch64|armv7) + apt install -y curl zip unzip tar git cmake g++ gcc build-essential pkg-config wget nasm yasm ninja-build libjpeg8-dev automake libtool + esac + cmake --version + gcc -v + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + case "${{ matrix.job.arch }}" in + x86_64) + export VCPKG_FORCE_SYSTEM_BINARIES=1 + pushd /artifacts + git clone https://github.com/microsoft/vcpkg.git || true + pushd vcpkg + git reset --hard ${{ env.VCPKG_COMMIT_ID }} + ./bootstrap-vcpkg.sh + ./vcpkg install libvpx libyuv opus + ;; + aarch64|armv7) + pushd /artifacts + # libyuv + git clone https://chromium.googlesource.com/libyuv/libyuv || true + pushd libyuv + git pull + mkdir -p build + pushd build + mkdir -p /artifacts/vcpkg/installed + cmake .. -DCMAKE_INSTALL_PREFIX=/artifacts/vcpkg/installed + make -j4 && make install + popd + popd + # libopus, ubuntu 18.04 prebuilt is not be compiled with -fPIC + wget -O opus.tar.gz http://archive.ubuntu.com/ubuntu/pool/main/o/opus/opus_1.1.2.orig.tar.gz + tar -zxvf opus.tar.gz; ls -l + pushd opus-1.1.2 + ./autogen.sh; ./configure --prefix=/artifacts/vcpkg/installed + make -j4; make install + ;; + esac + - name: Upload artifacts + uses: actions/upload-artifact@master + with: + name: vcpkg-artifact-${{ matrix.job.arch }} + path: | + /opt/artifacts/vcpkg/installed + + generate-bridge-linux: + name: generate bridge + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-args: "", + } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Install prerequisites + run: | + sudo apt update -y + sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang cmake libclang-dev ninja-build llvm-dev libclang-10-dev llvm-10-dev pkg-config + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + workspace: "/tmp/flutter_rust_bridge/frb_codegen" + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v3 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install flutter rust bridge deps + shell: bash + run: | + cargo install flutter_rust_bridge_codegen + pushd flutter && flutter pub get && popd + + - name: Run flutter rust bridge + run: | + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Upload Artifact + uses: actions/upload-artifact@master + with: + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./src/bridge_generated.io.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + + build-rustdesk-android-arm64: + needs: [generate-bridge-linux] + name: build rustdesk android apk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + job: + - { + arch: x86_64, + target: aarch64-linux-android, + os: ubuntu-18.04, + extra-build-features: "", + } + # - { + # arch: x86_64, + # target: armv7-linux-androideabi, + # os: ubuntu-18.04, + # extra-build-features: "", + # } + steps: + - name: Install dependencies + run: | + sudo apt update + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libvdpau-dev libva-dev libclang-dev llvm-dev libclang-10-dev llvm-10-dev pkg-config tree g++ libc6-dev gcc-multilib g++-multilib openjdk-11-jdk-headless + - name: Checkout source code + uses: actions/checkout@v3 + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: r22b + add-to-path: true + + - name: Download deps + shell: bash + run: | + pushd /opt + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz + tar xzf dep.tar.gz + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + + - name: Disable rust bridge build + run: | + sed -i "s/gen_flutter_rust_bridge();/\/\//g" build.rs + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + VCPKG_ROOT: /opt/vcpkg + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + esac + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-11-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-11-openjdk-amd64/bin:$PATH + # download so pushd flutter - flutter pub get - flutter build linux --debug -v + wget -O so.tar.gz https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz + tar xzvf so.tar.gz popd + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + # build flutter + pushd flutter + flutter build apk --release --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-release.apk . + + build-rustdesk-lib-linux-amd64: + needs: [generate-bridge-linux, build-vcpkg-deps-linux] + name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: true + matrix: + # use a high level qemu-user-static + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-20.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt update -y + sudo apt install qemu-user-static + + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache + key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }} + cache-directories: "/opt/rust-registry" + + - name: Install 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 + 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,${{ 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) + + - 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,${{ 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,${{ 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.0.5 + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + # reset to flutter 3.0.5 + git fetch + git reset --hard b09a90eee643859ce4e676839227edd9fd3feba8 + 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 + sed -i "s/flutter build linux --release/flutter-elinux build linux/g" ./build.py + # Setup flutter-elinux + export PATH=/opt/flutter-elinux/bin:$PATH + flutter-elinux doctor -v + # edit to corresponding arch + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/Architecture: amd64/Architecture: arm64/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + armv7) + sed -i "s/Architecture: amd64/Architecture: arm/g" ./build.py + sed -i "s/x64\/release/arm\/release/g" ./build.py + ;; + esac + python3 ./build.py --flutter --hwcodec --skip-cargo + + build-rustdesk-linux-amd64: + needs: [build-rustdesk-lib-linux-amd64] + name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}] + runs-on: ubuntu-20.04 + strategy: + fail-fast: true + matrix: + job: + # - { target: i686-unknown-linux-gnu , os: ubuntu-20.04, use-cross: true } + # - { target: i686-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "flatpak", + } + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + os: ubuntu-18.04, + extra-build-features: "appimage", + } + # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Prepare env + run: | + sudo apt update -y + sudo apt-get -qq install -y git curl wget nasm yasm libgtk-3-dev libarchive-tools + mkdir -p ./target/release/ + + - name: Restore the rustdesk lib file + uses: actions/download-artifact@master + with: + name: librustdesk-${{ matrix.job.arch }}-${{ matrix.job.extra-build-features }}.so + path: ./target/release/ + + - uses: Kingtous/run-on-arch-action@amd64-support + name: Build rustdesk binary for ${{ matrix.job.arch }} + id: vcpkg + with: + arch: ${{ matrix.job.arch }} + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt update -y + apt-get -qq install -y git cmake g++ gcc build-essential nasm yasm curl unzip xz-utils python3 wget pkg-config ninja-build pkg-config libgtk-3-dev liblzma-dev clang libappindicator3-dev rpm + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # Setup Flutter + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + ls -l . + export PATH=/opt/flutter/bin:$PATH + flutter doctor -v + pushd /workspace + python3 ./build.py --flutter --hwcodec --skip-cargo diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml index 68bf30ac0..b33a6dba0 100644 --- a/.github/workflows/flutter-nightly.yml +++ b/.github/workflows/flutter-nightly.yml @@ -15,6 +15,12 @@ env: # for multiarch gcc compatibility VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44" VERSION: "1.2.0" + #signing keys env variable checks + ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}' + MACOS_P12_BASE64: '${{ secrets.MACOS_P12_BASE64 }}' + # To make a custom build with your own servers set the below secret values + RS_PUB_KEY: '${{ secrets.RS_PUB_KEY }}' + RENDEZVOUS_SERVER: '${{ secrets.RENDEZVOUS_SERVER }}' jobs: build-for-windows: @@ -41,13 +47,14 @@ jobs: with: channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true - name: Replace engine with rustdesk custom flutter engine run: | flutter doctor -v flutter precache --windows - Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk/windows-x64-release-flutter.zip -OutFile windows-x64-release-flutter.zip - Expand-Archive windows-x64-release-flutter.zip -DestinationPath engine + 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 + 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/ - name: Install Rust toolchain @@ -65,12 +72,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 @@ -88,12 +90,41 @@ jobs: - name: Build rustdesk run: python3 .\build.py --portable --hwcodec --flutter - - name: Rename rustdesk + - name: Sign rustdesk files + uses: GermanBluefox/code-sign-action@v7 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.CERTNAME }}' + folder: './flutter/build/windows/runner/Release/' + recursive: true + + - name: Build self-extracted executable shell: bash run: | - for name in rustdesk*??-install.exe; do - mv "$name" "${name%%-install.exe}-${{ matrix.job.target }}.exe" - done + pushd ./libs/portable + python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}.exe + + # - name: Rename rustdesk + # shell: bash + # run: | + # for name in rustdesk*??-install.exe; do + # mv "$name" ./SignOutput/"${name%%-install.exe}-${{ matrix.job.target }}.exe" + # done + + - name: Sign rustdesk self-extracted file + uses: GermanBluefox/code-sign-action@v7 + with: + certificate: '${{ secrets.WINDOWS_PFX_BASE64 }}' + password: '${{ secrets.WINDOWS_PFX_PASSWORD }}' + certificatesha1: '${{ secrets.WINDOWS_PFX_SHA1_THUMBPRINT }}' + # certificatename: '${{ secrets.WINDOWS_PFX_NAME }}' + folder: './SignOutput' + recursive: false - name: Publish Release uses: softprops/action-gh-release@v1 @@ -101,7 +132,7 @@ jobs: prerelease: true tag_name: ${{ env.TAG_NAME }} files: | - rustdesk-*.exe + ./SignOutput/rustdesk-*.exe build-for-macOS: name: ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-args }}] @@ -112,13 +143,46 @@ 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 @@ -144,10 +208,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 @@ -161,10 +222,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: | @@ -180,10 +237,24 @@ 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 + mv ./flutter/build/macos/Build/Products/Release/rustdesk.app ./flutter/build/macos/Build/Products/Release/RustDesk.app + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep ./flutter/build/macos/Build/Products/Release/RustDesk.app -v + create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app + codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep rustdesk-${{ env.VERSION }}.dmg -v + # 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 @@ -331,27 +402,23 @@ jobs: flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - - name: Install ffigen - run: | - dart pub global activate ffigen --version 5.0.1 - - name: Install flutter rust bridge deps shell: bash run: | - pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 || true && popd - pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + cargo install flutter_rust_bridge_codegen pushd flutter && flutter pub get && popd - name: Run flutter rust bridge run: | ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart - - name: Upload Artifcat + - 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 @@ -480,6 +547,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 @@ -492,12 +560,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 @@ -505,6 +575,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 }}] @@ -528,6 +607,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 @@ -684,6 +769,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, @@ -862,6 +954,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, @@ -885,7 +984,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 @@ -947,7 +1046,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) @@ -964,12 +1063,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 @@ -980,8 +1097,31 @@ 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: Upload Artifcat + - name: Publish appimage package + if: ${{ matrix.job.extra-build-features == 'appimage' }} + uses: softprops/action-gh-release@v1 + with: + prerelease: true + tag_name: ${{ env.TAG_NAME }} + files: | + ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage + + - name: Upload Artifact uses: actions/upload-artifact@master if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} with: @@ -1077,6 +1217,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 @@ -1091,7 +1237,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 @@ -1141,15 +1287,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 @@ -1157,7 +1318,7 @@ jobs: files: | rustdesk-${{ env.VERSION }}-${{ matrix.job.target }}-${{ matrix.job.os }}.deb - - name: Upload Artifcat + - name: Upload Artifact uses: actions/upload-artifact@master if: ${{ contains(matrix.job.extra-build-features, 'flatpak') }} with: @@ -1213,6 +1374,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 diff --git a/.gitignore b/.gitignore index 1ecea7af8..fd5b5955e 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 diff --git a/Cargo.lock b/Cargo.lock index 529be08d6..693ae7d4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,11 +45,13 @@ dependencies = [ [[package]] name = "allo-isolate" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccb993621e6bf1b67591005b0adad126159a0ab31af379743906158aed5330d0" +checksum = "8ed55848be9f41d44c79df6045b680a74a78bc579e0813f7f196cd7928e22fb1" dependencies = [ + "anyhow", "atomic", + "chrono", ] [[package]] @@ -76,7 +78,7 @@ dependencies = [ "alsa-sys", "bitflags", "libc", - "nix 0.23.1", + "nix 0.23.2", ] [[package]] @@ -175,11 +177,11 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" +checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" dependencies = [ - "concurrent-queue 1.2.4", + "concurrent-queue", "event-listener", "futures-core", ] @@ -192,7 +194,7 @@ checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ "async-lock", "async-task", - "concurrent-queue 2.0.0", + "concurrent-queue", "fastrand", "futures-lite", "slab", @@ -200,13 +202,13 @@ dependencies = [ [[package]] name = "async-io" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" dependencies = [ "async-lock", "autocfg 1.1.0", - "concurrent-queue 1.2.4", + "concurrent-queue", "futures-lite", "libc", "log", @@ -215,7 +217,7 @@ dependencies = [ "slab", "socket2 0.4.7", "waker-fn", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -230,20 +232,20 @@ dependencies = [ [[package]] name = "async-process" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02111fd8655a613c25069ea89fc8d9bb89331fa77486eb3bc059ee757cfa481c" +checksum = "6381ead98388605d0d9ff86371043b5aa922a3905824244de40dc263a14fcba4" dependencies = [ "async-io", + "async-lock", "autocfg 1.1.0", "blocking", "cfg-if 1.0.0", "event-listener", "futures-lite", "libc", - "once_cell", "signal-hook", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -265,9 +267,9 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" [[package]] name = "async-trait" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" +checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364" dependencies = [ "proc-macro2", "quote", @@ -438,16 +440,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +checksum = "3c67b173a56acffd6d2326fb7ab938ba0b00a71480e14902b2591c87bc5741e8" dependencies = [ "async-channel", + "async-lock", "async-task", "atomic-waker", "fastrand", "futures-lite", - "once_cell", ] [[package]] @@ -471,6 +473,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "build-target" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b" + [[package]] name = "bumpalo" version = "3.11.1" @@ -495,15 +503,9 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" - [[package]] name = "cairo-rs" version = "0.15.12" @@ -544,7 +546,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -553,7 +555,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -565,15 +567,15 @@ dependencies = [ "camino", "cargo-platform", "semver 1.0.14", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", ] [[package]] name = "cbindgen" -version = "0.23.0" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6d248e3ca02f3fbfabcb9284464c596baec223a26d91bbf44a5a62ddb0d900" +checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb" dependencies = [ "clap 3.2.23", "heck 0.4.0", @@ -581,7 +583,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", "syn", "tempfile", @@ -643,7 +645,7 @@ dependencies = [ "js-sys", "num-integer", "num-traits 0.2.15", - "time 0.1.44", + "time 0.1.45", "wasm-bindgen", "winapi 0.3.9", ] @@ -733,7 +735,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "thiserror", ] @@ -824,15 +826,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "concurrent-queue" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af4780a44ab5696ea9e28294517f1fffb421a83a25af521333c838635509db9c" -dependencies = [ - "cache-padded", -] - [[package]] name = "concurrent-queue" version = "2.0.0" @@ -848,11 +841,21 @@ version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.147", + "serde 1.0.149", "thiserror", "toml", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen", +] + [[package]] name = "convert_case" version = "0.5.0" @@ -976,7 +979,7 @@ dependencies = [ "mach", "ndk 0.6.0", "ndk-glue 0.6.2", - "nix 0.23.1", + "nix 0.23.2", "oboe", "parking_lot 0.11.2", "stdweb", @@ -1068,12 +1071,12 @@ dependencies = [ [[package]] name = "ctrlc" -version = "3.2.3" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173" +checksum = "1631ca6e3c59112501a9d87fd86f21591ff77acd31331e8a73f8d80a65bbdd71" dependencies = [ - "nix 0.25.0", - "winapi 0.3.9", + "nix 0.26.1", + "windows-sys 0.42.0", ] [[package]] @@ -1084,9 +1087,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "cxx" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" +checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf" dependencies = [ "cc", "cxxbridge-flags", @@ -1096,9 +1099,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" +checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39" dependencies = [ "cc", "codespan-reporting", @@ -1111,15 +1114,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" +checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12" [[package]] name = "cxxbridge-macro" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" +checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6" dependencies = [ "proc-macro2", "quote", @@ -1332,8 +1335,7 @@ checksum = "f578e8e2c440e7297e008bb5486a3a8a194775224bbc23729b0dbdfaeebf162e" [[package]] name = "default-net" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e70d471b0ba4e722c85651b3bb04b6880dfdb1224a43ade80c1295314db646" +source = "git+https://github.com/Kingtous/default-net#bdaad8dd5b08efcba303e71729d3d0b1d5ccdb25" dependencies = [ "libc", "memalloc", @@ -1351,6 +1353,17 @@ dependencies = [ "byteorder", ] +[[package]] +name = "delegate" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derivative" version = "2.2.0" @@ -1458,7 +1471,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.147", + "serde 1.0.149", "strsim 0.10.0", ] @@ -1481,7 +1494,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "thiserror", ] @@ -1503,9 +1516,9 @@ checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "embed-resource" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0" +checksum = "e62abb876c07e4754fae5c14cafa77937841f01740637e17d78dc04352f32a5e" dependencies = [ "cc", "rustc_version 0.4.0", @@ -1534,7 +1547,7 @@ dependencies = [ "objc", "pkg-config", "rdev", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "tfc", "unicode-segmentation", @@ -1580,7 +1593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" dependencies = [ "enumflags2_derive", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -1679,7 +1692,7 @@ source = "git+https://github.com/fufesou/evdev#cec616e37790293d2cd2aa54a96601ed6 dependencies = [ "bitvec", "libc", - "nix 0.23.1", + "nix 0.23.2", ] [[package]] @@ -1688,6 +1701,18 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "extend" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5216e387a76eebaaf11f6d871ec8a4aae0b25f05456ee21f228e024b1b3610" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "failure" version = "0.1.8" @@ -1718,9 +1743,9 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9663d381d07ae25dc88dbdf27df458faa83a9b25336bcac83d5e452b5fc9d3" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" dependencies = [ "cfg-if 1.0.0", "libc", @@ -1730,12 +1755,12 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" dependencies = [ "crc32fast", - "miniz_oxide 0.5.4", + "miniz_oxide 0.6.2", ] [[package]] @@ -1760,46 +1785,63 @@ dependencies = [ [[package]] name = "flutter_rust_bridge" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8079119bbe8fb63d7ebb731fa2aa68c6c8375f4ac95ca26d5868e64c0f4b9244" dependencies = [ "allo-isolate", "anyhow", + "build-target", + "bytemuck", + "cc", + "chrono", + "console_error_panic_hook", "flutter_rust_bridge_macros", + "js-sys", "lazy_static", + "libc", + "log", "parking_lot 0.12.1", "threadpool", + "wasm-bindgen", + "web-sys", ] [[package]] name = "flutter_rust_bridge_codegen" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd7396bc479eae8aa24243e4c0e3d3dbda1909134f8de6bde4f080d262c9a0d" dependencies = [ "anyhow", "cargo_metadata", "cbindgen", + "clap 3.2.23", "convert_case", + "delegate", "enum_dispatch", "env_logger 0.9.3", + "extend", + "itertools 0.10.5", "lazy_static", "log", "pathdiff", "quote", "regex", - "serde 1.0.147", + "serde 1.0.149", "serde_yaml", - "structopt", "syn", "tempfile", "thiserror", "toml", + "topological-sort", ] [[package]] name = "flutter_rust_bridge_macros" -version = "1.32.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" +version = "1.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5cd827645690ef378be57a890d0581e17c28d07b712872af7d744f454fd27d" [[package]] name = "fnv" @@ -2007,7 +2049,7 @@ version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" dependencies = [ - "gio-sys", + "gio-sys 0.15.10", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", @@ -2022,7 +2064,7 @@ checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys", + "gio-sys 0.15.10", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", @@ -2078,7 +2120,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "gio-sys", + "gio-sys 0.15.10", "glib 0.15.12", "libc", "once_cell", @@ -2098,6 +2140,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "gio-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b693b8e39d042a95547fc258a7b07349b1f0b48f4b2fa3108ba3c51c0b5229" +dependencies = [ + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "system-deps 6.0.3", + "winapi 0.3.9", +] + [[package]] name = "glib" version = "0.10.3" @@ -2137,6 +2192,28 @@ dependencies = [ "thiserror", ] +[[package]] +name = "glib" +version = "0.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cd04d150a2c63e6779f43aec7e04f5374252479b7bed5f45146d9c0e821f161" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.16.3", + "glib-macros 0.16.3", + "glib-sys 0.16.3", + "gobject-sys 0.16.3", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + [[package]] name = "glib-macros" version = "0.10.1" @@ -2145,7 +2222,7 @@ checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" dependencies = [ "anyhow", "heck 0.3.3", - "itertools", + "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", "proc-macro2", @@ -2168,6 +2245,21 @@ dependencies = [ "syn", ] +[[package]] +name = "glib-macros" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf" +dependencies = [ + "anyhow", + "heck 0.4.0", + "proc-macro-crate 1.2.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "glib-sys" version = "0.10.1" @@ -2188,6 +2280,16 @@ dependencies = [ "system-deps 6.0.3", ] +[[package]] +name = "glib-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61a4f46316d06bfa33a7ac22df6f0524c8be58e3db2d9ca99ccb1f357b62a65" +dependencies = [ + "libc", + "system-deps 6.0.3", +] + [[package]] name = "glob" version = "0.3.0" @@ -2216,6 +2318,17 @@ dependencies = [ "system-deps 6.0.3", ] +[[package]] +name = "gobject-sys" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3520bb9c07ae2a12c7f2fbb24d4efc11231c8146a86956413fb1a79bb760a0f1" +dependencies = [ + "glib-sys 0.16.3", + "libc", + "system-deps 6.0.3", +] + [[package]] name = "gstreamer" version = "0.16.7" @@ -2382,7 +2495,7 @@ dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys", + "gio-sys 0.15.10", "glib-sys 0.15.10", "gobject-sys 0.15.10", "libc", @@ -2455,7 +2568,7 @@ dependencies = [ "quinn", "rand 0.8.5", "regex", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "serde_json 1.0.89", "socket2 0.3.19", @@ -2547,12 +2660,12 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#f54d69b35251ade110373403ddefcb8b49c87305" +source = "git+https://github.com/21pages/hwcodec#64f885b3787694b16dfcff08256750b0376b2eba" dependencies = [ "bindgen 0.59.2", "cc", "log", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "serde_json 1.0.89", ] @@ -2583,9 +2696,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59df7c4e19c950e6e0e868dcc0a300b09a9b88e9ec55bd879ca819087a77355d" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", @@ -2730,9 +2843,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" +checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded" [[package]] name = "itertools" @@ -2743,6 +2856,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" @@ -2847,9 +2969,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.137" +version = "0.2.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8" [[package]] name = "libdbus-sys" @@ -2979,7 +3101,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" dependencies = [ - "nix 0.23.1", + "nix 0.23.2", "winapi 0.3.9", ] @@ -3004,7 +3126,7 @@ dependencies = [ [[package]] name = "magnum-opus" version = "0.4.0" -source = "git+https://github.com/SoLongAndThanksForAllThePizza/magnum-opus#6247071a64af7b18e2d553e235729e6865f63ece" +source = "git+https://github.com/rustdesk/magnum-opus#79be072c939168e907fe851690759dcfd6a326af" dependencies = [ "bindgen 0.59.2", "target_build_utils", @@ -3104,6 +3226,15 @@ dependencies = [ "adler", ] +[[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.6.23" @@ -3180,7 +3311,7 @@ dependencies = [ [[package]] name = "mouce" version = "0.2.1" -source = "git+https://github.com/fufesou/mouce.git#aa18ba25bb47484282e972a4b95a8e1d753230b5" +source = "git+https://github.com/fufesou/mouce.git#ed83800d532b95d70e39915314f6052aa433e9b9" dependencies = [ "glob", ] @@ -3330,9 +3461,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ "bitflags", "cc", @@ -3343,9 +3474,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -3355,9 +3486,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" dependencies = [ "autocfg 1.1.0", "bitflags", @@ -3367,6 +3498,18 @@ dependencies = [ "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" @@ -3592,9 +3735,9 @@ dependencies = [ [[package]] name = "ordered-stream" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034ce384018b245e8d8424bbe90577fbd91a533be74107e465e3474eb2285eef" +checksum = "01ca8c99d73c6e92ac1358f9f692c22c0bfd9c4701fa086f5d365c0d4ea818ea" dependencies = [ "futures-core", "pin-project-lite", @@ -3676,7 +3819,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.4", + "parking_lot_core 0.9.5", ] [[package]] @@ -3695,9 +3838,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" dependencies = [ "cfg-if 1.0.0", "libc", @@ -3732,9 +3875,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.4.1" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528564cc62c19a7acac4d81e01f39e53e25e17b934878f4c6d25cc2836e62f8" +checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0" dependencies = [ "thiserror", "ucd-trie", @@ -3830,16 +3973,16 @@ dependencies = [ [[package]] name = "polling" -version = "2.4.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4609a838d88b73d8238967b60dd115cc08d38e2bbaf51ee1e4b695f89122e2" +checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", "libc", "log", "wepoll-ffi", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] @@ -4205,11 +4348,10 @@ dependencies = [ [[package]] name = "rayon" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" dependencies = [ - "crossbeam-deque", "either", "rayon-core", ] @@ -4229,7 +4371,7 @@ dependencies = [ [[package]] name = "rdev" version = "0.5.0-2" -source = "git+https://github.com/asur4s/rdev#fdcee04f10ea0ef00d36aa612eabb9605ae9f2fc" +source = "git+https://github.com/fufesou/rdev#1be26c7e8ed0d43cebdd8331d467bb61130a2e6e" dependencies = [ "cocoa", "core-foundation 0.9.3", @@ -4240,12 +4382,13 @@ dependencies = [ "inotify", "lazy_static", "libc", + "log", "mio 0.8.5", "strum 0.24.1", "strum_macros 0.24.3", "widestring 1.0.2", "winapi 0.3.9", - "x11 2.20.0", + "x11 2.20.1", ] [[package]] @@ -4259,9 +4402,9 @@ dependencies = [ [[package]] name = "realfft" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3052e66d6ebeff8049607775c41d39a58d1dfa91a2733e89f2b7816bce2ea4cc" +checksum = "93d6b8e8f0c6d2234aa58048d7290c60bf92cd36fd2888cd8331c66ad4f2e1d2" dependencies = [ "rustfft", ] @@ -4347,7 +4490,7 @@ dependencies = [ "pin-project-lite", "rustls", "rustls-pemfile 1.0.1", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", "serde_urlencoded", "tokio", @@ -4389,9 +4532,20 @@ dependencies = [ [[package]] name = "rpassword" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c9f5d2a0c3e2ea729ab3706d22217177770654c3ef5056b68b69d07332d3f5" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +dependencies = [ + "libc", + "rtoolbox", + "winapi 0.3.9", +] + +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" dependencies = [ "libc", "winapi 0.3.9", @@ -4475,6 +4629,7 @@ dependencies = [ "arboard", "async-process", "async-trait", + "backtrace", "base64", "bytes", "cc", @@ -4500,6 +4655,7 @@ dependencies = [ "flexi_logger", "flutter_rust_bridge", "flutter_rust_bridge_codegen", + "glib 0.16.5", "gtk", "hbb_common", "hound", @@ -4521,14 +4677,14 @@ dependencies = [ "rdev", "repng", "reqwest", - "rpassword 7.1.0", + "rpassword 7.2.0", "rubato", "runas", "rust-pulsectl", "samplerate", "sciter-rs", "scrap", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "serde_json 1.0.89", "sha2", @@ -4705,7 +4861,7 @@ dependencies = [ "num_cpus", "quest", "repng", - "serde 1.0.147", + "serde 1.0.149", "serde_json 1.0.89", "target_build_utils", "tracing", @@ -4767,7 +4923,7 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -4787,18 +4943,18 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4" dependencies = [ "proc-macro2", "quote", @@ -4825,7 +4981,7 @@ checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa 1.0.4", "ryu", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -4848,7 +5004,7 @@ dependencies = [ "form_urlencoded", "itoa 1.0.4", "ryu", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -4859,7 +5015,7 @@ checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ "indexmap", "ryu", - "serde 1.0.147", + "serde 1.0.149", "yaml-rust", ] @@ -4893,7 +5049,7 @@ checksum = "ba8593196da75d9dc4f69349682bd4c2099f8cde114257d1ef7ef1b33d1aba54" dependencies = [ "cfg-if 1.0.0", "libc", - "nix 0.23.1", + "nix 0.23.2", "rand 0.8.5", "win-sys", ] @@ -4941,7 +5097,7 @@ version = "0.1.0" dependencies = [ "confy", "hbb_common", - "serde 1.0.147", + "serde 1.0.149", "serde_derive", "walkdir", ] @@ -5016,7 +5172,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -5061,30 +5217,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" @@ -5124,9 +5256,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" +checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908" dependencies = [ "proc-macro2", "quote", @@ -5298,7 +5430,7 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "tfc" version = "0.6.1" -source = "git+https://github.com/asur4s/The-Fat-Controller#48303c5dacded6ea1873bc5d69bdde3175cf336a" +source = "git+https://github.com/fufesou/The-Fat-Controller#a5f13e6ef80327eb8d860aeb26b0af93eb5aee2b" dependencies = [ "core-graphics 0.22.3", "unicode-segmentation", @@ -5348,9 +5480,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", "wasi 0.10.0+wasi-snapshot-preview1", @@ -5392,9 +5524,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3" +checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46" dependencies = [ "autocfg 1.1.0", "bytes", @@ -5407,14 +5539,14 @@ dependencies = [ "signal-hook-registry", "socket2 0.4.7", "tokio-macros", - "winapi 0.3.9", + "windows-sys 0.42.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", @@ -5472,9 +5604,15 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", ] +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "tower-service" version = "0.3.2" @@ -5558,9 +5696,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "ucd-trie" @@ -5632,7 +5770,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", - "serde 1.0.147", + "serde 1.0.149", ] [[package]] @@ -5689,9 +5827,9 @@ dependencies = [ [[package]] name = "vswhom-sys" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" dependencies = [ "cc", "libc", @@ -5811,7 +5949,7 @@ dependencies = [ "bitflags", "downcast-rs", "libc", - "nix 0.24.2", + "nix 0.24.3", "scoped-tls", "wayland-commons", "wayland-scanner", @@ -5824,7 +5962,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" dependencies = [ - "nix 0.24.2", + "nix 0.24.3", "once_cell", "smallvec", "wayland-sys", @@ -5836,7 +5974,7 @@ version = "0.29.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" dependencies = [ - "nix 0.24.2", + "nix 0.24.3", "wayland-client", "xcursor", ] @@ -5915,9 +6053,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] @@ -6382,9 +6520,9 @@ dependencies = [ [[package]] name = "x11" -version = "2.20.0" +version = "2.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7ae97874a928d821b061fce3d1fc52f08071dd53c89a6102bc06efcac3b2908" +checksum = "c2638d5b9c17ac40575fb54bb461a4b1d2a8d1b4ffcc4ff237d254ec59ddeb82" dependencies = [ "libc", "pkg-config", @@ -6392,9 +6530,9 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.20.0" +version = "2.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c83627bc137605acc00bb399c7b908ef460b621fc37c953db2b09f88c449ea6" +checksum = "b1536d6965a5d4e573c7ef73a2c15ebcd0b2de3347bdf526c34c297c00ac40f0" dependencies = [ "lazy_static", "libc", @@ -6439,9 +6577,9 @@ dependencies = [ [[package]] name = "zbus" -version = "3.5.0" +version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25ae891bd547674b368906552115143031c16c23a0f2f4b2f5f5436ab2e6a9f" +checksum = "938ea6da98c75c2c37a86007bd17fd8e208cbec24e086108c87ece98e9edec0d" dependencies = [ "async-broadcast", "async-channel", @@ -6460,11 +6598,11 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix 0.25.0", + "nix 0.25.1", "once_cell", "ordered-stream", "rand 0.8.5", - "serde 1.0.147", + "serde 1.0.149", "serde_repr", "sha1", "static_assertions", @@ -6478,9 +6616,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "3.5.0" +version = "3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aa37701ce7b3a43632d2b0ad9d4aef602b46be6bdd7fba3b7c5007f9f6eb2c2" +checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2", @@ -6491,11 +6629,11 @@ dependencies = [ [[package]] name = "zbus_names" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69bb79b44e1901ed8b217e485d0f01991aec574479b68cb03415f142bc7ae67" +checksum = "6c737644108627748a660d038974160e0cbb62605536091bdfa28fd7f64d43c8" dependencies = [ - "serde 1.0.147", + "serde 1.0.149", "static_assertions", "zvariant", ] @@ -6531,23 +6669,23 @@ dependencies = [ [[package]] name = "zvariant" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c817f416f05fcbc833902f1e6064b72b1778573978cfeac54731451ccc9e207" +checksum = "56f8c89c183461e11867ded456db252eae90874bc6769b7adbea464caa777e51" dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.147", + "serde 1.0.149", "static_assertions", "zvariant_derive", ] [[package]] name = "zvariant_derive" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd24fffd02794a76eb10109de463444064c88f5adb9e9d1a78488adc332bfef" +checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81" dependencies = [ "proc-macro-crate 1.2.1", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index fd84b73aa..1e9af30e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ libc = "0.2" parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" } flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] } runas = "0.2" -magnum-opus = { git = "https://github.com/SoLongAndThanksForAllThePizza/magnum-opus" } +magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" } dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true } rubato = { version = "0.12", optional = true } samplerate = { version = "0.2", optional = true } @@ -59,11 +59,11 @@ base64 = "0.13" sysinfo = "0.24" num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } -default-net = "0.11.0" +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 } +flutter_rust_bridge = { version = "1.61.1", optional = true } errno = "0.2.8" -rdev = { git = "https://github.com/asur4s/rdev" } +rdev = { git = "https://github.com/fufesou/rdev" } url = { version = "2.1", features = ["serde"] } reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"], default-features=false } @@ -118,13 +118,15 @@ dbus = "0.9" dbus-crossroads = "0.5" gtk = "0.15" libappindicator = "0.7" +glib = "0.16.5" +backtrace = "0.3" [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"] @@ -142,7 +144,7 @@ winapi = { version = "0.3", features = [ "winnt" ] } cc = "1.0" hbb_common = { path = "libs/hbb_common" } simple_rc = { path = "libs/simple_rc", optional = true } -flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } +flutter_rust_bridge_codegen = "1.61.1" [dev-dependencies] hound = "3.5" @@ -151,9 +153,9 @@ hound = "3.5" name = "RustDesk" 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", "libappindicator3-1", "libvdpau1", "libva2"] +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"] +resources = ["res/mac-tray-light.png","res/mac-tray-dark.png", "res/mac-tray-light-x2.png","res/mac-tray-dark-x2.png"] #https://github.com/johnthagen/min-sized-rust [profile.release] @@ -162,4 +164,4 @@ codegen-units = 1 panic = 'abort' strip = true #opt-level = 'z' # only have smaller size after strip -rpath = true \ No newline at end of file +rpath = true diff --git a/README.md b/README.md index 2fd744429..bc9bacf19 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Below are the servers you are using for free, it may change along the time. If y | 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 | ## Dependencies @@ -70,7 +71,7 @@ Please download sciter dynamic library yourself. ```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 libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ``` ### openSUSE Tumbleweed @@ -197,7 +198,7 @@ Please ensure that you are running these commands from the root of the RustDesk - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Communicate with [rustdesk-server](https://github.com/rustdesk/rustdesk-server), wait for remote direct (TCP hole punching) or relayed connection - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform specific code - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter code for mobile -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript for Flutter web client +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter web client ## Snapshot diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml new file mode 100644 index 000000000..f3cd8f568 --- /dev/null +++ b/appimage/AppImageBuilder-aarch64.yml @@ -0,0 +1,85 @@ +# appimage-builder recipe see https://appimage-builder.readthedocs.io for details +version: 1 +script: + - rm -rf ./AppDir || true + - bsdtar -zxvf ../rustdesk-1.2.0.deb + - tar -xvf ./data.tar.xz + - mkdir ./AppDir + - mv ./usr ./AppDir/usr + # 32x32 icon + - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done + # desktop file + # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop + - rm -rf ./AppDir/usr/share/applications +AppDir: + path: ./AppDir + app_info: + id: rustdesk + name: rustdesk + icon: rustdesk + version: 1.2.0 + exec: usr/lib/rustdesk/rustdesk + exec_args: $@ + apt: + arch: + - arm64 + allow_unauthenticated: true + sources: + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-updates main restricted universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + - sourceline: deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports/ bionic-backports main restricted + universe multiverse + key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x3b4fe6acc0b21f32' + include: + - libc6 + - libgtk-3-0 + - libxcb-randr0 + - libxdo3 + - libxfixes3 + - libxcb-shape0 + - libxcb-xfixes0 + - libasound2 + - libsystemd0 + - curl + - libva-drm2 + - libva-x11-2 + - libvdpau1 + - libgstreamer-plugins-base1.0-0 + exclude: + - humanity-icon-theme + - hicolor-icon-theme + - adwaita-icon-theme + - ubuntu-mono + files: + include: [] + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + runtime: + env: + GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + GDK_BACKEND: x11 + test: + fedora-30: + image: appimagecrafters/tests-env:fedora-30 + command: ./AppRun + debian-stable: + image: appimagecrafters/tests-env:debian-stable + command: ./AppRun + archlinux-latest: + image: appimagecrafters/tests-env:archlinux-latest + command: ./AppRun + centos-7: + image: appimagecrafters/tests-env:centos-7 + command: ./AppRun + ubuntu-xenial: + image: appimagecrafters/tests-env:ubuntu-xenial + command: ./AppRun +AppImage: + arch: aarch64 + update-information: guess diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml new file mode 100644 index 000000000..59dd5164f --- /dev/null +++ b/appimage/AppImageBuilder-x86_64.yml @@ -0,0 +1,88 @@ +# appimage-builder recipe see https://appimage-builder.readthedocs.io for details +version: 1 +script: + - rm -rf ./AppDir || true + - bsdtar -zxvf ../rustdesk-1.2.0.deb + - tar -xvf ./data.tar.xz + - mkdir ./AppDir + - mv ./usr ./AppDir/usr + # 32x32 icon + - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done + # desktop file + # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop + - rm -rf ./AppDir/usr/share/applications +AppDir: + path: ./AppDir + app_info: + id: rustdesk + name: rustdesk + icon: rustdesk + version: 1.2.0 + exec: usr/lib/rustdesk/rustdesk + exec_args: $@ + apt: + arch: + - amd64 + allow_unauthenticated: true + sources: + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates main restricted + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates universe + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-updates multiverse + - sourceline: deb http://archive.ubuntu.com/ubuntu/ bionic-backports main restricted + universe multiverse + - sourceline: deb http://ppa.launchpad.net/pipewire-debian/pipewire-upstream/ubuntu + bionic main + include: + - libc6:amd64 + - libgtk-3-0 + - libxcb-randr0 + - libxdo3 + - libxfixes3 + - libxcb-shape0 + - libxcb-xfixes0 + - libasound2 + - libsystemd0 + - curl + - libva-drm2 + - libva-x11-2 + - libvdpau1 + - libgstreamer-plugins-base1.0-0 + exclude: + - humanity-icon-theme + - hicolor-icon-theme + - adwaita-icon-theme + - ubuntu-mono + files: + include: [] + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + runtime: + env: + GIO_MODULE_DIR: $APPDIR/usr/lib/x86_64-linux-gnu/gio/modules/ + GDK_BACKEND: x11 + test: + fedora-30: + image: appimagecrafters/tests-env:fedora-30 + command: ./AppRun + debian-stable: + image: appimagecrafters/tests-env:debian-stable + command: ./AppRun + archlinux-latest: + image: appimagecrafters/tests-env:archlinux-latest + command: ./AppRun + centos-7: + image: appimagecrafters/tests-env:centos-7 + command: ./AppRun + ubuntu-xenial: + image: appimagecrafters/tests-env:ubuntu-xenial + command: ./AppRun +AppImage: + arch: x86_64 + update-information: guess diff --git a/build.py b/build.py index 42438909f..6b107ff4b 100755 --- a/build.py +++ b/build.py @@ -8,6 +8,7 @@ import urllib.request import shutil import hashlib import argparse +import sys windows = platform.platform().startswith('Windows') osx = platform.platform().startswith( @@ -17,6 +18,14 @@ exe_path = 'target/release/' + hbb_name flutter_win_target_dir = 'flutter/build/windows/runner/Release/' skip_cargo = False +def custom_os_system(cmd): + err = os._system(cmd) + if err != 0: + print(f"Error occurred when executing: {cmd}. Exiting.") + sys.exit(-1) +# replace prebuilt os.system +os._system = os.system +os.system = custom_os_system def get_version(): with open("Cargo.toml", encoding="utf-8") as fh: @@ -29,13 +38,15 @@ def get_version(): def parse_rc_features(feature): available_features = { 'IddDriver': { - 'zip_url': 'https://github.com/fufesou/RustDeskIddDriver/releases/download/v0.1/RustDeskIddDriver_x64_pic_en.zip', - 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5', + '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'], }, 'PrivacyMode': { 'zip_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1' '/TempTopMostWindow_x64_pic_en.zip', 'checksum_url': 'https://github.com/fufesou/RustDeskTempTopMostWindow/releases/download/v0.1/checksum_md5', + 'include': ['WindowInjection.dll'], } } apply_features = {} @@ -88,6 +99,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', @@ -133,8 +149,9 @@ def generate_build_script_for_docker(): def download_extract_features(features, res_dir): - proxy = '' + import re + proxy = '' def req(url): if not proxy: return url @@ -145,6 +162,11 @@ def download_extract_features(features, res_dir): return r for (feat, feat_info) in features.items(): + includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else [] + includes = [ re.compile(p) for p in includes ] + excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else [] + excludes = [ re.compile(p) for p in excludes ] + print(f'{feat} download begin') download_filename = feat_info['zip_url'].split('/')[-1] checksum_md5_response = urllib.request.urlopen( @@ -161,7 +183,22 @@ def download_extract_features(features, res_dir): zip_file = zipfile.ZipFile(filename) zip_list = zip_file.namelist() for f in zip_list: - zip_file.extract(f, res_dir) + file_exclude = False + for p in excludes: + if p.match(f) is not None: + file_exclude = True + break + if file_exclude: + continue + + file_include = False if includes else True + for p in includes: + if p.match(f) is not None: + file_include = True + break + if file_include: + print(f'extract file {f}') + zip_file.extract(f, res_dir) zip_file.close() os.remove(download_filename) print(f'{feat} extract end') @@ -204,6 +241,8 @@ def get_features(args): features.append('flutter') if args.flatpak: features.append('flatpak') + if args.appimage: + features.append('appimage') print("features:", features) return features @@ -217,7 +256,7 @@ Version: %s Architecture: amd64 Maintainer: open-trade Homepage: https://rustdesk.com -Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libappindicator3-1, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0 +Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0 Description: A remote control software. """ % version @@ -243,7 +282,7 @@ def build_flutter_deb(version, features): os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') os.system('mkdir -p tmpdeb/usr/share/applications/') os.system('mkdir -p tmpdeb/usr/share/polkit-1/actions') - os.system('rm tmpdeb/usr/bin/rustdesk') + os.system('rm tmpdeb/usr/bin/rustdesk || true') os.system( 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') os.system( @@ -273,7 +312,8 @@ def build_flutter_deb(version, features): 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 + os.system(f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') # copy dylib os.system( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") @@ -437,6 +477,7 @@ def main(): if pa: os.system(''' # buggy: rcodesign sign ... path/*, have to sign one by one + # install rcodesign via cargo install apple-codesign #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app @@ -449,12 +490,18 @@ def main(): version, 'rustdesk-%s.dmg' % version) if pa: os.system(''' + # https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html + # https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html + # https://developer.apple.com/developer-id/ + # goto xcode and login with apple id, manager certificates (Developer ID Application and/or Developer ID Installer) online there (only download and double click (install) cer file can not export p12 because no private key) #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg - # https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html - rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg + # 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)) + '''.format(pa, version, os.environ.get('api-issuer'), os.environ.get('api-key'))) else: print('Not signed') else: diff --git a/build.rs b/build.rs index 67e40752c..d15f27424 100644 --- a/build.rs +++ b/build.rs @@ -1,9 +1,16 @@ #[cfg(windows)] fn build_windows() { - cc::Build::new().file("src/windows.cc").compile("windows"); + let file = "src/platform/windows.cc"; + cc::Build::new().file(file).compile("windows"); println!("cargo:rustc-link-lib=WtsApi32"); - println!("cargo:rerun-if-changed=build.rs"); - println!("cargo:rerun-if-changed=windows.cc"); + println!("cargo:rerun-if-changed={}", file); +} + +#[cfg(target_os = "macos")] +fn build_mac() { + let file = "src/platform/macos.mm"; + cc::Build::new().file(file).compile("macos"); + println!("cargo:rerun-if-changed={}", file); } #[cfg(all(windows, feature = "inline"))] @@ -78,26 +85,35 @@ fn install_oboe() { #[cfg(feature = "flutter")] fn gen_flutter_rust_bridge() { + use lib_flutter_rust_bridge_codegen::{ + config_parse, frb_codegen, get_symbols_if_no_duplicates, RawOpts, + }; let llvm_path = match std::env::var("LLVM_HOME") { Ok(path) => Some(vec![path]), Err(_) => None, }; // Tell Cargo that if the given file changes, to rerun this build script. println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); - // settings for fbr_codegen - let opts = lib_flutter_rust_bridge_codegen::Opts { + // Options for frb_codegen + let raw_opts = RawOpts { // Path of input Rust code - rust_input: "src/flutter_ffi.rs".to_string(), + rust_input: vec!["src/flutter_ffi.rs".to_string()], // Path of output generated Dart code - dart_output: "flutter/lib/generated_bridge.dart".to_string(), + dart_output: vec!["flutter/lib/generated_bridge.dart".to_string()], // Path of output generated C header c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]), - // for other options lets use default + /// Path to the installed LLVM llvm_path, + // for other options use defaults ..Default::default() }; - // run fbr_codegen - lib_flutter_rust_bridge_codegen::frb_codegen(opts).unwrap(); + // get opts from raw opts + let configs = config_parse(raw_opts); + // generation of rust api for ffi + let all_symbols = get_symbols_if_no_duplicates(&configs).unwrap(); + for config in configs.iter() { + frb_codegen(config, &all_symbols).unwrap(); + } } fn main() { @@ -117,5 +133,8 @@ fn main() { #[cfg(windows)] build_windows(); #[cfg(target_os = "macos")] + build_mac(); + #[cfg(target_os = "macos")] println!("cargo:rustc-link-lib=framework=ApplicationServices"); + println!("cargo:rerun-if-changed=build.rs"); } diff --git a/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/README-FA.md b/docs/README-FA.md index d86c82836..02b156dbb 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) [![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/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 966ad3df8..f78b3a20b 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -2,9 +2,9 @@ An open-source remote desktop application, the open source TeamViewer alternativ Source code: https://github.com/rustdesk/rustdesk Doc: https://rustdesk.com/docs/en/manual/mobile/ -In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Addroid remote control. +In order for a remote device to control your Android device via mouse or touch, you need to allow RustDesk to use the "Accessibility" service, RustDesk uses AccessibilityService API to implement Android remote control. -In addtion to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. +In addition to remote control, you can also transfer files between Android devices and PCs easily with RustDesk. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, or self-hosting, or write your own rendezvous/relay server. Self-hosting server is free and open source: https://github.com/rustdesk/rustdesk-server diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png index 543fe8346..3668c7106 100644 Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png index 32e7b3554..e84ed4d21 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png index 0f9368545..5a83dc1f0 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png index b59279552..629631ac7 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png index a4048ae69..39a15ba77 100644 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png index 5d726ab36..5574ee7dc 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png index 2c3fad113..8e0a83a6a 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png index 5873757f9..0618ae0b6 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png differ diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png index faea2eb16..560902b03 100644 Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png differ diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 000000000..effb820d6 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,11 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. +Code source : https://github.com/rustdesk/rustdesk +Doc : https://rustdesk.com/docs/en/manual/mobile/ + +Pour qu'un appareil distant puisse contrôler votre appareil Android via la souris ou le toucher, vous devez autoriser RustDesk à utiliser le service "Accessibilité", RustDesk utilise l'API AccessibilityService pour implémenter la télécommande Addroid. + +En plus du contrôle à distance, vous pouvez également transférer facilement des fichiers entre des appareils Android et des PC avec RustDesk. + +Vous avez le contrôle total de vos données, sans aucun souci de sécurité. Vous pouvez utiliser notre serveur de rendez-vous/relais, ou l'auto-hébergement, ou écrire votre propre serveur de rendez-vous/relais. Le serveur auto-hébergé est gratuit et open source : https://github.com/rustdesk/rustdesk-server + +Veuillez télécharger et installer la version de bureau à partir de : https://rustdesk.com, vous pourrez alors accéder et contrôler votre bureau à partir de votre mobile, ou contrôler votre mobile à partir du bureau. diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 000000000..e1f4b4b0f --- /dev/null +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Une application de bureau à distance open source, l'alternative open source à TeamViewer. diff --git a/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/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index b3dc255d5..d5d2c49c8 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-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index f24291dbf..e30cc5019 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-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 8a4e80f53..41ccba607 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-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 8bb8d570f..c10349d71 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-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 2130526a6..52fde7830 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/assets/kb_layout_iso.svg b/flutter/assets/kb_layout_iso.svg new file mode 100644 index 000000000..69f0c96cb --- /dev/null +++ b/flutter/assets/kb_layout_iso.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/kb_layout_not_iso.svg b/flutter/assets/kb_layout_not_iso.svg new file mode 100644 index 000000000..09a055be3 --- /dev/null +++ b/flutter/assets/kb_layout_not_iso.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/flutter/assets/logo.png b/flutter/assets/logo.png index f854a1dc3..ede0e00c4 100644 Binary files a/flutter/assets/logo.png and b/flutter/assets/logo.png differ diff --git a/flutter/build_android_deps.sh b/flutter/build_android_deps.sh index f120346cf..a30abd154 100755 --- a/flutter/build_android_deps.sh +++ b/flutter/build_android_deps.sh @@ -1,7 +1,7 @@ #!/bin/bash -# Build libyuv / opus / libvpx / oboe for Android -# Required: +# Build libyuv / opus / libvpx / oboe for Android +# Required: # 1. set VCPKG_ROOT / ANDROID_NDK path environment variables # 2. vcpkg initialized # 3. ndk, version: 22 (if ndk < 22 you need to change LD as `export LD=$TOOLCHAIN/bin/$NDK_LLVM_TARGET-ld`) @@ -23,7 +23,7 @@ HOST_TAG="linux-x86_64" # current platform, set as `ls $ANDROID_NDK/toolchains/l TOOLCHAIN=$ANDROID_NDK/toolchains/llvm/prebuilt/$HOST_TAG function build { - ANDROID_ABI=$1 + ANDROID_ABI=$1 VCPKG_TARGET=$2 NDK_LLVM_TARGET=$3 LIBVPX_TARGET=$4 @@ -111,15 +111,15 @@ patch -N -d build/oboe -p1 < ../src/oboe.patch # x86_64-linux-android # i686-linux-android -# LIBVPX_TARGET : -# arm64-android-gcc -# armv7-android-gcc +# LIBVPX_TARGET : +# arm64-android-gcc +# armv7-android-gcc # x86_64-android-gcc -# x86-android-gcc +# x86-android-gcc # args: ANDROID_ABI VCPKG_TARGET NDK_LLVM_TARGET LIBVPX_TARGET build arm64-v8a arm64-android aarch64-linux-android arm64-android-gcc -build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc +build armeabi-v7a arm-android arm-linux-androideabi armv7-android-gcc # rm -rf build/libvpx # rm -rf build/oboe \ No newline at end of file diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index d4ae9af18..c35862a8c 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 61c13f60b..900bd13fa 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 ed67c27ad..5fc34ce9a 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 f974850af..ab315a4c6 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 ff419814d..6d69c01e1 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 b3cdc7920..b6c8034cd 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 15b2e086d..cf6c7c775 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 ed67c27ad..5fc34ce9a 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 94665b74e..6928a4e6d 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 3cf8a0dc2..a13129e15 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 3cf8a0dc2..a13129e15 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 6e21d8ee9..319e70f91 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 01047f4be..229bdf563 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 ceb721793..caffb26a3 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 e416e5517..751104548 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 63ab39df2..f4e0c2d75 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -46,7 +46,7 @@ var isWebDesktop = false; var version = ""; int androidVersion = 0; -/// only avaliable for Windows target +/// only available for Windows target int windowsBuildNumber = 0; DesktopType? desktopType; @@ -99,22 +99,28 @@ class IconFont { class ColorThemeExtension extends ThemeExtension { const ColorThemeExtension({ required this.border, + required this.highlight, }); final Color? border; + final Color? highlight; static const light = ColorThemeExtension( border: Color(0xFFCCCCCC), + highlight: Color(0xFFE5E5E5), ); static const dark = ColorThemeExtension( border: Color(0xFF555555), + highlight: Color(0xFF3F3F3F), ); @override - ThemeExtension copyWith({Color? border}) { + ThemeExtension copyWith( + {Color? border, Color? highlight}) { return ColorThemeExtension( border: border ?? this.border, + highlight: highlight ?? this.highlight, ); } @@ -126,6 +132,7 @@ class ColorThemeExtension extends ThemeExtension { } return ColorThemeExtension( border: Color.lerp(border, other.border, t), + highlight: Color.lerp(highlight, other.highlight, t), ); } } @@ -215,18 +222,15 @@ class MyTheme { } static void changeDarkMode(ThemeMode mode) { - final preference = getThemeModePreference(); - if (preference != mode) { + Get.changeThemeMode(mode); + if (desktopType == DesktopType.main) { if (mode == ThemeMode.system) { bind.mainSetLocalOption(key: kCommConfKeyTheme, value: ''); } else { bind.mainSetLocalOption( key: kCommConfKeyTheme, value: mode.toShortString()); } - Get.changeThemeMode(mode); - if (desktopType == DesktopType.main) { - bind.mainChangeTheme(dark: currentThemeMode().toShortString()); - } + bind.mainChangeTheme(dark: mode.toShortString()); } } @@ -545,6 +549,10 @@ class OverlayDialogManager { hideMobileActionsOverlay(); } } + + bool existing(String tag) { + return _dialogs.keys.contains(tag); + } } void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { @@ -605,8 +613,9 @@ class CustomAlertDialog extends StatelessWidget { Future.delayed(Duration.zero, () { if (!focusNode.hasFocus) focusNode.requestFocus(); }); - return Focus( - focusNode: focusNode, + FocusScopeNode scopeNode = FocusScopeNode(); + return FocusScope( + node: scopeNode, autofocus: true, onKey: (node, key) { if (key.logicalKey == LogicalKeyboardKey.escape) { @@ -618,6 +627,11 @@ class CustomAlertDialog extends StatelessWidget { 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(); + } + return KeyEventResult.handled; } return KeyEventResult.ignored; }, @@ -626,8 +640,14 @@ class CustomAlertDialog extends StatelessWidget { title: title, contentPadding: EdgeInsets.symmetric( horizontal: contentPadding ?? 25, vertical: 10), - content: - ConstrainedBox(constraints: contentBoxConstraints, child: content), + content: ConstrainedBox( + constraints: contentBoxConstraints, + child: Theme( + data: ThemeData( + inputDecorationTheme: InputDecorationTheme( + isDense: true, contentPadding: EdgeInsets.all(15)), + ), + child: content)), actions: actions, ), ); @@ -660,24 +680,25 @@ 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 (link.isNotEmpty) { - buttons.insert(0, msgBoxButton(translate('JumpLink'), jumplink)); + buttons.insert(0, dialogButton('JumpLink', onPressed: jumplink)); } dialogManager.show( (setState, close) => CustomAlertDialog( @@ -926,7 +947,8 @@ bool option2bool(String option, String value) { } else if (option.startsWith("allow-") || option == "stop-service" || option == "direct-server" || - option == "stop-rendezvous-service") { + option == "stop-rendezvous-service" || + option == "force-always-relay") { res = value == "Y"; } else { assert(false); @@ -942,7 +964,8 @@ String bool2option(String option, bool b) { } else if (option.startsWith('allow-') || option == "stop-service" || option == "direct-server" || - option == "stop-rendezvous-service") { + option == "stop-rendezvous-service" || + option == "force-always-relay") { res = b ? 'Y' : ''; } else { assert(false); @@ -971,11 +994,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); } @@ -1014,7 +1039,7 @@ class LastWindowPosition { return LastWindowPosition(m["width"], m["height"], m["offsetWidth"], m["offsetHeight"], m["isMaximized"]); } catch (e) { - debugPrint(e.toString()); + debugPrintStack(label: e.toString()); return null; } } @@ -1148,7 +1173,7 @@ Future restoreWindowPosition(WindowType type, {int? windowId}) async { final pos = bind.getLocalFlutterConfig(k: kWindowPrefix + type.name); var lpos = LastWindowPosition.loadFromString(pos); if (lpos == null) { - debugPrint("window position saved, but cannot be parsed"); + debugPrint("no window position saved, ignoring position restoration"); return false; } @@ -1213,7 +1238,7 @@ Future initUniLinks() async { } parseRustdeskUri(initialLink); } catch (err) { - debugPrint("$err"); + debugPrintStack(label: "$err"); } } @@ -1236,23 +1261,28 @@ StreamSubscription? listenUniLinks() { /// Returns true if we successfully handle the startup arguments. bool checkArguments() { + // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05] // check connect args - final connectIndex = bootArgs.indexOf("--connect"); + final connectIndex = kBootArgs.indexOf("--connect"); if (connectIndex == -1) { return false; } - String? arg = - bootArgs.length < connectIndex + 1 ? null : bootArgs[connectIndex + 1]; - if (arg != null) { - if (arg.startsWith(kUniLinksPrefix)) { - return parseRustdeskUri(arg); + String? id = + kBootArgs.length < connectIndex + 1 ? null : kBootArgs[connectIndex + 1]; + final switchUuidIndex = kBootArgs.indexOf("--switch_uuid"); + String? switchUuid = kBootArgs.length < switchUuidIndex + 1 + ? null + : kBootArgs[switchUuidIndex + 1]; + if (id != null) { + if (id.startsWith(kUniLinksPrefix)) { + return parseRustdeskUri(id); } else { // remove "--connect xxx" in the `bootArgs` array - bootArgs.removeAt(connectIndex); - bootArgs.removeAt(connectIndex); + kBootArgs.removeAt(connectIndex); + kBootArgs.removeAt(connectIndex); // fallback to peer id Future.delayed(Duration.zero, () { - rustDeskWinManager.newRemoteDesktop(arg); + rustDeskWinManager.newRemoteDesktop(id, switch_uuid: switchUuid); }); return true; } @@ -1282,19 +1312,34 @@ 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; + return false; } return false; } +connectMainDesktop(String id, + {required bool isFileTransfer, + required bool isTcpTunneling, + required bool isRDP}) async { + if (isFileTransfer) { + await rustDeskWinManager.newFileTransfer(id); + } else if (isTcpTunneling || isRDP) { + await rustDeskWinManager.newPortForward(id, isRDP); + } else { + await rustDeskWinManager.newRemoteDesktop(id); + } +} + /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. /// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isRDP], starts a session only for rdp. -void connect(BuildContext context, String id, +connect(BuildContext context, String id, {bool isFileTransfer = false, bool isTcpTunneling = false, bool isRDP = false}) async { @@ -1304,12 +1349,20 @@ void connect(BuildContext context, String id, "more than one connect type"); if (isDesktop) { - if (isFileTransfer) { - await rustDeskWinManager.newFileTransfer(id); - } else if (isTcpTunneling || isRDP) { - await rustDeskWinManager.newPortForward(id, isRDP); + if (desktopType == DesktopType.main) { + await connectMainDesktop( + id, + isFileTransfer: isFileTransfer, + isTcpTunneling: isTcpTunneling, + isRDP: isRDP, + ); } else { - await rustDeskWinManager.newRemoteDesktop(id); + await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { + 'id': id, + 'isFileTransfer': isFileTransfer, + 'isTcpTunneling': isTcpTunneling, + 'isRDP': isRDP, + }); } } else { if (isFileTransfer) { @@ -1340,13 +1393,13 @@ void connect(BuildContext context, String id, } } -Future> getHttpHeaders() async { +Map getHttpHeaders() { return { 'Authorization': 'Bearer ${bind.mainGetLocalOption(key: 'access_token')}' }; } -// Simple wrapper of built-in types for refrence use. +// Simple wrapper of built-in types for reference use. class SimpleWrapper { T value; SimpleWrapper(this.value); @@ -1382,7 +1435,7 @@ Future reloadAllWindows() async { /// Indicate the flutter app is running in portable mode. /// /// [Note] -/// Portable build is only avaliable on Windows. +/// Portable build is only available on Windows. bool isRunningInPortableMode() { if (!Platform.isWindows) { return false; @@ -1391,7 +1444,7 @@ bool isRunningInPortableMode() { } /// Window status callback -void onActiveWindowChanged() async { +Future onActiveWindowChanged() async { print( "[MultiWindowHandler] active window changed: ${rustDeskWinManager.getActiveWindows()}"); if (rustDeskWinManager.getActiveWindows().isEmpty) { @@ -1402,7 +1455,7 @@ void onActiveWindowChanged() async { rustDeskWinManager.closeAllSubWindows() ]); } catch (err) { - debugPrint("$err"); + debugPrintStack(label: "$err"); } finally { await windowManager.setPreventClose(false); await windowManager.close(); @@ -1483,3 +1536,104 @@ Pointer getOSVERSIONINFOEXPointer() { bool get kUseCompatibleUiMode => Platform.isWindows && const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion); + +class ServerConfig { + late String idServer; + late String relayServer; + late String apiServer; + late String key; + + ServerConfig( + {String? idServer, String? relayServer, String? apiServer, String? key}) { + this.idServer = idServer?.trim() ?? ''; + this.relayServer = relayServer?.trim() ?? ''; + this.apiServer = apiServer?.trim() ?? ''; + this.key = key?.trim() ?? ''; + } + + /// decode from shared string (from user shared or rustdesk-server generated) + /// also see [encode] + /// throw when decoding failure + ServerConfig.decode(String msg) { + final input = msg.split('').reversed.join(''); + final bytes = base64Decode(base64.normalize(input)); + final json = jsonDecode(utf8.decode(bytes)); + + idServer = json['host'] ?? ''; + relayServer = json['relay'] ?? ''; + apiServer = json['api'] ?? ''; + key = json['key'] ?? ''; + } + + /// encode to shared string + /// also see [ServerConfig.decode] + String encode() { + Map config = {}; + config['host'] = idServer.trim(); + config['relay'] = relayServer.trim(); + config['api'] = apiServer.trim(); + config['key'] = key.trim(); + return base64Encode(Uint8List.fromList(jsonEncode(config).codeUnits)) + .split('') + .reversed + .join(); + } + + /// from local options + ServerConfig.fromOptions(Map options) + : idServer = options['custom-rendezvous-server'] ?? "", + relayServer = options['relay-server'] ?? "", + apiServer = options['api-server'] ?? "", + key = options['key'] ?? ""; +} + +Widget dialogButton(String text, + {required VoidCallback? onPressed, + bool isOutline = false, + TextStyle? style}) { + if (isDesktop) { + if (isOutline) { + return OutlinedButton( + onPressed: onPressed, + child: Text(translate(text), style: style), + ); + } else { + return ElevatedButton( + style: ElevatedButton.styleFrom(elevation: 0), + onPressed: onPressed, + child: Text(translate(text), style: style), + ); + } + } else { + return TextButton( + onPressed: onPressed, + child: Text( + translate(text), + style: style, + )); + } +} + +int version_cmp(String v1, String v2) { + return bind.versionToNumber(v: v1) - bind.versionToNumber(v: v2); +} + +String getWindowName({WindowType? overrideType}) { + switch (overrideType ?? kWindowType) { + case WindowType.Main: + return "RustDesk"; + case WindowType.FileTransfer: + return "File Transfer - RustDesk"; + case WindowType.PortForward: + return "Port Forward - RustDesk"; + case WindowType.RemoteDesktop: + return "Remote Desktop - RustDesk"; + default: + break; + } + return "RustDesk"; +} + +String getWindowNameWithId(String id, {WindowType? overrideType}) { + return "${DesktopTab.labelGetterAlias(id).value} - ${getWindowName(overrideType: overrideType)}"; +} diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart new file mode 100644 index 000000000..27238db67 --- /dev/null +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -0,0 +1,119 @@ +import 'package:flutter_hbb/models/peer_model.dart'; + +class HttpType { + static const kAuthReqTypeAccount = "account"; + static const kAuthReqTypeMobile = "mobile"; + static const kAuthReqTypeSMSCode = "sms_code"; + static const kAuthReqTypeEmailCode = "email_code"; + + static const kAuthResTypeToken = "access_token"; + static const kAuthResTypeEmailCheck = "email_check"; +} + +class UserPayload { + String name = ''; + String email = ''; + String note = ''; + int? status; + String grp = ''; + bool isAdmin = false; + + UserPayload.fromJson(Map json) + : name = json['name'] ?? '', + email = json['email'] ?? '', + note = json['note'] ?? '', + status = json['status'], + grp = json['grp'] ?? '', + isAdmin = json['is_admin'] == true; +} + +class PeerPayload { + String id = ''; + String info = ''; + int? status; + String user = ''; + String user_name = ''; + String note = ''; + + PeerPayload.fromJson(Map json) + : id = json['id'] ?? '', + info = json['info'] ?? '', + status = json['status'], + user = json['user'] ?? '', + user_name = json['user_name'] ?? '', + note = json['note'] ?? ''; + + static Peer toPeer(PeerPayload p) { + return Peer.fromJson({"id": p.id}); + } +} + +class LoginRequest { + String? username; + String? password; + String? id; + String? uuid; + bool? autoLogin; + String? type; + String? verificationCode; + String? deviceInfo; + + LoginRequest( + {this.username, + this.password, + this.id, + this.uuid, + this.autoLogin, + this.type, + this.verificationCode, + this.deviceInfo}); + + LoginRequest.fromJson(Map json) { + username = json['username']; + password = json['password']; + id = json['id']; + uuid = json['uuid']; + autoLogin = json['autoLogin']; + type = json['type']; + verificationCode = json['verificationCode']; + deviceInfo = json['deviceInfo']; + } + + 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/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index c96dc115a..5c1e1218c 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -1,15 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; -import '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; @@ -27,7 +26,6 @@ class _AddressBookState extends State { @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => gFFI.abModel.pullAb()); } @override @@ -41,25 +39,12 @@ class _AddressBookState extends State { } }); - handleLogin() { - // TODO refactor login dialog for desktop and mobile - if (isDesktop) { - loginDialog().then((success) { - if (success) { - gFFI.abModel.pullAb(); - } - }); - } else { - showLogin(gFFI.dialogManager); - } - } - Future buildBody(BuildContext context) async { return Obx(() { if (gFFI.userModel.userName.value.isEmpty) { return Center( child: InkWell( - onTap: handleLogin, + onTap: loginDialog, child: Text( translate("Login"), style: const TextStyle(decoration: TextDecoration.underline), @@ -90,7 +75,7 @@ class _AddressBookState extends State { Text(translate(error)), TextButton( onPressed: () { - setState(() {}); + gFFI.abModel.pullAb(); }, child: Text(translate("Retry"))) ], @@ -237,29 +222,32 @@ class _AddressBookState extends State { } void abAddId() async { - var field = ""; - var msg = ""; var isInProgress = false; - TextEditingController controller = TextEditingController(text: field); + IDTextEditingController idController = IDTextEditingController(text: ''); + TextEditingController aliasController = TextEditingController(text: ''); + final tags = List.of(gFFI.abModel.tags); + var selectedTag = List.empty(growable: true).obs; + final style = TextStyle(fontSize: 14.0); + String? errorMsg; gFFI.dialogManager.show((setState, close) { submit() async { setState(() { - msg = ""; isInProgress = true; + errorMsg = null; }); - field = controller.text.trim(); - if (field.isEmpty) { + String id = idController.id; + if (id.isEmpty) { // pass } else { - final ids = field.trim().split(RegExp(r"[\s,;\n]+")); - field = ids.join(','); - for (final newId in ids) { - if (gFFI.abModel.idContainBy(newId)) { - continue; - } - gFFI.abModel.addId(newId); + if (gFFI.abModel.idContainBy(id)) { + setState(() { + isInProgress = false; + errorMsg = translate('ID already exists'); + }); + return; } + gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag); await gFFI.abModel.pushAb(); this.setState(() {}); // final currentPeers @@ -272,21 +260,70 @@ class _AddressBookState extends State { content: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(translate("whitelist_sep")), - const SizedBox( - height: 8.0, - ), - Row( + Column( children: [ - Expanded( - child: TextField( - maxLines: null, - decoration: InputDecoration( - border: const OutlineInputBorder(), - errorText: msg.isEmpty ? null : translate(msg), + Align( + alignment: Alignment.centerLeft, + child: Row( + children: [ + Text( + '*', + style: TextStyle(color: Colors.red, fontSize: 14), ), - controller: controller, - focusNode: FocusNode()..requestFocus()), + Text( + 'ID', + style: style, + ), + ], + ), + ), + TextField( + controller: idController, + inputFormatters: [IDTextInputFormatter()], + decoration: InputDecoration( + isDense: true, + border: OutlineInputBorder(), + errorText: errorMsg), + style: style, + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate('Alias'), + style: style, + ), + ).marginOnly(top: 8, bottom: 2), + TextField( + controller: aliasController, + decoration: InputDecoration( + border: OutlineInputBorder(), + isDense: true, + ), + style: style, + ), + Align( + alignment: Alignment.centerLeft, + child: Text( + translate('Tags'), + style: style, + ), + ).marginOnly(top: 8), + Container( + child: Wrap( + children: tags + .map((e) => AddressBookTag( + name: e, + tags: selectedTag, + onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + }, + showActionMenu: false)) + .toList(growable: false), + ), ), ], ), @@ -298,8 +335,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, @@ -365,8 +402,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/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..837a197dc 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -64,8 +64,8 @@ void changeIdDialog() { ], ), 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, @@ -111,48 +111,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, ); @@ -195,14 +193,12 @@ Future changeDirectAccessPort( ], ), actions: [ - TextButton(onPressed: close, child: Text(translate("Cancel"))), - TextButton( - onPressed: () async { - await bind.mainSetOption( - key: 'direct-access-port', value: controller.text); - close(); - }, - child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("OK", onPressed: () async { + await bind.mainSetOption( + key: 'direct-access-port', value: controller.text); + close(); + }), ], onCancel: close, ); diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart new file mode 100644 index 000000000..2f10ac005 --- /dev/null +++ b/flutter/lib/common/widgets/login.dart @@ -0,0 +1,676 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../common.dart'; + +class _IconOP extends StatelessWidget { + final String icon; + final double iconWidth; + final EdgeInsets margin; + const _IconOP( + {Key? key, + required this.icon, + required this.iconWidth, + this.margin = const EdgeInsets.symmetric(horizontal: 4.0)}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: margin, + child: SvgPicture.asset( + 'assets/$icon.svg', + width: iconWidth, + ), + ); + } +} + +class ButtonOP extends StatelessWidget { + final String op; + final RxString curOP; + final double iconWidth; + final Color primaryColor; + final double height; + final Function() onTap; + + const ButtonOP({ + Key? key, + required this.op, + required this.curOP, + required this.iconWidth, + required this.primaryColor, + required this.height, + required this.onTap, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row(children: [ + Container( + height: height, + width: 200, + child: Obx(() => ElevatedButton( + style: ElevatedButton.styleFrom( + primary: curOP.value.isEmpty || curOP.value == op + ? primaryColor + : Colors.grey, + ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)), + onPressed: curOP.value.isEmpty || curOP.value == op ? onTap : null, + child: Row( + children: [ + SizedBox( + width: 30, + child: _IconOP( + icon: op, + iconWidth: iconWidth, + margin: EdgeInsets.only(right: 5), + )), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Center( + child: Text('${translate("Continue with")} $op')))), + ], + ))), + ), + ]); + } +} + +class ConfigOP { + final String op; + final double iconWidth; + ConfigOP({required this.op, required this.iconWidth}); +} + +class WidgetOP extends StatefulWidget { + final ConfigOP config; + final RxString curOP; + final Function(String) cbLogin; + const WidgetOP({ + Key? key, + required this.config, + required this.curOP, + required this.cbLogin, + }) : super(key: key); + + @override + State createState() { + return _WidgetOPState(); + } +} + +class _WidgetOPState extends State { + Timer? _updateTimer; + String _stateMsg = ''; + String _failedMsg = ''; + String _url = ''; + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + _updateTimer?.cancel(); + } + + _beginQueryState() { + _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + _updateState(); + }); + } + + _updateState() { + bind.mainAccountAuthResult().then((result) { + if (result.isEmpty) { + return; + } + final resultMap = jsonDecode(result); + if (resultMap == null) { + return; + } + final String stateMsg = resultMap['state_msg']; + String failedMsg = resultMap['failed_msg']; + final String? url = resultMap['url']; + final authBody = resultMap['auth_body']; + if (_stateMsg != stateMsg || _failedMsg != failedMsg) { + if (_url.isEmpty && url != null && url.isNotEmpty) { + launchUrl(Uri.parse(url)); + _url = url; + } + if (authBody != null) { + _updateTimer?.cancel(); + final String username = authBody['user']['name']; + widget.curOP.value = ''; + widget.cbLogin(username); + } + + setState(() { + _stateMsg = stateMsg; + _failedMsg = failedMsg; + if (failedMsg.isNotEmpty) { + widget.curOP.value = ''; + _updateTimer?.cancel(); + } + }); + } + }); + } + + _resetState() { + _stateMsg = ''; + _failedMsg = ''; + _url = ''; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ButtonOP( + op: widget.config.op, + curOP: widget.curOP, + iconWidth: widget.config.iconWidth, + primaryColor: str2color(widget.config.op, 0x7f), + height: 36, + onTap: () async { + _resetState(); + widget.curOP.value = widget.config.op; + await bind.mainAccountAuth(op: widget.config.op); + _beginQueryState(); + }, + ), + Obx(() { + if (widget.curOP.isNotEmpty && + widget.curOP.value != widget.config.op) { + _failedMsg = ''; + } + return Offstage( + offstage: + _failedMsg.isEmpty && widget.curOP.value != widget.config.op, + child: 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: Container( + width: 200, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: children, + ))); + } +} + +class LoginWidgetUserPass extends StatelessWidget { + final TextEditingController username; + final TextEditingController pass; + final String? usernameMsg; + final String? passMsg; + final bool isInProgress; + final RxString curOP; + final RxBool autoLogin; + final Function() onLogin; + final FocusNode? userFocusNode; + const LoginWidgetUserPass({ + Key? key, + this.userFocusNode, + required this.username, + required this.pass, + required this.usernameMsg, + required this.passMsg, + required this.isInProgress, + required this.curOP, + required this.autoLogin, + required this.onLogin, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 8.0), + DialogTextField( + title: '${translate("Username")}:', + controller: username, + focusNode: userFocusNode, + prefixIcon: Icon(Icons.account_circle_outlined), + errorText: usernameMsg), + DialogTextField( + title: '${translate("Password")}:', + obscureText: true, + controller: pass, + prefixIcon: Icon(Icons.lock_outline), + errorText: passMsg), + Obx(() => CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate("Remember me"), + ), + value: autoLogin.value, + onChanged: (v) { + if (v == null) return; + autoLogin.value = v; + }, + )), + Offstage( + offstage: !isInProgress, + child: const LinearProgressIndicator()), + const SizedBox(height: 12.0), + FittedBox( + child: + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + Container( + height: 38, + width: 200, + child: Obx(() => ElevatedButton( + child: Text( + translate('Login'), + style: TextStyle(fontSize: 16), + ), + onPressed: + curOP.value.isEmpty || curOP.value == 'rustdesk' + ? () { + onLogin(); + } + : null, + )), + ), + ])), + ], + )); + } +} + +class DialogTextField extends StatelessWidget { + final String title; + final bool obscureText; + final String? errorText; + final String? helperText; + final Widget? prefixIcon; + final TextEditingController controller; + final FocusNode? focusNode; + + DialogTextField( + {Key? key, + this.focusNode, + this.obscureText = false, + this.errorText, + this.helperText, + this.prefixIcon, + required this.title, + required this.controller}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + labelText: title, + border: const OutlineInputBorder(), + prefixIcon: prefixIcon, + helperText: helperText, + helperMaxLines: 8, + errorText: errorText), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, + ), + ), + ], + ).paddingSymmetric(vertical: 4.0); + } +} + +/// common login dialog for desktop +/// call this directly +Future loginDialog() async { + var username = TextEditingController(); + var password = TextEditingController(); + final userFocusNode = FocusNode()..requestFocus(); + Timer(Duration(milliseconds: 100), () => userFocusNode..requestFocus()); + + String? usernameMsg; + String? passwordMsg; + var isInProgress = false; + final autoLogin = true.obs; + final RxString curOP = ''.obs; + + final res = await gFFI.dialogManager.show((setState, close) { + username.addListener(() { + if (usernameMsg != null) { + setState(() => usernameMsg = null); + } + }); + + password.addListener(() { + if (passwordMsg != null) { + setState(() => passwordMsg = null); + } + }); + + onDialogCancel() { + isInProgress = false; + close(false); + } + + onLogin() async { + // validate + if (username.text.isEmpty) { + setState(() => usernameMsg = translate('Username missed')); + return; + } + if (password.text.isEmpty) { + setState(() => passwordMsg = translate('Password missed')); + return; + } + curOP.value = 'rustdesk'; + setState(() => isInProgress = true); + try { + final resp = await gFFI.userModel.login(LoginRequest( + username: username.text, + password: password.text, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin.value, + type: HttpType.kAuthReqTypeAccount)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + await bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + case HttpType.kAuthResTypeEmailCheck: + setState(() => isInProgress = false); + final res = await verificationCodeDialog(resp.user); + if (res == true) { + close(true); + return; + } + break; + default: + passwordMsg = "Failed, bad response from server"; + break; + } + } on RequestException catch (err) { + passwordMsg = translate(err.cause); + debugPrintStack(label: err.toString()); + } catch (err) { + passwordMsg = "Unknown Error: $err"; + debugPrintStack(label: err.toString()); + } + curOP.value = ''; + setState(() => isInProgress = false); + } + + return CustomAlertDialog( + title: Text(translate('Login')), + contentBoxConstraints: BoxConstraints(minWidth: 400), + content: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 8.0, + ), + LoginWidgetUserPass( + username: username, + pass: password, + usernameMsg: usernameMsg, + passMsg: passwordMsg, + isInProgress: isInProgress, + curOP: curOP, + autoLogin: autoLogin, + onLogin: onLogin, + userFocusNode: userFocusNode, + ), + const SizedBox( + height: 8.0, + ), + Center( + child: Text( + translate('or'), + style: TextStyle(fontSize: 16), + )), + const SizedBox( + height: 8.0, + ), + LoginWidgetOP( + ops: [ + ConfigOP(op: 'GitHub', iconWidth: 20), + ConfigOP(op: 'Google', iconWidth: 20), + ConfigOP(op: 'Okta', iconWidth: 38), + ], + curOP: curOP, + cbLogin: (String username) { + gFFI.userModel.userName.value = username; + close(true); + }, + ), + ], + ), + actions: [dialogButton('Close', onPressed: onDialogCancel)], + onCancel: onDialogCancel, + ); + }); + + if (res != null) { + // update ab and group status + await gFFI.abModel.pullAb(); + await gFFI.groupModel.pull(); + } + + return res; +} + +Future verificationCodeDialog(UserPayload? user) async { + var autoLogin = true; + var isInProgress = false; + String? errorText; + + final code = TextEditingController(); + final focusNode = FocusNode()..requestFocus(); + Timer(Duration(milliseconds: 100), () => focusNode..requestFocus()); + + final res = await gFFI.dialogManager.show((setState, close) { + bool validate() { + return code.text.length >= 6; + } + + code.addListener(() { + if (errorText != null) { + setState(() => errorText = null); + } + }); + + void onVerify() async { + if (!validate()) { + setState( + () => errorText = translate('Too short, at least 6 characters.')); + return; + } + setState(() => isInProgress = true); + + try { + final resp = await gFFI.userModel.login(LoginRequest( + verificationCode: code.text, + username: user?.name, + id: await bind.mainGetMyId(), + uuid: await bind.mainGetUuid(), + autoLogin: autoLogin, + type: HttpType.kAuthReqTypeEmailCode)); + + switch (resp.type) { + case HttpType.kAuthResTypeToken: + if (resp.access_token != null) { + await bind.mainSetLocalOption( + key: 'access_token', value: resp.access_token!); + close(true); + return; + } + break; + default: + errorText = "Failed, bad response from server"; + break; + } + } on RequestException catch (err) { + errorText = translate(err.cause); + debugPrintStack(label: err.toString()); + } catch (err) { + errorText = "Unknown Error: $err"; + debugPrintStack(label: err.toString()); + } + + setState(() => isInProgress = false); + } + + return CustomAlertDialog( + title: Text(translate("Verification code")), + contentBoxConstraints: BoxConstraints(maxWidth: 300), + content: Column( + children: [ + Offstage( + offstage: user?.email == null, + child: TextField( + decoration: InputDecoration( + labelText: "Email", + prefixIcon: Icon(Icons.email), + border: InputBorder.none), + readOnly: true, + controller: TextEditingController(text: user?.email), + )), + const SizedBox(height: 8), + DialogTextField( + title: '${translate("Verification code")}:', + controller: code, + errorText: errorText, + focusNode: focusNode, + helperText: translate('verification_tip'), + ), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Row(children: [ + Expanded(child: Text(translate("Trust this device"))) + ]), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = !autoLogin); + }, + ), + Offstage( + offstage: !isInProgress, + child: const LinearProgressIndicator()), + ], + ), + actions: [ + dialogButton("Cancel", onPressed: close, isOutline: true), + dialogButton("Verify", onPressed: onVerify), + ]); + }); + + return res; +} diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart new file mode 100644 index 000000000..65eaba40f --- /dev/null +++ b/flutter/lib/common/widgets/my_group.dart @@ -0,0 +1,225 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/peers_view.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; + +class MyGroup extends StatefulWidget { + final EdgeInsets? menuPadding; + const MyGroup({Key? key, this.menuPadding}) : super(key: key); + + @override + State createState() { + return _MyGroupState(); + } +} + +class _MyGroupState extends State { + static final RxString selectedUser = ''.obs; + static final RxString searchUserText = ''.obs; + static TextEditingController searchUserController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => FutureBuilder( + future: buildBody(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return const Offstage(); + } + }); + + Future buildBody(BuildContext context) async { + return Obx(() { + if (gFFI.groupModel.userLoading.value) { + return const Center( + child: CircularProgressIndicator(), + ); + } + if (gFFI.groupModel.userLoadError.isNotEmpty) { + return _buildShowError(gFFI.groupModel.userLoadError.value); + } + if (isDesktop) { + return _buildDesktop(); + } else { + return _buildMobile(); + } + }); + } + + Widget _buildShowError(String error) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate(error)), + TextButton( + onPressed: () { + gFFI.groupModel.pull(); + }, + child: Text(translate("Retry"))) + ], + )); + } + + Widget _buildDesktop() { + return Obx( + () => Row( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + width: 200, + height: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + _buildLeftHeader(), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(2)), + child: _buildUserContacts(), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ), + ); + } + + Widget _buildMobile() { + return Obx( + () => Column( + children: [ + Card( + margin: EdgeInsets.symmetric(horizontal: 4.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).scaffoldBackgroundColor)), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildLeftHeader(), + Container( + width: double.infinity, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(4)), + child: _buildUserContacts(), + ).marginSymmetric(vertical: 8.0) + ], + ), + ), + ), + Divider(), + Expanded( + child: Align( + alignment: Alignment.topLeft, + child: MyGroupPeerView( + menuPadding: widget.menuPadding, + initPeers: gFFI.groupModel.peersShow.value)), + ) + ], + ), + ); + } + + Widget _buildLeftHeader() { + return Row( + children: [ + Expanded( + child: TextField( + controller: searchUserController, + onChanged: (value) { + searchUserText.value = value; + }, + decoration: InputDecoration( + prefixIcon: Icon( + Icons.search_rounded, + color: Theme.of(context).hintColor, + ), + contentPadding: const EdgeInsets.symmetric(vertical: 10), + hintText: translate("Search"), + hintStyle: + TextStyle(fontSize: 14, color: Theme.of(context).hintColor), + border: InputBorder.none, + isDense: true, + ), + )), + ], + ); + } + + Widget _buildUserContacts() { + return Obx(() { + return Column( + children: gFFI.groupModel.users + .where((p0) { + if (searchUserText.isNotEmpty) { + return p0.name.contains(searchUserText.value); + } + return true; + }) + .map((e) => _buildUserItem(e.name)) + .toList()); + }); + } + + Widget _buildUserItem(String username) { + return InkWell(onTap: () { + if (selectedUser.value != username) { + selectedUser.value = username; + gFFI.groupModel.pullUserPeers(username); + } + }, child: Obx( + () { + bool selected = selectedUser.value == username; + return Container( + decoration: BoxDecoration( + color: selected ? MyTheme.color(context).highlight : null, + border: Border( + bottom: BorderSide( + width: 0.7, + color: Theme.of(context).dividerColor.withOpacity(0.1))), + ), + child: Container( + child: Row( + children: [ + Icon(Icons.person_outline_rounded, color: Colors.grey, size: 16) + .marginOnly(right: 4), + Expanded(child: Text(username)), + ], + ).paddingSymmetric(vertical: 4), + ), + ); + }, + )).marginSymmetric(horizontal: 12); + } +} diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 8df84af6c..3feaef51d 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -56,6 +56,9 @@ class _PeerCardState extends State<_PeerCard> Widget _buildMobile() { final peer = super.widget.peer; + final name = + '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; + return Card( margin: EdgeInsets.symmetric(horizontal: 2), child: GestureDetector( @@ -90,7 +93,7 @@ class _PeerCardState extends State<_PeerCard> ? formatID(peer.id) : peer.alias) ]), - Text('${peer.username}@${peer.hostname}') + Text(name) ], ).paddingOnly(left: 8.0), ), @@ -145,6 +148,8 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerTile( BuildContext context, Peer peer, Rx deco) { + final name = + '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; final greyStyle = TextStyle( fontSize: 11, color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6)); @@ -184,7 +189,7 @@ class _PeerCardState extends State<_PeerCard> Align( alignment: Alignment.centerLeft, child: Text( - '${peer.username}@${peer.hostname}', + name, style: greyStyle, textAlign: TextAlign.start, overflow: TextOverflow.ellipsis, @@ -206,7 +211,8 @@ class _PeerCardState extends State<_PeerCard> Widget _buildPeerCard( BuildContext context, Peer peer, Rx deco) { - final name = '${peer.username}@${peer.hostname}'; + final name = + '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}'; return Card( color: Colors.transparent, elevation: 0, @@ -448,7 +454,7 @@ abstract class BasePeerCard extends StatelessWidget { ); } - /// Only avaliable on Windows. + /// Only available on Windows. @protected MenuEntryBase _createShortCutAction(String id) { return MenuEntryButton( @@ -464,6 +470,12 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + Future _isForceAlwaysRelay(String id) async { + return (await bind.mainGetPeerOption(id: id, key: 'force-always-relay')) + .isNotEmpty; + } + @protected Future> _forceAlwaysRelayAction(String id) async { const option = 'force-always-relay'; @@ -471,17 +483,12 @@ abstract class BasePeerCard extends StatelessWidget { switchType: SwitchType.scheckbox, text: translate('Always connect via relay'), getter: () async { - return (await bind.mainGetPeerOption(id: id, key: option)).isNotEmpty; + return await _isForceAlwaysRelay(id); }, setter: (bool v) async { - String value; - String oldValue = await bind.mainGetPeerOption(id: id, key: option); - if (oldValue.isEmpty) { - value = 'Y'; - } else { - value = ''; - } - await bind.mainSetPeerOption(id: id, key: option, value: value); + gFFI.abModel.setPeerForceAlwaysRelay(id, v); + await bind.mainSetPeerOption( + id: id, key: option, value: bool2option(option, v)); }, padding: menuPadding, dismissOnClicked: true, @@ -489,14 +496,14 @@ abstract class BasePeerCard extends StatelessWidget { } @protected - MenuEntryBase _renameAction(String id, bool isAddressBook) { + MenuEntryBase _renameAction(String id) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Rename'), style: style, ), proc: () { - _rename(id, isAddressBook); + _rename(id); }, padding: menuPadding, dismissOnClicked: true, @@ -586,33 +593,41 @@ abstract class BasePeerCard extends StatelessWidget { ); } - void _rename(String id, bool isAddressBook) async { + @protected + MenuEntryBase _addToAb(Peer peer) { + return MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Add to Address Book'), + style: style, + ), + proc: () { + () async { + if (!gFFI.abModel.idContainBy(peer.id)) { + gFFI.abModel.addPeer(peer); + await gFFI.abModel.pushAb(); + } + }(); + }, + padding: menuPadding, + dismissOnClicked: true, + ); + } + + @protected + Future _getAlias(String id) async => + await bind.mainGetPeerOption(id: id, key: 'alias'); + + void _rename(String id) async { RxBool isInProgress = false.obs; - var name = peer.alias; + String name = await _getAlias(id); var controller = TextEditingController(text: name); - if (isAddressBook) { - final peer = gFFI.abModel.peers.firstWhereOrNull((p) => id == p.id); - if (peer == null) { - // this should not happen - } else { - name = peer.alias; - } - } gFFI.dialogManager.show((setState, close) { submit() async { isInProgress.value = true; - name = controller.text; + String name = controller.text.trim(); await bind.mainSetPeerAlias(id: id, alias: name); - if (isAddressBook) { - gFFI.abModel.setPeerAlias(id, name); - await gFFI.abModel.pushAb(); - } - if (isAddressBook) { - gFFI.abModel.pullAb(); - } else { - bind.mainLoadRecentPeers(); - bind.mainLoadFavPeers(); - } + gFFI.abModel.setPeerAlias(id, name); + _update(); close(); isInProgress.value = false; } @@ -638,14 +653,17 @@ abstract class BasePeerCard extends StatelessWidget { ], ), 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, ); }); } + + @protected + void _update(); } class RecentPeerCard extends BasePeerCard { @@ -671,7 +689,7 @@ class RecentPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } menuItems.add(MenuEntryDivider()); - menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_renameAction(peer.id)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadRecentPeers(); })); @@ -679,8 +697,15 @@ class RecentPeerCard extends BasePeerCard { menuItems.add(_unrememberPasswordAction(peer.id)); } menuItems.add(_addFavAction(peer.id)); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } return menuItems; } + + @protected + @override + void _update() => bind.mainLoadRecentPeers(); } class FavoritePeerCard extends BasePeerCard { @@ -706,7 +731,7 @@ class FavoritePeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } menuItems.add(MenuEntryDivider()); - menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_renameAction(peer.id)); menuItems.add(_removeAction(peer.id, () async { await bind.mainLoadFavPeers(); })); @@ -716,8 +741,15 @@ class FavoritePeerCard extends BasePeerCard { menuItems.add(_rmFavAction(peer.id, () async { await bind.mainLoadFavPeers(); })); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } return menuItems; } + + @protected + @override + void _update() => bind.mainLoadFavPeers(); } class DiscoveredPeerCard extends BasePeerCard { @@ -744,8 +776,15 @@ class DiscoveredPeerCard extends BasePeerCard { } menuItems.add(MenuEntryDivider()); menuItems.add(_removeAction(peer.id, () async {})); + if (!gFFI.abModel.idContainBy(peer.id)) { + menuItems.add(_addToAb(peer)); + } return menuItems; } + + @protected + @override + void _update() => bind.mainLoadLanPeers(); } class AddressBookPeerCard extends BasePeerCard { @@ -771,7 +810,7 @@ class AddressBookPeerCard extends BasePeerCard { menuItems.add(_createShortCutAction(peer.id)); } menuItems.add(MenuEntryDivider()); - menuItems.add(_renameAction(peer.id, false)); + menuItems.add(_renameAction(peer.id)); menuItems.add(_removeAction(peer.id, () async {})); if (await bind.mainPeerHasPassword(id: peer.id)) { menuItems.add(_unrememberPasswordAction(peer.id)); @@ -782,6 +821,20 @@ class AddressBookPeerCard extends BasePeerCard { 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( @@ -862,8 +915,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, @@ -872,23 +925,61 @@ class AddressBookPeerCard extends BasePeerCard { } } +class MyGroupPeerCard extends BasePeerCard { + MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key}) + : super(peer: peer, menuPadding: menuPadding, key: key); + + @override + Future>> _buildMenuItems( + BuildContext context) async { + final List> menuItems = [ + _connectAction(context, peer), + _transferFileAction(context, peer.id), + ]; + if (isDesktop && peer.platform != 'Android') { + menuItems.add(_tcpTunnelingAction(context, peer.id)); + } + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (peer.platform == 'Windows') { + menuItems.add(_rdpAction(context, peer.id)); + } + menuItems.add(_wolAction(peer.id)); + if (Platform.isWindows) { + menuItems.add(_createShortCutAction(peer.id)); + } + menuItems.add(MenuEntryDivider()); + menuItems.add(_renameAction(peer.id)); + if (await bind.mainPeerHasPassword(id: peer.id)) { + menuItems.add(_unrememberPasswordAction(peer.id)); + } + return menuItems; + } + + @protected + @override + void _update() => gFFI.groupModel.pull(); +} + void _rdpDialog(String id) async { - final portController = TextEditingController( - text: await bind.mainGetPeerOption(id: id, key: 'rdp_port')); - final userController = TextEditingController( - text: await bind.mainGetPeerOption(id: id, key: 'rdp_username')); + final port = await bind.mainGetPeerOption(id: id, key: 'rdp_port'); + final username = await bind.mainGetPeerOption(id: id, key: 'rdp_username'); + final portController = TextEditingController(text: port); + final userController = TextEditingController(text: username); final passwordController = TextEditingController( text: await bind.mainGetPeerOption(id: id, key: 'rdp_password')); RxBool secure = true.obs; gFFI.dialogManager.show((setState, close) { submit() async { + String port = portController.text.trim(); + String username = userController.text; + String password = passwordController.text; + await bind.mainSetPeerOption(id: id, key: 'rdp_port', value: port); await bind.mainSetPeerOption( - id: id, key: 'rdp_port', value: portController.text.trim()); + id: id, key: 'rdp_username', value: username); await bind.mainSetPeerOption( - id: id, key: 'rdp_username', value: userController.text); - await bind.mainSetPeerOption( - id: id, key: 'rdp_password', value: passwordController.text); + id: id, key: 'rdp_password', value: password); + gFFI.abModel.setRdp(id, port, username); close(); } @@ -981,8 +1072,8 @@ void _rdpDialog(String id) async { ), ), 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/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 523230810..0c24fe7ea 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -1,36 +1,165 @@ +import 'dart:ui' as ui; + +import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common/widgets/address_book.dart'; +import 'package:flutter_hbb/common/widgets/my_group.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; import 'package:get/get.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 { - final List tabs; - final List children; - const PeerTabPage({required this.tabs, required this.children, Key? key}) - : super(key: key); + const PeerTabPage({Key? key}) : super(key: key); @override State createState() => _PeerTabPageState(); } +class _TabEntry { + final Widget widget; + final Function() load; + _TabEntry(this.widget, this.load); +} + +EdgeInsets? _menuPadding() { + return isDesktop ? kDesktopMenuPadding : null; +} + class _PeerTabPageState extends State with SingleTickerProviderStateMixin { - final RxInt _tabIndex = 0.obs; + final List<_TabEntry> entries = [ + _TabEntry( + RecentPeersView( + menuPadding: _menuPadding(), + ), + bind.mainLoadRecentPeers), + _TabEntry( + FavoritePeersView( + menuPadding: _menuPadding(), + ), + bind.mainLoadFavPeers), + _TabEntry( + DiscoveredPeersView( + menuPadding: _menuPadding(), + ), + bind.mainDiscover), + _TabEntry( + AddressBook( + menuPadding: _menuPadding(), + ), + () => {}), + _TabEntry( + MyGroup( + menuPadding: _menuPadding(), + ), + () => {}), + ]; @override void initState() { - setPeer(); - super.initState(); - } - - setPeer() { - final index = bind.getLocalFlutterConfig(k: 'peer-tab-index'); - if (index != '') { - _tabIndex.value = int.parse(index); - } + adjustTab(); final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type'); if (uiType != '') { @@ -38,26 +167,13 @@ class _PeerTabPageState extends State ? PeerUiType.list : PeerUiType.grid; } + super.initState(); } - // hard code for now - Future _handleTabSelection(int index) async { - _tabIndex.value = index; - await bind.setLocalFlutterConfig(k: 'peer-tab-index', v: index.toString()); - switch (index) { - case 0: - bind.mainLoadRecentPeers(); - break; - case 1: - bind.mainLoadFavPeers(); - break; - case 2: - bind.mainDiscover(); - break; - case 3: - - /// AddressBook initState will refresh ab state - break; + Future handleTabSelection(int tabIndex) async { + if (tabIndex < entries.length) { + statePeerTab.currentTab.value = tabIndex; + entries[tabIndex].load(); } } @@ -80,8 +196,9 @@ class _PeerTabPageState extends State child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded(child: _createSwitchBar(context)), - const SizedBox(width: 10), + Expanded( + child: visibleContextMenuListener( + _createSwitchBar(context))), const PeerSearchBar(), Offstage( offstage: !isDesktop, @@ -97,45 +214,81 @@ class _PeerTabPageState extends State Widget _createSwitchBar(BuildContext context) { final textColor = Theme.of(context).textTheme.titleLarge?.color; - return ListView( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - controller: ScrollController(), - children: super.widget.tabs.asMap().entries.map((t) { - return Obx(() => InkWell( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - color: _tabIndex.value == t.key - ? Theme.of(context).backgroundColor - : null, - borderRadius: BorderRadius.circular(isDesktop ? 2 : 6), + 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)), ), - child: Align( - alignment: Alignment.center, - child: Text( - t.value, - textAlign: TextAlign.center, - style: TextStyle( - height: 1, - fontSize: 14, - color: - _tabIndex.value == t.key ? textColor : textColor - ?..withOpacity(0.5)), - ), - )), - onTap: () async => await _handleTabSelection(t.key), - )); - }).toList()); + )), + 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 _createPeersView() { final verticalMargin = isDesktop ? 12.0 : 6.0; return Expanded( - child: Obx(() => widget - .children[_tabIndex.value]) //: (to) => _tabIndex.value = to) - .marginSymmetric(vertical: verticalMargin), - ); + child: Obx(() { + var tabs = statePeerTab.currentTabs(); + if (tabs.isEmpty) { + return visibleContextMenuListener(Center( + child: Text(translate('Right click to select tabs')), + )); + } 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; + } + } + }).marginSymmetric(vertical: verticalMargin)); } Widget _createPeerViewTypeSwitch(BuildContext context) { @@ -167,6 +320,72 @@ class _PeerTabPageState extends State .toList(), ); } + + adjustTab() { + var tabs = statePeerTab.currentTabs(); + if (tabs.isNotEmpty && !tabs.contains(statePeerTab.currentTab.value)) { + statePeerTab.currentTab.value = tabs[0]; + } + } + + Widget visibleContextMenuListener(Widget child) { + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + if (e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return visibleContextMenu(cancelFunc); + }, + target: e.position, + ); + } + }, + child: child); + } + + Widget visibleContextMenu(CancelFunc cancelFunc) { + 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()); + }); + } } class PeerSearchBar extends StatefulWidget { diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 6e52bfeb8..9c98f24b8 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -326,3 +326,21 @@ class AddressBookPeersView extends BasePeersView { return true; } } + +class MyGroupPeerView extends BasePeersView { + MyGroupPeerView( + {Key? key, + EdgeInsets? menuPadding, + ScrollController? scrollController, + required List initPeers}) + : super( + key: key, + name: 'my group peer', + loadEvent: 'load_my_group_peers', + peerCardBuilder: (Peer peer) => MyGroupPeerCard( + peer: peer, + menuPadding: menuPadding, + ), + initPeers: initPeers, + ); +} diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index 3a79d24fb..2d0dcacdf 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import '../../models/input_model.dart'; @@ -9,11 +10,12 @@ class RawKeyFocusScope extends StatelessWidget { final InputModel inputModel; final Widget child; - RawKeyFocusScope( - {this.focusNode, - this.onFocusChange, - required this.inputModel, - required this.child}); + RawKeyFocusScope({ + this.focusNode, + this.onFocusChange, + required this.inputModel, + required this.child, + }); @override Widget build(BuildContext context) { @@ -24,7 +26,8 @@ class RawKeyFocusScope extends StatelessWidget { canRequestFocus: true, focusNode: focusNode, onFocusChange: onFocusChange, - onKey: inputModel.handleRawKeyEvent, + onKey: + stateGlobal.grabKeyboard ? inputModel.handleRawKeyEvent : null, child: child)); } } @@ -35,11 +38,15 @@ class RawPointerMouseRegion extends StatelessWidget { final MouseCursor? cursor; final PointerEnterEventListener? onEnter; final PointerExitEventListener? onExit; + final PointerDownEventListener? onPointerDown; + final PointerUpEventListener? onPointerUp; RawPointerMouseRegion( {this.onEnter, this.onExit, this.cursor, + this.onPointerDown, + this.onPointerUp, required this.inputModel, required this.child}); @@ -47,8 +54,14 @@ class RawPointerMouseRegion extends StatelessWidget { Widget build(BuildContext context) { return Listener( onPointerHover: inputModel.onPointHoverImage, - onPointerDown: inputModel.onPointDownImage, - onPointerUp: inputModel.onPointUpImage, + onPointerDown: (evt) { + onPointerDown?.call(evt); + inputModel.onPointDownImage(evt); + }, + onPointerUp: (evt) { + onPointerUp?.call(evt); + inputModel.onPointUpImage(evt); + }, onPointerMove: inputModel.onPointMoveImage, onPointerSignal: inputModel.onPointerSignalImage, /* diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index b0099ca7c..e4081d9a5 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -5,15 +5,23 @@ import 'package:flutter_hbb/common.dart'; const double kDesktopRemoteTabBarHeight = 28.0; +const String kPeerPlatformWindows = "Windows"; +const String kPeerPlatformLinux = "Linux"; +const String kPeerPlatformMacOS = "Mac OS"; +const String kPeerPlatformAndroid = "Android"; + /// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page', "Install Page" const String kAppTypeMain = "main"; 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 kWindowActionRebuild = "rebuild"; const String kWindowEventHide = "hide"; const String kWindowEventShow = "show"; +const String kWindowConnect = "connect"; const String kUniLinksPrefix = "rustdesk://"; const String kActionNewConnection = "connection/new/"; @@ -97,6 +105,8 @@ const kRemoteImageQualityLow = 'low'; /// [kRemoteImageQualityCustom] Custom image quality. const kRemoteImageQualityCustom = 'custom'; +const kIgnoreDpi = true; + /// 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 a830d6399..2dae03250 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -6,7 +6,6 @@ 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:get/get.dart'; @@ -16,7 +15,6 @@ 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'; @@ -46,7 +44,7 @@ class _ConnectionPageState extends State var svcStatusCode = 0.obs; var svcIsUsingPublicServer = true.obs; - bool isWindowMinisized = false; + bool isWindowMinimized = false; @override void initState() { @@ -82,13 +80,13 @@ 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; } } @@ -113,7 +111,7 @@ class _ConnectionPageState extends State delegate: SliverChildListDelegate([ Row( children: [ - _buildRemoteIDTextField(context), + Flexible(child: _buildRemoteIDTextField(context)), ], ).marginOnly(top: 22), SizedBox(height: 12), @@ -121,28 +119,7 @@ class _ConnectionPageState extends State ])), SliverFillRemaining( hasScrollBody: false, - child: PeerTabPage( - tabs: [ - translate('Recent Sessions'), - translate('Favorites'), - translate('Discovered'), - translate('Address Book') - ], - children: [ - RecentPeersView( - menuPadding: kDesktopMenuPadding, - ), - FavoritePeersView( - menuPadding: kDesktopMenuPadding, - ), - DiscoveredPeersView( - menuPadding: kDesktopMenuPadding, - ), - const AddressBook( - menuPadding: kDesktopMenuPadding, - ), - ], - ).paddingOnly(right: 12.0), + child: PeerTabPage().paddingOnly(right: 12.0), ) ], ).paddingOnly(left: 12.0), @@ -193,6 +170,7 @@ class _ConnectionPageState extends State Expanded( child: Obx( () => TextField( + maxLength: 90, autocorrect: false, enableSuggestions: false, keyboardType: TextInputType.visiblePassword, @@ -200,12 +178,13 @@ class _ConnectionPageState extends State style: const TextStyle( fontFamily: 'WorkSans', fontSize: 22, - height: 1, + height: 1.25, ), maxLines: 1, cursorColor: Theme.of(context).textTheme.titleLarge?.color, decoration: InputDecoration( + counterText: '', hintText: _idInputFocused.value ? null : translate('Enter Remote ID'), @@ -258,9 +237,8 @@ class _ConnectionPageState extends State ), ), ); - return Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 600), child: w)); + return Container( + constraints: const BoxConstraints(maxWidth: 600), child: w); } Widget buildStatus() { diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 53f4d4d90..0501c298a 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -6,6 +6,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart' hide MenuItem; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/custom_password.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; @@ -14,11 +15,8 @@ import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; -import 'package:flutter_hbb/utils/tray_manager.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; -import 'package:tray_manager/tray_manager.dart'; -import 'package:window_manager/window_manager.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_size/window_size.dart' as window_size; @@ -34,7 +32,7 @@ class DesktopHomePage extends StatefulWidget { const borderColor = Color(0xFF2F65BA); class _DesktopHomePageState extends State - with TrayListener, AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin { final _leftPaneScrollController = ScrollController(); @override @@ -45,6 +43,7 @@ class _DesktopHomePageState extends State var svcStopped = false.obs; var watchIsCanScreenRecording = false; var watchIsProcessTrust = false; + var watchIsInputMonitoring = false; Timer? _updateTimer; @override @@ -304,15 +303,6 @@ class _DesktopHomePageState extends State } Widget buildHelpCards() { - if (Platform.isWindows) { - if (!bind.mainIsInstalled()) { - return buildInstallCard( - "", "install_tip", "Install", bind.mainGotoInstall); - } else if (bind.mainIsInstalledLowerVersion()) { - return buildInstallCard("Status", "Your installation is lower version.", - "Click to upgrade", bind.mainUpdateMe); - } - } if (updateUrl.isNotEmpty) { return buildInstallCard( "Status", @@ -325,7 +315,15 @@ class _DesktopHomePageState extends State if (systemError.isNotEmpty) { return buildInstallCard("", systemError, "", () {}); } - if (Platform.isMacOS) { + if (Platform.isWindows) { + if (!bind.mainIsInstalled()) { + return buildInstallCard( + "", "install_tip", "Install", bind.mainGotoInstall); + } else if (bind.mainIsInstalledLowerVersion()) { + return buildInstallCard("Status", "Your installation is lower version.", + "Click to upgrade", bind.mainUpdateMe); + } + } else if (Platform.isMacOS) { if (!bind.mainIsCanScreenRecording(prompt: false)) { return buildInstallCard("Permissions", "config_screen", "Configure", () async { @@ -338,6 +336,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)) { @@ -345,8 +349,19 @@ class _DesktopHomePageState extends State bind.mainIsInstalledDaemon(prompt: true); }); } + } else if (Platform.isLinux) { + if (bind.mainCurrentIsWayland()) { + return buildInstallCard( + "Warning", translate("wayland_experiment_tip"), "", () async {}, + help: 'Help', + link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required'); + } else if (bind.mainIsLoginWayland()) { + return buildInstallCard("Warning", + "Login screen using Wayland is not supported", "", () async {}, + help: 'Help', + link: 'https://rustdesk.com/docs/en/manual/linux/#login-screen'); + } } - if (bind.mainIsInstalledLowerVersion()) {} return Container(); } @@ -428,39 +443,9 @@ class _DesktopHomePageState extends State ); } - @override - void onTrayIconMouseDown() { - windowManager.show(); - } - - @override - void onTrayIconRightMouseDown() { - // linux does not support popup menu manually. - // linux will handle popup action ifself. - if (Platform.isMacOS || Platform.isWindows) { - trayManager.popUpContextMenu(); - } - } - - @override - void onTrayMenuItemClick(MenuItem menuItem) { - switch (menuItem.key) { - case kTrayItemQuitKey: - windowManager.close(); - break; - case kTrayItemShowKey: - windowManager.show(); - windowManager.focus(); - break; - default: - break; - } - } - @override void initState() { super.initState(); - bind.mainStartGrabKeyboard(); _updateTimer = periodic_immediate(const Duration(seconds: 1), () async { await gFFI.serverModel.fetchID(); final url = await bind.mainGetSoftwareUpdateUrl(); @@ -490,19 +475,22 @@ class _DesktopHomePageState extends State setState(() {}); } } + if (watchIsInputMonitoring) { + if (bind.mainIsCanInputMonitoring(prompt: false)) { + watchIsInputMonitoring = false; + setState(() {}); + } + } }); Get.put(svcStopped, tag: 'stop-service'); - // disable this tray because we use tray function provided by rust now - // initTray(); - trayManager.addListener(this); rustDeskWinManager.registerActiveWindowListener(onActiveWindowChanged); rustDeskWinManager.setMethodHandler((call, fromWindowId) async { debugPrint( "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId"); - if (call.method == "main_window_on_top") { + if (call.method == kWindowMainWindowOnTop) { window_on_top(null); - } else if (call.method == "get_window_info") { + } else if (call.method == kWindowGetWindowInfo) { final screen = (await window_size.getWindowInfo()).screen; if (screen == null) { return ""; @@ -526,9 +514,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'], + ); } }); _uniLinksSubscription = listenUniLinks(); @@ -536,10 +531,6 @@ class _DesktopHomePageState extends State @override void dispose() { - // destoryTray(); - // fix: disable unregister to prevent from receiving events from other windows - // rustDeskWinManager.unregisterActiveWindowListener(onActiveWindowChanged); - trayManager.removeListener(this); _uniLinksSubscription?.cancel(); Get.delete(tag: 'stop-service'); _updateTimer?.cancel(); @@ -553,6 +544,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() { @@ -561,15 +560,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) { + for (var r in rules) { + if (!r.validate(pass)) { + setState(() { + errMsg0 = '${translate('Prompt')}: ${r.name}'; + }); + return; + } + } } if (p1.text.trim() != pass) { setState(() { - errMsg1 = translate("The confirmation is not identical."); + errMsg1 = + '${translate('Prompt')}: ${translate("The confirmation is not identical.")}'; }); return; } @@ -589,23 +593,40 @@ void setPasswordDialog() async { ), Row( children: [ - ConstrainedBox( - constraints: const BoxConstraints(minWidth: 100), - child: Text( - "${translate('Password')}:", - textAlign: TextAlign.start, - ).marginOnly(bottom: 16.0)), - const SizedBox( - width: 24.0, + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + labelText: translate('Password'), + border: const OutlineInputBorder(), + errorText: errMsg0.isNotEmpty ? errMsg0 : null), + controller: p0, + focusNode: FocusNode()..requestFocus(), + onChanged: (value) { + rxPass.value = value.trim(); + }, + ), ), + ], + ), + Row( + children: [ + Expanded(child: PasswordStrengthIndicator(password: rxPass)), + ], + ).marginSymmetric(vertical: 8), + const SizedBox( + height: 8.0, + ), + Row( + children: [ Expanded( child: TextField( obscureText: true, decoration: InputDecoration( border: const OutlineInputBorder(), - errorText: errMsg0.isNotEmpty ? errMsg0 : null), - controller: p0, - focusNode: FocusNode()..requestFocus(), + labelText: translate('Confirmation'), + errorText: errMsg1.isNotEmpty ? errMsg1 : null), + controller: p1, ), ), ], @@ -613,32 +634,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 a51b4d035..df87a0ead 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,6 +17,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart'; import '../../common/widgets/dialog.dart'; +import '../../common/widgets/login.dart'; const double _kTabWidth = 235; const double _kTabHeight = 42; @@ -63,7 +63,7 @@ class DesktopSettingPage extends StatefulWidget { DesktopTabPage.onAddSetting(initialPage: page); } } catch (e) { - debugPrint('$e'); + debugPrintStack(label: '$e'); } } } @@ -125,6 +125,7 @@ class _DesktopSettingPageState extends State scrollController: controller, child: PageView( controller: controller, + physics: NeverScrollableScrollPhysics(), children: const [ _General(), _Safety(), @@ -273,6 +274,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', + ), + ) ]); } @@ -932,6 +942,10 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { return false; } } + final old = await bind.mainGetOption(key: 'custom-rendezvous-server'); + if (old.isNotEmpty && old != idServer) { + await gFFI.userModel.logOut(); + } // should set one by one await bind.mainSetOption( key: 'custom-rendezvous-server', value: idServer); @@ -954,23 +968,17 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { import() { Clipboard.getData(Clipboard.kTextPlain).then((value) { - TextEditingController mytext = TextEditingController(); - String? aNullableString = ''; - aNullableString = value?.text; - mytext.text = aNullableString.toString(); - if (mytext.text.isNotEmpty) { + final text = value?.text; + if (text != null && text.isNotEmpty) { try { - Map config = jsonDecode(mytext.text); - if (config.containsKey('IdServer')) { - String id = config['IdServer'] ?? ''; - String relay = config['RelayServer'] ?? ''; - String api = config['ApiServer'] ?? ''; - String key = config['Key'] ?? ''; - idController.text = id; - relayController.text = relay; - apiController.text = api; - keyController.text = key; - Future success = set(id, relay, api, key); + final sc = ServerConfig.decode(text); + if (sc.idServer.isNotEmpty) { + idController.text = sc.idServer; + relayController.text = sc.relayServer; + apiController.text = sc.apiServer; + keyController.text = sc.key; + Future success = + set(sc.idServer, sc.relayServer, sc.apiServer, sc.key); success.then((value) { if (value) { showToast( @@ -992,12 +1000,15 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { } export() { - Map config = {}; - config['IdServer'] = idController.text.trim(); - config['RelayServer'] = relayController.text.trim(); - config['ApiServer'] = apiController.text.trim(); - config['Key'] = keyController.text.trim(); - Clipboard.setData(ClipboardData(text: jsonEncode(config))); + final text = ServerConfig( + idServer: idController.text, + relayServer: relayController.text, + apiServer: apiController.text, + key: keyController.text) + .encode(); + debugPrint("ServerConfig export: $text"); + + Clipboard.setData(ClipboardData(text: text)); showToast(translate('Export server configuration successfully')); } @@ -1059,21 +1070,13 @@ class _AccountState extends State<_Account> { } Widget accountAction() { - return _futureBuilder(future: () async { - return await gFFI.userModel.getUserName(); - }(), hasData: (_) { - return Obx(() => _Button( - gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', - () => { - gFFI.userModel.userName.value.isEmpty - ? loginDialog().then((success) { - if (success) { - gFFI.abModel.pullAb(); - } - }) - : gFFI.userModel.logOut() - })); - }); + return Obx(() => _Button( + gFFI.userModel.userName.value.isEmpty ? 'Login' : 'Logout', + () => { + gFFI.userModel.userName.value.isEmpty + ? loginDialog() + : gFFI.userModel.logOut() + })); } } @@ -1103,29 +1106,31 @@ class _AboutState extends State<_About> { child: SingleChildScrollView( controller: scrollController, physics: NeverScrollableScrollPhysics(), - child: _Card(title: 'About RustDesk', children: [ + child: _Card(title: '${translate('About')} RustDesk', children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( height: 8.0, ), - Text('Version: $version').marginSymmetric(vertical: 4.0), - Text('Build Date: $buildDate').marginSymmetric(vertical: 4.0), + Text('${translate('Version')}: $version') + .marginSymmetric(vertical: 4.0), + Text('${translate('Build Date')}: $buildDate') + .marginSymmetric(vertical: 4.0), InkWell( onTap: () { launchUrlString('https://rustdesk.com/privacy'); }, - child: const Text( - 'Privacy Statement', + child: Text( + translate('Privacy Statement'), style: linkStyle, ).marginSymmetric(vertical: 4.0)), InkWell( onTap: () { launchUrlString('https://rustdesk.com'); }, - child: const Text( - 'Website', + child: Text( + translate('Website'), style: linkStyle, ).marginSymmetric(vertical: 4.0)), Container( @@ -1142,8 +1147,8 @@ class _AboutState extends State<_About> { 'Copyright © 2022 Purslane Ltd.\n$license', style: const TextStyle(color: Colors.white), ), - const Text( - 'Made with heart in this chaotic world!', + Text( + translate('Slogan_tip'), style: TextStyle( fontWeight: FontWeight.w800, color: Colors.white), @@ -1227,7 +1232,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(); } } @@ -1431,7 +1436,7 @@ Widget _lock( _LabeledTextField( BuildContext context, - String lable, + String label, TextEditingController controller, String errorText, bool enabled, @@ -1442,7 +1447,7 @@ _LabeledTextField( Expanded( flex: 4, child: Text( - '${translate(lable)}:', + '${translate(label)}:', textAlign: TextAlign.right, style: TextStyle(color: _disabledTextColor(context, enabled)), ), @@ -1455,6 +1460,8 @@ _LabeledTextField( enabled: enabled, obscureText: secure, decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric(vertical: 15), errorText: errorText.isNotEmpty ? errorText : null), style: TextStyle( color: _disabledTextColor(context, enabled), @@ -1664,8 +1671,8 @@ void changeSocks5Proxy() async { ), ), 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 794dd1c08..57c7fe4b8 100644 --- a/flutter/lib/desktop/pages/desktop_tab_page.dart +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -31,7 +31,7 @@ class DesktopTabPage extends StatefulWidget { initialPage: initialPage, ))); } catch (e) { - debugPrint('$e'); + debugPrintStack(label: '$e'); } } } diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 021f03713..b6a9e5fed 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -7,6 +7,7 @@ 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/tabbar_widget.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:get/get.dart'; @@ -32,6 +33,18 @@ enum LocationStatus { fileSearchBar } +/// The status of currently focused scope of the mouse +enum MouseFocusScope { + /// Mouse is in local field. + local, + + /// Mouse is in remote field. + remote, + + /// Mouse is not in local field, remote neither. + none +} + class FileManagerPage extends StatefulWidget { const FileManagerPage({Key? key, required this.id}) : super(key: key); final String id; @@ -55,6 +68,11 @@ class _FileManagerPageState extends State 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 = @@ -93,6 +111,7 @@ 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); @@ -100,17 +119,18 @@ class _FileManagerPageState extends State @override void dispose() { - model.onClose(); - _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(); + model.onClose().whenComplete(() { + _ffi.close(); + _ffi.dialogManager.dismissAll(); + if (!Platform.isLinux) { + Wakelock.disable(); + } + Get.delete(tag: 'ft_${widget.id}'); + _locationNodeLocal.removeListener(onLocalLocationFocusChanged); + _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); + _locationNodeLocal.dispose(); + _locationNodeRemote.dispose(); + }); super.dispose(); } @@ -195,6 +215,7 @@ class _FileManagerPageState extends State } Widget body({bool isLocal = false}) { + final scrollController = ScrollController(); return Container( decoration: BoxDecoration(border: Border.all(color: Colors.black26)), margin: const EdgeInsets.all(16.0), @@ -215,8 +236,8 @@ class _FileManagerPageState extends State children: [ Expanded( child: SingleChildScrollView( - controller: ScrollController(), - child: _buildDataTable(context, isLocal), + controller: scrollController, + child: _buildDataTable(context, isLocal, scrollController), ), ) ], @@ -226,7 +247,9 @@ class _FileManagerPageState extends State ); } - Widget _buildDataTable(BuildContext context, bool isLocal) { + Widget _buildDataTable( + BuildContext context, bool isLocal, ScrollController scrollController) { + const rowHeight = 25.0; final fd = model.getCurrentDir(isLocal); final entries = fd.entries; final sortIndex = (SortBy style) { @@ -244,130 +267,219 @@ class _FileManagerPageState extends State final sortAscending = isLocal ? model.localSortAscending : model.remoteSortAscending; - return 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: 25, - 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, + return MouseRegion( + onEnter: (evt) { + _mouseFocusScope.value = + isLocal ? MouseFocusScope.local : MouseFocusScope.remote; + if (isLocal) { + _keyboardNodeLocal.requestFocus(); + } else { + _keyboardNodeRemote.requestFocus(); + } + }, + onExit: (evt) { + _mouseFocusScope.value = MouseFocusScope.none; + }, + child: ListSearchActionListener( + node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote, + buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote, + onNext: (buffer) { + debugPrint("searching next for $buffer"); + assert(buffer.length == 1); + 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)) - .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); + ?.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); - }, - ), - DataCell(FittedBox( - child: Tooltip( + // handle double click + if (_checkDoubleClick(entry)) { + openDirectory(entry.path, isLocal: isLocal); + items.clear(); + return; + } + _onSelectedChanged( + items, filteredEntries, entry, isLocal); + }, + ), + 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: lastModifiedStr, + message: sizeStr, child: Text( - lastModifiedStr, + sizeStr, + overflow: TextOverflow.ellipsis, 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, + fontSize: 10, color: MyTheme.darkGray), + ))), + ]); + }).toList(growable: false), + ); + }, + isLocal ? _searchTextLocal : _searchTextRemote, + ), + ), ); } + void _jumpToEntry(bool isLocal, Entry entry, + ScrollController scrollController, double rowHeight, String buffer) { + final entries = model.getCurrentDir(isLocal).entries; + final index = entries.indexOf(entry); + if (index == -1) { + debugPrint("entry is not valid: ${entry.path}"); + } + final selectedEntries = getSelectedItems(isLocal); + final searchResult = + entries.where((element) => element.name.startsWith(buffer)); + selectedEntries.clear(); + if (searchResult.isEmpty) { + return; + } + final offset = min( + max(scrollController.position.minScrollExtent, + entries.indexOf(searchResult.first) * rowHeight), + scrollController.position.maxScrollExtent); + scrollController.jumpTo(offset); + setState(() { + selectedEntries.add(isLocal, searchResult.first); + debugPrint("focused on ${searchResult.first.name}"); + }); + } + void _onSelectedChanged(SelectedItems selectedItems, List entries, Entry entry, bool isLocal) { final isCtrlDown = RawKeyboard.instance.keysPressed @@ -456,7 +568,7 @@ class _FileManagerPageState extends State Wrap( children: [ Text( - '${item.state.display()} ${max(0, item.fileNum)}/${item.fileCount} '), + '${item.display()} ${max(0, item.fileNum)}/${item.fileCount} '), Text( '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), Offstage( @@ -487,8 +599,8 @@ class _FileManagerPageState extends State icon: const Icon(Icons.restart_alt_rounded)), ), IconButton( - icon: const Icon(Icons.delete_forever_outlined), - splashRadius: kDesktopIconButtonSplashRadius, + icon: const Icon(Icons.close), + splashRadius: 1, onPressed: () { model.jobTable.removeAt(index); model.cancelJob(item.id); @@ -636,7 +748,6 @@ class _FileManagerPageState extends State }), IconButton( onPressed: () { - breadCrumbScrollToEnd(isLocal); model.refresh(isLocal: isLocal); }, splashRadius: kDesktopIconButtonSplashRadius, @@ -691,14 +802,9 @@ class _FileManagerPageState extends State ], ), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - ElevatedButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))) + dialogButton("Cancel", + onPressed: cancel, isOutline: true), + dialogButton("OK", onPressed: submit) ], onSubmit: submit, onCancel: cancel, @@ -800,7 +906,8 @@ class _FileManagerPageState extends State onPointerSignal: (e) { if (e is PointerScrollEvent) { final sc = getBreadCrumbScrollController(isLocal); - sc.jumpTo(sc.offset + e.scrollDelta.dy / 4); + final scale = Platform.isWindows ? 2 : 4; + sc.jumpTo(sc.offset + e.scrollDelta.dy / scale); } }, child: BreadCrumb( @@ -824,7 +931,7 @@ class _FileManagerPageState extends State final x = offset.dx; final y = offset.dy + size.height + 1; - final isPeerWindows = isWindows(isLocal); + final isPeerWindows = model.getCurrentIsWindows(isLocal); final List menuItems = [ MenuEntryButton( childBuilder: (TextStyle? style) => isPeerWindows @@ -870,6 +977,8 @@ class _FileManagerPageState extends State }, dismissOnClicked: true)); } + } catch (e) { + debugPrint("buildBread fetchDirectory err=$e"); } finally { if (!isLocal) { _ffi.dialogManager.dismissByTag(loadingTag); @@ -912,7 +1021,8 @@ class _FileManagerPageState extends State bool isLocal, void Function(List) onPressed) { final path = model.getCurrentDir(isLocal).path; final breadCrumbList = List.empty(growable: true); - if (isWindows(isLocal) && path == '/') { + final isWindows = model.getCurrentIsWindows(isLocal); + if (isWindows && path == '/') { breadCrumbList.add(BreadCrumbItem( content: TextButton( child: buildWindowsThisPC(), @@ -921,7 +1031,7 @@ class _FileManagerPageState extends State onPressed: () => onPressed(['/'])) .marginSymmetric(horizontal: 4))); } else { - final list = PathUtil.split(path, model.getCurrentIsWindows(isLocal)); + final list = PathUtil.split(path, isWindows); breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( content: TextButton( child: Text(e.value), @@ -933,14 +1043,6 @@ class _FileManagerPageState extends State return breadCrumbList; } - bool isWindows(bool isLocal) { - if (isLocal) { - return Platform.isWindows; - } else { - return _ffi.ffiModel.pi.platform.toLowerCase() == "windows"; - } - } - breadCrumbScrollToEnd(bool isLocal) { Future.delayed(Duration(milliseconds: 200), () { final breadCrumbScroller = getBreadCrumbScrollController(isLocal); @@ -999,9 +1101,7 @@ class _FileManagerPageState extends State } openDirectory(String path, {bool isLocal = false}) { - model.openDirectory(path, isLocal: isLocal).then((_) { - breadCrumbScrollToEnd(isLocal); - }); + model.openDirectory(path, isLocal: isLocal); } void handleDragDone(DropDoneDetails details, bool isLocal) { @@ -1022,4 +1122,14 @@ class _FileManagerPageState extends State } model.sendFiles(items, isRemote: false); } + + void refocusKeyboardListener(bool isLocal) { + Future.delayed(Duration.zero, () { + if (isLocal) { + _keyboardNodeLocal.requestFocus(); + } else { + _keyboardNodeRemote.requestFocus(); + } + }); + } } diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index 7e07eaa9a..b2566e267 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -31,6 +31,10 @@ 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'], diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index b2458d096..f513a1c6a 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -127,8 +127,8 @@ 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) @@ -241,8 +241,8 @@ class _PortForwardPageState extends State } Widget buildTunnelDataRow(BuildContext context, _PortForward pf, int index) { - text(String lable) => Expanded( - child: Text(lable, style: const TextStyle(fontSize: 20)) + text(String label) => Expanded( + child: Text(label, style: const TextStyle(fontSize: 20)) .marginOnly(left: _kTextLeftMargin)); return Container( @@ -285,11 +285,11 @@ 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( diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index d4c0a86f8..ca354f297 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'], diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart index 7b3e0fe82..fb67154bc 100644 --- a/flutter/lib/desktop/pages/remote_page.dart +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -2,9 +2,12 @@ import 'dart:async'; 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' + as custom_cursor_manager; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; @@ -20,6 +23,7 @@ import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../../common/shared_state.dart'; import '../widgets/remote_menubar.dart'; +import '../widgets/kb_layout_type_chooser.dart'; bool _isCustomCursorInited = false; final SimpleWrapper _firstEnterImage = SimpleWrapper(false); @@ -29,10 +33,12 @@ class RemotePage extends StatefulWidget { Key? key, required this.id, required this.menubarState, + this.switchUuid, }) : super(key: key); final String id; final MenubarState menubarState; + final String? switchUuid; final SimpleWrapper?> _lastState = SimpleWrapper(null); FFI get ffi => (_lastState.value! as _RemotePageState)._ffi; @@ -46,17 +52,17 @@ class RemotePage extends StatefulWidget { } class _RemotePageState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, MultiWindowListener { Timer? _timer; String keyboardMode = "legacy"; + bool _isWindowBlur = false; final _cursorOverImage = false.obs; late RxBool _showRemoteCursor; late RxBool _zoomCursor; late RxBool _remoteCursorMoved; late RxBool _keyboardEnabled; - final FocusNode _rawKeyFocusNode = FocusNode(); - var _imageFocused = false; + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); Function(bool)? _onEnterOrLeaveImage4Menubar; @@ -92,7 +98,14 @@ class _RemotePageState extends State _initStates(widget.id); _ffi = FFI(); Get.put(_ffi, tag: widget.id); - _ffi.start(widget.id); + _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + showKBLayoutTypeChooserIfNeeded( + _ffi.ffiModel.pi.platform, _ffi.dialogManager); + }); + _ffi.start( + widget.id, + switchUuid: widget.switchUuid, + ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); _ffi.dialogManager @@ -101,7 +114,6 @@ class _RemotePageState extends State if (!Platform.isLinux) { Wakelock.enable(); } - _rawKeyFocusNode.requestFocus(); _ffi.ffiModel.updateEventListener(widget.id); _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); // Session option should be set after models.dart/FFI.start @@ -109,22 +121,59 @@ class _RemotePageState extends State id: widget.id, arg: 'show-remote-cursor'); _zoomCursor.value = bind.sessionGetToggleOptionSync(id: widget.id, arg: 'zoom-cursor'); - if (!_isCustomCursorInited) { - customCursorController.registerNeedUpdateCursorCallback( - (String? lastKey, String? currentKey) async { - if (_firstEnterImage.value) { - _firstEnterImage.value = false; - return true; - } - return lastKey == null || lastKey != currentKey; - }); - _isCustomCursorInited = true; + DesktopMultiWindow.addListener(this); + // if (!_isCustomCursorInited) { + // customCursorController.registerNeedUpdateCursorCallback( + // (String? lastKey, String? currentKey) async { + // if (_firstEnterImage.value) { + // _firstEnterImage.value = false; + // return true; + // } + // return lastKey == null || lastKey != currentKey; + // }); + // _isCustomCursorInited = true; + // } + } + + @override + void onWindowBlur() { + super.onWindowBlur(); + // On windows, we use `focus` way to handle keyboard better. + // Now on Linux, there's some rdev issues which will break the input. + // We disable the `focus` way for non-Windows temporarily. + if (Platform.isWindows) { + _isWindowBlur = true; + // unfocus the primary-focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } + } + + @override + void onWindowFocus() { + super.onWindowFocus(); + // See [onWindowBlur]. + if (Platform.isWindows) { + _isWindowBlur = false; + } + } + + @override + void onWindowRestore() { + super.onWindowRestore(); + // On windows, we use `onWindowRestore` way to handle window restore from + // a minimized state. + if (Platform.isWindows) { + _isWindowBlur = false; } } @override void dispose() { debugPrint("REMOTE PAGE dispose ${widget.id}"); + // ensure we leave this session, this is a double check + bind.sessionEnterOrLeave(id: widget.id, enter: false); + DesktopMultiWindow.removeListener(this); _ffi.dialogManager.hideMobileActionsOverlay(); _ffi.recordingModel.onClose(); _rawKeyFocusNode.dispose(); @@ -153,8 +202,23 @@ class _RemotePageState extends State color: Colors.black, child: RawKeyFocusScope( focusNode: _rawKeyFocusNode, - onFocusChange: (bool v) { - _imageFocused = v; + 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))); @@ -181,9 +245,6 @@ class _RemotePageState extends State } void enterView(PointerEnterEvent evt) { - if (!_imageFocused) { - _rawKeyFocusNode.requestFocus(); - } _cursorOverImage.value = true; _firstEnterImage.value = true; if (_onEnterOrLeaveImage4Menubar != null) { @@ -193,7 +254,13 @@ class _RemotePageState extends State // } } - _ffi.inputModel.enterOrLeave(true); + // See [onWindowBlur]. + if (!Platform.isWindows) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + bind.sessionEnterOrLeave(id: widget.id, enter: true); + } } void leaveView(PointerExitEvent evt) { @@ -206,7 +273,10 @@ class _RemotePageState extends State // } } - _ffi.inputModel.enterOrLeave(false); + // See [onWindowBlur]. + if (!Platform.isWindows) { + bind.sessionEnterOrLeave(id: widget.id, enter: false); + } } Widget getBodyForDesktop(BuildContext context) { @@ -228,6 +298,21 @@ class _RemotePageState extends State 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, ), @@ -235,9 +320,9 @@ class _RemotePageState extends State })) ]; - if (!_ffi.canvasModel.cursorEmbeded) { - paints.add(Obx(() => Visibility( - visible: _showRemoteCursor.isTrue && _remoteCursorMoved.isTrue, + if (!_ffi.canvasModel.cursorEmbedded) { + paints.add(Obx(() => Offstage( + offstage: _showRemoteCursor.isFalse || _remoteCursorMoved.isFalse, child: CursorPaint( id: widget.id, zoomCursor: _zoomCursor, @@ -302,7 +387,7 @@ class _ImagePaintState extends State { mouseRegion({child}) => Obx(() => MouseRegion( cursor: cursorOverImage.isTrue - ? c.cursorEmbeded + ? c.cursorEmbedded ? SystemMouseCursors.none : keyboardEnabled.isTrue ? (() { @@ -322,35 +407,36 @@ class _ImagePaintState extends State { onHover: (evt) {}, child: child)); - if (c.scrollStyle == ScrollStyle.scrollbar) { + if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { final imageWidth = c.getDisplayWidth() * s; final imageHeight = c.getDisplayHeight() * s; + final imageSize = Size(imageWidth, imageHeight); final imageWidget = CustomPaint( - size: Size(imageWidth, imageHeight), + 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), @@ -366,15 +452,23 @@ class _ImagePaintState extends State { return MouseCursor.defer; } else { final key = cache.updateGetKey(scale, zoomCursor.value); - cursor.addKey(key); - return FlutterCustomMemoryImageCursor( - pixbuf: cache.data, - key: key, - hotx: cache.hotx, - hoty: cache.hoty, - imageWidth: (cache.width * cache.scale).toInt(), - imageHeight: (cache.height * cache.scale).toInt(), - ); + if (!cursor.cachedKeys.contains(key)) { + debugPrint("Register custom cursor with key $key"); + // [Safety] + // It's ok to call async registerCursor in current synchronous context, + // because activating the cursor is also an async call and will always + // be executed after this. + custom_cursor_manager.CursorManager.instance + .registerCursor(custom_cursor_manager.CursorData() + ..buffer = cache.data! + ..height = (cache.height * cache.scale).toInt() + ..width = (cache.width * cache.scale).toInt() + ..hotX = cache.hotx + ..hotY = cache.hoty + ..name = key); + cursor.addKey(key); + } + return FlutterCustomMemoryImageCursor(key: key); } } @@ -477,24 +571,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); @@ -529,7 +605,8 @@ class CursorPaint extends StatelessWidget { double cx = c.x; double cy = c.y; - if (c.scrollStyle == ScrollStyle.scrollbar) { + if (c.viewStyle.style == kRemoteViewStyleOriginal && + c.scrollStyle == ScrollStyle.scrollbar) { final d = c.parent.target!.ffiModel.display; final imageWidth = d.width * c.scale; final imageHeight = d.height * c.scale; @@ -538,7 +615,7 @@ class CursorPaint extends StatelessWidget { } double x = (m.x - hotx) * c.scale + cx; - double y = (m.y - hoty) * c.scale + cx; + double y = (m.y - hoty) * c.scale + cy; double scale = 1.0; if (zoomCursor.isTrue) { x = m.x - hotx + cx / c.scale; diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index 713c3d13c..83928c3fe 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -39,8 +39,7 @@ class ConnectionTabPage extends StatefulWidget { class _ConnectionTabPageState extends State { final tabController = Get.put(DesktopTabController( - tabType: DesktopTabType.remoteScreen, - onSelected: (_, id) => bind.setCurSessionId(id: id))); + tabType: DesktopTabType.remoteScreen)); static const IconData selectedIcon = Icons.desktop_windows_sharp; static const IconData unselectedIcon = Icons.desktop_windows_outlined; @@ -54,6 +53,11 @@ class _ConnectionTabPageState extends State { final peerId = params['id']; if (peerId != null) { ConnectionTypeState.init(peerId); + tabController.onSelected = (_, id) { + bind.setCurSessionId(id: id); + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; tabController.add(TabInfo( key: peerId, label: peerId, @@ -64,6 +68,7 @@ class _ConnectionTabPageState extends State { key: ValueKey(peerId), id: peerId, menubarState: _menubarState, + switchUuid: params['switch_uuid'], ), )); _update_remote_count(); @@ -75,6 +80,7 @@ class _ConnectionTabPageState extends State { super.initState(); tabController.onRemoved = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { print( @@ -84,6 +90,7 @@ class _ConnectionTabPageState extends State { if (call.method == "new_remote_desktop") { final args = jsonDecode(call.arguments); final id = args['id']; + final switchUuid = args['switch_uuid']; window_on_top(windowId()); ConnectionTypeState.init(id); tabController.add(TabInfo( @@ -96,6 +103,7 @@ class _ConnectionTabPageState extends State { key: ValueKey(id), id: id, menubarState: _menubarState, + switchUuid: switchUuid, ), )); } else if (call.method == "onDestroy") { @@ -257,7 +265,7 @@ class _ConnectionTabPageState extends State { ), ]); - if (!ffi.canvasModel.cursorEmbeded) { + if (!ffi.canvasModel.cursorEmbedded) { menu.add(MenuEntryDivider()); menu.add(() { final state = ShowRemoteCursorState.find(key); @@ -308,7 +316,7 @@ class _ConnectionTabPageState extends State { dismissOnClicked: true, )); - if (pi.platform == 'Linux' || pi.sasEnabled) { + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { menu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( '${translate("Insert")} Ctrl + Alt + Del', diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 6c586994b..521413647 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -30,7 +30,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(); } @@ -238,7 +243,7 @@ Widget buildConnectionCard(Client client) { key: ValueKey(client.id), children: [ _CmHeader(client: client), - client.isFileTransfer || client.disconnected + client.type_() != ClientType.remote || client.disconnected ? Offstage() : _PrivilegeBoard(client: client), Expanded( @@ -376,7 +381,7 @@ class _CmHeaderState extends State<_CmHeader> ), ), Offstage( - offstage: !client.authorized || client.isFileTransfer, + offstage: !client.authorized || client.type_() != ClientType.remote, child: IconButton( onPressed: () => checkClickTime( client.id, () => gFFI.chatModel.toggleCMChatPage(client.id)), @@ -510,10 +515,21 @@ class _CmControlPanel extends StatelessWidget { buildAuthorized(BuildContext context) { final bool canElevate = bind.cmCanElevate(); final model = Provider.of(context); - final showElevation = canElevate && model.showElevation; + final showElevation = canElevate && + model.showElevation && + client.type_() == ClientType.remote; return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ + 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: () { @@ -560,7 +576,9 @@ class _CmControlPanel extends StatelessWidget { buildUnAuthorized(BuildContext context) { final bool canElevate = bind.cmCanElevate(); final model = Provider.of(context); - final showElevation = canElevate && model.showElevation; + final showElevation = canElevate && + model.showElevation && + client.type_() == ClientType.remote; final showAccept = model.approveMode != 'password'; return Column( mainAxisAlignment: MainAxisAlignment.end, @@ -670,6 +688,10 @@ class _CmControlPanel extends StatelessWidget { windowManager.close(); } } + + void handleSwitchBack(BuildContext context) { + bind.cmSwitchBack(connId: 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 57886b2f2..bb6bc431b 100644 --- a/flutter/lib/desktop/screen/desktop_remote_screen.dart +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/desktop/pages/remote_tab_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; +import 'package:flutter_hbb/models/state_model.dart'; import 'package:provider/provider.dart'; /// multi-tab desktop remote screen class DesktopRemoteScreen extends StatelessWidget { final Map params; - const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key); + DesktopRemoteScreen({Key? key, required this.params}) : super(key: key) { + if (!bind.mainStartGrabKeyboard()) { + stateGlobal.grabKeyboard = true; + } + } @override Widget build(BuildContext context) { diff --git a/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart new file mode 100644 index 000000000..90e72cd40 --- /dev/null +++ b/flutter/lib/desktop/widgets/kb_layout_type_chooser.dart @@ -0,0 +1,225 @@ +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 KBChosenCallback = Future Function(String); + +const double _kImageMarginVertical = 6.0; +const double _kImageMarginHorizontal = 10.0; +const double _kImageBoarderWidth = 4.0; +const double _kImagePaddingWidth = 4.0; +const Color _kImageBorderColor = Color.fromARGB(125, 202, 247, 2); +const double _kBorderRadius = 6.0; +const String _kKBLayoutTypeISO = 'ISO'; +const String _kKBLayoutTypeNotISO = 'Not ISO'; + +const _kKBLayoutImageMap = { + _kKBLayoutTypeISO: 'kb_layout_iso', + _kKBLayoutTypeNotISO: 'kb_layout_not_iso', +}; + +class _KBImage extends StatelessWidget { + final String kbLayoutType; + final double imageWidth; + final RxString chosenType; + const _KBImage({ + Key? key, + required this.kbLayoutType, + required this.imageWidth, + required this.chosenType, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(_kBorderRadius), + border: Border.all( + color: chosenType.value == kbLayoutType + ? _kImageBorderColor + : Colors.transparent, + width: _kImageBoarderWidth, + ), + ), + margin: EdgeInsets.symmetric( + horizontal: _kImageMarginHorizontal, + vertical: _kImageMarginVertical, + ), + padding: EdgeInsets.all(_kImagePaddingWidth), + child: SvgPicture.asset( + 'assets/${_kKBLayoutImageMap[kbLayoutType] ?? ""}.svg', + width: imageWidth - + _kImageMarginHorizontal * 2 - + _kImagePaddingWidth * 2 - + _kImageBoarderWidth * 2, + ), + ); + }); + } +} + +class _KBChooser extends StatelessWidget { + final String kbLayoutType; + final double imageWidth; + final RxString chosenType; + final KBChosenCallback cb; + const _KBChooser({ + Key? key, + required this.kbLayoutType, + required this.imageWidth, + required this.chosenType, + required this.cb, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + onChanged(String? v) async { + if (v != null) { + if (await cb(v)) { + chosenType.value = v; + } + } + } + + return Column( + children: [ + TextButton( + onPressed: () { + onChanged(kbLayoutType); + }, + child: _KBImage( + kbLayoutType: kbLayoutType, + imageWidth: imageWidth, + chosenType: chosenType, + ), + style: TextButton.styleFrom(padding: EdgeInsets.zero), + ), + TextButton( + child: Row( + children: [ + Obx(() => Radio( + splashRadius: 0, + value: kbLayoutType, + groupValue: chosenType.value, + onChanged: onChanged, + )), + Text(kbLayoutType), + ], + ), + onPressed: () { + onChanged(kbLayoutType); + }, + ), + ], + ); + } +} + +class KBLayoutTypeChooser extends StatelessWidget { + final RxString chosenType; + final double width; + final double height; + final double dividerWidth; + final KBChosenCallback cb; + KBLayoutTypeChooser({ + Key? key, + required this.chosenType, + required this.width, + required this.height, + required this.dividerWidth, + required this.cb, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final imageWidth = width / 2 - dividerWidth; + return SizedBox( + width: width, + height: height, + child: Center( + child: Row( + children: [ + _KBChooser( + kbLayoutType: _kKBLayoutTypeISO, + imageWidth: imageWidth, + chosenType: chosenType, + cb: cb, + ), + VerticalDivider( + width: dividerWidth * 2, + ), + _KBChooser( + kbLayoutType: _kKBLayoutTypeNotISO, + imageWidth: imageWidth, + chosenType: chosenType, + cb: cb, + ), + ], + ), + ), + ); + } +} + +RxString KBLayoutType = ''.obs; + +String getLocalPlatformForKBLayoutType(String peerPlatform) { + String localPlatform = ''; + if (peerPlatform != kPeerPlatformMacOS) { + return localPlatform; + } + + if (Platform.isWindows) { + localPlatform = kPeerPlatformWindows; + } else if (Platform.isLinux) { + localPlatform = kPeerPlatformLinux; + } + // to-do: web desktop support ? + return localPlatform; +} + +showKBLayoutTypeChooserIfNeeded( + String peerPlatform, + OverlayDialogManager dialogManager, +) async { + final localPlatform = getLocalPlatformForKBLayoutType(peerPlatform); + if (localPlatform == '') { + return; + } + KBLayoutType.value = bind.getLocalKbLayoutType(); + if (KBLayoutType.value == _kKBLayoutTypeISO || + KBLayoutType.value == _kKBLayoutTypeNotISO) { + return; + } + showKBLayoutTypeChooser(localPlatform, dialogManager); +} + +showKBLayoutTypeChooser( + String localPlatform, + OverlayDialogManager dialogManager, +) { + dialogManager.show((setState, close) { + return CustomAlertDialog( + title: + Text('${translate('Select local keyboard type')} ($localPlatform)'), + content: KBLayoutTypeChooser( + chosenType: KBLayoutType, + width: 360, + height: 200, + dividerWidth: 4.0, + cb: (String v) async { + await bind.setLocalKbLayoutType(kbLayoutType: v); + KBLayoutType.value = bind.getLocalKbLayoutType(); + return v == KBLayoutType.value; + }), + 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 new file mode 100644 index 000000000..9598c3400 --- /dev/null +++ b/flutter/lib/desktop/widgets/list_search_action_listener.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; + +class ListSearchActionListener extends StatelessWidget { + final FocusNode node; + final TimeoutStringBuffer buffer; + final Widget child; + final Function(String) onNext; + final Function(String) onSearch; + + const ListSearchActionListener( + {super.key, + required this.node, + required this.buffer, + required this.child, + required this.onNext, + required this.onSearch}); + + @mustCallSuper + @override + Widget build(BuildContext context) { + return KeyboardListener( + autofocus: true, + onKeyEvent: (kv) { + final ch = kv.character; + if (ch == null) { + return; + } + final action = buffer.input(ch); + switch (action) { + case ListSearchAction.search: + onSearch(buffer.buffer); + break; + case ListSearchAction.next: + onNext(buffer.buffer); + break; + } + }, + focusNode: node, + child: child); + } +} + +enum ListSearchAction { search, next } + +class TimeoutStringBuffer { + var _buffer = ""; + late DateTime _duration; + + static int timeoutMilliSec = 1500; + + String get buffer => _buffer; + + TimeoutStringBuffer() { + _duration = DateTime.now(); + } + + ListSearchAction input(String ch) { + final curr = DateTime.now(); + try { + if (curr.difference(_duration).inMilliseconds > timeoutMilliSec) { + _buffer = ch; + return ListSearchAction.search; + } else { + if (ch == _buffer) { + return ListSearchAction.next; + } else { + _buffer += ch; + return ListSearchAction.search; + } + } + } finally { + _duration = curr; + } + } +} diff --git a/flutter/lib/desktop/widgets/login.dart b/flutter/lib/desktop/widgets/login.dart deleted file mode 100644 index 3e58a6de2..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) { - debugPrint(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/popup_menu.dart b/flutter/lib/desktop/widgets/popup_menu.dart index 20ab31ed9..0cbdad929 100644 --- a/flutter/lib/desktop/widgets/popup_menu.dart +++ b/flutter/lib/desktop/widgets/popup_menu.dart @@ -118,6 +118,15 @@ abstract class MenuEntryBase { this.enabled, }); 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 { @@ -189,54 +198,76 @@ 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); + } + 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, + )), + ), ); } @@ -567,12 +598,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( @@ -605,14 +633,6 @@ class MenuEntryButton extends MenuEntryBase { ); 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, @@ -631,7 +651,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..60e816044 100644 --- a/flutter/lib/desktop/widgets/refresh_wrapper.dart +++ b/flutter/lib/desktop/widgets/refresh_wrapper.dart @@ -26,7 +26,7 @@ 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); } diff --git a/flutter/lib/desktop/widgets/remote_menubar.dart b/flutter/lib/desktop/widgets/remote_menubar.dart index d40a21e5d..227002645 100644 --- a/flutter/lib/desktop/widgets/remote_menubar.dart +++ b/flutter/lib/desktop/widgets/remote_menubar.dart @@ -8,9 +8,10 @@ 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:rxdart/rxdart.dart' as rxdart; +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; @@ -21,6 +22,7 @@ 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'; @@ -118,10 +120,11 @@ class RemoteMenubar extends StatefulWidget { } class _RemoteMenubarState extends State { - final Rx _hideColor = Colors.white12.obs; - final _rxHideReplay = rxdart.ReplaySubject(); + late Debouncer _debouncerHide; bool _isCursorOverImage = false; window_size.Screen? _screen; + final _fractionX = 0.5.obs; + final _dragging = false.obs; int get windowId => stateGlobal.windowId; @@ -138,23 +141,26 @@ class _RemoteMenubarState extends State { initState() { super.initState(); + _debouncerHide = Debouncer( + Duration(milliseconds: 5000), + onChanged: _debouncerHideProc, + initialValue: 0, + ); + widget.onEnterOrLeaveImageSetter((enter) { if (enter) { - _rxHideReplay.add(0); + _debouncerHide.value = 0; _isCursorOverImage = true; } else { _isCursorOverImage = false; } }); + } - _rxHideReplay - .throttleTime(const Duration(milliseconds: 5000), - trailing: true, leading: false) - .listen((int v) { - if (!pin && show.isTrue && _isCursorOverImage) { - show.value = false; - } - }); + _debouncerHideProc(int v) { + if (!pin && show.isTrue && _isCursorOverImage && _dragging.isFalse) { + show.value = false; + } } @override @@ -166,42 +172,38 @@ class _RemoteMenubarState extends State { @override Widget build(BuildContext context) { + // No need to use future builder here. + _updateScreen(); return Align( alignment: Alignment.topCenter, - child: Obx( - () => show.value ? _buildMenubar(context) : _buildShowHide(context)), + child: Obx(() => show.value + ? _buildMenubar(context) + : _buildDraggableShowHide(context)), ); } - Widget _buildShowHide(BuildContext context) { - return Obx(() => Tooltip( - message: translate(show.value ? 'Hide Menubar' : 'Show Menubar'), - child: SizedBox( - width: 100, - height: 13, - child: TextButton( - onHover: (bool v) { - _hideColor.value = v ? Colors.white60 : Colors.white24; - }, - onPressed: () { - show.value = !show.value; - _hideColor.value = Colors.white24; - if (show.isTrue) { - _updateScreen(); - } - }, - child: Obx(() => Container( - decoration: BoxDecoration( - color: _hideColor.value, - border: Border.all(color: MyTheme.border), - borderRadius: BorderRadius.all(Radius.circular(5.0)), - ), - ).marginOnly(bottom: 8.0)), - )))); + 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 DesktopMultiWindow.invokeMethod(0, 'get_window_info', ''); + final v = await rustDeskWinManager.call( + WindowType.Main, kWindowGetWindowInfo, ''); final String valueStr = v; if (valueStr.isEmpty) { _screen = null; @@ -253,13 +255,12 @@ class _RemoteMenubarState extends State { decoration: BoxDecoration( color: Colors.white, border: Border.all(color: MyTheme.border), - borderRadius: BorderRadius.all(Radius.circular(10.0)), ), child: Row( mainAxisSize: MainAxisSize.min, children: menubarItems, )), - _buildShowHide(context), + _buildDraggableShowHide(context), ])); } @@ -364,8 +365,6 @@ class _RemoteMenubarState extends State { RxInt display = CurrentDisplayState.find(widget.id); if (display.value != i) { bind.sessionSwitchDisplay(id: widget.id, value: i); - pi.currentDisplay = i; - display.value = i; } }, ) @@ -510,6 +509,7 @@ class _RemoteMenubarState extends State { List> _getControlMenu(BuildContext context) { final pi = widget.ffi.ffiModel.pi; final perms = widget.ffi.ffiModel.permissions; + final peer_version = widget.ffi.ffiModel.pi.version; const EdgeInsets padding = EdgeInsets.only(left: 14.0, right: 5.0); final List> displayMenu = []; displayMenu.addAll([ @@ -571,7 +571,8 @@ class _RemoteMenubarState extends State { ), ]); // {handler.get_audit_server() &&
  • {translate('Note')}
  • } - final auditServer = bind.sessionGetAuditServerSync(id: widget.id); + final auditServer = + bind.sessionGetAuditServerSync(id: widget.id, typ: "conn"); if (auditServer.isNotEmpty) { displayMenu.add( MenuEntryButton( @@ -589,7 +590,7 @@ class _RemoteMenubarState extends State { } displayMenu.add(MenuEntryDivider()); if (perms['keyboard'] != false) { - if (pi.platform == 'Linux' || pi.sasEnabled) { + if (pi.platform == kPeerPlatformLinux || pi.sasEnabled) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( '${translate("Insert")} Ctrl + Alt + Del', @@ -604,9 +605,9 @@ class _RemoteMenubarState 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)) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Text( translate('Restart Remote Device'), @@ -633,7 +634,7 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); - if (pi.platform == 'Windows') { + if (pi.platform == kPeerPlatformWindows) { displayMenu.add(MenuEntryButton( childBuilder: (TextStyle? style) => Obx(() => Text( translate( @@ -651,6 +652,20 @@ class _RemoteMenubarState extends State { dismissOnClicked: true, )); } + if (false && + pi.platform != kPeerPlatformAndroid && + version_cmp(peer_version, '1.2.0') >= 0) { + displayMenu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Switch Sides'), + style: style, + ), + proc: () => + showConfirmSwitchSidesDialog(widget.id, widget.ffi.dialogManager), + padding: padding, + dismissOnClicked: true, + )); + } } if (pi.version.isNotEmpty) { @@ -699,12 +714,12 @@ class _RemoteMenubarState extends State { if (_screen == null) { return false; } - double scale = _screen!.scaleFactor; - double selfWidth = _screen!.frame.width; - double selfHeight = _screen!.frame.height; + final scale = kIgnoreDpi ? 1.0 : _screen!.scaleFactor; + double selfWidth = _screen!.visibleFrame.width; + double selfHeight = _screen!.visibleFrame.height; if (isFullscreen) { - selfWidth = _screen!.visibleFrame.width; - selfHeight = _screen!.visibleFrame.height; + selfWidth = _screen!.frame.width; + selfHeight = _screen!.frame.height; } final canvasModel = widget.ffi.canvasModel; @@ -721,6 +736,7 @@ class _RemoteMenubarState extends State { List> _getDisplayMenu( dynamic futureData, int remoteCount) { const EdgeInsets padding = EdgeInsets.only(left: 18.0, right: 8.0); + final peer_version = widget.ffi.ffiModel.pi.version; final displayMenu = [ MenuEntryRadios( text: translate('Ratio'), @@ -809,7 +825,7 @@ class _RemoteMenubarState extends State { } if (newValue == kRemoteImageQualityCustom) { - final btnClose = msgBoxButton(translate('Close'), () async { + final btnClose = dialogButton('Close', onPressed: () async { await setCustomValues(); widget.ffi.dialogManager.dismissAll(); }); @@ -829,15 +845,13 @@ class _RemoteMenubarState extends State { qualityInitValue = qualityMaxValue; } final RxDouble qualitySliderValue = RxDouble(qualityInitValue); - final qualityRxReplay = rxdart.ReplaySubject(); - qualityRxReplay - .throttleTime(const Duration(milliseconds: 1000), - trailing: true, leading: false) - .listen((double v) { - () async { - await setCustomValues(quality: v); - }(); - }); + final debouncerQuality = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(quality: v); + }, + initialValue: qualityInitValue, + ); final qualitySlider = Obx(() => Row( children: [ Slider( @@ -847,7 +861,7 @@ class _RemoteMenubarState extends State { divisions: 90, onChanged: (double value) { qualitySliderValue.value = value; - qualityRxReplay.add(value); + debouncerQuality.value = value; }, ), SizedBox( @@ -867,15 +881,13 @@ class _RemoteMenubarState extends State { fpsInitValue = 30; } final RxDouble fpsSliderValue = RxDouble(fpsInitValue); - final fpsRxReplay = rxdart.ReplaySubject(); - fpsRxReplay - .throttleTime(const Duration(milliseconds: 1000), - trailing: true, leading: false) - .listen((double v) { - () async { - await setCustomValues(fps: v); - }(); - }); + final debouncerFps = Debouncer( + Duration(milliseconds: 1000), + onChanged: (double v) { + setCustomValues(fps: v); + }, + initialValue: qualityInitValue, + ); bool? direct; try { direct = ConnectionTypeState.find(widget.id).direct.value == @@ -884,9 +896,7 @@ class _RemoteMenubarState extends State { 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')), + version_cmp(peer_version, '1.2.0') < 0, child: Row( children: [ Obx((() => Slider( @@ -896,7 +906,7 @@ class _RemoteMenubarState extends State { divisions: 22, onChanged: (double value) { fpsSliderValue.value = value; - fpsRxReplay.add(value); + debouncerFps.value = value; }, ))), SizedBox( @@ -940,11 +950,13 @@ class _RemoteMenubarState extends State { text: translate('ScrollAuto'), value: kRemoteScrollStyleAuto, dismissOnClicked: true, + enabled: widget.ffi.canvasModel.imageOverflow, ), MenuEntryRadioOption( text: translate('Scrollbar'), value: kRemoteScrollStyleBar, dismissOnClicked: true, + enabled: widget.ffi.canvasModel.imageOverflow, ), ], curOptionGetter: () async => @@ -958,75 +970,77 @@ class _RemoteMenubarState extends State { 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; + 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; + final canvasModel = widget.ffi.canvasModel; + final width = + (canvasModel.getDisplayWidth() * canvasModel.scale + + canvasModel.windowBorderWidth * 2) * + scale + + magicWidth; + final height = + (canvasModel.getDisplayHeight() * canvasModel.scale + + 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; + 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)); } - 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, - ), - ); + }(); + }, + padding: padding, + dismissOnClicked: true, + ), + ); + } } /// Show Codec Preference @@ -1038,7 +1052,9 @@ class _RemoteMenubarState extends State { final h265 = codecsJson['h265'] ?? false; codecs.add(h264); codecs.add(h265); - } finally {} + } catch (e) { + debugPrint("Show Codec Preference err=$e"); + } if (codecs.length == 2 && (codecs[0] || codecs[1])) { displayMenu.add(MenuEntryRadios( text: translate('Codec Preference'), @@ -1088,7 +1104,7 @@ class _RemoteMenubarState extends State { } /// Show remote cursor - if (!widget.ffi.canvasModel.cursorEmbeded) { + if (!widget.ffi.canvasModel.cursorEmbedded) { displayMenu.add(() { final state = ShowRemoteCursorState.find(widget.id); return MenuEntrySwitch2( @@ -1155,7 +1171,7 @@ class _RemoteMenubarState extends State { } if (Platform.isWindows && - pi.platform == 'Windows' && + pi.platform == kPeerPlatformWindows && perms['file'] != false) { displayMenu.add(_createSwitchMenuEntry( 'Allow file copy and paste', 'enable-file-transfer', padding, true)); @@ -1168,7 +1184,7 @@ class _RemoteMenubarState extends State { } displayMenu.add(_createSwitchMenuEntry( 'Lock after session end', 'lock-after-session-end', padding, true)); - if (pi.platform == 'Windows') { + if (pi.features.privacyMode) { displayMenu.add(MenuEntrySwitch2( switchType: SwitchType.scheckbox, text: translate('Privacy mode'), @@ -1188,22 +1204,85 @@ class _RemoteMenubarState extends State { } List> _getKeyboardMenu() { - final keyboardMenu = [ + final List> keyboardMenu = [ MenuEntryRadios( text: translate('Ratio'), - optionsGetter: () => [ - MenuEntryRadioOption(text: translate('Legacy mode'), value: 'legacy'), - MenuEntryRadioOption(text: translate('Map mode'), value: 'map'), - ], - curOptionGetter: () async => - await bind.sessionGetKeyboardName(id: widget.id), + optionsGetter: () { + List list = []; + List modes = ["legacy"]; + + if (bind.sessionIsKeyboardModeSupported(id: widget.id, mode: "map")) { + modes.add("map"); + } + + for (String mode in modes) { + if (mode == "legacy") { + list.add(MenuEntryRadioOption( + text: translate('Legacy mode'), value: 'legacy')); + } else if (mode == "map") { + list.add(MenuEntryRadioOption( + text: translate('Map mode'), value: 'map')); + } + } + return list; + }, + curOptionGetter: () async { + return await bind.sessionGetKeyboardMode(id: widget.id) ?? "legacy"; + }, optionSetter: (String oldValue, String newValue) async { - await bind.sessionSetKeyboardMode( - id: widget.id, keyboardMode: newValue); + await bind.sessionSetKeyboardMode(id: widget.id, value: newValue); }, ) ]; - + 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; } @@ -1261,16 +1340,8 @@ void showSetOSPassword( ), ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: close, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: close, @@ -1334,19 +1405,125 @@ void showAuditDialog(String id, dialogManager) async { focusNode: focusNode, )), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: close, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit) ], onSubmit: submit, onCancel: close, ); }); } + +void showConfirmSwitchSidesDialog( + String id, OverlayDialogManager dialogManager) async { + dialogManager.show((setState, close) { + submit() async { + await bind.sessionSwitchSides(id: id); + closeConnection(id: id); + } + + return CustomAlertDialog( + title: Text(translate('Switch Sides')), + content: Column( + children: [ + Text(translate('Please confirm if you want to share your desktop?')), + ], + ), + actions: [ + dialogButton('Cancel', onPressed: close, isOutline: true), + dialogButton('OK', onPressed: submit), + ], + 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: 20, + color: Colors.grey, + ), + 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: Colors.white, + border: Border.all(color: MyTheme.border), + ), + child: SizedBox( + height: 20, + child: child, + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index daf9272f7..ddc0e7729 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -331,6 +331,7 @@ class DesktopTab extends StatelessWidget { return _buildBlock( child: Obx(() => PageView( controller: state.value.pageController, + physics: NeverScrollableScrollPhysics(), children: state.value.tabs .map((tab) => tab.page) .toList(growable: false)))); @@ -373,7 +374,7 @@ class DesktopTab extends StatelessWidget { width: 78, )), Offstage( - offstage: kUseCompatibleUiMode, + offstage: kUseCompatibleUiMode || Platform.isMacOS, child: Row(children: [ Offstage( offstage: !showLogo, @@ -485,7 +486,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) { @@ -526,13 +527,19 @@ class WindowActionPanelState extends State void onWindowClose() async { // hide window on close if (widget.isMainWindow) { + await rustDeskWinManager.unregisterActiveWindow(0); + // `hide` must be placed after unregisterActiveWindow, because once all windows are hidden, + // flutter closes the application on macOS. We should ensure the post-run logic has ran successfully. + // e.g.: saving window position. 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 + await WindowController.fromWindowId(kWindowId!).hide(); + await Future.wait([ + rustDeskWinManager + .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}), + widget.onClose?.call() ?? Future.microtask(() => null) + ]); } super.onWindowClose(); } @@ -548,7 +555,7 @@ class WindowActionPanelState extends State child: Row( children: [ Offstage( - offstage: !widget.showMinimize, + offstage: !widget.showMinimize || Platform.isMacOS, child: ActionIcon( message: 'Minimize', icon: IconFont.min, @@ -556,13 +563,13 @@ 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", @@ -573,7 +580,7 @@ class WindowActionPanelState extends State isClose: false, ))), Offstage( - offstage: !widget.showClose, + offstage: !widget.showClose || Platform.isMacOS, child: ActionIcon( message: 'Close', icon: IconFont.close, @@ -586,7 +593,7 @@ class WindowActionPanelState extends State if (widget.isMainWindow) { await windowManager.close(); } else { - await WindowController.fromWindowId(windowId!) + await WindowController.fromWindowId(kWindowId!) .close(); } }); @@ -615,7 +622,7 @@ void startDragging(bool isMainWindow) { if (isMainWindow) { windowManager.startDragging(); } else { - WindowController.fromWindowId(windowId!).startDragging(); + WindowController.fromWindowId(kWindowId!).startDragging(); } } @@ -631,7 +638,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; @@ -680,8 +687,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, @@ -824,7 +831,7 @@ class _TabState extends State<_Tab> with RestorationMixin { return ConstrainedBox( constraints: BoxConstraints(maxWidth: widget.maxLabelWidth ?? 200), child: Text( - translate(widget.label.value), + widget.label.value, textAlign: TextAlign.center, style: TextStyle( color: isSelected @@ -899,7 +906,7 @@ 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(), ))) @@ -931,13 +938,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); @@ -947,7 +954,7 @@ class _CloseButton extends StatelessWidget { return SizedBox( width: _kIconSize, child: Offstage( - offstage: !visiable, + offstage: !visible, child: InkWell( customBorder: const RoundedRectangleBorder(), onTap: () => onClose(), @@ -1033,8 +1040,8 @@ class AddButton extends StatelessWidget { return ActionIcon( message: 'New Connection', icon: IconFont.add, - onTap: () => - rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), + onTap: () => rustDeskWinManager.call( + WindowType.Main, kWindowMainWindowOnTop, ""), isClose: false); } } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 2015c02b2..1ec963f22 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -26,13 +26,15 @@ 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; Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); debugPrint("launch args: $args"); - bootArgs = List.from(args); + kBootArgs = List.from(args); if (!isDesktop) { runMobileApp(); @@ -40,10 +42,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 +53,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 +70,7 @@ Future main(List args) async { runMultiWindow( argument, kAppTypeDesktopFileTransfer, - 'RustDesk - File Transfer', + windowName, ); break; case WindowType.PortForward: @@ -75,7 +78,7 @@ Future main(List args) async { runMultiWindow( argument, kAppTypeDesktopPortForward, - 'RustDesk - Port Forward', + windowName, ); break; default: @@ -117,6 +120,7 @@ void runMainApp(bool startService) async { // await windowManager.ensureInitialized(); gFFI.serverModel.startService(); } + gFFI.userModel.refreshCurrentUser(); runApp(App()); // restore the location of the main window before window hide or show await restoreWindowPosition(WindowType.Main); @@ -134,6 +138,7 @@ void runMainApp(bool startService) async { windowManager.waitUntilReadyToShow(windowOptions, () async { windowManager.setOpacity(1); }); + windowManager.setTitle(getWindowName()); } void runMobileApp() async { @@ -149,7 +154,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: @@ -178,23 +183,26 @@ 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 { diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index e99226c4d..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'; @@ -75,20 +74,7 @@ class _ConnectionPageState extends State { ])), SliverFillRemaining( hasScrollBody: false, - child: PeerTabPage( - tabs: [ - translate('Recent Sessions'), - translate('Favorites'), - translate('Discovered'), - translate('Address Book') - ], - children: [ - RecentPeersView(), - FavoritePeersView(), - DiscoveredPeersView(), - const AddressBook(), - ], - ), + child: PeerTabPage(), ) ], ).marginOnly(top: 2, left: 10, right: 10); @@ -271,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 6e5c91484..7aa9a0005 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -32,15 +32,17 @@ class _FileManagerPageState extends State { .showLoading(translate('Connecting...'), onCancel: closeConnection); }); gFFI.ffiModel.updateEventListener(widget.id); + model.onDirChanged = (_) => breadCrumbScrollToEnd(); Wakelock.enable(); } @override void dispose() { - model.onClose(); - gFFI.close(); - gFFI.dialogManager.dismissAll(); - Wakelock.disable(); + model.onClose().whenComplete(() { + gFFI.close(); + gFFI.dialogManager.dismissAll(); + Wakelock.disable(); + }); super.dispose(); } @@ -136,7 +138,7 @@ class _FileManagerPageState extends State { child: Row( children: [ Icon( - model.currentShowHidden + model.getCurrentShowHidden() ? Icons.check_box_outlined : Icons.check_box_outline_blank, color: Theme.of(context).iconTheme.color), @@ -172,22 +174,18 @@ class _FileManagerPageState extends State { ], ), actions: [ - TextButton( - style: flatButtonStyle, + dialogButton("Cancel", 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.currentIsWindows)); - close(); - } - }, - child: Text(translate("OK"))) + isOutline: true), + dialogButton("OK", onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir(PathUtil.join( + model.currentDir.path, + name.value.text, + model.getCurrentIsWindows())); + close(); + } + }) ])); } else if (v == "hidden") { model.toggleShowHidden(); @@ -309,7 +307,6 @@ class _FileManagerPageState extends State { } if (entries[index].isDirectory || entries[index].isDrive) { model.openDirectory(entries[index].path); - breadCrumbScrollToEnd(); } else { // Perform file-related tasks. } @@ -333,10 +330,12 @@ class _FileManagerPageState extends State { breadCrumbScrollToEnd() { Future.delayed(Duration(milliseconds: 200), () { - _breadCrumbScroller.animateTo( - _breadCrumbScroller.position.maxScrollExtent, - duration: Duration(milliseconds: 200), - curve: Curves.fastLinearToSlowEaseIn); + if (_breadCrumbScroller.hasClients) { + _breadCrumbScroller.animateTo( + _breadCrumbScroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + } }); } @@ -350,12 +349,12 @@ class _FileManagerPageState extends State { if (model.currentHome.startsWith(list[0])) { // absolute path for (var item in list) { - path = PathUtil.join(path, item, model.currentIsWindows); + path = PathUtil.join(path, item, model.getCurrentIsWindows()); } } else { path += model.currentHome; for (var item in list) { - path = PathUtil.join(path, item, model.currentIsWindows); + path = PathUtil.join(path, item, model.getCurrentIsWindows()); } } model.openDirectory(path); @@ -477,7 +476,7 @@ class _FileManagerPageState extends State { return BottomSheetBody( leading: Icon(Icons.check), title: "${translate("Successful")}!", - text: "", + text: model.jobProgress.display(), onCanceled: () => model.jobReset(), ); case JobState.error: @@ -499,7 +498,7 @@ class _FileManagerPageState extends State { List getPathBreadCrumbItems( void Function() onHome, void Function(List) onPressed) { final path = model.currentShortPath; - final list = PathUtil.split(path, model.currentIsWindows); + final list = PathUtil.split(path, model.getCurrentIsWindows()); final breadCrumbList = [ BreadCrumbItem( content: IconButton( diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index fcfb8ad60..0a10d8011 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -518,7 +518,7 @@ class _RemotePageState extends State { ), ), ]; - if (!gFFI.canvasModel.cursorEmbeded) { + if (!gFFI.canvasModel.cursorEmbedded) { paints.add(CursorPaint()); } return paints; @@ -527,7 +527,7 @@ class _RemotePageState extends State { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; - if (!gFFI.canvasModel.cursorEmbeded) { + if (!gFFI.canvasModel.cursorEmbedded) { final cursor = bind.sessionGetToggleOptionSync( id: widget.id, arg: 'show-remote-cursor'); if (keyboard || cursor) { @@ -574,14 +574,14 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Text(translate('Physical Keyboard Input Mode')), value: 'input-mode')); - if (pi.platform == 'Linux' || pi.sasEnabled) { + 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')); } @@ -692,10 +692,11 @@ class _RemotePageState extends State { } void changePhysicalKeyboardInputMode() async { - var current = await bind.sessionGetKeyboardName(id: widget.id); + 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, keyboardMode: v ?? ''); + await bind.sessionPeerOption( + id: widget.id, name: "keyboard-mode", value: v ?? ""); setState(() => current = v ?? ''); Future.delayed(Duration(milliseconds: 300), close); } @@ -739,7 +740,7 @@ class _RemotePageState extends State { } 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); @@ -977,7 +978,9 @@ void showOptions( final h265 = codecsJson['h265'] ?? false; codecs.add(h264); codecs.add(h265); - } finally {} + } catch (e) { + debugPrint("Show Codec Preference err=$e"); + } } dialogManager.show((setState, close) { @@ -992,7 +995,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')); } } @@ -1055,7 +1058,7 @@ void showOptions( final toggles = [ getToggle(id, setState, 'show-quality-monitor', 'Show quality monitor'), ]; - if (!gFFI.canvasModel.cursorEmbeded) { + if (!gFFI.canvasModel.cursorEmbedded) { toggles.insert(0, getToggle(id, setState, 'show-remote-cursor', 'Show remote cursor')); } @@ -1095,15 +1098,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); @@ -1114,7 +1111,6 @@ void showSetOSPassword( } close(); }, - child: Text(translate('OK')), ), ]); }); diff --git a/flutter/lib/mobile/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart index 3bd381d92..8778d78f7 100644 --- a/flutter/lib/mobile/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -9,11 +8,11 @@ import 'package:qr_code_scanner/qr_code_scanner.dart'; import 'package:zxing2/qrcode.dart'; import '../../common.dart'; -import '../../models/platform_model.dart'; +import '../widgets/dialog.dart'; class ScanPage extends StatefulWidget { @override - _ScanPageState createState() => _ScanPageState(); + State createState() => _ScanPageState(); } class _ScanPageState extends State { @@ -42,9 +41,9 @@ class _ScanPageState extends State { icon: Icon(Icons.image_search), iconSize: 32.0, onPressed: () async { - final ImagePicker _picker = ImagePicker(); + final ImagePicker picker = ImagePicker(); final XFile? file = - await _picker.pickImage(source: ImageSource.gallery); + await picker.pickImage(source: ImageSource.gallery); if (file != null) { var image = img.decodeNamedImage( File(file.path).readAsBytesSync(), file.path)!; @@ -139,155 +138,12 @@ class _ScanPageState extends State { return; } try { - Map values = json.decode(data.substring(7)); - var host = values['host'] != null ? values['host'] as String : ''; - var key = values['key'] != null ? values['key'] as String : ''; - var api = values['api'] != null ? values['api'] as String : ''; + final sc = ServerConfig.decode(data.substring(7)); Timer(Duration(milliseconds: 60), () { - showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager); + showServerSettingsWithValue(sc, gFFI.dialogManager); }); } catch (e) { showToast('Invalid QR code'); } } } - -void showServerSettingsWithValue(String id, String relay, String key, - String api, OverlayDialogManager dialogManager) async { - Map oldOptions = jsonDecode(await bind.mainGetOptions()); - String id0 = oldOptions['custom-rendezvous-server'] ?? ""; - String relay0 = oldOptions['relay-server'] ?? ""; - String api0 = oldOptions['api-server'] ?? ""; - String key0 = oldOptions['key'] ?? ""; - var isInProgress = false; - final idController = TextEditingController(text: id); - final relayController = TextEditingController(text: relay); - final apiController = TextEditingController(text: api); - - String? idServerMsg; - String? relayServerMsg; - String? apiServerMsg; - - dialogManager.show((setState, close) { - Future validate() async { - if (idController.text != id) { - final res = await validateAsync(idController.text); - setState(() => idServerMsg = res); - if (idServerMsg != null) return false; - id = idController.text; - } - if (relayController.text != relay) { - relayServerMsg = await validateAsync(relayController.text); - if (relayServerMsg != null) return false; - relay = relayController.text; - } - if (apiController.text != relay) { - apiServerMsg = await validateAsync(apiController.text); - if (apiServerMsg != null) return false; - api = apiController.text; - } - return true; - } - - return CustomAlertDialog( - title: Text(translate('ID/Relay Server')), - content: Form( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - controller: idController, - decoration: InputDecoration( - labelText: translate('ID Server'), - errorText: idServerMsg), - ) - ] + - (isAndroid - ? [ - TextFormField( - controller: relayController, - decoration: InputDecoration( - labelText: translate('Relay Server'), - errorText: relayServerMsg), - ) - ] - : []) + - [ - TextFormField( - controller: apiController, - decoration: InputDecoration( - labelText: translate('API Server'), - ), - autovalidateMode: AutovalidateMode.onUserInteraction, - validator: (v) { - if (v != null && v.length > 0) { - if (!(v.startsWith('http://') || - v.startsWith("https://"))) { - return translate("invalid_http"); - } - } - return apiServerMsg; - }, - ), - TextFormField( - initialValue: key, - decoration: InputDecoration( - labelText: 'Key', - ), - onChanged: (String? value) { - if (value != null) key = value.trim(); - }, - ), - Offstage( - offstage: !isInProgress, - child: LinearProgressIndicator()) - ])), - actions: [ - TextButton( - style: flatButtonStyle, - onPressed: () { - close(); - }, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: () async { - setState(() { - idServerMsg = null; - relayServerMsg = null; - apiServerMsg = null; - isInProgress = true; - }); - if (await validate()) { - if (id != id0) { - bind.mainSetOption(key: "custom-rendezvous-server", value: id); - } - if (relay != relay0) { - bind.mainSetOption(key: "relay-server", value: relay); - } - if (key != key0) bind.mainSetOption(key: "key", value: key); - if (api != api0) { - bind.mainSetOption(key: "api-server", value: api); - } - close(); - } - setState(() { - isInProgress = false; - }); - }, - child: Text(translate('OK')), - ), - ], - ); - }); -} - -Future validateAsync(String value) async { - value = value.trim(); - if (value.isEmpty) { - return null; - } - final res = await bind.mainTestIfValidServer(server: value); - return res.isEmpty ? null : res; -} diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index 0b2a51d40..abccdf683 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:provider/provider.dart'; @@ -107,12 +109,23 @@ class ServerPage extends StatefulWidget implements PageShape { } class _ServerPageState extends State { + Timer? _updateTimer; + @override void initState() { super.initState(); + _updateTimer = periodic_immediate(const Duration(seconds: 3), () async { + await gFFI.serverModel.fetchID(); + }); gFFI.serverModel.checkAndroidPermission(); } + @override + void dispose() { + _updateTimer?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { checkService(); @@ -564,7 +577,7 @@ void androidChannelInit() { } } } catch (e) { - debugPrint("MethodCallHandler err:$e"); + debugPrintStack(label: "MethodCallHandler err:$e"); } return ""; }); diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index 269439b1d..c5f3b6935 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -9,6 +9,7 @@ import 'package:url_launcher/url_launcher.dart'; import '../../common.dart'; import '../../common/widgets/dialog.dart'; +import '../../common/widgets/login.dart'; import '../../models/model.dart'; import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; @@ -272,13 +273,12 @@ 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) { @@ -300,7 +300,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(); } @@ -391,17 +391,13 @@ class _SettingsState extends State with WidgetsBindingObserver { void showServerSettings(OverlayDialogManager dialogManager) async { Map options = jsonDecode(await bind.mainGetOptions()); - String id = options['custom-rendezvous-server'] ?? ""; - String relay = options['relay-server'] ?? ""; - String api = options['api-server'] ?? ""; - String key = options['key'] ?? ""; - showServerSettingsWithValue(id, relay, key, api, dialogManager); + showServerSettingsWithValue(ServerConfig.fromOptions(options), dialogManager); } 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) { @@ -486,78 +482,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; - } - gFFI.abModel.pullAb(); - } - 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 96f96658a..0eb403833 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,5 +1,8 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/widgets/button.dart'; +import 'package:get/get.dart'; import '../../common.dart'; import '../../models/model.dart'; @@ -31,10 +34,8 @@ void showRestartRemoteDevice( 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"))), + dialogButton("Cancel", onPressed: () => close(), isOutline: true), + dialogButton("OK", onPressed: () => close(true)), ], )); if (res == true) bind.sessionRestartRemoteDevice(id: id); @@ -94,15 +95,15 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { ), ])), actions: [ - TextButton( - style: flatButtonStyle, + dialogButton( + 'Cancel', onPressed: () { close(); }, - child: Text(translate('Cancel')), + isOutline: true, ), - TextButton( - style: flatButtonStyle, + dialogButton( + 'OK', onPressed: (validateLength && validateSame) ? () async { close(); @@ -116,7 +117,6 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { } } : null, - child: Text(translate('OK')), ), ], ); @@ -196,16 +196,8 @@ void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { ), ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate('Cancel')), - ), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate('OK')), - ), + dialogButton('Cancel', onPressed: cancel, isOutline: true), + dialogButton('OK', onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -218,24 +210,375 @@ void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { title: Text(translate('Wrong Password')), content: Text(translate('Do you want to enter again?')), actions: [ - TextButton( - style: flatButtonStyle, + dialogButton( + 'Cancel', onPressed: () { close(); closeConnection(); }, - child: Text(translate('Cancel')), + isOutline: true, ), - TextButton( - style: flatButtonStyle, + dialogButton( + 'Retry', onPressed: () { enterPasswordDialog(id, dialogManager); }, - child: Text(translate('Retry')), ), ])); } +void showServerSettingsWithValue( + ServerConfig serverConfig, OverlayDialogManager dialogManager) async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + final oldCfg = ServerConfig.fromOptions(oldOptions); + + var isInProgress = false; + final idCtrl = TextEditingController(text: serverConfig.idServer); + final relayCtrl = TextEditingController(text: serverConfig.relayServer); + final apiCtrl = TextEditingController(text: serverConfig.apiServer); + final keyCtrl = TextEditingController(text: serverConfig.key); + + String? idServerMsg; + String? relayServerMsg; + String? apiServerMsg; + + dialogManager.show((setState, close) { + Future validate() async { + if (idCtrl.text != oldCfg.idServer) { + final res = await validateAsync(idCtrl.text); + setState(() => idServerMsg = res); + if (idServerMsg != null) return false; + } + if (relayCtrl.text != oldCfg.relayServer) { + relayServerMsg = await validateAsync(relayCtrl.text); + if (relayServerMsg != null) return false; + } + if (apiCtrl.text != oldCfg.apiServer) { + if (apiServerMsg != null) return false; + } + return true; + } + + return CustomAlertDialog( + title: Text(translate('ID/Relay Server')), + content: Form( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + controller: idCtrl, + decoration: InputDecoration( + labelText: translate('ID Server'), + errorText: idServerMsg), + ) + ] + + (isAndroid + ? [ + TextFormField( + controller: relayCtrl, + decoration: InputDecoration( + labelText: translate('Relay Server'), + errorText: relayServerMsg), + ) + ] + : []) + + [ + TextFormField( + controller: apiCtrl, + decoration: InputDecoration( + labelText: translate('API Server'), + ), + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (v) { + if (v != null && v.isNotEmpty) { + if (!(v.startsWith('http://') || + v.startsWith("https://"))) { + return translate("invalid_http"); + } + } + return apiServerMsg; + }, + ), + TextFormField( + controller: keyCtrl, + decoration: InputDecoration( + labelText: 'Key', + ), + ), + Offstage( + offstage: !isInProgress, + child: LinearProgressIndicator()) + ])), + actions: [ + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + dialogButton( + 'OK', + onPressed: () async { + setState(() { + idServerMsg = null; + relayServerMsg = null; + apiServerMsg = null; + isInProgress = true; + }); + if (await validate()) { + if (idCtrl.text != oldCfg.idServer) { + if (oldCfg.idServer.isNotEmpty) { + await gFFI.userModel.logOut(); + } + bind.mainSetOption( + key: "custom-rendezvous-server", value: idCtrl.text); + } + if (relayCtrl.text != oldCfg.relayServer) { + bind.mainSetOption(key: "relay-server", value: relayCtrl.text); + } + if (keyCtrl.text != oldCfg.key) { + bind.mainSetOption(key: "key", value: keyCtrl.text); + } + if (apiCtrl.text != oldCfg.apiServer) { + bind.mainSetOption(key: "api-server", value: apiCtrl.text); + } + close(); + showToast(translate('Successful')); + } + setState(() { + isInProgress = false; + }); + }, + ), + ], + ); + }); +} + +void showWaitUacDialog(String id, OverlayDialogManager dialogManager) { + dialogManager.dismissAll(); + dialogManager.show( + tag: '$id-wait-uac', + (setState, close) => CustomAlertDialog( + title: Text(translate('Wait')), + content: Text(translate('wait_accept_uac_tip')).marginAll(10), + )); +} + +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: '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; + } + var content = Column(children: [ + Align( + alignment: Alignment.centerLeft, + child: Text( + "${translate(text)}${type.contains('uac') ? '\n' : '\n\n'}${translate('request_elevation_tip')}", + textAlign: TextAlign.left, + style: TextStyle(fontWeight: FontWeight.w400), + ).marginSymmetric(vertical: 15), + ), + ]); + dialogManager.show(tag: '$id-$type', (setState, close) { + void submit() { + close(); + _showRequestElevationDialog(id, dialogManager); + } + + return CustomAlertDialog( + title: Text(translate(title)), + content: content, + 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: Text(translate(title)), + content: Text(translate(text)), + actions: [ + dialogButton('Cancel', onPressed: () { + close(); + }, isOutline: true), + dialogButton('Retry', onPressed: submit), + ], + onSubmit: submit, + onCancel: close, + ); + }); +} + +Future validateAsync(String value) async { + value = value.trim(); + if (value.isEmpty) { + return null; + } + 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); @@ -285,7 +628,7 @@ class _PasswordWidgetState extends State { color: Theme.of(context).primaryColorDark, ), onPressed: () { - // Update the state i.e. toogle the state of passwordVisible variable + // Update the state i.e. toggle the state of passwordVisible variable setState(() { _passwordVisible = !_passwordVisible; }); diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 5a055fd14..175c8424b 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -21,26 +21,30 @@ class AbModel { AbModel(this.parent); - FFI? get _ffi => parent.target; - Future pullAb() async { - if (_ffi!.userModel.userName.isEmpty) return; + if (gFFI.userModel.userName.isEmpty) return; abLoading.value = true; 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')) { - abError = json['error']; + abError.value = json['error']; } else if (json.containsKey('data')) { final data = jsonDecode(json['data']); - tags.value = data['tags']; - peers.clear(); - for (final peer in data['peers']) { - peers.add(Peer.fromJson(peer)); + if (data != null) { + tags.clear(); + peers.clear(); + if (data['tags'] is List) { + tags.value = data['tags']; + } + if (data['peers'] is List) { + for (final peer in data['peers']) { + peers.add(Peer.fromJson(peer)); + } + } } } return resp.body; @@ -56,16 +60,27 @@ class AbModel { return null; } - void reset() { + Future reset() async { + await bind.mainSetLocalOption(key: "selected-tags", value: ''); tags.clear(); peers.clear(); } - void addId(String id) async { + void addId(String id, String alias, List tags) { if (idContainBy(id)) { return; } - peers.add(Peer.fromJson({"id": id})); + final peer = Peer.fromJson({ + 'id': id, + 'alias': alias, + 'tags': tags, + }); + peers.add(peer); + } + + void addPeer(Peer peer) { + peers.removeWhere((e) => e.id == peer.id); + peers.add(peer); } void addTag(String tag) async { @@ -86,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({ @@ -105,6 +120,10 @@ class AbModel { } } + Peer? find(String id) { + return peers.firstWhereOrNull((e) => e.id == id); + } + bool idContainBy(String id) { return peers.where((element) => element.id == id).isNotEmpty; } @@ -143,18 +162,28 @@ class AbModel { } } - void setPeerAlias(String id, String value) { + Future setPeerAlias(String id, String value) async { final it = peers.where((p0) => p0.id == id); - if (it.isEmpty) { - debugPrint("$id is not exists"); - return; - } else { + if (it.isNotEmpty) { it.first.alias = value; + await pushAb(); } } - void clear() { - peers.clear(); - tags.clear(); + Future setPeerForceAlwaysRelay(String id, bool value) async { + final it = peers.where((p0) => p0.id == id); + if (it.isNotEmpty) { + it.first.forceAlwaysRelay = value; + await pushAb(); + } + } + + Future setRdp(String id, String port, String username) async { + final it = peers.where((p0) => p0.id == id); + if (it.isNotEmpty) { + it.first.rdpPort = port; + it.first.rdpUsername = username; + await pushAb(); + } } } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 4beac01be..18d42d143 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; @@ -42,6 +44,9 @@ class FileModel extends ChangeNotifier { /// JobTable final _jobTable = List.empty(growable: true).obs; + /// `isLocal` bool + Function(bool)? onDirChanged; + RxList get jobTable => _jobTable; bool get isLocal => _isSelectedLocal; @@ -141,18 +146,14 @@ class FileModel extends ChangeNotifier { } } - bool get currentShowHidden => - _isSelectedLocal ? _localOption.showHidden : _remoteOption.showHidden; - - bool getCurrentShowHidden(bool isLocal) { - return isLocal ? _localOption.showHidden : _remoteOption.showHidden; + bool getCurrentShowHidden([bool? isLocal]) { + final isLocal_ = isLocal ?? _isSelectedLocal; + return isLocal_ ? _localOption.showHidden : _remoteOption.showHidden; } - bool get currentIsWindows => - _isSelectedLocal ? _localOption.isWindows : _remoteOption.isWindows; - - bool getCurrentIsWindows(bool isLocal) { - return isLocal ? _localOption.isWindows : _remoteOption.isWindows; + bool getCurrentIsWindows([bool? isLocal]) { + final isLocal_ = isLocal ?? _isSelectedLocal; + return isLocal_ ? _localOption.isWindows : _remoteOption.isWindows; } final _fileFetcher = FileFetcher(); @@ -213,7 +214,6 @@ class FileModel extends ChangeNotifier { } receiveFileDir(Map evt) { - debugPrint("recv file dir:$evt"); if (evt['is_local'] == "false") { // init remote home, the connection will automatic read remote home when established, try { @@ -237,7 +237,9 @@ class FileModel extends ChangeNotifier { debugPrint("init remote home:${fd.path}"); _currentRemoteDir = fd; } - } finally {} + } catch (e) { + debugPrint("receiveFileDir err=$e"); + } } _fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); notifyListeners(); @@ -268,6 +270,7 @@ class FileModel extends ChangeNotifier { } jobError(Map evt) { + final err = evt['err'].toString(); if (!isDesktop) { if (_jobResultListener.isListening) { _jobResultListener.complete(evt); @@ -275,12 +278,24 @@ class FileModel extends ChangeNotifier { } _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"); @@ -327,13 +342,13 @@ class FileModel extends ChangeNotifier { _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 == "Windows"; - - debugPrint("remote platform: ${parent.target?.ffiModel.pi.platform}"); + _remoteOption.isWindows = + parent.target?.ffiModel.pi.platform == kPeerPlatformWindows; await Future.delayed(Duration(milliseconds: 100)); @@ -350,14 +365,14 @@ class FileModel extends ChangeNotifier { if (_currentRemoteDir.path.isEmpty) { openDirectory(_remoteOption.home, isLocal: false); } - // load last transfer jobs - await bind.sessionLoadLastTransferJobs(id: '${parent.target?.id}'); } - onClose() { + Future onClose() async { parent.target?.dialogManager.dismissAll(); jobReset(); + onDirChanged = null; + // save config Map msgMap = {}; @@ -367,7 +382,7 @@ class FileModel extends ChangeNotifier { msgMap["remote_show_hidden"] = _remoteOption.showHidden ? "Y" : ""; final id = parent.target?.id ?? ""; for (final msg in msgMap.entries) { - bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); + await bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); } _currentLocalDir.clear(); _currentRemoteDir.clear(); @@ -399,14 +414,10 @@ class FileModel extends ChangeNotifier { if (!isBack) { pushHistory(isLocal); } - final showHidden = - isLocal ? _localOption.showHidden : _remoteOption.showHidden; - final isWindows = - isLocal ? _localOption.isWindows : _remoteOption.isWindows; + final showHidden = getCurrentShowHidden(isLocal); + final isWindows = getCurrentIsWindows(isLocal); // process /C:\ -> C:\ on Windows - if (isLocal - ? _localOption.isWindows - : _remoteOption.isWindows && path.length > 1 && path[0] == '/') { + if (isWindows && path.length > 1 && path[0] == '/') { path = path.substring(1); if (path[path.length - 1] != '\\') { path = "$path\\"; @@ -421,6 +432,7 @@ class FileModel extends ChangeNotifier { _currentRemoteDir = fd; } notifyListeners(); + onDirChanged?.call(isLocal); } catch (e) { debugPrint("Failed to openDirectory $path: $e"); } @@ -653,14 +665,8 @@ class FileModel extends ChangeNotifier { : const SizedBox.shrink() ]), actions: [ - TextButton( - style: flatButtonStyle, - onPressed: cancel, - child: Text(translate("Cancel"))), - TextButton( - style: flatButtonStyle, - onPressed: submit, - child: Text(translate("OK"))), + dialogButton("Cancel", onPressed: cancel, isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -712,18 +718,9 @@ 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", onPressed: cancel, isOutline: true), + dialogButton("Skip", onPressed: () => close(null), isOutline: true), + dialogButton("OK", onPressed: submit), ], onSubmit: submit, onCancel: cancel, @@ -1090,6 +1087,7 @@ class JobProgress { var remote = ""; var to = ""; var showHidden = false; + var err = ""; clear() { state = JobState.none; @@ -1101,6 +1099,14 @@ class JobProgress { fileCount = 0; remote = ""; to = ""; + err = ""; + } + + String display() { + if (state == JobState.done && err == "skipped") { + return translate("Skipped"); + } + return state.display(); } } diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart new file mode 100644 index 000000000..4d9fab0e4 --- /dev/null +++ b/flutter/lib/models/group_model.dart @@ -0,0 +1,140 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/widgets/peer_tab_page.dart'; +import 'package:flutter_hbb/common/hbbs/hbbs.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; + +class GroupModel { + final RxBool userLoading = false.obs; + final RxString userLoadError = "".obs; + final RxBool peerLoading = false.obs; //to-do: not used + final RxString peerLoadError = "".obs; + final RxList users = RxList.empty(growable: true); + final RxList peerPayloads = RxList.empty(growable: true); + final RxList peersShow = RxList.empty(growable: true); + WeakReference parent; + + GroupModel(this.parent); + + Future reset() async { + userLoading.value = false; + userLoadError.value = ""; + peerLoading.value = false; + peerLoadError.value = ""; + users.clear(); + peerPayloads.clear(); + peersShow.clear(); + } + + Future pull() async { + await reset(); + if (gFFI.userModel.userName.isEmpty || + (gFFI.userModel.isAdmin.isFalse && gFFI.userModel.groupName.isEmpty)) { + statePeerTab.check(); + return; + } + userLoading.value = true; + userLoadError.value = ""; + final api = "${await bind.mainGetApiServer()}/api/users"; + try { + var uri0 = Uri.parse(api); + final pageSize = 20; + var total = 0; + int current = 0; + do { + current += 1; + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': current.toString(), + 'pageSize': pageSize.toString(), + if (gFFI.userModel.isAdmin.isFalse) + 'grp': gFFI.userModel.groupName.value, + }); + 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')) { + throw json['error']; + } else { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final user in data) { + users.add(UserPayload.fromJson(user)); + } + } + } + } + } + } while (current * pageSize < total); + } catch (err) { + debugPrint('$err'); + userLoadError.value = err.toString(); + } finally { + userLoading.value = false; + statePeerTab.check(); + } + } + + Future pullUserPeers(String username) async { + peerPayloads.clear(); + peersShow.clear(); + peerLoading.value = true; + peerLoadError.value = ""; + final api = "${await bind.mainGetApiServer()}/api/peers"; + try { + var uri0 = Uri.parse(api); + final pageSize = 20; + var total = 0; + int current = 0; + do { + current += 1; + var uri = Uri( + scheme: uri0.scheme, + host: uri0.host, + path: uri0.path, + port: uri0.port, + queryParameters: { + 'current': current.toString(), + 'pageSize': pageSize.toString(), + 'grp': gFFI.userModel.groupName.value, + 'target_user': username + }); + 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')) { + throw json['error']; + } else { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final p in data) { + final peer = PeerPayload.fromJson(p); + peerPayloads.add(peer); + peersShow.add(PeerPayload.toPeer(peer)); + } + } + } + } + } + } while (current * pageSize < total); + } catch (err) { + debugPrint('$err'); + peerLoadError.value = err.toString(); + } finally { + peerLoading.value = false; + } + } +} diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index bd1131c7a..7356c6ec8 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) { @@ -46,7 +50,7 @@ class InputModel { // mouse final isPhysicalMouse = false.obs; - int _lastMouseDownButtons = 0; + int _lastButtons = 0; Offset lastMousePos = Offset.zero; get id => parent.target?.id ?? ""; @@ -54,7 +58,7 @@ class InputModel { InputModel(this.parent); KeyEventResult handleRawKeyEvent(FocusNode data, RawKeyEvent e) { - bind.sessionGetKeyboardName(id: id).then((result) { + bind.sessionGetKeyboardMode(id: id).then((result) { keyboardMode = result.toString(); }); @@ -115,8 +119,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 +138,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 +207,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; } @@ -260,7 +306,7 @@ class InputModel { isPhysicalMouse.value = true; } if (isPhysicalMouse.value) { - handleMouse(getEvent(e, 'mousemove')); + handleMouse(getEvent(e, _kMouseEventMove)); } } @@ -325,21 +371,21 @@ class InputModel { } } 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)); } } @@ -388,13 +434,13 @@ 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: @@ -440,15 +486,21 @@ class InputModel { evt['y'] = '${y.round()}'; 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 f6bfde941..986d93fe8 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -12,6 +12,7 @@ import 'package:flutter_hbb/generated_bridge.dart'; 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/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -19,8 +20,9 @@ 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/flutter_custom_cursor.dart'; +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'; @@ -59,7 +61,7 @@ class FfiModel with ChangeNotifier { bool get touchMode => _touchMode; - bool get isPeerAndroid => _pi.platform == 'Android'; + bool get isPeerAndroid => _pi.platform == kPeerPlatformAndroid; set inputBlocked(v) { _inputBlocked = v; @@ -140,7 +142,7 @@ class FfiModel with ChangeNotifier { setConnectionType( peerId, evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { - handleSwitchDisplay(evt); + handleSwitchDisplay(evt, peerId); } else if (name == 'cursor_data') { await parent.target?.cursorModel.updateCursorData(evt); } else if (name == 'cursor_id') { @@ -182,13 +184,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); @@ -197,6 +195,10 @@ 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); } }; } @@ -213,7 +215,7 @@ class FfiModel with ChangeNotifier { } } - handleSwitchDisplay(Map evt) { + handleSwitchDisplay(Map evt, String peerId) { final oldOrientation = _display.width > _display.height; var old = _pi.currentDisplay; _pi.currentDisplay = int.parse(evt['display']); @@ -221,15 +223,26 @@ class FfiModel with ChangeNotifier { _display.y = double.parse(evt['y']); _display.width = int.parse(evt['width']); _display.height = int.parse(evt['height']); - _display.cursorEmbeded = int.parse(evt['cursor_embeded']) == 1; + _display.cursorEmbedded = int.parse(evt['cursor_embedded']) == 1; if (old != _pi.currentDisplay) { parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); } + try { + CurrentDisplayState.find(peerId).value = _pi.currentDisplay; + } catch (e) { + // + } + // remote is mobile, and orientation changed if ((_display.width > _display.height) != oldOrientation) { gFFI.canvasModel.updateViewStyle(); } + if (_pi.platform == kPeerPlatformLinux || + _pi.platform == kPeerPlatformWindows || + _pi.platform == kPeerPlatformMacOS) { + parent.target?.canvasModel.updateViewStyle(); + } parent.target?.recordingModel.onSwitchDisplay(); notifyListeners(); } @@ -258,7 +271,13 @@ class FfiModel with ChangeNotifier { hasCancel: false); } else if (type == 'wait-remote-accept-nook') { msgBoxCommon(dialogManager, title, Text(translate(text)), - [msgBoxButton("Cancel", closeConnection)]); + [dialogButton("Cancel", onPressed: closeConnection)]); + } else if (type == 'on-uac' || type == 'on-foreground-elevated') { + showOnBlockDialog(id, type, title, text, dialogManager); + } else if (type == 'wait-uac') { + showWaitUacDialog(id, dialogManager); + } else if (type == 'elevation-error') { + showElevationError(id, type, title, text, dialogManager); } else { var hasRetry = evt['hasRetry'] == 'true'; showMsgBox(id, type, title, text, link, hasRetry, dialogManager); @@ -331,7 +350,7 @@ class FfiModel with ChangeNotifier { d.y = d0['y'].toDouble(); d.width = d0['width']; d.height = d0['height']; - d.cursorEmbeded = d0['cursor_embeded'] == 1; + d.cursorEmbedded = d0['cursor_embedded'] == 1; _pi.displays.add(d); } if (_pi.currentDisplay < _pi.displays.length) { @@ -344,6 +363,8 @@ class FfiModel with ChangeNotifier { _waitForImage[peerId] = true; _reconnects = 1; } + Map features = json.decode(evt['features']); + _pi.features.privacyMode = features['privacy_mode'] == 1; } notifyListeners(); } @@ -378,12 +399,22 @@ class ImageModel with ChangeNotifier { WeakReference parent; + final List _callbacksOnFirstImage = []; + ImageModel(this.parent); + 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) { + cb(id); + } + } } final pid = parent.target?.id; ui.decodeImageFromPixels( @@ -493,7 +524,7 @@ class ViewStyle { double get scale { double s = 1.0; - if (style == 'adaptive') { + if (style == kRemoteViewStyleAdaptive) { final s1 = width / displayWidth; final s2 = height / displayHeight; s = s1 < s2 ? s1 : s2; @@ -509,6 +540,7 @@ class CanvasModel with ChangeNotifier { double _y = 0; // image scale double _scale = 1.0; + Size _size = Size.zero; // the tabbar over the image // double tabBarHeight = 0.0; // the window border's width @@ -522,6 +554,8 @@ class CanvasModel with ChangeNotifier { ScrollStyle _scrollStyle = ScrollStyle.scrollauto; ViewStyle _lastViewStyle = ViewStyle(); + final _imageOverflow = false.obs; + WeakReference parent; CanvasModel(this.parent); @@ -529,7 +563,12 @@ class CanvasModel with ChangeNotifier { double get x => _x; double get y => _y; double get scale => _scale; + Size get size => _size; ScrollStyle get scrollStyle => _scrollStyle; + ViewStyle get viewStyle => _lastViewStyle; + RxBool get imageOverflow => _imageOverflow; + + _resetScroll() => setScrollPercent(0.0, 0.0); setScrollPercent(double x, double y) { _scrollX = x; @@ -540,28 +579,44 @@ class CanvasModel with ChangeNotifier { double get scrollY => _scrollY; updateViewStyle() async { + Size getSize() { + 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); + } + 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, ); if (_lastViewStyle == viewStyle) { return; } + if (_lastViewStyle.style != viewStyle.style) { + _resetScroll(); + } _lastViewStyle = viewStyle; _scale = viewStyle.scale; - _x = (sizeWidth - displayWidth * _scale) / 2; - _y = (sizeHeight - displayHeight * _scale) / 2; + + if (kIgnoreDpi && style == kRemoteViewStyleOriginal) { + _scale = 1.0 / ui.window.devicePixelRatio; + } + _x = (size.width - displayWidth * _scale) / 2; + _y = (size.height - displayHeight * _scale) / 2; + _imageOverflow.value = _x < 0 || y < 0; notifyListeners(); } @@ -569,8 +624,7 @@ class CanvasModel with ChangeNotifier { final style = await bind.sessionGetScrollStyle(id: id); if (style == kRemoteScrollStyleBar) { _scrollStyle = ScrollStyle.scrollbar; - _scrollX = 0.0; - _scrollY = 0.0; + _resetScroll(); } else { _scrollStyle = ScrollStyle.scrollauto; } @@ -584,8 +638,8 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - bool get cursorEmbeded => - parent.target?.ffiModel.display.cursorEmbeded ?? false; + bool get cursorEmbedded => + parent.target?.ffiModel.display.cursorEmbedded ?? false; int getDisplayWidth() { final defaultWidth = (isDesktop || isWebDesktop) @@ -604,14 +658,6 @@ class CanvasModel with ChangeNotifier { 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); - } - moveDesktopMouse(double x, double y) { // On mobile platforms, move the canvas with the cursor. final dw = getDisplayWidth() * _scale; @@ -1111,7 +1157,8 @@ class CursorModel with ChangeNotifier { _clearCache() { final keys = {...cachedKeys}; for (var k in keys) { - customCursorController.freeCache(k); + debugPrint("deleting cursor with key $k"); + CursorManager.instance.deleteCursor(k); } } } @@ -1218,6 +1265,7 @@ class FFI { late final ChatModel chatModel; // session late final FileModel fileModel; // session late final AbModel abModel; // global + late final GroupModel groupModel; // global late final UserModel userModel; // global late final QualityMonitorModel qualityMonitorModel; // session late final RecordingModel recordingModel; // recording @@ -1231,8 +1279,9 @@ class FFI { serverModel = ServerModel(WeakReference(this)); chatModel = ChatModel(WeakReference(this)); fileModel = FileModel(WeakReference(this)); - abModel = AbModel(WeakReference(this)); userModel = UserModel(WeakReference(this)); + abModel = AbModel(WeakReference(this)); + groupModel = GroupModel(WeakReference(this)); qualityMonitorModel = QualityMonitorModel(WeakReference(this)); recordingModel = RecordingModel(WeakReference(this)); inputModel = InputModel(WeakReference(this)); @@ -1240,7 +1289,9 @@ class FFI { /// 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}) { assert(!(isFileTransfer && isPortForward), 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; @@ -1256,19 +1307,23 @@ class FFI { } // ignore: unused_local_variable final addRes = bind.sessionAddSync( - id: id, isFileTransfer: isFileTransfer, isPortForward: isPortForward); + id: id, + isFileTransfer: isFileTransfer, + isPortForward: isPortForward, + switchUuid: switchUuid ?? "", + ); final stream = bind.sessionStart(id: id); final cb = ffiModel.startEventListener(id); () async { await for (final message in stream) { - if (message is Event) { + if (message is EventToUI_Event) { try { Map event = json.decode(message.field0); await cb(event); } catch (e) { debugPrint('json.decode fail1(): $e, ${message.field0}'); } - } else if (message is Rgba) { + } else if (message is EventToUI_Rgba) { imageModel.onRgba(message.field0); } } @@ -1316,7 +1371,7 @@ class Display { double y = 0; int width = 0; int height = 0; - bool cursorEmbeded = false; + bool cursorEmbedded = false; Display() { width = (isDesktop || isWebDesktop) @@ -1328,6 +1383,10 @@ class Display { } } +class Features { + bool privacyMode = false; +} + class PeerInfo { String version = ''; String username = ''; @@ -1336,6 +1395,7 @@ class PeerInfo { bool sasEnabled = false; int currentDisplay = 0; List displays = []; + Features features = Features(); } const canvasKey = 'canvas'; diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index 68b85968a..cf2de4219 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -29,6 +29,7 @@ typedef HandleEvent = Future Function(Map evt); /// Hides the platform differences. class PlatformFFI { String _dir = ''; + // _homeDir is only needed for Android and IOS. String _homeDir = ''; F2? _translate; final _eventHandlers = >{}; @@ -119,11 +120,13 @@ class PlatformFFI { if (isAndroid) { // only support for android _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; + } else if (isIOS) { + _homeDir = _ffiBind.mainGetDataDirIos(); } else { - _homeDir = (await getDownloadsDirectory())?.path ?? ''; + // no need to set home dir } } catch (e) { - debugPrint('initialize failed: $e'); + debugPrintStack(label: 'initialize failed: $e'); } String id = 'NA'; String name = 'Flutter'; @@ -148,9 +151,8 @@ class PlatformFFI { WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo; name = winInfo.computerName; id = winInfo.computerName; - } catch (e, stacktrace) { - debugPrint("get windows device info failed: $e"); - debugPrintStack(stackTrace: stacktrace); + } catch (e) { + debugPrintStack(label: "get windows device info failed: $e"); name = "unknown"; id = "unknown"; } @@ -159,14 +161,19 @@ class PlatformFFI { name = macOsInfo.computerName; id = macOsInfo.systemGUID ?? ''; } - debugPrint( - '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir'); + if (isAndroid || isIOS) { + debugPrint( + '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir'); + } else { + debugPrint( + '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir'); + } await _ffiBind.mainDeviceId(id: id); await _ffiBind.mainDeviceName(name: name); await _ffiBind.mainSetHomeDir(home: _homeDir); await _ffiBind.mainInit(appDir: _dir); } catch (e) { - debugPrint('initialize failed: $e'); + debugPrintStack(label: 'initialize failed: $e'); } version = await getVersion(); } diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 6dd94bcf4..ad5183ae3 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -9,6 +9,9 @@ class Peer { final String platform; String alias; List tags; + bool forceAlwaysRelay = false; + String rdpPort; + String rdpUsername; bool online = false; Peer.fromJson(Map json) @@ -17,7 +20,10 @@ class Peer { hostname = json['hostname'] ?? '', platform = json['platform'] ?? '', alias = json['alias'] ?? '', - tags = json['tags'] ?? []; + tags = json['tags'] ?? [], + forceAlwaysRelay = json['forceAlwaysRelay'] == 'true', + rdpPort = json['rdpPort'] ?? '', + rdpUsername = json['rdpUsername'] ?? ''; Map toJson() { return { @@ -27,6 +33,9 @@ class Peer { "platform": platform, "alias": alias, "tags": tags, + "forceAlwaysRelay": forceAlwaysRelay.toString(), + "rdpPort": rdpPort, + "rdpUsername": rdpUsername, }; } @@ -37,16 +46,23 @@ class Peer { required this.platform, required this.alias, required this.tags, + required this.forceAlwaysRelay, + required this.rdpPort, + required this.rdpUsername, }); Peer.loading() : this( - id: '...', - username: '...', - hostname: '...', - platform: '...', - alias: '', - tags: []); + id: '...', + username: '...', + hostname: '...', + platform: '...', + alias: '', + tags: [], + forceAlwaysRelay: false, + rdpPort: '', + rdpUsername: '', + ); } class Peers extends ChangeNotifier { diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 344733324..176b1ba2d 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -304,8 +304,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, @@ -501,8 +501,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, @@ -581,10 +581,17 @@ class ServerModel with ChangeNotifier { } } +enum ClientType { + remote, + file, + portForward, +} + class Client { int id = 0; // client connections inner count id bool authorized = false; bool isFileTransfer = false; + String portForward = ""; String name = ""; String peerId = ""; // peer user's id,show at app bool keyboard = false; @@ -594,6 +601,7 @@ class Client { bool restart = false; bool recording = false; bool disconnected = false; + bool fromSwitch = false; RxBool hasUnreadChatMessage = false.obs; @@ -604,6 +612,7 @@ class Client { id = json['id']; authorized = json['authorized']; isFileTransfer = json['is_file_transfer']; + portForward = json['port_forward']; name = json['name']; peerId = json['peer_id']; keyboard = json['keyboard']; @@ -613,6 +622,7 @@ class Client { restart = json['restart']; recording = json['recording']; disconnected = json['disconnected']; + fromSwitch = json['from_switch']; } Map toJson() { @@ -620,6 +630,7 @@ class Client { data['id'] = id; data['is_start'] = authorized; data['is_file_transfer'] = isFileTransfer; + data['port_forward'] = portForward; data['name'] = name; data['peer_id'] = peerId; data['keyboard'] = keyboard; @@ -629,8 +640,19 @@ class Client { data['restart'] = restart; data['recording'] = recording; data['disconnected'] = disconnected; + data['from_switch'] = fromSwitch; return data; } + + ClientType type_() { + if (isFileTransfer) { + return ClientType.file; + } else if (portForward.isNotEmpty) { + return ClientType.portForward; + } else { + return ClientType.remote; + } + } } String getLoginDialogTag(int id) { @@ -655,9 +677,8 @@ 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, diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart index 53f1a19b1..e4c9fa03f 100644 --- a/flutter/lib/models/state_model.dart +++ b/flutter/lib/models/state_model.dart @@ -9,6 +9,7 @@ import '../consts.dart'; class StateGlobal { int _windowId = -1; bool _fullscreen = false; + bool grabKeyboard = false; final RxBool _showTabBar = true.obs; final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize); final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth); diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index e6065743c..b0eebee53 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/material.dart'; +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; @@ -10,17 +11,19 @@ import 'model.dart'; import 'platform_model.dart'; class UserModel { - var userName = ''.obs; + final RxString userName = ''.obs; + final RxString groupName = ''.obs; + final RxBool isAdmin = false.obs; WeakReference parent; - UserModel(this.parent) { - refreshCurrentUser(); - } + UserModel(this.parent); void refreshCurrentUser() async { - await getUserName(); final token = bind.mainGetLocalOption(key: 'access_token'); - if (token == '') return; + if (token == '') { + await _updateOtherModels(); + return; + } final url = await bind.mainGetApiServer(); final body = { 'id': await bind.mainGetMyId(), @@ -35,96 +38,95 @@ class UserModel { body: json.encode(body)); final status = response.statusCode; if (status == 401 || status == 400) { - resetToken(); + reset(); return; } - await _parseResp(response.body); + final data = json.decode(response.body); + final error = data['error']; + if (error != null) { + throw error; + } + + final user = UserPayload.fromJson(data); + await _parseAndUpdateUser(user); } catch (e) { print('Failed to refreshCurrentUser: $e'); + } finally { + await _updateOtherModels(); } } - void resetToken() async { + 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(); } - Future _parseResp(String body) async { - final data = json.decode(body); - final error = data['error']; - if (error != null) { - return error!; - } - final token = data['access_token']; - if (token != null) { - await bind.mainSetLocalOption(key: 'access_token', value: token); - } - final info = data['user']; - if (info != null) { - final value = json.encode(info); - await bind.mainSetOption(key: 'user_info', value: value); - userName.value = info['name']; - } - return ''; + Future _parseAndUpdateUser(UserPayload user) async { + userName.value = user.name; + groupName.value = user.grp; + isAdmin.value = user.isAdmin; } - Future getUserName() async { - if (userName.isNotEmpty) { - return userName.value; - } - final userInfo = bind.mainGetLocalOption(key: 'user_info'); - if (userInfo.trim().isEmpty) { - return ''; - } - final m = jsonDecode(userInfo); - if (m == null) { - userName.value = ''; - } else { - userName.value = m['name'] ?? ''; - } - return userName.value; + Future _updateOtherModels() async { + await gFFI.abModel.pullAb(); + await gFFI.groupModel.pull(); } Future logOut() async { final tag = gFFI.dialogManager.showLoading(translate('Waiting')); - final url = await bind.mainGetApiServer(); - final _ = await http.post(Uri.parse('$url/api/logout'), - body: { - 'id': await bind.mainGetMyId(), - 'uuid': await bind.mainGetUuid(), - }, - headers: await getHttpHeaders()); - await Future.wait([ - bind.mainSetLocalOption(key: 'access_token', value: ''), - bind.mainSetLocalOption(key: 'user_info', value: ''), - bind.mainSetLocalOption(key: 'selected-tags', value: ''), - ]); - parent.target?.abModel.clear(); - userName.value = ''; - gFFI.dialogManager.dismissByTag(tag); - } - - Future> login(String userName, String pass) async { - final url = await bind.mainGetApiServer(); 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'] ?? ''); - bind.mainSetLocalOption( - key: 'user_info', value: jsonEncode(body['user'])); - this.userName.value = body['user']?['name'] ?? ''; - return body; - } catch (err) { - return {'error': '$err'}; + final url = await bind.mainGetApiServer(); + await http + .post(Uri.parse('$url/api/logout'), + body: { + 'id': await bind.mainGetMyId(), + 'uuid': await bind.mainGetUuid(), + }, + headers: getHttpHeaders()) + .timeout(Duration(seconds: 2)); + } catch (e) { + print("request /api/logout failed: err=$e"); + } finally { + await reset(); + gFFI.dialogManager.dismissByTag(tag); } } + + /// 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 { + 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/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 91cb9a08a..ee19ac485 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -1,6 +1,7 @@ import 'dart:convert'; 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 +35,18 @@ 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? switch_uuid}) async { + final msg = jsonEncode({ + "type": WindowType.RemoteDesktop.index, + "id": remoteId, + "switch_uuid": switch_uuid ?? "" + }); try { final ids = await DesktopMultiWindow.getAllSubWindowIds(); @@ -57,7 +62,8 @@ class RustDeskMultiWindowManager { remoteDesktopController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - remote desktop") + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.RemoteDesktop)) ..show(); registerActiveWindow(remoteDesktopController.windowId); _remoteDesktopWindowId = remoteDesktopController.windowId; @@ -83,7 +89,8 @@ class RustDeskMultiWindowManager { fileTransferController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - file transfer") + ..setTitle(getWindowNameWithId(remoteId, + overrideType: WindowType.FileTransfer)) ..show(); registerActiveWindow(fileTransferController.windowId); _fileTransferWindowId = fileTransferController.windowId; @@ -109,7 +116,8 @@ class RustDeskMultiWindowManager { portForwardController ..setFrame(const Offset(0, 0) & const Size(1280, 720)) ..center() - ..setTitle("rustdesk - port forward") + ..setTitle( + getWindowNameWithId(remoteId, overrideType: WindowType.PortForward)) ..show(); registerActiveWindow(portForwardController.windowId); _portForwardWindowId = portForwardController.windowId; @@ -191,41 +199,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`] - /// - /// [Avaliability] + /// + /// [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/tray_manager.dart b/flutter/lib/utils/tray_manager.dart deleted file mode 100644 index 91550e1d8..000000000 --- a/flutter/lib/utils/tray_manager.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'dart:io'; - -import 'package:tray_manager/tray_manager.dart'; - -import '../common.dart'; - -const kTrayItemShowKey = "show"; -const kTrayItemQuitKey = "quit"; - -Future initTray({List? extra_item}) async { - List items = [ - MenuItem(key: kTrayItemShowKey, label: translate("Show RustDesk")), - MenuItem.separator(), - MenuItem(key: kTrayItemQuitKey, label: translate("Quit")), - ]; - if (extra_item != null) { - items.insertAll(0, extra_item); - } - if (Platform.isMacOS || Platform.isWindows) { - await trayManager.setToolTip("rustdesk"); - } - if (Platform.isMacOS || Platform.isLinux) { - await trayManager.setTitle("rustdesk"); - } - await trayManager - .setIcon(Platform.isWindows ? "assets/logo.ico" : "assets/logo.png"); - await trayManager.setContextMenu(Menu(items: items)); -} - -Future destoryTray() async { - return trayManager.destroy(); -} 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 952996e5f..8d41945c8 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -20,8 +20,6 @@ PODS: - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) - - tray_manager (0.0.1): - - FlutterMacOS - uni_links_desktop (0.0.1): - FlutterMacOS - url_launcher_macos (0.0.1): @@ -43,7 +41,6 @@ DEPENDENCIES: - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/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`) @@ -73,8 +70,6 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos - tray_manager: - :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos uni_links_desktop: :path: Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos url_launcher_macos: @@ -97,7 +92,6 @@ SPEC CHECKSUMS: path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea - tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index ec4baf141..fbf52403c 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -26,6 +26,10 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */; }; + 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */; }; + 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */; }; + 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */; }; 84010BA8292CF66600152837 /* liblibrustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (Weak, ); }; }; 84010BA9292CF68300152837 /* liblibrustdesk.dylib in Embed Libraries */ = {isa = PBXBuildFile; fileRef = 84010BA7292CF66600152837 /* liblibrustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; }; @@ -60,7 +64,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 = ""; }; @@ -74,6 +78,10 @@ 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; 7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light.png"; path = "../../res/mac-tray-light.png"; sourceTree = ""; }; + 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark.png"; path = "../../res/mac-tray-dark.png"; sourceTree = ""; }; + 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-light-x2.png"; path = "../../res/mac-tray-light-x2.png"; sourceTree = ""; }; + 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "mac-tray-dark-x2.png"; path = "../../res/mac-tray-dark-x2.png"; sourceTree = ""; }; 84010BA7292CF66600152837 /* liblibrustdesk.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = liblibrustdesk.dylib; path = ../../target/release/liblibrustdesk.dylib; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; @@ -119,7 +127,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* rustdesk.app */, + 33CC10ED2044A3C60003C045 /* RustDesk.app */, ); name = Products; sourceTree = ""; @@ -127,6 +135,10 @@ 33CC11242044D66E0003C045 /* Resources */ = { isa = PBXGroup; children = ( + 7E881463296E991200A0C54F /* mac-tray-dark-x2.png */, + 7E881461296E98ED00A0C54F /* mac-tray-light-x2.png */, + 7E4BCD752966B0EC006D24E2 /* mac-tray-dark.png */, + 7E4BCD742966B0EC006D24E2 /* mac-tray-light.png */, 33CC10F22044A3C60003C045 /* Assets.xcassets */, 33CC10F42044A3C60003C045 /* MainMenu.xib */, 33CC10F72044A3C60003C045 /* Info.plist */, @@ -200,7 +212,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* rustdesk.app */; + productReference = 33CC10ED2044A3C60003C045 /* RustDesk.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -253,8 +265,12 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7E881462296E98EE00A0C54F /* mac-tray-light-x2.png in Resources */, + 7E4BCD762966B0EC006D24E2 /* mac-tray-light.png in Resources */, + 7E4BCD772966B0EC006D24E2 /* mac-tray-dark.png in Resources */, 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + 7E881464296E991200A0C54F /* mac-tray-dark-x2.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -378,7 +394,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = x86_64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -403,6 +419,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; @@ -428,8 +445,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)", @@ -459,7 +479,7 @@ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = x86_64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -513,7 +533,7 @@ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ARCHS = x86_64; + ARCHS = "$(ARCHS_STANDARD)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -538,6 +558,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 +571,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 +590,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)", @@ -590,8 +619,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,6 +634,12 @@ ../../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[arch=*]" = Runner/bridge_generated.h; 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..5708e35cb 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -3,8 +3,21 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { + var lauched = false; override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { dummy_method_to_enforce_bundling() return true } + + override func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool { + if (lauched) { + handle_applicationShouldOpenUntitledFile(); + } + return true + } + + override func applicationDidFinishLaunching(_ aNotification: Notification) { + lauched = true; + NSApplication.shared.activate(ignoringOtherApps: true); + } } 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 9a3310e01..1c6cf008a 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 1e8713fda..8c2837a94 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 a529bd927..77b76503e 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 7d06c1d80..ac1b10766 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 2ce172957..9e593fcbd 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 5c192efb8..1205d915e 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 c9bf7b6bb..76d846c4a 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 7b985c870..d1077e0e4 100644 --- a/flutter/macos/Runner/Info.plist +++ b/flutter/macos/Runner/Info.plist @@ -40,6 +40,8 @@ NSMainNibFile MainMenu NSPrincipalClass - NSApplication + NSApplication + LSUIElement + 1 diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift index 2ebdf7fc0..540cd9ab9 100644 --- a/flutter/macos/Runner/MainFlutterWindow.swift +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -10,7 +10,7 @@ import package_info_plus_macos import path_provider_macos import screen_retriever import sqflite -import tray_manager +// import tray_manager import uni_links_desktop import url_launcher_macos import wakelock_macos @@ -39,7 +39,7 @@ class MainFlutterWindow: NSWindow { FLTPackageInfoPlusPlugin.register(with: controller.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: controller.registrar(forPlugin: "PathProviderPlugin")) SqflitePlugin.register(with: controller.registrar(forPlugin: "SqflitePlugin")) - TrayManagerPlugin.register(with: controller.registrar(forPlugin: "TrayManagerPlugin")) + // TrayManagerPlugin.register(with: controller.registrar(forPlugin: "TrayManagerPlugin")) UniLinksDesktopPlugin.register(with: controller.registrar(forPlugin: "UniLinksDesktopPlugin")) UrlLauncherPlugin.register(with: controller.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: controller.registrar(forPlugin: "WakelockMacosPlugin")) @@ -49,7 +49,8 @@ class MainFlutterWindow: NSWindow { 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() + } } 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 7440b5b9e..15a1a23ac 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -78,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.3.1" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" build_config: dependency: transitive description: @@ -105,7 +112,7 @@ packages: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.3.2" + version: "2.3.3" build_runner_core: dependency: transitive description: @@ -169,6 +176,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" clock: dependency: transitive description: @@ -182,7 +196,7 @@ packages: name: code_builder url: "https://pub.dartlang.org" source: hosted - version: "4.3.0" + version: "4.4.0" collection: dependency: transitive description: @@ -190,6 +204,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + colorize: + dependency: transitive + description: + name: colorize + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" contextmenu: dependency: "direct main" description: @@ -246,6 +267,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.15" + debounce_throttle: + dependency: "direct main" + description: + name: debounce_throttle + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" desktop_drop: dependency: "direct main" description: @@ -257,8 +285,8 @@ packages: dependency: "direct main" description: path: "." - ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 - resolved-ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 + ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" + resolved-ref: "057e6eb1bc7dcbcf9dafd1384274a611e4fe7124" url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -332,6 +360,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.1" + ffigen: + dependency: "direct dev" + description: + name: ffigen + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.4" file: dependency: transitive description: @@ -345,7 +380,7 @@ packages: name: file_picker url: "https://pub.dartlang.org" source: hosted - version: "5.2.2" + version: "5.2.4" fixnum: dependency: transitive description: @@ -382,12 +417,10 @@ packages: flutter_custom_cursor: dependency: "direct main" description: - path: "." - ref: bfb19c84a8244771488bc05cc5f9c9b5e0324cfd - resolved-ref: bfb19c84a8244771488bc05cc5f9c9b5e0324cfd - url: "https://github.com/Kingtous/rustdesk_flutter_custom_cursor" - source: git - version: "0.0.1" + name: flutter_custom_cursor + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" flutter_improved_scrolling: dependency: "direct main" description: @@ -424,12 +457,10 @@ packages: 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 + url: "https://pub.dartlang.org" + source: hosted + version: "1.61.1" flutter_svg: dependency: "direct main" description: @@ -448,7 +479,7 @@ packages: name: freezed url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.3.2" freezed_annotation: dependency: "direct main" description: @@ -539,7 +570,7 @@ packages: name: image_picker_android url: "https://pub.dartlang.org" source: hosted - version: "0.8.5+3" + version: "0.8.5+4" image_picker_for_web: dependency: transitive description: @@ -553,7 +584,7 @@ packages: name: image_picker_ios url: "https://pub.dartlang.org" source: hosted - version: "0.8.6+1" + version: "0.8.6+3" image_picker_platform_interface: dependency: transitive description: @@ -617,13 +648,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4" - menu_base: - dependency: transitive - description: - name: menu_base - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.1" meta: dependency: transitive description: @@ -637,7 +661,7 @@ packages: name: mime url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.3" nested: dependency: transitive description: @@ -701,6 +725,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + password_strength: + dependency: "direct main" + description: + name: password_strength + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" path: dependency: "direct main" description: @@ -826,7 +857,7 @@ packages: name: provider url: "https://pub.dartlang.org" source: hosted - version: "6.0.4" + version: "6.0.5" pub_semver: dependency: transitive description: @@ -841,6 +872,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" + puppeteer: + dependency: transitive + description: + name: puppeteer + url: "https://pub.dartlang.org" + source: hosted + version: "2.12.0" qr_code_scanner: dependency: "direct main" description: @@ -848,8 +886,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" rxdart: - dependency: "direct main" + dependency: transitive description: name: rxdart url: "https://pub.dartlang.org" @@ -885,6 +930,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + shelf_static: + dependency: transitive + description: + name: shelf_static + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" shelf_web_socket: dependency: transitive description: @@ -892,13 +944,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" - shortid: + simple_observable: dependency: transitive description: - name: shortid + name: simple_observable url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -988,15 +1040,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.4.0" - tray_manager: - dependency: "direct main" - description: - path: "." - ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" - resolved-ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" - url: "https://github.com/Kingtous/rustdesk_tray_manager" - source: git - version: "0.1.8" tuple: dependency: "direct main" description: @@ -1021,12 +1064,10 @@ packages: uni_links_desktop: dependency: "direct main" description: - path: "." - ref: "5be5113d59c753989dbf1106241379e3fd4c9b18" - resolved-ref: "5be5113d59c753989dbf1106241379e3fd4c9b18" - url: "https://github.com/fufesou/uni_links_desktop.git" - source: git - version: "0.1.3" + name: uni_links_desktop + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.4" uni_links_platform_interface: dependency: transitive description: @@ -1124,35 +1165,35 @@ packages: name: video_player url: "https://pub.dartlang.org" source: hosted - version: "2.4.8" + version: "2.4.10" video_player_android: dependency: transitive description: name: video_player_android url: "https://pub.dartlang.org" 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" 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" 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" source: hosted - version: "2.0.12" + version: "2.0.13" visibility_detector: dependency: "direct main" description: @@ -1210,12 +1251,12 @@ packages: source: hosted version: "2.2.0" win32: - dependency: transitive + dependency: "direct main" description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "3.1.2" + version: "3.1.3" win32_registry: dependency: transitive description: @@ -1262,6 +1303,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.1" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" zxing2: dependency: "direct main" description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 8de0be4d6..a5535c8b7 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -51,11 +51,7 @@ dependencies: 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 + flutter_rust_bridge: ^1.61.1 window_manager: git: url: https://github.com/Kingtous/rustdesk_window_manager @@ -63,16 +59,9 @@ dependencies: desktop_multi_window: git: url: https://github.com/Kingtous/rustdesk_desktop_multi_window - ref: cb086219bd4760a95a483cb14c1791d2a39ca5a0 + ref: 057e6eb1bc7dcbcf9dafd1384274a611e4fe7124 freezed_annotation: ^2.0.3 - tray_manager: - git: - url: https://github.com/Kingtous/rustdesk_tray_manager - ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a - flutter_custom_cursor: - git: - url: https://github.com/Kingtous/rustdesk_flutter_custom_cursor - ref: 74b1b314142b6775c1243067a3503ac568ebc74b + flutter_custom_cursor: ^0.0.2 window_size: git: url: https://github.com/google/flutter-desktop-embedding.git @@ -83,7 +72,7 @@ dependencies: contextmenu: ^3.0.0 desktop_drop: ^0.3.3 scroll_pos: ^0.3.0 - rxdart: ^0.27.5 + debounce_throttle: ^2.0.0 file_picker: ^5.1.0 flutter_svg: ^1.1.5 flutter_improved_scrolling: ^0.0.3 @@ -95,14 +84,12 @@ dependencies: # url: https://github.com/Kingtous/flutter_improved_scrolling # ref: 62f09545149f320616467c306c8c5f71714a18e6 uni_links: ^0.5.1 - uni_links_desktop: - git: - url: https://github.com/fufesou/uni_links_desktop.git - ref: 5be5113d59c753989dbf1106241379e3fd4c9b18 + 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 dev_dependencies: @@ -112,6 +99,7 @@ dev_dependencies: 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: @@ -125,6 +113,7 @@ icons_launcher: enable: true macos: enable: true + image_path: "../res/mac-icon.png" linux: enable: true diff --git a/flutter/web/icons/Icon-192.png b/flutter/web/icons/Icon-192.png index db3e76713..5d4566850 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 6910947a3..2b1abc3f2 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 36597c1bc..30147e96e 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 f2f79e64b..e84ca5bc7 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/gen_js_from_hbb.py b/flutter/web/js/gen_js_from_hbb.py index 0bdde54e4..8ee553b35 100755 --- a/flutter/web/js/gen_js_from_hbb.py +++ b/flutter/web/js/gen_js_from_hbb.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import re +import re import os import glob from tabnanny import check @@ -69,7 +69,7 @@ def main(): for ln in open('../../../Cargo.toml', encoding='utf-8'): if ln.startswith('version ='): print('export const ' + ln) - + def removeComment(ln): return re.sub('\s+\/\/.*$', '', ln) diff --git a/flutter/web/js/src/codec.js b/flutter/web/js/src/codec.js index dc579b5f3..27c9565ec 100644 --- a/flutter/web/js/src/codec.js +++ b/flutter/web/js/src/codec.js @@ -22,8 +22,8 @@ import { simd } from "wasm-feature-detect"; export async function loadVp9(callback) { - // Multithreading is used only if `options.threading` is true. - // This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs, + // Multithreading is used only if `options.threading` is true. + // This requires browser support for the new `SharedArrayBuffer` and `Atomics` APIs, // currently available in Firefox and Chrome with experimental flags enabled. // 所有主流浏览器均默认于2018年1月5日禁用SharedArrayBuffer const isSIMD = await simd(); 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/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/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/win32_window.cpp b/flutter/windows/runner/win32_window.cpp index 9ada9ab2e..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(); @@ -116,7 +116,7 @@ bool Win32Window::CreateAndShow(const std::wstring& title, HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); double scale_factor = dpi / 96.0; - + HWND window = CreateWindow( window_class, title.c_str(), WS_OVERLAPPEDWINDOW, Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), 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/docs/assets/scene3.png b/libs/clipboard/docs/assets/scene3.png index 639236460..a65a7ac06 100644 Binary files a/libs/clipboard/docs/assets/scene3.png and b/libs/clipboard/docs/assets/scene3.png differ diff --git a/libs/clipboard/docs/assets/win_A_B.png b/libs/clipboard/docs/assets/win_A_B.png index 0b6b25d0e..b65e571e7 100644 Binary files a/libs/clipboard/docs/assets/win_A_B.png and b/libs/clipboard/docs/assets/win_A_B.png differ diff --git a/libs/clipboard/docs/assets/win_B_A.png b/libs/clipboard/docs/assets/win_B_A.png index 377fa801f..4484b21ac 100644 Binary files a/libs/clipboard/docs/assets/win_B_A.png and b/libs/clipboard/docs/assets/win_B_A.png differ 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/.vscode/launch.json b/libs/enigo/.vscode/launch.json index a7a40dcfe..123e0bc42 100644 --- a/libs/enigo/.vscode/launch.json +++ b/libs/enigo/.vscode/launch.json @@ -1,7 +1,7 @@ { "version": "0.2.0", "configurations": [ - + { "name": "Debug", "type": "gdb", diff --git a/libs/enigo/Cargo.toml b/libs/enigo/Cargo.toml index 83c79e064..cc4173a97 100644 --- a/libs/enigo/Cargo.toml +++ b/libs/enigo/Cargo.toml @@ -22,8 +22,8 @@ appveyor = { repository = "pythoneer/enigo-85xiy" } serde = { version = "1.0", optional = true } serde_derive = { version = "1.0", optional = true } log = "0.4" -rdev = { git = "https://github.com/asur4s/rdev" } -tfc = { git = "https://github.com/asur4s/The-Fat-Controller" } +rdev = { git = "https://github.com/fufesou/rdev" } +tfc = { git = "https://github.com/fufesou/The-Fat-Controller" } hbb_common = { path = "../hbb_common" } [features] 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/examples/mouse.rs b/libs/enigo/examples/mouse.rs index 50a3506cf..f963e041e 100644 --- a/libs/enigo/examples/mouse.rs +++ b/libs/enigo/examples/mouse.rs @@ -23,15 +23,18 @@ fn main() { enigo.mouse_click(MouseButton::Left); thread::sleep(wait_time); - enigo.mouse_scroll_x(2); - thread::sleep(wait_time); + #[cfg(not(target_os = "macos"))] + { + enigo.mouse_scroll_x(2); + thread::sleep(wait_time); - enigo.mouse_scroll_x(-2); - thread::sleep(wait_time); + enigo.mouse_scroll_x(-2); + thread::sleep(wait_time); - enigo.mouse_scroll_y(2); - thread::sleep(wait_time); + enigo.mouse_scroll_y(2); + thread::sleep(wait_time); - enigo.mouse_scroll_y(-2); - thread::sleep(wait_time); + enigo.mouse_scroll_y(-2); + thread::sleep(wait_time); + } } diff --git a/libs/enigo/src/lib.rs b/libs/enigo/src/lib.rs index 083345e63..509bbf97c 100644 --- a/libs/enigo/src/lib.rs +++ b/libs/enigo/src/lib.rs @@ -19,9 +19,9 @@ //! 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 laguage. +//! interfaces unaccessible by a public API or scripting language. //! //! For the keyboard there are currently two modes you can use. The first mode //! is represented by the [key_sequence]() function @@ -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 @@ -447,8 +451,9 @@ pub trait KeyboardControllable { where Self: Sized, { - self.key_sequence_parse_try(sequence) - .expect("Could not parse sequence"); + if let Err(..) = self.key_sequence_parse_try(sequence) { + println!("Could not parse sequence"); + } } /// Same as key_sequence_parse except returns any errors fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), dsl::ParseError> @@ -463,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 4eb890c29..f6e172677 100644 --- a/libs/enigo/src/linux/nix_impl.rs +++ b/libs/enigo/src/linux/nix_impl.rs @@ -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,7 @@ impl Enigo { } /// Get custom mouse. pub fn get_custom_mouse(&mut self) -> &mut Option { - &mut self.cutsom_mouse + &mut self.custom_mouse } fn tfc_key_down_or_up(&mut self, key: Key, down: bool, up: bool) -> bool { @@ -88,12 +88,18 @@ impl Default for Enigo { Self { is_x11, tfc: if is_x11 { - Some(TFC_Context::new().expect("kbd context error")) + match TFC_Context::new() { + Ok(ctx) => Some(ctx), + Err(..) => { + println!("kbd context error"); + None + } + } } else { None }, custom_keyboard: None, - cutsom_mouse: None, + custom_mouse: None, xdo: EnigoXdo::default(), } } @@ -112,7 +118,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) } } @@ -121,7 +127,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) } } @@ -130,7 +136,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(()) @@ -141,7 +147,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) } } @@ -150,7 +156,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) } } @@ -159,7 +165,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) } } @@ -168,7 +174,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) } } @@ -177,6 +183,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", _ => { diff --git a/libs/enigo/src/linux/xdo.rs b/libs/enigo/src/linux/xdo.rs index ed2d28dc1..2115d7283 100644 --- a/libs/enigo/src/linux/xdo.rs +++ b/libs/enigo/src/linux/xdo.rs @@ -57,6 +57,8 @@ fn mousebutton(button: MouseButton) -> c_int { MouseButton::ScrollDown => 5, MouseButton::ScrollLeft => 6, MouseButton::ScrollRight => 7, + MouseButton::Back => 8, + MouseButton::Forward => 9, } } @@ -391,8 +393,9 @@ impl KeyboardControllable for EnigoXdo { where Self: Sized, { - self.key_sequence_parse_try(sequence) - .expect("Could not parse sequence"); + if let Err(..) = self.key_sequence_parse_try(sequence) { + println!("Could not parse sequence"); + } } fn key_sequence_parse_try(&mut self, sequence: &str) -> Result<(), crate::dsl::ParseError> diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index 937320f7d..55f350895 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -40,6 +40,7 @@ const BUF_LEN: usize = 4; #[allow(improper_ctypes)] #[allow(non_snake_case)] #[link(name = "ApplicationServices", kind = "framework")] +#[link(name = "Carbon", kind = "framework")] extern "C" { fn CFDataGetBytePtr(theData: CFDataRef) -> *const u8; fn TISCopyCurrentKeyboardInputSource() -> TISInputSourceRef; @@ -68,7 +69,7 @@ extern "C" { ) -> Boolean; fn CGEventPost(tapLocation: CGEventTapLocation, event: *mut MyCGEvent); - // Actually return CFDataRef which is const here, but for coding convienence, return *mut c_void + // Actually return CFDataRef which is const here, but for coding convenience, return *mut c_void fn TISGetInputSourceProperty(source: TISInputSourceRef, property: *const c_void) -> *mut c_void; // not present in servo/core-graphics @@ -225,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() { @@ -248,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..2e1108b9e 100644 --- a/libs/enigo/src/win/win_impl.rs +++ b/libs/enigo/src/win/win_impl.rs @@ -56,6 +56,20 @@ fn keybd_event(flags: u32, vk: u16, scan: u16) -> DWORD { input.type_ = INPUT_KEYBOARD; unsafe { let dst_ptr = (&mut input.u as *mut _) as *mut u8; + let flags = match vk as _ { + winapi::um::winuser::VK_HOME | + winapi::um::winuser::VK_UP | + winapi::um::winuser::VK_PRIOR | + winapi::um::winuser::VK_LEFT | + winapi::um::winuser::VK_RIGHT | + winapi::um::winuser::VK_END | + winapi::um::winuser::VK_DOWN | + winapi::um::winuser::VK_NEXT | + winapi::um::winuser::VK_INSERT | + winapi::um::winuser::VK_DELETE => flags | winapi::um::winuser::KEYEVENTF_EXTENDEDKEY, + _ => flags, + }; + let k = KEYBDINPUT { wVk: vk, wScan: scan, @@ -134,9 +148,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 +178,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/build.rs b/libs/hbb_common/build.rs index 225ec34c7..bff0cfafc 100644 --- a/libs/hbb_common/build.rs +++ b/libs/hbb_common/build.rs @@ -1,8 +1,11 @@ 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") + .out_dir(out_dir) .inputs(&["protos/rendezvous.proto", "protos/message.proto"]) .include("protos") .customize( diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto index 9217388aa..12d698045 100644 --- a/libs/hbb_common/protos/message.proto +++ b/libs/hbb_common/protos/message.proto @@ -40,7 +40,7 @@ message DisplayInfo { int32 height = 4; string name = 5; bool online = 6; - bool cursor_embeded = 7; + bool cursor_embedded = 7; } message PortForward { @@ -420,7 +420,7 @@ message SwitchDisplay { sint32 y = 3; int32 width = 4; int32 height = 5; - bool cursor_embeded = 6; + bool cursor_embedded = 6; } message PermissionInfo { @@ -445,7 +445,7 @@ enum ImageQuality { } message VideoCodecState { - enum PerferCodec { + enum PreferCodec { Auto = 0; VPX = 1; H264 = 2; @@ -455,7 +455,7 @@ message VideoCodecState { int32 score_vpx = 1; int32 score_h264 = 2; int32 score_h265 = 3; - PerferCodec perfer = 4; + PreferCodec prefer = 4; } message OptionMessage { @@ -503,7 +503,7 @@ message AudioFrame { // Notify peer to show message box. message MessageBox { - // Message type. Refer to flutter/lib/commom.dart/msgBox(). + // Message type. Refer to flutter/lib/common.dart/msgBox(). string msgtype = 1; string title = 2; // English @@ -552,6 +552,29 @@ message BackNotification { } } +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; @@ -567,6 +590,11 @@ message Misc { 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; } } @@ -591,5 +619,6 @@ message Message { Misc misc = 19; Cliprdr cliprdr = 20; MessageBox message_box = 21; + SwitchSidesResponse switch_sides_response = 22; } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index f476816ef..20334ed12 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1,7 +1,7 @@ use std::{ collections::HashMap, fs, - net::{IpAddr, Ipv4Addr, SocketAddr}, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, path::{Path, PathBuf}, sync::{Arc, Mutex, RwLock}, time::SystemTime, @@ -49,7 +49,10 @@ lazy_static::lazy_static! { static ref CONFIG2: Arc> = Arc::new(RwLock::new(Config2::load())); static ref LOCAL_CONFIG: Arc> = Arc::new(RwLock::new(LocalConfig::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 HW_CODEC_CONFIG: Arc> = Arc::new(RwLock::new(HwCodecConfig::load())); @@ -57,6 +60,10 @@ lazy_static::lazy_static! { lazy_static::lazy_static! { pub static ref APP_DIR: Arc> = Default::default(); +} + +#[cfg(any(target_os = "android", target_os = "ios"))] +lazy_static::lazy_static! { pub static ref APP_HOME_DIR: Arc> = Default::default(); } @@ -73,12 +80,17 @@ const CHARS: &'static [char] = &[ 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ]; -pub const RENDEZVOUS_SERVERS: &'static [&'static str] = &[ +const RENDEZVOUS_SERVERS: &'static [&'static 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: &'static 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; @@ -199,6 +211,8 @@ pub struct PeerConfig { pub enable_file_transfer: bool, #[serde(default)] pub show_quality_monitor: bool, + #[serde(default)] + pub keyboard_mode: String, // The other scalar value must before this #[serde(default, deserialize_with = "PeerConfig::deserialize_options")] @@ -505,8 +519,12 @@ impl Config { } #[inline] - pub fn get_any_listen_addr() -> SocketAddr { - SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0) + pub fn get_any_listen_addr(is_ipv4: bool) -> SocketAddr { + if is_ipv4 { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0) + } else { + SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) + } } pub fn get_rendezvous_server() -> String { @@ -987,6 +1005,8 @@ pub struct LocalConfig { #[serde(default)] remote_id: String, // latest used one #[serde(default)] + kb_layout_type: String, + #[serde(default)] size: Size, #[serde(default)] pub fav: Vec, @@ -1006,6 +1026,16 @@ impl LocalConfig { Config::store_(self, "_local"); } + pub fn get_kb_layout_type() -> String { + LOCAL_CONFIG.read().unwrap().kb_layout_type.clone() + } + + pub fn set_kb_layout_type(kb_layout_type: String) { + let mut config = LOCAL_CONFIG.write().unwrap(); + config.kb_layout_type = kb_layout_type; + config.store(); + } + pub fn get_size() -> Size { LOCAL_CONFIG.read().unwrap().size } diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs index dd8a7530e..fec8b8670 100644 --- a/libs/hbb_common/src/fs.rs +++ b/libs/hbb_common/src/fs.rs @@ -215,6 +215,8 @@ pub struct TransferJob { transferred: u64, enable_overwrite_detection: bool, file_confirmed: bool, + // indicating the last file is skipped + file_skipped: bool, file_is_waiting: bool, default_overwrite_strategy: Option, } @@ -541,25 +543,66 @@ impl TransferJob { pub fn set_file_confirmed(&mut self, file_confirmed: bool) { log::info!("id: {}, file_confirmed: {}", self.id, file_confirmed); self.file_confirmed = file_confirmed; + self.file_skipped = false; } pub fn set_file_is_waiting(&mut self, file_is_waiting: bool) { self.file_is_waiting = file_is_waiting; } + #[inline] pub fn file_is_waiting(&self) -> bool { self.file_is_waiting } + #[inline] pub fn file_confirmed(&self) -> bool { self.file_confirmed } - pub fn skip_current_file(&mut self) -> bool { + /// Indicating whether the last file is skipped + #[inline] + pub fn file_skipped(&self) -> bool { + self.file_skipped + } + + /// Indicating whether the whole task is skipped + #[inline] + pub fn job_skipped(&self) -> bool { + self.file_skipped() && self.files.len() == 1 + } + + /// Check whether the job is completed after `read` returns `None` + /// This is a helper function which gives additional lifecycle when the job reads `None`. + /// If returns `true`, it means we can delete the job automatically. `False` otherwise. + /// + /// [`Note`] + /// Conditions: + /// 1. Files are not waiting for confirmation by peers. + #[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; + } + + /// Get job error message, useful for getting status when job had finished + pub fn job_error(&self) -> Option { + if self.job_skipped() { + return Some("skipped".to_string()); + } + None + } + + pub fn set_file_skipped(&mut self) -> bool { + log::debug!("skip file {} in job {}", self.file_num, self.id); self.file.take(); self.set_file_confirmed(false); self.set_file_is_waiting(false); self.file_num += 1; + self.file_skipped = true; true } @@ -570,8 +613,7 @@ impl TransferJob { match r.union { Some(file_transfer_send_confirm_request::Union::Skip(s)) => { if s { - log::debug!("skip file id:{}, file_num:{}", r.id, r.file_num); - self.skip_current_file(); + self.set_file_skipped(); } else { self.set_file_confirmed(true); } @@ -717,10 +759,16 @@ pub async fn handle_read_jobs( stream.send(&new_block(block)).await?; } Ok(None) => { - if !job.enable_overwrite_detection || (!job.file_confirmed && !job.file_is_waiting) - { + if job.job_completed() { finished.push(job.id()); - stream.send(&new_done(job.id(), job.file_num())).await?; + let err = job.job_error(); + if err.is_some() { + stream + .send(&new_error(job.id(), err.unwrap(), job.file_num())) + .await?; + } else { + stream.send(&new_done(job.id(), job.file_num())).await?; + } } else { // waiting confirmation. } 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 ae564685f..e57994f34 100644 --- a/libs/hbb_common/src/lib.rs +++ b/libs/hbb_common/src/lib.rs @@ -10,7 +10,7 @@ pub use protos::rendezvous as rendezvous_proto; use std::{ fs::File, io::{self, BufRead}, - net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, path::Path, time::{self, SystemTime, UNIX_EPOCH}, }; @@ -40,6 +40,7 @@ pub use tokio_socks::TargetAddr; pub mod password_security; pub use chrono; pub use directories_next; +pub mod keyboard; #[cfg(feature = "quic")] pub type Stream = quic::Connection; @@ -66,6 +67,21 @@ macro_rules! allow_err { } else { } }; + + ($e:expr, $($arg:tt)*) => { + if let Err(err) = $e { + log::debug!( + "{:?}, {}, {}:{}:{}:{}", + err, + format_args!($($arg)*), + module_path!(), + file!(), + line!(), + column!() + ); + } else { + } + }; } #[inline] @@ -102,13 +118,31 @@ impl AddrMangle { } bytes[..(16 - n_padding)].to_vec() } - _ => { - panic!("Only support ipv4"); + SocketAddr::V6(addr_v6) => { + let mut x = addr_v6.ip().octets().to_vec(); + let port: [u8; 2] = addr_v6.port().to_le_bytes(); + x.push(port[0]); + x.push(port[1]); + x } } } pub fn decode(bytes: &[u8]) -> SocketAddr { + 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 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 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); let number = u128::from_le_bytes(padded); @@ -249,5 +283,94 @@ mod tests { fn test_mangle() { let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(192, 168, 16, 32), 21116)); assert_eq!(addr, AddrMangle::decode(&AddrMangle::encode(addr))); + + 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))); + } + + #[test] + fn test_allow_err() { + allow_err!(Err("test err") as Result<(), &str>); + allow_err!( + Err("test err with msg") as Result<(), &str>, + "prompt {}", + "failed" + ); + } +} + +#[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) +} + +#[cfg(test)] +mod test_lib { + use super::*; + + #[test] + fn test_ipv6() { + assert_eq!(is_ipv6_str("1:2:3"), true); + assert_eq!(is_ipv6_str("[ab:2:3]:12"), true); + assert_eq!(is_ipv6_str("[ABEF:2a:3]:12"), true); + assert_eq!(is_ipv6_str("[ABEG:2a:3]:12"), false); + assert_eq!(is_ipv6_str("1[ab:2:3]:12"), false); + assert_eq!(is_ipv6_str("1.1.1.1"), false); + assert_eq!(is_ip_str("1.1.1.1"), true); + assert_eq!(is_ipv6_str("1:2:"), false); + assert_eq!(is_ipv6_str("1:2::0"), true); + assert_eq!(is_ipv6_str("[1:2::0]:1"), true); + assert_eq!(is_ipv6_str("[1:2::0]:"), false); + assert_eq!(is_ipv6_str("1:2::0]:1"), false); + } + + #[test] + fn test_hostname_port() { + assert_eq!(is_domain_port_str("a:12"), false); + assert_eq!(is_domain_port_str("a.b.c:12"), false); + assert_eq!(is_domain_port_str("test.com:12"), true); + assert_eq!(is_domain_port_str("test-UPPER.com:12"), true); + assert_eq!(is_domain_port_str("some-other.domain.com:12"), true); + assert_eq!(is_domain_port_str("under_score:12"), false); + assert_eq!(is_domain_port_str("a@bc:12"), false); + assert_eq!(is_domain_port_str("1.1.1.1:12"), false); + assert_eq!(is_domain_port_str("1.2.3:12"), false); + assert_eq!(is_domain_port_str("1.2.3.45:12"), false); + assert_eq!(is_domain_port_str("a.b.c:123456"), false); + assert_eq!(is_domain_port_str("---:12"), false); + assert_eq!(is_domain_port_str(".:12"), false); + // todo: should we also check for these edge cases? + // out-of-range port + assert_eq!(is_domain_port_str("test.com:0"), true); + assert_eq!(is_domain_port_str("test.com:98989"), true); } } diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs index 1a20ed0e1..e82416309 100644 --- a/libs/hbb_common/src/platform/linux.rs +++ b/libs/hbb_common/src/platform/linux.rs @@ -1,15 +1,15 @@ use crate::ResultType; 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() @@ -43,7 +43,8 @@ pub fn get_display_server() -> String { } fn get_display_server_of_session(session: &str) -> String { - if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "Type", session])) + let mut display_server = if let Ok(output) = + run_loginctl(Some(vec!["show-session", "-p", "Type", session])) // Check session type of the session { let display_server = String::from_utf8_lossy(&output.stdout) @@ -64,28 +65,23 @@ fn get_display_server_of_session(session: &str) -> String { { if xorg_results.trim_end().to_string() != "" { // If it is, manually return "x11", otherwise return tty - "x11".to_owned() - } else { - display_server + return "x11".to_owned(); } - } else { - // If any of these commands fail just fall back to the display server - display_server } - } else { - display_server } - } else { - // loginctl has not given the expected output. try something else. - if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { - return sestype.to_owned(); - } - // If the session is not a tty, then just return the type as usual - display_server } + display_server } else { "".to_owned() + }; + 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 } pub fn get_values_of_seat0(indices: Vec) -> Vec { @@ -126,8 +122,7 @@ pub fn get_values_of_seat0(indices: Vec) -> Vec { } fn is_active(sid: &str) -> bool { - if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) - { + if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) { String::from_utf8_lossy(&output.stdout).contains("active") } else { false diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs new file mode 100644 index 000000000..c001c58fb --- /dev/null +++ b/libs/hbb_common/src/protos/mod.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); \ No newline at end of file diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs index 72ab73f16..6f62163d1 100644 --- a/libs/hbb_common/src/socket_client.rs +++ b/libs/hbb_common/src/socket_client.rs @@ -9,31 +9,52 @@ use std::net::SocketAddr; use tokio::net::ToSocketAddrs; use tokio_socks::{IntoTargetAddr, TargetAddr}; -fn to_socket_addr(host: &str) -> ResultType { - use std::net::ToSocketAddrs; - host.to_socket_addrs()? - .filter(|x| x.is_ipv4()) - .next() - .context("Failed to solve") +#[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); + } + return host; } -pub fn get_target_addr(host: &str) -> ResultType> { - let addr = match Config::get_network_type() { - NetworkType::Direct => to_socket_addr(&host)?.into_target_addr()?, - NetworkType::ProxySocks => host.into_target_addr()?, +#[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); + } + } } - .to_owned(); - Ok(addr) + return host; } pub fn test_if_valid_server(host: &str) -> String { - let mut host = host.to_owned(); - if !host.contains(":") { - host = format!("{}:{}", host, 0); - } + let host = check_port(host, 0); + use std::net::ToSocketAddrs; match Config::get_network_type() { - NetworkType::Direct => match to_socket_addr(&host) { + NetworkType::Direct => match host.to_socket_addrs() { Err(err) => err.to_string(), Ok(_) => "".to_owned(), }, @@ -44,33 +65,126 @@ pub fn test_if_valid_server(host: &str) -> String { } } -pub async fn connect_tcp<'t, T: IntoTargetAddr<'t>>( +pub trait IsResolvedSocketAddr { + fn resolve(&self) -> Option<&SocketAddr>; +} + +impl IsResolvedSocketAddr for SocketAddr { + fn resolve(&self) -> Option<&SocketAddr> { + Some(&self) + } +} + +impl IsResolvedSocketAddr for String { + fn resolve(&self) -> Option<&SocketAddr> { + None + } +} + +impl IsResolvedSocketAddr for &str { + fn resolve(&self) -> Option<&SocketAddr> { + None + } +} + +#[inline] +pub async fn connect_tcp< + 't, + T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, +>( target: T, - local: SocketAddr, ms_timeout: u64, ) -> ResultType { - let target_addr = target.into_target_addr()?; + connect_tcp_local(target, None, ms_timeout).await +} +pub async fn connect_tcp_local< + 't, + T: IntoTargetAddr<'t> + ToSocketAddrs + IsResolvedSocketAddr + std::fmt::Display, +>( + target: T, + local: Option, + ms_timeout: u64, +) -> ResultType { if let Some(conf) = Config::get_socks() { - FramedStream::connect( + return FramedStream::connect( conf.proxy.as_str(), - target_addr, + target, local, conf.username.as_str(), conf.password.as_str(), ms_timeout, ) - .await - } else { - let addr = std::net::ToSocketAddrs::to_socket_addrs(&target_addr)? - .filter(|x| x.is_ipv4()) - .next() - .context("Invalid target addr, no valid ipv4 address can be resolved.")?; - Ok(FramedStream::new(addr, local, ms_timeout).await?) + .await; + } + 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?); + } + } + } + Ok(FramedStream::new(target, local, ms_timeout).await?) +} + +#[inline] +pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { + match target { + TargetAddr::Ip(addr) => addr.is_ipv4(), + _ => true, } } -pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType { +#[inline] +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() + .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)); + } + } + addr +} + +async fn test_target(target: &str) -> ResultType { + if let Ok(Ok(s)) = super::timeout(1000, tokio::net::TcpStream::connect(target)).await { + if let Ok(addr) = s.peer_addr() { + return Ok(addr); + } + } + tokio::net::lookup_host(target) + .await? + .next() + .context(format!("Failed to look up host for {}", target)) +} + +#[inline] +pub async fn new_udp_for( + target: &str, + ms_timeout: u64, +) -> ResultType<(FramedSocket, TargetAddr<'static>)> { + let (ipv4, target) = if NetworkType::Direct == Config::get_network_type() { + let addr = test_target(target).await?; + (addr.is_ipv4(), addr.into_target_addr()?) + } else { + (true, target.into_target_addr()?) + }; + Ok(( + new_udp(Config::get_any_listen_addr(ipv4), ms_timeout).await?, + target.to_owned(), + )) +} + +async fn new_udp(local: T, ms_timeout: u64) -> ResultType { match Config::get_socks() { None => Ok(FramedSocket::new(local).await?), Some(conf) => { @@ -87,9 +201,82 @@ pub async fn new_udp(local: T, ms_timeout: u64) -> ResultType< } } -pub async fn rebind_udp(local: T) -> ResultType> { - match Config::get_network_type() { - NetworkType::Direct => Ok(Some(FramedSocket::new(local).await?)), - _ => Ok(None), +pub async fn rebind_udp_for( + target: &str, +) -> ResultType)>> { + if Config::get_network_type() != NetworkType::Direct { + return Ok(None); + } + let addr = test_target(target).await?; + let v4 = addr.is_ipv4(); + Ok(Some(( + FramedSocket::new(Config::get_any_listen_addr(v4)).await?, + addr.into_target_addr()?.to_owned(), + ))) +} + +#[cfg(test)] +mod tests { + use std::net::ToSocketAddrs; + + use super::*; + + #[test] + fn test_nat64() { + test_nat64_async(); + } + + #[tokio::main(flavor = "current_thread")] + async fn test_nat64_async() { + assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), true), "1.1.1.1"); + assert_eq!(ipv4_to_ipv6("1.1.1.1".to_owned(), false), "1.1.1.1.nip.io"); + assert_eq!( + ipv4_to_ipv6("1.1.1.1:8080".to_owned(), false), + "1.1.1.1.nip.io:8080" + ); + assert_eq!( + ipv4_to_ipv6("rustdesk.com".to_owned(), false), + "rustdesk.com" + ); + if ("rustdesk.com:80") + .to_socket_addrs() + .unwrap() + .next() + .unwrap() + .is_ipv6() + { + assert!(query_nip_io(&"1.1.1.1:80".parse().unwrap()) + .await + .unwrap() + .is_ipv6()); + return; + } + 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 7966920c2..a1322fc15 100644 --- a/libs/hbb_common/src/tcp.rs +++ b/libs/hbb_common/src/tcp.rs @@ -5,7 +5,7 @@ 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}, @@ -73,73 +73,79 @@ fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result( - remote_addr: T1, - local_addr: T2, + pub async fn new( + remote_addr: T, + local_addr: Option, ms_timeout: u64, ) -> ResultType { - for local_addr in lookup_host(&local_addr).await? { - for remote_addr in lookup_host(&remote_addr).await? { - let stream = super::timeout( - ms_timeout, - new_socket(local_addr, true)?.connect(remote_addr), - ) - .await??; - stream.set_nodelay(true).ok(); - let addr = stream.local_addr()?; - return Ok(Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )); + for remote_addr in lookup_host(&remote_addr).await? { + let local = if let Some(addr) = local_addr { + addr + } else { + crate::config::Config::get_any_listen_addr(remote_addr.is_ipv4()) + }; + if let Ok(socket) = new_socket(local, true) { + if let Ok(Ok(stream)) = + super::timeout(ms_timeout, socket.connect(remote_addr)).await + { + stream.set_nodelay(true).ok(); + let addr = stream.local_addr()?; + return Ok(Self( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )); + } } } - bail!("could not resolve to any address"); + bail!(format!("Failed to connect to {}", remote_addr)); } - pub async fn connect<'a, 't, P, T1, T2>( + pub async fn connect<'a, 't, P, T>( proxy: P, - target: T1, - local: T2, + target: T, + local_addr: Option, username: &'a str, password: &'a str, ms_timeout: u64, ) -> ResultType where P: ToProxyAddrs, - T1: IntoTargetAddr<'t>, - T2: ToSocketAddrs, + T: IntoTargetAddr<'t>, { - if let Some(local) = lookup_host(&local).await?.next() { - if let Some(proxy) = proxy.to_proxy_addrs().next().await { - let stream = - super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy?)).await??; - stream.set_nodelay(true).ok(); - let stream = if username.trim().is_empty() { - super::timeout( - ms_timeout, - Socks5Stream::connect_with_socket(stream, target), - ) - .await?? - } else { - super::timeout( - ms_timeout, - Socks5Stream::connect_with_password_and_socket( - stream, target, username, password, - ), - ) - .await?? - }; - let addr = stream.local_addr()?; - return Ok(Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )); + if let Some(Ok(proxy)) = proxy.to_proxy_addrs().next().await { + let local = if let Some(addr) = local_addr { + addr + } else { + crate::config::Config::get_any_listen_addr(proxy.is_ipv4()) }; - }; + let stream = + super::timeout(ms_timeout, new_socket(local, true)?.connect(proxy)).await??; + stream.set_nodelay(true).ok(); + let stream = if username.trim().is_empty() { + super::timeout( + ms_timeout, + Socks5Stream::connect_with_socket(stream, target), + ) + .await?? + } else { + super::timeout( + ms_timeout, + Socks5Stream::connect_with_password_and_socket( + stream, target, username, password, + ), + ) + .await?? + }; + let addr = stream.local_addr()?; + return Ok(Self( + Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + addr, + None, + 0, + )); + } bail!("could not resolve to any address"); } @@ -252,6 +258,38 @@ pub async fn new_listener(addr: T, reuse: bool) -> ResultType< } } +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 3532dd1e0..38121a4e1 100644 --- a/libs/hbb_common/src/udp.rs +++ b/libs/hbb_common/src/udp.rs @@ -49,7 +49,7 @@ impl FramedSocket { #[allow(clippy::never_loop)] pub async fn new_reuse(addr: T) -> ResultType { - for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) { + 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)?, @@ -63,7 +63,7 @@ impl FramedSocket { addr: T, buf_size: usize, ) -> ResultType { - for addr in addr.to_socket_addrs()?.filter(|x| x.is_ipv4()) { + 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(), @@ -164,4 +164,13 @@ impl FramedSocket { None } } + + pub fn is_ipv4(&self) -> bool { + if let FramedSocket::Direct(x) = self { + if let Ok(v) = x.get_ref().local_addr() { + return v.is_ipv4(); + } + } + true + } } diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index 499c18e2c..0a6cd8ef9 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -4,7 +4,10 @@ use std::{ path::PathBuf, }; +#[cfg(windows)] const BIN_DATA: &[u8] = include_bytes!("../data.bin"); +#[cfg(not(windows))] +const BIN_DATA: &[u8] = &[]; // 4bytes const LENGTH: usize = 4; const IDENTIFIER_LENGTH: usize = 8; @@ -71,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 { @@ -118,7 +121,7 @@ impl BinaryReader { (parsed, executable) } - #[cfg(unix)] + #[cfg(linux)] pub fn configure_permission(&self, prefix: &PathBuf) { use std::os::unix::prelude::PermissionsExt; diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index edcbdd1fd..13dd0c3dc 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -30,7 +30,7 @@ fn setup(reader: BinaryReader, dir: Option, clear: bool) -> Option, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index d729342d6..acfd4c674 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -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); @@ -218,7 +218,7 @@ impl Encoder { #[inline] pub fn current_hw_encoder_name() -> Option { #[cfg(feature = "hwcodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { return HwEncoder::current_name().lock().unwrap().clone(); } else { return None; @@ -229,7 +229,7 @@ impl Encoder { pub fn supported_encoding() -> (bool, bool) { #[cfg(feature = "hwcodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { let best = HwEncoder::best(); ( best.h264.as_ref().map_or(false, |c| c.score > 0), @@ -246,18 +246,18 @@ impl Encoder { impl Decoder { pub fn video_codec_state(_id: &str) -> VideoCodecState { #[cfg(feature = "hwcodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { let best = HwDecoder::best(); return VideoCodecState { 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() }; } #[cfg(feature = "mediacodec")] - if check_hwcodec_config() { + if enable_hwcodec_option() { let score_h264 = if H264_DECODER_SUPPORT.load(std::sync::atomic::Ordering::SeqCst) { 92 } else { @@ -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() }; } @@ -287,11 +287,19 @@ impl Decoder { Decoder { vpx, #[cfg(feature = "hwcodec")] - hw: HwDecoder::new_decoders(), + hw: if enable_hwcodec_option() { + HwDecoder::new_decoders() + } else { + HwDecoders::default() + }, #[cfg(feature = "hwcodec")] i420: vec![], #[cfg(feature = "mediacodec")] - media_codec: MediaCodecDecoder::new_decoders(), + media_codec: if enable_hwcodec_option() { + MediaCodecDecoder::new_decoders() + } else { + MediaCodecDecoders::default() + }, } } @@ -397,25 +405,25 @@ impl Decoder { } #[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 } } } #[cfg(any(feature = "hwcodec", feature = "mediacodec"))] -fn check_hwcodec_config() -> bool { +fn enable_hwcodec_option() -> bool { if let Some(v) = Config2::get().options.get("enable-hwcodec") { return v != "N"; } diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 166f7516c..9cd6077a6 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -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, @@ -94,7 +94,7 @@ impl EncoderApi for HwEncoder { frames.push(EncodedVideoFrame { data: Bytes::from(frame.data), pts: frame.pts as _, - key:frame.key == 1, + key: frame.key == 1, ..Default::default() }); } @@ -175,6 +175,7 @@ pub struct HwDecoder { pub info: CodecInfo, } +#[derive(Default)] pub struct HwDecoders { pub h264: Option, pub h265: Option, @@ -292,8 +293,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) { diff --git a/libs/scrap/src/common/mediacodec.rs b/libs/scrap/src/common/mediacodec.rs index fa821246c..406baecb5 100644 --- a/libs/scrap/src/common/mediacodec.rs +++ b/libs/scrap/src/common/mediacodec.rs @@ -37,6 +37,7 @@ impl Deref for MediaCodecDecoder { } } +#[derive(Default)] pub struct MediaCodecDecoders { pub h264: Option, pub h265: Option, diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index 82f65537b..1de2f89d6 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -44,8 +44,8 @@ pub mod record; mod vpx; #[inline] -pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { - let b = unsafe { std::slice::from_raw_parts::(b.as_ptr() as _, b.len() / 16) }; +pub fn would_block_if_equal(old: &mut Vec, b: &[u8]) -> std::io::Result<()> { + // does this really help? if b == &old[..] { return Err(std::io::ErrorKind::WouldBlock.into()); } @@ -72,16 +72,16 @@ pub fn is_x11() -> bool { #[cfg(x11)] #[inline] -pub fn is_cursor_embeded() -> bool { +pub fn is_cursor_embedded() -> bool { if is_x11() { - x11::IS_CURSOR_EMBEDED + x11::IS_CURSOR_EMBEDDED } else { - wayland::IS_CURSOR_EMBEDED + wayland::IS_CURSOR_EMBEDDED } } #[cfg(not(x11))] #[inline] -pub fn is_cursor_embeded() -> bool { +pub fn is_cursor_embedded() -> bool { false } diff --git a/libs/scrap/src/common/quartz.rs b/libs/scrap/src/common/quartz.rs index 6e29c2441..a02d55ebb 100644 --- a/libs/scrap/src/common/quartz.rs +++ b/libs/scrap/src/common/quartz.rs @@ -8,7 +8,7 @@ pub struct Capturer { frame: Arc>>, use_yuv: bool, i420: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 83bd9eee7..9f38f2d6a 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -12,11 +12,10 @@ use hwcodec::mux::{MuxContext, Muxer}; use std::{ fs::{File, OpenOptions}, io, - time::Instant, -}; -use std::{ ops::{Deref, DerefMut}, path::PathBuf, + sync::mpsc::Sender, + time::Instant, }; use webm::mux::{self, Segment, Track, VideoTrack, Writer}; @@ -31,12 +30,14 @@ pub enum RecordCodecID { #[derive(Debug, Clone)] pub struct RecorderContext { + pub server: bool, pub id: String, pub default_dir: String, pub filename: String, pub width: usize, pub height: usize, pub codec_id: RecordCodecID, + pub tx: Option>, } impl RecorderContext { @@ -52,7 +53,8 @@ impl RecorderContext { std::fs::create_dir_all(&dir)?; } } - let file = self.id.clone() + let file = if self.server { "s" } else { "c" }.to_string() + + &self.id.clone() + &chrono::Local::now().format("_%Y%m%d%H%M%S").to_string() + if self.codec_id == RecordCodecID::VP9 { ".webm" @@ -60,7 +62,7 @@ impl RecorderContext { ".mp4" }; self.filename = PathBuf::from(&dir).join(file).to_string_lossy().to_string(); - log::info!("video save to:{}", self.filename); + log::info!("video will save to:{}", self.filename); Ok(()) } } @@ -75,6 +77,14 @@ pub trait RecorderApi { fn write_video(&mut self, frame: &EncodedVideoFrame) -> bool; } +#[derive(Debug)] +pub enum RecordState { + NewFile(String), + NewFrame, + WriteTail, + RemoveFile, +} + pub struct Recorder { pub inner: Box, ctx: RecorderContext, @@ -110,6 +120,7 @@ impl Recorder { #[cfg(not(feature = "hwcodec"))] _ => bail!("unsupported codec type"), }; + recorder.send_state(RecordState::NewFile(recorder.ctx.filename.clone())); Ok(recorder) } @@ -123,6 +134,7 @@ impl Recorder { _ => bail!("unsupported codec type"), }; self.ctx = ctx; + self.send_state(RecordState::NewFile(self.ctx.filename.clone())); Ok(()) } @@ -171,8 +183,13 @@ impl Recorder { } _ => bail!("unsupported frame type"), } + self.send_state(RecordState::NewFrame); Ok(()) } + + fn send_state(&self, state: RecordState) { + self.ctx.tx.as_ref().map(|tx| tx.send(state)); + } } struct WebmRecorder { @@ -237,9 +254,12 @@ impl RecorderApi for WebmRecorder { impl Drop for WebmRecorder { fn drop(&mut self) { std::mem::replace(&mut self.webm, None).map_or(false, |webm| webm.finalize(None)); + let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { std::fs::remove_file(&self.ctx.filename).ok(); + state = RecordState::RemoveFile; } + self.ctx.tx.as_ref().map(|tx| tx.send(state)); } } @@ -292,8 +312,11 @@ impl RecorderApi for HwRecorder { impl Drop for HwRecorder { fn drop(&mut self) { self.muxer.write_tail().ok(); + let mut state = RecordState::WriteTail; if !self.written || self.start.elapsed().as_secs() < MIN_SECS { std::fs::remove_file(&self.ctx.filename).ok(); + state = RecordState::RemoveFile; } + self.ctx.tx.as_ref().map(|tx| tx.send(state)); } } diff --git a/libs/scrap/src/common/wayland.rs b/libs/scrap/src/common/wayland.rs index 2593e56fe..e625fca7e 100644 --- a/libs/scrap/src/common/wayland.rs +++ b/libs/scrap/src/common/wayland.rs @@ -4,7 +4,7 @@ use std::{io, sync::RwLock, time::Duration}; pub struct Capturer(Display, Box, bool, Vec); -pub const IS_CURSOR_EMBEDED: bool = true; +pub const IS_CURSOR_EMBEDDED: bool = true; lazy_static::lazy_static! { static ref MAP_ERR: RwLock io::Error>> = Default::default(); @@ -50,6 +50,12 @@ impl TraitCapturer for Capturer { } else { x })), + PixelProvider::RGB0(w, h, x) => Ok(Frame(if self.2 { + crate::common::rgba_to_i420(w as _, h as _, &x, &mut self.3); + &self.3[..] + } else { + x + })), PixelProvider::NONE => Err(std::io::ErrorKind::WouldBlock.into()), _ => Err(map_err("Invalid data")), } diff --git a/libs/scrap/src/common/x11.rs b/libs/scrap/src/common/x11.rs index dacc265ff..61112bff7 100644 --- a/libs/scrap/src/common/x11.rs +++ b/libs/scrap/src/common/x11.rs @@ -3,7 +3,7 @@ use std::{io, ops, time::Duration}; pub struct Capturer(x11::Capturer); -pub const IS_CURSOR_EMBEDED: bool = false; +pub const IS_CURSOR_EMBEDDED: bool = false; impl Capturer { pub fn new(display: Display, yuv: bool) -> io::Result { diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs index 78f14194c..0de86055e 100644 --- a/libs/scrap/src/dxgi/mag.rs +++ b/libs/scrap/src/dxgi/mag.rs @@ -339,7 +339,7 @@ impl CapturerMag { } // Register the host window class. See the MSDN documentation of the - // Magnification API for more infomation. + // Magnification API for more information. let wcex = WNDCLASSEXA { cbSize: size_of::() as _, style: 0, diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 6b60b256d..152f502a3 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -50,7 +50,7 @@ pub struct Capturer { rotated: Vec, gdi_capturer: Option, gdi_buffer: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { @@ -262,7 +262,7 @@ impl Capturer { _ => { return Err(io::Error::new( io::ErrorKind::Other, - "Unknown roration".to_string(), + "Unknown rotation".to_string(), )); } }; diff --git a/libs/scrap/src/quartz/display.rs b/libs/scrap/src/quartz/display.rs index ff96b2c1c..47ace49db 100644 --- a/libs/scrap/src/quartz/display.rs +++ b/libs/scrap/src/quartz/display.rs @@ -13,6 +13,7 @@ impl Display { pub fn online() -> Result, CGError> { unsafe { + #[allow(invalid_value)] let mut arr: [u32; 16] = mem::MaybeUninit::uninit().assume_init(); let mut len: u32 = 0; diff --git a/libs/scrap/src/wayland/capturable.rs b/libs/scrap/src/wayland/capturable.rs index 05a5ec71d..61f80ecbf 100644 --- a/libs/scrap/src/wayland/capturable.rs +++ b/libs/scrap/src/wayland/capturable.rs @@ -4,6 +4,7 @@ use std::error::Error; pub enum PixelProvider<'a> { // 8 bits per color RGB(usize, usize, &'a [u8]), + RGB0(usize, usize, &'a [u8]), BGR0(usize, usize, &'a [u8]), // width, height, stride BGR0S(usize, usize, usize, &'a [u8]), @@ -14,6 +15,7 @@ impl<'a> PixelProvider<'a> { pub fn size(&self) -> (usize, usize) { match self { PixelProvider::RGB(w, h, _) => (*w, *h), + PixelProvider::RGB0(w, h, _) => (*w, *h), PixelProvider::BGR0(w, h, _) => (*w, *h), PixelProvider::BGR0S(w, h, _, _) => (*w, *h), PixelProvider::NONE => (0, 0), diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index a7b4c1357..c1c84f98e 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -117,12 +117,13 @@ impl Capturable for PipeWireCapturable { pub struct PipeWireRecorder { buffer: Option>, buffer_cropped: Vec, + pix_fmt: String, is_cropped: bool, pipeline: gst::Pipeline, appsink: AppSink, width: usize, height: usize, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl PipeWireRecorder { @@ -144,19 +145,27 @@ impl PipeWireRecorder { pipeline.add_many(&[&src, &sink])?; src.link(&sink)?; + let appsink = sink .dynamic_cast::() .map_err(|_| GStreamerError("Sink element is expected to be an appsink!".into()))?; - appsink.set_caps(Some(&gst::Caps::new_simple( + let mut caps = gst::Caps::new_empty(); + caps.merge_structure(gst::structure::Structure::new( "video/x-raw", &[("format", &"BGRx")], - ))); + )); + caps.merge_structure(gst::structure::Structure::new( + "video/x-raw", + &[("format", &"RGBx")], + )); + appsink.set_caps(Some(&caps)); pipeline.set_state(gst::State::Playing)?; Ok(Self { pipeline, appsink, buffer: None, + pix_fmt: "".into(), width: 0, height: 0, buffer_cropped: vec![], @@ -181,6 +190,11 @@ impl Recorder for PipeWireRecorder { let h: i32 = cap.get_value("height")?.get_some()?; let w = w as usize; let h = h as usize; + self.pix_fmt = cap + .get::<&str>("format")? + .ok_or("Failed to get pixel format")? + .to_string(); + let buf = sample .get_buffer_owned() .ok_or_else(|| GStreamerError("Failed to get owned buffer.".into()))?; @@ -241,15 +255,22 @@ impl Recorder for PipeWireRecorder { if self.buffer.is_none() { return Err(Box::new(GStreamerError("No buffer available!".into()))); } - Ok(PixelProvider::BGR0( - self.width, - self.height, - if self.is_cropped { - self.buffer_cropped.as_slice() - } else { - self.buffer.as_ref().unwrap().as_slice() - }, - )) + let buf = if self.is_cropped { + self.buffer_cropped.as_slice() + } else { + self.buffer + .as_ref() + .ok_or("Failed to get buffer as ref")? + .as_slice() + }; + match self.pix_fmt.as_str() { + "BGRx" => Ok(PixelProvider::BGR0(self.width, self.height, buf)), + "RGBx" => Ok(PixelProvider::RGB0(self.width, self.height, buf)), + _ => Err(Box::new(GStreamerError(format!( + "Unreachable! Unknown pix_fmt, {}", + &self.pix_fmt + )))), + } } } diff --git a/libs/scrap/src/x11/capturer.rs b/libs/scrap/src/x11/capturer.rs index ed424c35a..0dcfcfdab 100644 --- a/libs/scrap/src/x11/capturer.rs +++ b/libs/scrap/src/x11/capturer.rs @@ -14,7 +14,7 @@ pub struct Capturer { size: usize, use_yuv: bool, yuv: Vec, - saved_raw_data: Vec, // for faster compare and copy + saved_raw_data: Vec, // for faster compare and copy } impl Capturer { diff --git a/libs/virtual_display/dylib/src/win10/IddController.c b/libs/virtual_display/dylib/src/win10/IddController.c index a30fa9d0a..c1faccfc2 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.c +++ b/libs/virtual_display/dylib/src/win10/IddController.c @@ -66,7 +66,7 @@ const char* GetLastMsg() BOOL InstallUpdate(LPCWSTR fullInfPath, PBOOL rebootRequired) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); // UpdateDriverForPlugAndPlayDevicesW may return FALSE while driver was successfully installed... if (FALSE == UpdateDriverForPlugAndPlayDevicesW( @@ -96,7 +96,7 @@ BOOL InstallUpdate(LPCWSTR fullInfPath, PBOOL rebootRequired) BOOL Uninstall(LPCWSTR fullInfPath, PBOOL rebootRequired) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (FALSE == DiUninstallDriverW( NULL, @@ -122,7 +122,7 @@ BOOL Uninstall(LPCWSTR fullInfPath, PBOOL rebootRequired) BOOL IsDeviceCreated(PBOOL created) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); HDEVINFO hardwareDeviceInfo = SetupDiGetClassDevs( &GUID_DEVINTERFACE_IDD_DRIVER_DEVICE, @@ -181,7 +181,7 @@ BOOL IsDeviceCreated(PBOOL created) BOOL DeviceCreate(PHSWDEVICE hSwDevice) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (*hSwDevice != NULL) { @@ -221,7 +221,7 @@ BOOL DeviceCreate(PHSWDEVICE hSwDevice) SW_DEVICE_CREATE_INFO createInfo = { 0 }; PCWSTR description = L"RustDesk Idd Driver"; - // These match the Pnp id's in the inf file so OS will load the driver when the device is created + // These match the Pnp id's in the inf file so OS will load the driver when the device is created PCWSTR instanceId = L"RustDeskIddDriver"; PCWSTR hardwareIds = L"RustDeskIddDriver\0\0"; PCWSTR compatibleIds = L"RustDeskIddDriver\0\0"; @@ -274,7 +274,7 @@ BOOL DeviceCreate(PHSWDEVICE hSwDevice) VOID DeviceClose(HSWDEVICE hSwDevice) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (hSwDevice != INVALID_HANDLE_VALUE && hSwDevice != NULL) { @@ -284,7 +284,7 @@ VOID DeviceClose(HSWDEVICE hSwDevice) BOOL MonitorPlugIn(UINT index, UINT edid, INT retries) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); if (retries < 0) { @@ -359,7 +359,7 @@ BOOL MonitorPlugIn(UINT index, UINT edid, INT retries) BOOL MonitorPlugOut(UINT index) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); HANDLE hDevice = DeviceOpenHandle(); if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) @@ -400,7 +400,7 @@ BOOL MonitorPlugOut(UINT index) BOOL MonitorModesUpdate(UINT index, UINT modeCount, PMonitorMode modes) { - SetLastMsg("Sucess"); + SetLastMsg("Success"); HANDLE hDevice = DeviceOpenHandle(); if (hDevice == INVALID_HANDLE_VALUE || hDevice == NULL) @@ -721,7 +721,7 @@ Clean0: // https://stackoverflow.com/questions/67164846/createfile-fails-unless-i-disable-enable-my-device HANDLE DeviceOpenHandle() { - SetLastMsg("Sucess"); + SetLastMsg("Success"); // const int maxDevPathLen = 256; TCHAR devicePath[256] = { 0 }; diff --git a/libs/virtual_display/dylib/src/win10/IddController.h b/libs/virtual_display/dylib/src/win10/IddController.h index f92f72647..909f17423 100644 --- a/libs/virtual_display/dylib/src/win10/IddController.h +++ b/libs/virtual_display/dylib/src/win10/IddController.h @@ -14,7 +14,7 @@ extern "C" { * @param rebootRequired [out] Indicates whether a restart is required. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg */ BOOL InstallUpdate(LPCTSTR fullInfPath, PBOOL rebootRequired); @@ -34,11 +34,11 @@ BOOL Uninstall(LPCTSTR fullInfPath, PBOOL rebootRequired); /** * @brief Check if RustDeskIddDriver device is created before. * The driver device(adapter) should be single instance. - * + * * @param created [out] Indicates whether the device is created before. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg * */ @@ -47,12 +47,12 @@ BOOL IsDeviceCreated(PBOOL created); /** * @brief Create device. * Only one device should be created. - * If device is installed ealier, this function returns FALSE. - * + * If device is installed earlier, this function returns FALSE. + * * @param hSwDevice [out] Handler of software device, used by DeviceCreate(). Should be **NULL**. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg * */ @@ -79,9 +79,9 @@ VOID DeviceClose(HSWDEVICE hSwDevice); * 1 means doing once and retry one time... * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg - * + * * @remark Plug in monitor may fail if device is created in a very short time. * System need some time to prepare the device. * @@ -94,7 +94,7 @@ BOOL MonitorPlugIn(UINT index, UINT edid, INT retries); * @param index [in] Monitor index, should be 0, 1, 2. * * @return TRUE/FALSE. If FALSE returned, error message can be retrieved by GetLastMsg() - * + * * @see GetLastMsg#GetLastMsg * */ @@ -133,9 +133,9 @@ const char* GetLastMsg(); * @brief Set if print error message when debug. * * @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 index cd35a0bc8..26cbf702c 100644 Binary files a/res/128x128.png and b/res/128x128.png differ diff --git a/res/128x128@2x.png b/res/128x128@2x.png index 3da699f1d..d6f8d20fa 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 index 21440d422..33dc80537 100644 Binary files a/res/32x32.png and b/res/32x32.png differ diff --git a/res/64x64.png b/res/64x64.png new file mode 100644 index 000000000..d93638e6e Binary files /dev/null and b/res/64x64.png differ diff --git a/res/gen_icon.sh b/res/gen_icon.sh index 40b67aa53..83252a6ae 100644 --- a/res/gen_icon.sh +++ b/res/gen_icon.sh @@ -3,5 +3,5 @@ for size in 16 32 64 128 256 512 1024; do #inkscape -z -o $size.png -w $size -h $size icon.svg >/dev/null 2>/dev/null convert icon.png -resize ${size}x${size} app_icon_$size.png done -# from ImageMagick +# from ImageMagick #/bin/rm 16.png 32.png 48.png 128.png 256.png diff --git a/res/icon-margin.png b/res/icon-margin.png index dfc650e9d..6449ec490 100644 Binary files a/res/icon-margin.png and b/res/icon-margin.png differ diff --git a/res/icon.png b/res/icon.png index 573165a06..823967c49 100644 Binary files a/res/icon.png and b/res/icon.png differ diff --git a/res/lang.py b/res/lang.py index d1d4254ed..481d65553 100644 --- a/res/lang.py +++ b/res/lang.py @@ -4,20 +4,22 @@ import os import glob import sys import csv - -def get_lang(lang): - out = {} - for ln in open('./src/lang/%s.rs'%lang): + +def get_lang(lang): + out = {} + for ln in open('./src/lang/%s.rs'%lang, encoding='utf8'): ln = ln.strip() if ln.startswith('("'): k, v = line_split(ln) out[k] = v - return out + return out def line_split(line): - toks = line.split('", "') - assert(len(toks) == 2) - k = toks[0][2:] + toks = line.split('", "') + if len(toks) != 2: + print(line) + assert(0) + k = toks[0][2:] v = toks[1][:-3] return k, v @@ -32,31 +34,33 @@ def main(): def expand(): - for fn in glob.glob('./src/lang/*'): - lang = os.path.basename(fn)[:-3] - if lang in ['en','cn']: continue + for fn in glob.glob('./src/lang/*'): + lang = os.path.basename(fn)[:-3] + if lang in ['en','cn']: continue + print(lang) dict = get_lang(lang) - fw = open("./src/lang/%s.rs"%lang, "wt") - for line in open('./src/lang/cn.rs'): + fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') + for line in open('./src/lang/cn.rs', encoding='utf8'): line_strip = line.strip() if line_strip.startswith('("'): k, v = line_split(line_strip) if k in dict: - line = line.replace(v, dict[k]) + # embraced with " to avoid empty v + line = line.replace('"%s"'%v, '"%s"'%dict[k]) else: line = line.replace(v, "") fw.write(line) else: fw.write(line) fw.close() - + def to_csv(): - for fn in glob.glob('./src/lang/*.rs'): - lang = os.path.basename(fn)[:-3] - csvfile = open('./src/lang/%s.csv'%lang, "wt") + for fn in glob.glob('./src/lang/*.rs'): + lang = os.path.basename(fn)[:-3] + csvfile = open('./src/lang/%s.csv'%lang, "wt", encoding='utf8') csvwriter = csv.writer(csvfile) - for line in open(fn): + for line in open(fn, encoding='utf8'): line_strip = line.strip() if line_strip.startswith('("'): k, v = line_split(line_strip) @@ -65,8 +69,8 @@ def to_csv(): def to_rs(lang): - csvfile = open('%s.csv'%lang, "rt") - fw = open("./src/lang/%s.rs"%lang, "wt") + csvfile = open('%s.csv'%lang, "rt", encoding='utf8') + fw = open("./src/lang/%s.rs"%lang, "wt", encoding='utf8') fw.write('''lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ diff --git a/res/mac-icon.png b/res/mac-icon.png new file mode 100644 index 000000000..a9813152e 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..860f9fcf5 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 index 0ca3a9dd8..ba8ed8c12 100644 Binary files a/res/mac-tray-dark.png and b/res/mac-tray-dark.png differ diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png new file mode 100644 index 000000000..f723d980e 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 index c3e107410..ad8bfa396 100644 Binary files a/res/mac-tray-light.png and b/res/mac-tray-light.png differ diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec new file mode 100644 index 000000000..6c7055b4a --- /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) + +%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/src/cli.rs b/src/cli.rs index 59c356a5a..117486ee4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,9 +1,12 @@ use crate::client::*; use hbb_common::{ config::PeerConfig, + config::READ_TIMEOUT, + futures::{SinkExt, StreamExt}, log, message_proto::*, protobuf::Message as _, + rendezvous_proto::ConnType, tokio::{self, sync::mpsc}, Stream, }; @@ -33,14 +36,18 @@ impl Session { .lc .write() .unwrap() - .initialize(id.to_owned(), false, true); + .initialize(id.to_owned(), ConnType::PORT_FORWARD); session } } #[async_trait] impl Interface for Session { - fn msgbox(&self, msgtype: &str, title: &str, text: &str) { + fn get_login_config_handler(&self) -> Arc> { + return self.lc.clone(); + } + + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { if msgtype == "input-password" { self.sender .send(Data::Login((self.password.clone(), true))) @@ -57,16 +64,19 @@ impl Interface for Session { } fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) + handle_login_error(self.lc.clone(), err, self) } fn handle_peer_info(&mut self, pi: PeerInfo) { - let username = self.lc.read().unwrap().get_username(&pi); - self.lc.write().unwrap().handle_peer_info(username, pi); + self.lc.write().unwrap().handle_peer_info(&pi); } - async fn handle_hash(&mut self, hash: Hash, peer: &mut Stream) { - handle_hash(self.lc.clone(), hash, self, peer).await; + 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; } async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream) { @@ -82,6 +92,42 @@ impl Interface for Session { } } +#[tokio::main(flavor = "current_thread")] +pub async fn connect_test(id: &str, key: String, token: String) { + let (sender, mut receiver) = mpsc::unbounded_channel::(); + let handler = Session::new(&id, sender); + match crate::client::Client::start(id, &key, &token, ConnType::PORT_FORWARD, handler).await { + Err(err) => { + log::error!("Failed to connect {}: {}", &id, err); + } + Ok((mut stream, direct)) => { + log::info!("direct: {}", direct); + // rpassword::prompt_password("Input anything to exit").ok(); + loop { + tokio::select! { + res = hbb_common::timeout(READ_TIMEOUT, stream.next()) => match res { + Err(_) => { + log::error!("Timeout"); + break; + } + Ok(Some(Ok(bytes))) => { + let msg_in = Message::parse_from_bytes(&bytes).unwrap(); + match msg_in.union { + Some(message::Union::Hash(hash)) => { + log::info!("Got hash"); + break; + } + _ => {} + } + } + _ => {} + } + } + } + } + } +} + #[tokio::main(flavor = "current_thread")] pub async fn start_one_port_forward( id: String, @@ -95,9 +141,19 @@ pub async fn start_one_port_forward( crate::common::test_nat_type(); let (sender, mut receiver) = mpsc::unbounded_channel::(); let handler = Session::new(&id, sender); - handler.lc.write().unwrap().port_forward = (remote_host, remote_port); - if let Err(err) = - crate::port_forward::listen(handler.id.clone(), port, handler.clone(), receiver, &key, &token).await + if let Err(err) = crate::port_forward::listen( + handler.id.clone(), + handler.password.clone(), + port, + handler.clone(), + receiver, + &key, + &token, + handler.lc.clone(), + remote_host, + remote_port, + ) + .await { log::error!("Failed to listen on {}: {}", port, err); } diff --git a/src/client.rs b/src/client.rs index c646b2b7f..e9b8edf39 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,4 +1,5 @@ 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,12 +7,11 @@ use cpal::{ }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; use sha2::{Digest, Sha256}; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -use std::sync::atomic::Ordering; use std::{ collections::HashMap, net::SocketAddr, ops::{Deref, Not}, + str::FromStr, sync::{atomic::AtomicBool, mpsc, Arc, Mutex, RwLock}, }; use uuid::Uuid; @@ -25,7 +25,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, @@ -50,8 +50,8 @@ pub mod file_trait; pub mod helper; pub mod io_loop; use crate::{ + common::{self, is_keyboard_mode_supported}, server::video_service::{SCRAP_X11_REF_URL, SCRAP_X11_REQUIRED}, - ui_session_interface::global_save_keyboard_mode, }; pub static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); pub static SERVER_FILE_TRANSFER_ENABLED: AtomicBool = AtomicBool::new(true); @@ -172,26 +172,30 @@ impl Client { interface: impl Interface, ) -> ResultType<(Stream, bool)> { // to-do: remember the port for each peer, so that we can retry easier - let any_addr = Config::get_any_listen_addr(); - if crate::is_ip(peer) { + if hbb_common::is_ip_str(peer) { return Ok(( socket_client::connect_tcp( crate::check_port(peer, RELAY_PORT + 1), - any_addr, RENDEZVOUS_TIMEOUT, ) .await?, 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, any_addr, RENDEZVOUS_TIMEOUT).await; + let mut socket = socket_client::connect_tcp(&*rendezvous_server, RENDEZVOUS_TIMEOUT).await; debug_assert!(!servers.contains(&rendezvous_server)); if socket.is_err() && !servers.is_empty() { log::info!("try the other servers: {:?}", servers); for server in servers { - socket = socket_client::connect_tcp(&*server, any_addr, RENDEZVOUS_TIMEOUT).await; + socket = socket_client::connect_tcp(&*server, RENDEZVOUS_TIMEOUT).await; if socket.is_ok() { rendezvous_server = server; break; @@ -208,7 +212,7 @@ impl Client { let mut relay_server = "".to_owned(); let start = std::time::Instant::now(); - let mut peer_addr = any_addr; + let mut peer_addr = Config::get_any_listen_addr(true); let mut peer_nat_type = NatType::UNKNOWN_NAT; let my_nat_type = crate::get_nat_type(100).await; let mut is_local = false; @@ -269,9 +273,15 @@ impl Client { rr.relay_server ); signed_id_pk = rr.pk().into(); - let mut conn = - Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type) - .await?; + let mut conn = Self::create_relay( + peer, + rr.uuid, + rr.relay_server, + key, + conn_type, + my_addr.is_ipv4(), + ) + .await?; Self::secure_connection( peer, signed_id_pk, @@ -378,7 +388,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(peer, 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() { @@ -422,7 +433,7 @@ impl Client { key: &str, conn: &mut Stream, direct: bool, - mut interface: impl Interface, + interface: impl Interface, ) -> ResultType<()> { let rs_pk = get_rs_pk(if key.is_empty() { hbb_common::config::RS_PUB_KEY @@ -511,16 +522,16 @@ impl Client { token: &str, conn_type: ConnType, ) -> ResultType { - let any_addr = Config::get_any_listen_addr(); let mut succeed = false; let mut uuid = "".to_owned(); + let mut ipv4 = true; for i in 1..=3 { // use different socket due to current hbbs implement requiring different nat address for each attempt - let mut socket = - socket_client::connect_tcp(rendezvous_server, any_addr, RENDEZVOUS_TIMEOUT) - .await - .with_context(|| "Failed to connect to rendezvous server")?; + let mut socket = socket_client::connect_tcp(rendezvous_server, RENDEZVOUS_TIMEOUT) + .await + .with_context(|| "Failed to connect to rendezvous server")?; + ipv4 = socket.local_addr().is_ipv4(); let mut msg_out = RendezvousMessage::new(); uuid = Uuid::new_v4().to_string(); log::info!( @@ -555,7 +566,7 @@ impl Client { if !succeed { bail!("Timeout"); } - Self::create_relay(peer, uuid, relay_server, key, conn_type).await + Self::create_relay(peer, uuid, relay_server, key, conn_type, ipv4).await } /// Create a relay connection to the server. @@ -565,10 +576,10 @@ impl Client { relay_server: String, key: &str, conn_type: ConnType, + ipv4: bool, ) -> ResultType { let mut conn = socket_client::connect_tcp( - crate::check_port(relay_server, RELAY_PORT), - Config::get_any_listen_addr(), + socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4), CONNECT_TIMEOUT, ) .await @@ -826,7 +837,7 @@ impl VideoHandler { /// Handle a new video frame. pub fn handle_frame(&mut self, vf: VideoFrame) -> ResultType { if vf.timestamp != 0 { - // Update the lantency controller with the latest timestamp. + // Update the latency controller with the latest timestamp. self.latency_controller .lock() .unwrap() @@ -863,12 +874,14 @@ impl VideoHandler { self.record = false; if start { self.recorder = Recorder::new(RecorderContext { + server: false, id, default_dir: crate::ui_interface::default_video_save_directory(), filename: "".to_owned(), width: w as _, height: h as _, codec_id: scrap::record::RecordCodecID::VP9, + tx: None, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); } else { @@ -895,6 +908,9 @@ pub struct LoginConfigHandler { pub supported_encoding: Option<(bool, bool)>, pub restarting_remote_device: bool, pub force_relay: bool, + pub direct: Option, + pub received: bool, + switch_uuid: Option, } impl Deref for LoginConfigHandler { @@ -922,7 +938,7 @@ 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) { self.id = id; self.conn_type = conn_type; let config = self.load_config(); @@ -932,6 +948,9 @@ impl LoginConfigHandler { self.supported_encoding = None; self.restarting_remote_device = false; self.force_relay = !self.get_option("force-always-relay").is_empty(); + self.direct = None; + self.received = false; + self.switch_uuid = switch_uuid; } /// Check if the client should auto login. @@ -987,6 +1006,17 @@ impl LoginConfigHandler { self.save_config(config); } + /// Save keyboard mode to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. + pub fn save_keyboard_mode(&mut self, value: String) { + let mut config = self.load_config(); + config.keyboard_mode = value; + self.save_config(config); + } + /// Save scroll style to the current config. /// /// # Arguments @@ -1330,32 +1360,6 @@ impl LoginConfigHandler { } } - /// Handle login error. - /// Return true if the password is wrong, return false if there's an actual error. - pub fn handle_login_error(&mut self, err: &str, interface: &impl Interface) -> bool { - if err == "Wrong Password" { - self.password = Default::default(); - interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); - true - } else if err == "No Password Access" { - self.password = Default::default(); - interface.msgbox( - "wait-remote-accept-nook", - "Prompt", - "Please wait for the remote side to accept your session request...", - "", - ); - true - } else { - if err.contains(SCRAP_X11_REQUIRED) { - interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); - } else { - interface.msgbox("error", "Login Error", err, ""); - } - false - } - } - /// Get user name. /// Return the name of the given peer. If the peer has no name, return the name in the config. /// @@ -1380,9 +1384,6 @@ impl LoginConfigHandler { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); } - if hbb_common::get_version_number(&pi.version) < hbb_common::get_version_number("1.2.0") { - global_save_keyboard_mode("legacy".to_owned()); - } self.features = pi.features.clone().into_option(); let serde = PeerInfoSerde { username: pi.username.clone(), @@ -1405,6 +1406,20 @@ impl LoginConfigHandler { log::debug!("remove password of {}", self.id); } } + 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 = 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; // no matter if change, for update file time self.save_config(config); @@ -1487,6 +1502,19 @@ impl LoginConfigHandler { msg_out.set_misc(misc); msg_out } + + pub fn set_force_relay(&mut self, direct: bool, received: bool) { + self.force_relay = false; + if direct && !received { + let errno = errno::errno().0; + log::info!("errno is {}", errno); + // TODO: check mac and ios + if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { + self.force_relay = true; + self.set_option("force-always-relay".to_owned(), "Y".to_owned()); + } + } + } } /// Media data. @@ -1654,7 +1682,7 @@ pub fn send_mouse( interface.send(Data::Message(msg_out)); } -/// Avtivate OS by sending mouse movement. +/// Activate OS by sending mouse movement. /// /// # Arguments /// @@ -1682,7 +1710,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 || { @@ -1695,7 +1723,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 { @@ -1713,6 +1741,36 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { interface.send(Data::Message(msg_out)); } +/// Handle login error. +/// Return true if the password is wrong, return false if there's an actual error. +pub fn handle_login_error( + lc: Arc>, + err: &str, + interface: &impl Interface, +) -> bool { + if err == "Wrong Password" { + lc.write().unwrap().password = Default::default(); + interface.msgbox("re-input-password", err, "Do you want to enter again?", ""); + true + } else if err == "No Password Access" { + lc.write().unwrap().password = Default::default(); + interface.msgbox( + "wait-remote-accept-nook", + "Prompt", + "Please wait for the remote side to accept your session request...", + "", + ); + true + } else { + if err.contains(SCRAP_X11_REQUIRED) { + interface.msgbox("error", "Login Error", err, SCRAP_X11_REF_URL); + } else { + interface.msgbox("error", "Login Error", err, ""); + } + false + } +} + /// Handle hash message sent by peer. /// Hash will be used for login. /// @@ -1729,6 +1787,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() { @@ -1793,6 +1859,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 { @@ -1801,17 +1887,23 @@ pub trait Interface: Send + Clone + 'static + Sized { fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str); fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); - fn set_force_relay(&mut self, direct: bool, received: bool); - fn is_file_transfer(&self) -> bool; - fn is_port_forward(&self) -> bool; - fn is_rdp(&self) -> bool; fn on_error(&self, err: &str) { self.msgbox("error", "Error", err, ""); } - fn is_force_relay(&self) -> bool; async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); + + 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); + } + fn is_force_relay(&self) -> bool { + self.get_login_config_handler().read().unwrap().force_relay + } } /// Data used by the client interface. @@ -1836,6 +1928,8 @@ pub enum Data { AddJob((i32, String, String, i32, bool, bool)), ResumeJob((i32, bool)), RecordScreen(bool, i32, i32, String), + ElevateDirect, + ElevateWithLogon(String, String), } /// Keycode for key events. @@ -1977,11 +2071,10 @@ lazy_static::lazy_static! { /// * `title` - The title of the message. /// * `text` - The text of the message. #[inline] -pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { +pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: bool) -> bool { msgtype == "error" && title == "Connection Error" - && (text.contains("10054") - || text.contains("104") + && ((text.contains("10054") || text.contains("104")) && retry_for_relay || (!text.to_lowercase().contains("offline") && !text.to_lowercase().contains("exist") && !text.to_lowercase().contains("handshake") @@ -1989,7 +2082,8 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { && !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("not allowed") + && !text.to_lowercase().contains("reset by the peer"))) } #[inline] @@ -2022,8 +2116,3 @@ fn decode_id_pk(signed: &[u8], key: &sign::PublicKey) -> ResultType<(String, [u8 bail!("Wrong public length"); } } - -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] -pub fn disable_keyboard_listening() { - crate::ui_session_interface::KEYBOARD_HOOKED.store(true, Ordering::SeqCst); -} diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index b94177c51..2ecfca837 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -22,7 +22,7 @@ pub trait FileManager: Interface { #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] fn read_dir(&self, path: &str, include_hidden: bool) -> String { - use crate::flutter::make_fd_to_json; + use crate::common::make_fd_to_json; match fs::read_dir(&fs::get_path(path), include_hidden) { Ok(fd) => make_fd_to_json(fd.id, fd.path, &fd.entries), Err(_) => "".into(), diff --git a/src/client/helper.rs b/src/client/helper.rs index d38fbf223..e4736c0e8 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -15,7 +15,7 @@ const MIN_LATENCY: i64 = 100; /// 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 + last_video_remote_ts: i64, // generated on remote device update_time: Instant, allow_audio: bool, } diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index 16f91d89d..ff6d6c004 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -107,6 +107,7 @@ impl Remote { 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); // just build for now #[cfg(not(windows))] @@ -144,7 +145,10 @@ impl Remote { } Ok(ref bytes) => { last_recv_time = Instant::now(); - received = true; + if !received { + received = true; + self.handler.set_connection_info(direct, true); + } self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); if !self.handle_msg_from_peer(bytes, &mut peer).await { break @@ -499,7 +503,7 @@ impl Remote { } let mut msg = Message::new(); let mut file_action = FileAction::new(); - file_action.set_send_confirm(FileTransferSendConfirmRequest { + let req = FileTransferSendConfirmRequest { id, file_num, union: if need_override { @@ -508,7 +512,9 @@ impl Remote { Some(file_transfer_send_confirm_request::Union::Skip(true)) }, ..Default::default() - }); + }; + job.confirm(&req); + file_action.set_send_confirm(req); msg.set_file_action(file_action); allow_err!(peer.send(&msg).await); } @@ -626,6 +632,28 @@ 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); + } + 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); + } _ => {} } true @@ -722,11 +750,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() }) }; @@ -862,28 +890,30 @@ impl Remote { match fs::is_write_need_confirmation(&write_path, &digest) { Ok(res) => match res { DigestCheckResult::IsSame => { - let msg= new_send_confirm(FileTransferSendConfirmRequest { + let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::Skip(true)), ..Default::default() - }); + }; + job.confirm(&req); + let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } DigestCheckResult::NeedConfirm(digest) => { if let Some(overwrite) = overwrite_strategy { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip(true) - }), - ..Default::default() - }, - ); + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } else { self.handler.override_file_confirm( @@ -895,19 +925,19 @@ impl Remote { } } DigestCheckResult::NoSuchFile => { - let msg = new_send_confirm( - FileTransferSendConfirmRequest { + let req = FileTransferSendConfirmRequest { id: digest.id, file_num: digest.file_num, union: Some(file_transfer_send_confirm_request::Union::OffsetBlk(0)), ..Default::default() - }, - ); + }; + job.confirm(&req); + let msg = new_send_confirm(req); allow_err!(peer.send(&msg).await); } }, Err(err) => { - println!("error recving digest: {}", err); + println!("error receiving digest: {}", err); } } } @@ -923,13 +953,18 @@ impl Remote { } } Some(file_response::Union::Done(d)) => { + let mut err: Option = None; if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { job.modify_time(); + err = job.job_error(); fs::remove_job(d.id, &mut self.write_jobs); } - self.handle_job_status(d.id, d.file_num, None); + self.handle_job_status(d.id, d.file_num, err); } Some(file_response::Union::Error(e)) => { + if let Some(_job) = fs::get_job(e.id, &mut self.write_jobs) { + fs::remove_job(e.id, &mut self.write_jobs); + } self.handle_job_status(e.id, e.file_num, Some(e.error)); } _ => {} @@ -976,7 +1011,13 @@ impl Remote { 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_embeded); + self.handler.set_display( + s.x, + s.y, + s.width, + s.height, + s.cursor_embedded, + ); } } Some(misc::Union::CloseReason(c)) => { @@ -989,33 +1030,91 @@ 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)) => { + if b { + self.handler.msgbox( + "custom-nocancel", + "Successful", + "Elevate successfully", + "", + ); + } + } + Some(misc::Union::SwitchBack(_)) => { + #[cfg(feature = "flutter")] + self.handler.switch_back(&self.handler.id); + } _ => {} }, Some(message::Union::TestDelay(t)) => { 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 ea02cf810..cdf57ae3d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -3,6 +3,14 @@ use std::{ sync::{Arc, Mutex}, }; +#[derive(Debug, Eq, PartialEq)] +pub enum GrabState { + Ready, + Run, + Wait, + Exit, +} + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use arboard::Clipboard as ClipboardContext; @@ -10,8 +18,7 @@ pub use arboard::Clipboard as ClipboardContext; use hbb_common::compress::decompress; use hbb_common::{ allow_err, - anyhow::bail, - compress::{compress as compress_func}, + compress::compress as compress_func, config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, get_version_number, log, message_proto::*, @@ -44,7 +51,7 @@ lazy_static::lazy_static! { pub fn global_init() -> bool { #[cfg(target_os = "linux")] { - if !scrap::is_x11() { + if !*IS_X11 { crate::server::wayland::set_wayland_scrap_map_err(); } } @@ -285,15 +292,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 { @@ -302,21 +301,18 @@ async fn test_nat_type_() -> ResultType { }); let mut port1 = 0; let mut port2 = 0; - let server1 = socket_client::get_target_addr(&server1)?; - let server2 = socket_client::get_target_addr(&server2)?; - let mut addr = Config::get_any_listen_addr(); for i in 0..2 { let mut socket = socket_client::connect_tcp( - if i == 0 { - server1.clone() - } else { - server2.clone() - }, - addr, + if i == 0 { &*server1 } else { &*server2 }, RENDEZVOUS_TIMEOUT, ) .await?; - addr = socket.local_addr(); + if i == 0 { + Config::set_option( + "local-ip-addr".to_owned(), + socket.local_addr().ip().to_string(), + ); + } socket.send(&msg_out).await?; if let Some(Ok(bytes)) = socket.next_timeout(RENDEZVOUS_TIMEOUT).await { if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { @@ -339,7 +335,6 @@ async fn test_nat_type_() -> ResultType { break; } } - Config::set_option("local-ip-addr".to_owned(), addr.ip().to_string()); let ok = port1 > 0 && port2 > 0; if ok { let t = if port1 == port2 { @@ -360,13 +355,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(); @@ -416,7 +405,6 @@ async fn test_rendezvous_server_() { let tm = std::time::Instant::now(); if socket_client::connect_tcp( crate::check_port(&host, RENDEZVOUS_PORT), - Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT, ) .await @@ -473,11 +461,12 @@ pub fn username() -> String { #[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"; @@ -516,10 +505,9 @@ pub fn check_software_update() { async fn check_software_update_() -> hbb_common::ResultType<()> { sleep(3.).await; - let rendezvous_server = - socket_client::get_target_addr(&format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT))?; - let mut socket = - socket_client::new_udp(Config::get_any_listen_addr(), RENDEZVOUS_TIMEOUT).await?; + let rendezvous_server = format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT); + let (mut socket, rendezvous_server) = + socket_client::new_udp_for(&rendezvous_server, RENDEZVOUS_TIMEOUT).await?; let mut msg_out = RendezvousMessage::new(); msg_out.set_software_update(SoftwareUpdate { @@ -559,12 +547,6 @@ pub fn get_full_name() -> String { ) } -pub fn is_ip(id: &str) -> bool { - hbb_common::regex::Regex::new(r"^\d+\.\d+\.\d+\.\d+(:\d+)?$") - .unwrap() - .is_match(id) -} - pub fn is_setup(name: &str) -> bool { name.to_lowercase().ends_with("install.exe") } @@ -595,29 +577,24 @@ 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); - } - } + let s0 = get_custom_rendezvous_server(custom); + if !s0.is_empty() { + let s = crate::increase_port(&s0, -2); + if s == s0 { + format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); } else { - return format!("http://{}:{}", s, config::RENDEZVOUS_PORT - 2); + format!("http://{}", s); } } "https://admin.rustdesk.com".to_owned() } -pub fn get_audit_server(api: String, custom: String) -> String { +pub fn get_audit_server(api: String, custom: String, typ: String) -> String { let url = get_api_server(api, custom); if url.is_empty() || url.contains("rustdesk.com") { return "".to_owned(); } - format!("{}/api/audit", url) + format!("{}/api/audit/{}", url, typ) } pub async fn post_request(url: String, body: String, header: &str) -> ResultType { @@ -661,7 +638,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()); } } @@ -681,13 +658,53 @@ 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 => false, + KeyboardMode::Auto => false, + } +} + +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 { + use serde_json::json; + let mut fd_json = serde_json::Map::new(); + fd_json.insert("id".into(), json!(id)); + fd_json.insert("path".into(), json!(path)); + + let mut entries_out = vec![]; + for entry in entries { + let mut entry_map = serde_json::Map::new(); + entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); + entry_map.insert("name".into(), json!(entry.name)); + entry_map.insert("size".into(), json!(entry.size)); + entry_map.insert("modified_time".into(), json!(entry.modified_time)); + entries_out.push(entry_map); + } + fd_json.insert("entries".into(), json!(entries_out)); + serde_json::to_string(&fd_json).unwrap_or("".into()) +} + +#[cfg(test)] +mod test_common { + use super::*; } diff --git a/src/core_main.rs b/src/core_main.rs index f890a9525..8b99f6131 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -38,6 +38,17 @@ pub fn core_main() -> Option> { } i += 1; } + #[cfg(target_os = "linux")] + #[cfg(feature = "flutter")] + { + crate::platform::linux::register_breakdown_handler(); + 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()); @@ -95,7 +106,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); } } @@ -173,7 +185,7 @@ pub fn core_main() -> Option> { crate::start_os_service(); return None; } else if args[0] == "--server" { - log::info!("start --server"); + log::info!("start --server with user {}", crate::username()); #[cfg(target_os = "windows")] { crate::start_server(true); @@ -182,6 +194,7 @@ pub fn core_main() -> Option> { #[cfg(target_os = "macos")] { std::thread::spawn(move || crate::start_server(true)); + crate::platform::macos::hide_dock(); crate::tray::make_tray(); return None; } @@ -214,7 +227,11 @@ pub fn core_main() -> Option> { return None; } else if args[0] == "--password" { if args.len() == 2 { - crate::ipc::set_permanent_password(args[1].to_owned()).unwrap(); + if crate::platform::is_root() { + crate::ipc::set_permanent_password(args[1].to_owned()).unwrap(); + } else { + println!("Administrative privileges required!"); + } } return None; } else if args[0] == "--check-hwcodec-config" { @@ -227,6 +244,8 @@ pub fn core_main() -> Option> { #[cfg(feature = "flutter")] crate::flutter::connection_manager::start_listen_ipc_thread(); crate::ui_interface::start_option_status_sync(); + #[cfg(target_os = "macos")] + crate::platform::macos::hide_dock(); } } //_async_logger_holder.map(|x| x.flush()); @@ -279,11 +298,27 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option { return None; } @@ -297,13 +332,12 @@ 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 } @@ -38,6 +35,15 @@ pub extern "C" fn rustdesk_core_main() -> bool { false } +#[cfg(target_os = "macos")] +#[no_mangle] +pub extern "C" fn handle_applicationShouldOpenUntitledFile() { + hbb_common::log::debug!("icon clicked on finder"); + if std::env::args().nth(1) == Some("--server".to_owned()) { + crate::platform::macos::check_main_window(); + } +} + #[cfg(windows)] #[no_mangle] pub extern "C" fn rustdesk_core_main_args(args_len: *mut c_int) -> *mut *mut c_char { @@ -155,7 +161,7 @@ impl InvokeUiSession for FlutterHandler { } /// unused in flutter, use switch_display or set_peer_info - fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32, _cursor_embeded: bool) {} + fn set_display(&self, _x: i32, _y: i32, _w: i32, _h: i32, _cursor_embedded: bool) {} fn update_privacy_mode(&self) { self.push_event("update_privacy_mode", [].into()); @@ -241,7 +247,7 @@ impl InvokeUiSession for FlutterHandler { self.push_event( "file_dir", vec![ - ("value", &make_fd_to_json(id, path, entries)), + ("value", &crate::common::make_fd_to_json(id, path, entries)), ("is_local", "false"), ], ); @@ -295,10 +301,19 @@ impl InvokeUiSession for FlutterHandler { h.insert("y", d.y); h.insert("width", d.width); h.insert("height", d.height); - h.insert("cursor_embeded", if d.cursor_embeded { 1 } else { 0 }); + 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 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 }); + } + // compatible with 1.1.9 + if get_version_number(&pi.version) < get_version_number("1.2.0") { + features.insert("privacy_mode", 0); + } + let features = serde_json::ser::to_string(&features).unwrap_or("".to_owned()); self.push_event( "peer_info", vec![ @@ -308,11 +323,14 @@ impl InvokeUiSession for FlutterHandler { ("sas_enabled", &pi.sas_enabled.to_string()), ("displays", &displays), ("version", &pi.version), + ("features", &features), ("current_display", &pi.current_display.to_string()), ], ); } + fn on_connected(&self, _conn_type: ConnType) {} + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { let has_retry = if retry { "true" } else { "" }; self.push_event( @@ -344,7 +362,17 @@ impl InvokeUiSession for FlutterHandler { ("y", &display.y.to_string()), ("width", &display.width.to_string()), ("height", &display.height.to_string()), - ("cursor_embeded", &{if display.cursor_embeded {1} else {0}}.to_string()), + ( + "cursor_embedded", + &{ + if display.cursor_embedded { + 1 + } else { + 0 + } + } + .to_string(), + ), ], ); } @@ -360,6 +388,10 @@ 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()); + } } /// Create a new remote session with the given id. @@ -369,7 +401,12 @@ 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, +) -> ResultType<()> { let session_id = get_session_id(id.to_owned()); LocalConfig::set_remote_id(&session_id); @@ -387,11 +424,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); if let Some(same_id_session) = SESSIONS.write().unwrap().insert(id.to_owned(), session) { same_id_session.close(); @@ -411,8 +454,6 @@ pub fn session_start_(id: &str, event_stream: StreamSink) -> ResultTy *session.event_stream.write().unwrap() = Some(event_stream); let session = session.clone(); std::thread::spawn(move || { - // if flutter : disable keyboard listen - crate::client::disable_keyboard_listening(); io_loop(session); }); Ok(()) @@ -534,24 +575,6 @@ pub fn get_session_id(id: String) -> String { }; } -pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> String { - let mut fd_json = serde_json::Map::new(); - fd_json.insert("id".into(), json!(id)); - fd_json.insert("path".into(), json!(path)); - - let mut entries_out = vec![]; - for entry in entries { - let mut entry_map = serde_json::Map::new(); - entry_map.insert("entry_type".into(), json!(entry.entry_type.value())); - entry_map.insert("name".into(), json!(entry.name)); - entry_map.insert("size".into(), json!(entry.size)); - entry_map.insert("modified_time".into(), json!(entry.modified_time)); - entries_out.push(entry_map); - } - fd_json.insert("entries".into(), json!(entries_out)); - serde_json::to_string(&fd_json).unwrap_or("".into()) -} - pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { let mut m = serde_json::Map::new(); m.insert("id".into(), json!(id)); diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0f3fb5ef6..992fff853 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -7,21 +7,23 @@ use std::{ use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; use serde_json::json; +use crate::common::is_keyboard_mode_supported; +use hbb_common::message_proto::KeyboardMode; use hbb_common::ResultType; use hbb_common::{ config::{self, LocalConfig, PeerConfig, ONLINE}, fs, log, }; +use std::str::FromStr; // use crate::hbbs_http::account::AuthResult; use crate::flutter::{self, SESSIONS}; use crate::ui_interface::{self, *}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::ui_session_interface::CUR_SESSION; use crate::{ client::file_trait::FileManager, - flutter::{make_fd_to_json, session_add, session_start_}, + common::make_fd_to_json, + flutter::{session_add, session_start_}, }; fn initialize(app_dir: &str) { *config::APP_DIR.write().unwrap() = app_dir.to_owned(); @@ -82,8 +84,9 @@ pub fn session_add_sync( id: String, is_file_transfer: bool, is_port_forward: bool, + switch_uuid: 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) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -183,6 +186,14 @@ pub fn set_local_flutter_config(k: String, v: String) { ui_interface::set_local_flutter_config(k, v); } +pub fn get_local_kb_layout_type() -> SyncReturn { + SyncReturn(ui_interface::get_kb_layout_type()) +} + +pub fn set_local_kb_layout_type(kb_layout_type: String) { + ui_interface::set_kb_layout_type(kb_layout_type) +} + pub fn session_get_view_style(id: String) -> Option { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_view_style()) @@ -225,6 +236,20 @@ pub fn session_set_image_quality(id: String, value: String) { } } +pub fn session_get_keyboard_mode(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_keyboard_mode()) + } else { + None + } +} + +pub fn session_set_keyboard_mode(id: String, value: String) { + if let Some(session) = SESSIONS.write().unwrap().get_mut(&id) { + session.save_keyboard_mode(value); + } +} + pub fn session_get_custom_image_quality(id: String) -> Option> { if let Some(session) = SESSIONS.read().unwrap().get(&id) { Some(session.get_custom_image_quality()) @@ -233,6 +258,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); @@ -268,10 +308,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); } } @@ -279,10 +320,8 @@ 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 { - *CUR_SESSION.lock().unwrap() = Some(session.clone()); session.enter(); } else { - *CUR_SESSION.lock().unwrap() = None; session.leave(); } } @@ -299,12 +338,14 @@ pub fn session_input_key( command: bool, ) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { + // #[cfg(any(target_os = "android", target_os = "ios"))] session.input_key(&name, down, press, alt, ctrl, shift, command); } } pub fn session_input_string(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { + // #[cfg(any(target_os = "android", target_os = "ios"))] session.input_string(&value); } } @@ -329,19 +370,6 @@ pub fn session_get_peer_option(id: String, name: String) -> String { "".to_string() } -pub fn session_get_keyboard_name(id: String) -> String { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - return session.get_keyboard_mode(); - } - "legacy".to_string() -} - -pub fn session_set_keyboard_mode(id: String, keyboard_mode: String) { - if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.save_keyboard_mode(keyboard_mode); - } -} - pub fn session_input_os_password(id: String, value: String) { if let Some(session) = SESSIONS.read().unwrap().get(&id) { session.input_os_password(value, true); @@ -464,6 +492,24 @@ 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 main_get_sound_inputs() -> Vec { #[cfg(not(any(target_os = "android", target_os = "ios")))] return get_sound_inputs(); @@ -529,6 +575,7 @@ pub fn main_get_app_name() -> String { pub fn main_get_app_name_sync() -> SyncReturn { SyncReturn(get_app_name()) } + pub fn main_get_license() -> String { get_license() } @@ -578,10 +625,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() } @@ -640,45 +683,11 @@ pub fn main_peer_has_password(id: String) -> bool { peer_has_password(id) } -pub fn main_get_recent_peers() -> String { - if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec> = PeerConfig::peers() - .drain(..) - .map(|(id, _, p)| { - HashMap::<&str, String>::from_iter([ - ("id", id), - ("username", p.info.username.clone()), - ("hostname", p.info.hostname.clone()), - ("platform", p.info.platform.clone()), - ( - "alias", - p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), - ), - ]) - }) - .collect(); - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()) - } else { - String::new() - } -} - pub fn main_load_recent_peers() { if !config::APP_DIR.read().unwrap().is_empty() { let peers: Vec> = PeerConfig::peers() .drain(..) - .map(|(id, _, p)| { - HashMap::<&str, String>::from_iter([ - ("id", id), - ("username", p.info.username.clone()), - ("hostname", p.info.hostname.clone()), - ("platform", p.info.platform.clone()), - ( - "alias", - p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), - ), - ]) - }) + .map(|(id, _, p)| peer_to_map(id, p)) .collect(); if let Some(s) = flutter::GLOBAL_EVENT_STREAM .read() @@ -704,16 +713,7 @@ pub fn main_load_fav_peers() { .into_iter() .filter_map(|(id, _, p)| { if favs.contains(&id) { - Some(HashMap::<&str, String>::from_iter([ - ("id", id), - ("username", p.info.username.clone()), - ("hostname", p.info.hostname.clone()), - ("platform", p.info.platform.clone()), - ( - "alias", - p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), - ), - ])) + Some(peer_to_map(id, p)) } else { None } @@ -918,9 +918,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; } @@ -936,9 +938,9 @@ pub fn session_restart_remote_device(id: String) { } } -pub fn session_get_audit_server_sync(id: String) -> SyncReturn { +pub fn session_get_audit_server_sync(id: String, typ: String) -> SyncReturn { let res = if let Some(session) = SESSIONS.read().unwrap().get(&id) { - session.get_audit_server() + session.get_audit_server(typ) } else { "".to_owned() }; @@ -967,8 +969,22 @@ pub fn session_change_prefer_codec(id: String) { } } -pub fn main_set_home_dir(home: String) { - *config::APP_HOME_DIR.write().unwrap() = home; +pub fn main_set_home_dir(_home: String) { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + *config::APP_HOME_DIR.write().unwrap() = _home; + } +} + +// This is a temporary method to get data dir for ios +pub fn main_get_data_dir_ios() -> SyncReturn { + let data_dir = config::Config::path("data"); + if !data_dir.exists() { + if let Err(e) = std::fs::create_dir_all(&data_dir) { + log::warn!("Failed to create data dir {}", e); + } + } + SyncReturn(data_dir.to_string_lossy().to_string()) } pub fn main_stop_service() { @@ -1056,6 +1072,10 @@ pub fn cm_elevate_portable(conn_id: i32) { crate::ui_cm_interface::elevate_portable(conn_id); } +pub fn cm_switch_back(conn_id: i32) { + crate::ui_cm_interface::switch_back(conn_id); +} + pub fn main_get_icon() -> String { #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] return ui_interface::get_icon(); @@ -1098,8 +1118,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 { @@ -1110,9 +1130,13 @@ pub fn main_is_installed() -> SyncReturn { SyncReturn(is_installed()) } -pub fn main_start_grab_keyboard() { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - crate::ui_session_interface::global_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(); + SyncReturn(true) } pub fn main_is_installed_lower_version() -> SyncReturn { @@ -1131,6 +1155,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()) } @@ -1193,6 +1221,14 @@ pub fn main_on_main_window_close() { crate::portable_service::client::drop_portable_service_shared_memory(); } +pub fn main_current_is_wayland() -> SyncReturn { + SyncReturn(current_is_wayland()) +} + +pub fn main_is_login_wayland() -> SyncReturn { + SyncReturn(is_login_wayland()) +} + #[cfg(target_os = "android")] pub mod server_side { use jni::{ diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index ceb3a6081..76ced87a0 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -4,6 +4,8 @@ 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/record_upload.rs b/src/hbbs_http/record_upload.rs new file mode 100644 index 000000000..93bc745c2 --- /dev/null +++ b/src/hbbs_http/record_upload.rs @@ -0,0 +1,204 @@ +use bytes::Bytes; +use hbb_common::{bail, config::Config, lazy_static, log, ResultType}; +use reqwest::blocking::{Body, Client}; +use scrap::record::RecordState; +use serde::Serialize; +use serde_json::Map; +use std::{ + fs::File, + io::{prelude::*, SeekFrom}, + sync::{mpsc::Receiver, Arc, Mutex}, + time::{Duration, Instant}, +}; + +const MAX_HEADER_LEN: usize = 1024; +const SHOULD_SEND_TIME: Duration = Duration::from_secs(1); +const SHOULD_SEND_SIZE: u64 = 1024 * 1024; + +lazy_static::lazy_static! { + static ref ENABLE: Arc> = Default::default(); +} + +pub fn is_enable() -> bool { + ENABLE.lock().unwrap().clone() +} + +pub fn run(rx: Receiver) { + let mut uploader = RecordUploader { + client: Client::new(), + api_server: crate::get_api_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + ), + filepath: Default::default(), + filename: Default::default(), + upload_size: Default::default(), + running: Default::default(), + last_send: Instant::now(), + }; + std::thread::spawn(move || loop { + if let Err(e) = match rx.recv() { + Ok(state) => match state { + RecordState::NewFile(filepath) => uploader.handle_new_file(filepath), + RecordState::NewFrame => { + if uploader.running { + uploader.handle_frame(false) + } else { + Ok(()) + } + } + RecordState::WriteTail => { + if uploader.running { + uploader.handle_tail() + } else { + Ok(()) + } + } + RecordState::RemoveFile => { + if uploader.running { + uploader.handle_remove() + } else { + Ok(()) + } + } + }, + Err(e) => { + log::trace!("upload thread stop:{}", e); + break; + } + } { + uploader.running = false; + log::error!("upload stop:{}", e); + } + }); +} + +struct RecordUploader { + client: Client, + api_server: String, + filepath: String, + filename: String, + upload_size: u64, + running: bool, + last_send: Instant, +} +impl RecordUploader { + fn send(&self, query: &Q, body: B) -> ResultType<()> + where + Q: Serialize + ?Sized, + B: Into, + { + match self + .client + .post(format!("{}/api/record", self.api_server)) + .query(query) + .body(body) + .send() + { + Ok(resp) => { + if let Ok(m) = resp.json::>() { + if let Some(e) = m.get("error") { + bail!(e.to_string()); + } + } + Ok(()) + } + Err(e) => bail!(e.to_string()), + } + } + + fn handle_new_file(&mut self, filepath: String) -> ResultType<()> { + match std::path::PathBuf::from(&filepath).file_name() { + Some(filename) => match filename.to_owned().into_string() { + Ok(filename) => { + self.filename = filename.clone(); + self.filepath = filepath.clone(); + self.upload_size = 0; + self.running = true; + self.last_send = Instant::now(); + self.send(&[("type", "new"), ("file", &filename)], Bytes::new())?; + Ok(()) + } + Err(_) => bail!("can't parse filename:{:?}", filename), + }, + None => bail!("can't parse filepath:{}", filepath), + } + } + + fn handle_frame(&mut self, flush: bool) -> ResultType<()> { + if !flush && self.last_send.elapsed() < SHOULD_SEND_TIME { + return Ok(()); + } + match File::open(&self.filepath) { + Ok(mut file) => match file.metadata() { + Ok(m) => { + let len = m.len(); + if len <= self.upload_size { + return Ok(()); + } + if !flush && len - self.upload_size < SHOULD_SEND_SIZE { + return Ok(()); + } + let mut buf = Vec::new(); + match file.seek(SeekFrom::Start(self.upload_size)) { + Ok(_) => match file.read_to_end(&mut buf) { + Ok(length) => { + self.send( + &[ + ("type", "part"), + ("file", &self.filename), + ("offset", &self.upload_size.to_string()), + ("length", &length.to_string()), + ], + buf, + )?; + self.upload_size = len; + self.last_send = Instant::now(); + Ok(()) + } + Err(e) => bail!(e.to_string()), + }, + Err(e) => bail!(e.to_string()), + } + } + Err(e) => bail!(e.to_string()), + }, + Err(e) => bail!(e.to_string()), + } + } + + fn handle_tail(&mut self) -> ResultType<()> { + self.handle_frame(true)?; + match File::open(&self.filepath) { + Ok(mut file) => { + let mut buf = vec![0u8; MAX_HEADER_LEN]; + match file.read(&mut buf) { + Ok(length) => { + buf.truncate(length); + self.send( + &[ + ("type", "tail"), + ("file", &self.filename), + ("offset", "0"), + ("length", &length.to_string()), + ], + buf, + )?; + log::info!("upload success, file:{}", self.filename); + Ok(()) + } + Err(e) => bail!(e.to_string()), + } + } + Err(e) => bail!(e.to_string()), + } + } + + fn handle_remove(&mut self) -> ResultType<()> { + self.send( + &[("type", "remove"), ("file", &self.filename)], + Bytes::new(), + )?; + Ok(()) + } +} 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 eb2d364ae..d4d803aec 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, @@ -75,6 +75,11 @@ pub enum FS { id: i32, file_num: i32, }, + WriteError { + id: i32, + file_num: i32, + err: String, + }, WriteOffset { id: i32, file_num: i32, @@ -106,7 +111,7 @@ pub enum DataKeyboardResponse { GetKeyState(bool), } -#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "t", content = "c")] pub enum DataMouse { @@ -161,6 +166,7 @@ pub enum Data { file_transfer_enabled: bool, restart: bool, recording: bool, + from_switch: bool, }, ChatMessage { text: String, @@ -186,15 +192,15 @@ pub enum Data { Test, SyncConfig(Option<(Config, Config2)>), #[cfg(not(any(target_os = "android", target_os = "ios")))] - ClipbaordFile(ClipbaordFile), + ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), PrivacyModeState((i32, PrivacyModeState)), TestRendezvousServer, - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Keyboard(DataKeyboard), - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] KeyboardResponse(DataKeyboardResponse), - #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] Mouse(DataMouse), Control(DataControl), Theme(String), @@ -202,6 +208,8 @@ pub enum Data { Empty, Disconnected, DataPortableService(DataPortableService), + SwitchSidesRequest(String), + SwitchSidesBack, } #[tokio::main(flavor = "current_thread")] @@ -422,6 +430,15 @@ async fn handle(data: Data, stream: &mut Connection) { 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 + ); + } _ => {} } } @@ -539,7 +556,7 @@ async fn check_pid(postfix: &str) { } } } - hbb_common::allow_err!(std::fs::remove_file(&Config::ipc_path(postfix))); + std::fs::remove_file(&Config::ipc_path(postfix)).ok(); } #[inline] diff --git a/src/keyboard.rs b/src/keyboard.rs new file mode 100644 index 000000000..de1abd231 --- /dev/null +++ b/src/keyboard.rs @@ -0,0 +1,697 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::client::get_key_state; +use crate::common::GrabState; +#[cfg(feature = "flutter")] +use crate::flutter::{CUR_SESSION_ID, SESSIONS}; +#[cfg(not(any(feature = "flutter", feature = "cli")))] +use crate::ui::CUR_SESSION; +use hbb_common::{log, message_proto::*}; +use rdev::{Event, EventType, Key}; +#[cfg(any(target_os = "windows", target_os = "macos"))] +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex}, + time::SystemTime, +}; + +#[cfg(windows)] +static mut IS_ALT_GR: bool = false; + +#[cfg(any(target_os = "windows", target_os = "macos"))] +static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); + +lazy_static::lazy_static! { + static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); + static ref MODIFIERS_STATE: Mutex> = { + let mut m = HashMap::new(); + m.insert(Key::ShiftLeft, false); + m.insert(Key::ShiftRight, false); + m.insert(Key::ControlLeft, false); + m.insert(Key::ControlRight, false); + m.insert(Key::Alt, false); + m.insert(Key::AltGr, false); + m.insert(Key::MetaLeft, false); + m.insert(Key::MetaRight, false); + Mutex::new(m) + }; +} + +pub mod client { + use super::*; + + pub fn get_keyboard_mode() -> String { + #[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() + } + + pub fn start_grab_loop() { + super::start_grab_loop(); + } + + pub fn change_grab_status(state: GrabState) { + match state { + GrabState::Ready => {} + GrabState::Run => { + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(true, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::enable_grab(); + } + GrabState::Wait => { + release_remote_keys(); + + #[cfg(any(target_os = "windows", target_os = "macos"))] + KEYBOARD_HOOKED.swap(false, Ordering::SeqCst); + + #[cfg(target_os = "linux")] + rdev::disable_grab(); + } + GrabState::Exit => { + #[cfg(target_os = "linux")] + rdev::exit_grab_listen(); + } + } + } + + pub fn process_event(event: &Event, lock_modes: Option) { + if is_long_press(&event) { + return; + } + if let Some(key_event) = event_to_key_event(&event, lock_modes) { + send_key_event(&key_event); + } + } + + pub fn get_modifiers_state( + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) -> (bool, bool, bool, bool) { + let modifiers_lock = MODIFIERS_STATE.lock().unwrap(); + let ctrl = *modifiers_lock.get(&Key::ControlLeft).unwrap() + || *modifiers_lock.get(&Key::ControlRight).unwrap() + || ctrl; + let shift = *modifiers_lock.get(&Key::ShiftLeft).unwrap() + || *modifiers_lock.get(&Key::ShiftRight).unwrap() + || shift; + let command = *modifiers_lock.get(&Key::MetaLeft).unwrap() + || *modifiers_lock.get(&Key::MetaRight).unwrap() + || command; + let alt = *modifiers_lock.get(&Key::Alt).unwrap() + || *modifiers_lock.get(&Key::AltGr).unwrap() + || alt; + + (alt, ctrl, shift, command) + } + + pub fn legacy_modifiers( + key_event: &mut KeyEvent, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, + ) { + if alt + && !crate::is_control_key(&key_event, &ControlKey::Alt) + && !crate::is_control_key(&key_event, &ControlKey::RAlt) + { + key_event.modifiers.push(ControlKey::Alt.into()); + } + if shift + && !crate::is_control_key(&key_event, &ControlKey::Shift) + && !crate::is_control_key(&key_event, &ControlKey::RShift) + { + key_event.modifiers.push(ControlKey::Shift.into()); + } + if ctrl + && !crate::is_control_key(&key_event, &ControlKey::Control) + && !crate::is_control_key(&key_event, &ControlKey::RControl) + { + key_event.modifiers.push(ControlKey::Control.into()); + } + if command + && !crate::is_control_key(&key_event, &ControlKey::Meta) + && !crate::is_control_key(&key_event, &ControlKey::RWin) + { + key_event.modifiers.push(ControlKey::Meta.into()); + } + } + + 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(); + key_event + } + + #[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); + key_event.down = true; + } else { + key_event.set_control_key(ControlKey::Delete); + legacy_modifiers(&mut key_event, true, true, false, false); + key_event.press = true; + } + key_event.mode = KeyboardMode::Legacy.into(); + key_event + } + + #[inline] + pub fn ctrl_alt_del() { + send_key_event(&event_ctrl_alt_del()); + } +} + +pub fn start_grab_loop() { + #[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 { + // fix #2211:CAPS LOCK don't work + if key == Key::CapsLock || key == Key::NumLock { + return Some(event); + } + if KEYBOARD_HOOKED.load(Ordering::SeqCst) { + client::process_event(&event, None); + if is_press { + return None; + } else { + return Some(event); + } + } else { + return Some(event); + } + }; + 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), + }; + if let Err(error) = rdev::grab(func) { + log::error!("rdev Error: {:?}", error) + } + }); + + #[cfg(target_os = "linux")] + if let Err(err) = rdev::start_grab_listen(move |event: Event| match event.event_type { + EventType::KeyPress(key) | EventType::KeyRelease(key) => { + if let Key::Unknown(keycode) = key { + log::error!("rdev get unknown key, keycode is : {:?}", keycode); + } else { + client::process_event(&event, None); + } + None + } + _ => Some(event), + }) { + log::error!("Failed to init rdev grab thread: {:?}", err); + }; +} + +pub fn is_long_press(event: &Event) -> bool { + let keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if let Some(&state) = keys.get(&k) { + if state == true { + return true; + } + } + } + _ => {} + }; + return false; +} + +pub fn release_remote_keys() { + // todo!: client quit suddenly, how to release keys? + let to_release = TO_RELEASE.lock().unwrap().clone(); + TO_RELEASE.lock().unwrap().clear(); + for key in to_release { + let event_type = EventType::KeyRelease(key); + let event = event_type_to_event(event_type); + // to-do: BUG + // Release events should be sent to the corresponding sessions, instead of current session. + client::process_event(&event, None); + } +} + +pub fn get_keyboard_mode_enum() -> KeyboardMode { + match client::get_keyboard_mode().as_str() { + "map" => KeyboardMode::Map, + "translate" => KeyboardMode::Translate, + _ => KeyboardMode::Legacy, + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +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()); + } + if get_key_state(enigo::Key::NumLock) { + key_event.modifiers.push(ControlKey::NumLock.into()); + } +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn convert_numpad_keys(key: Key) -> Key { + if get_key_state(enigo::Key::NumLock) { + return key; + } + match key { + Key::Kp0 => Key::Insert, + Key::KpDecimal => Key::Delete, + Key::Kp1 => Key::End, + Key::Kp2 => Key::DownArrow, + Key::Kp3 => Key::PageDown, + Key::Kp4 => Key::LeftArrow, + Key::Kp5 => Key::Clear, + Key::Kp6 => Key::RightArrow, + Key::Kp7 => Key::Home, + Key::Kp8 => Key::UpArrow, + Key::Kp9 => Key::PageUp, + _ => key, + } +} + +fn update_modifiers_state(event: &Event) { + // for mouse + let mut keys = MODIFIERS_STATE.lock().unwrap(); + match event.event_type { + EventType::KeyPress(k) => { + if keys.contains_key(&k) { + keys.insert(k, true); + } + } + EventType::KeyRelease(k) => { + if keys.contains_key(&k) { + keys.insert(k, false); + } + } + _ => {} + }; +} + +pub fn event_to_key_event(event: &Event, lock_modes: Option) -> Option { + let mut key_event = KeyEvent::new(); + update_modifiers_state(event); + + match event.event_type { + EventType::KeyPress(key) => { + TO_RELEASE.lock().unwrap().insert(key); + } + EventType::KeyRelease(key) => { + TO_RELEASE.lock().unwrap().remove(&key); + } + _ => {} + } + + let keyboard_mode = get_keyboard_mode_enum(); + 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)?, + _ => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + legacy_keyboard_mode(event, key_event)? + } + #[cfg(any(target_os = "android", target_os = "ios"))] + { + None? + } + } + }; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(lock_modes) = lock_modes { + add_numlock_capslock_with_lock_modes(&mut key_event, lock_modes); + } else { + add_numlock_capslock_status(&mut key_event); + } + + return Some(key_event); +} + +pub fn event_type_to_event(event_type: EventType) -> Event { + Event { + event_type, + time: SystemTime::now(), + name: None, + code: 0, + scan_code: 0, + } +} + +pub fn send_key_event(key_event: &KeyEvent) { + #[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(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 { + // 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; + } + }; + + let peer = get_peer_platform(); + let is_win = peer == "Windows"; + if is_win { + key = convert_numpad_keys(key); + } + + let alt = get_key_state(enigo::Key::Alt); + #[cfg(windows)] + let ctrl = { + let mut tmp = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + unsafe { + if IS_ALT_GR { + if alt || key == Key::AltGr { + if tmp { + tmp = false; + } + } else { + IS_ALT_GR = false; + } + } + } + tmp + }; + #[cfg(not(windows))] + let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); + let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); + #[cfg(windows)] + let command = crate::platform::windows::get_win_key_state(); + #[cfg(not(windows))] + let command = get_key_state(enigo::Key::Meta); + let control_key = match key { + Key::Alt => Some(ControlKey::Alt), + Key::AltGr => Some(ControlKey::RAlt), + Key::Backspace => Some(ControlKey::Backspace), + Key::ControlLeft => { + // when pressing AltGr, an extra VK_LCONTROL with a special + // scancode with bit 9 set is sent, let's ignore this. + #[cfg(windows)] + if (event.scan_code >> 8) == 0xE0 { + unsafe { + IS_ALT_GR = true; + } + return None; + } + Some(ControlKey::Control) + } + Key::ControlRight => Some(ControlKey::RControl), + Key::DownArrow => Some(ControlKey::DownArrow), + Key::Escape => Some(ControlKey::Escape), + Key::F1 => Some(ControlKey::F1), + Key::F10 => Some(ControlKey::F10), + Key::F11 => Some(ControlKey::F11), + Key::F12 => Some(ControlKey::F12), + Key::F2 => Some(ControlKey::F2), + Key::F3 => Some(ControlKey::F3), + Key::F4 => Some(ControlKey::F4), + Key::F5 => Some(ControlKey::F5), + Key::F6 => Some(ControlKey::F6), + Key::F7 => Some(ControlKey::F7), + Key::F8 => Some(ControlKey::F8), + Key::F9 => Some(ControlKey::F9), + Key::LeftArrow => Some(ControlKey::LeftArrow), + Key::MetaLeft => Some(ControlKey::Meta), + Key::MetaRight => Some(ControlKey::RWin), + Key::Return => Some(ControlKey::Return), + Key::RightArrow => Some(ControlKey::RightArrow), + Key::ShiftLeft => Some(ControlKey::Shift), + Key::ShiftRight => Some(ControlKey::RShift), + Key::Space => Some(ControlKey::Space), + Key::Tab => Some(ControlKey::Tab), + Key::UpArrow => Some(ControlKey::UpArrow), + Key::Delete => { + if is_win && ctrl && alt { + client::ctrl_alt_del(); + return None; + } + Some(ControlKey::Delete) + } + Key::Apps => Some(ControlKey::Apps), + Key::Cancel => Some(ControlKey::Cancel), + Key::Clear => Some(ControlKey::Clear), + Key::Kana => Some(ControlKey::Kana), + Key::Hangul => Some(ControlKey::Hangul), + Key::Junja => Some(ControlKey::Junja), + Key::Final => Some(ControlKey::Final), + Key::Hanja => Some(ControlKey::Hanja), + Key::Hanji => Some(ControlKey::Hanja), + Key::Convert => Some(ControlKey::Convert), + Key::Print => Some(ControlKey::Print), + Key::Select => Some(ControlKey::Select), + Key::Execute => Some(ControlKey::Execute), + Key::PrintScreen => Some(ControlKey::Snapshot), + Key::Help => Some(ControlKey::Help), + Key::Sleep => Some(ControlKey::Sleep), + Key::Separator => Some(ControlKey::Separator), + Key::KpReturn => Some(ControlKey::NumpadEnter), + Key::Kp0 => Some(ControlKey::Numpad0), + Key::Kp1 => Some(ControlKey::Numpad1), + Key::Kp2 => Some(ControlKey::Numpad2), + Key::Kp3 => Some(ControlKey::Numpad3), + Key::Kp4 => Some(ControlKey::Numpad4), + Key::Kp5 => Some(ControlKey::Numpad5), + Key::Kp6 => Some(ControlKey::Numpad6), + Key::Kp7 => Some(ControlKey::Numpad7), + Key::Kp8 => Some(ControlKey::Numpad8), + Key::Kp9 => Some(ControlKey::Numpad9), + Key::KpDivide => Some(ControlKey::Divide), + Key::KpMultiply => Some(ControlKey::Multiply), + Key::KpDecimal => Some(ControlKey::Decimal), + Key::KpMinus => Some(ControlKey::Subtract), + Key::KpPlus => Some(ControlKey::Add), + Key::CapsLock | Key::NumLock | Key::ScrollLock => { + return None; + } + Key::Home => Some(ControlKey::Home), + Key::End => Some(ControlKey::End), + Key::Insert => Some(ControlKey::Insert), + Key::PageUp => Some(ControlKey::PageUp), + Key::PageDown => Some(ControlKey::PageDown), + Key::Pause => Some(ControlKey::Pause), + _ => None, + }; + if let Some(k) = control_key { + key_event.set_control_key(k); + } else { + let mut chr = match event.name { + Some(ref s) => { + if s.len() <= 2 { + // exclude chinese characters + s.chars().next().unwrap_or('\0') + } else { + '\0' + } + } + _ => '\0', + }; + if chr == '·' { + // special for Chinese + chr = '`'; + } + if chr == '\0' { + chr = match key { + Key::Num1 => '1', + Key::Num2 => '2', + Key::Num3 => '3', + Key::Num4 => '4', + Key::Num5 => '5', + Key::Num6 => '6', + Key::Num7 => '7', + Key::Num8 => '8', + Key::Num9 => '9', + Key::Num0 => '0', + Key::KeyA => 'a', + Key::KeyB => 'b', + Key::KeyC => 'c', + Key::KeyD => 'd', + Key::KeyE => 'e', + Key::KeyF => 'f', + Key::KeyG => 'g', + Key::KeyH => 'h', + Key::KeyI => 'i', + Key::KeyJ => 'j', + Key::KeyK => 'k', + Key::KeyL => 'l', + Key::KeyM => 'm', + Key::KeyN => 'n', + Key::KeyO => 'o', + Key::KeyP => 'p', + Key::KeyQ => 'q', + Key::KeyR => 'r', + Key::KeyS => 's', + Key::KeyT => 't', + Key::KeyU => 'u', + Key::KeyV => 'v', + Key::KeyW => 'w', + Key::KeyX => 'x', + Key::KeyY => 'y', + Key::KeyZ => 'z', + Key::Comma => ',', + Key::Dot => '.', + Key::SemiColon => ';', + Key::Quote => '\'', + Key::LeftBracket => '[', + Key::RightBracket => ']', + Key::Slash => '/', + Key::BackSlash => '\\', + Key::Minus => '-', + Key::Equal => '=', + Key::BackQuote => '`', + _ => '\0', + } + } + if chr != '\0' { + if chr == 'l' && is_win && command { + client::lock_screen(); + return None; + } + key_event.set_chr(chr as _); + } else { + log::error!("Unknown key {:?}", &event); + return None; + } + } + let (alt, ctrl, shift, command) = client::get_modifiers_state(alt, ctrl, shift, command); + client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + + if down_or_up == true { + key_event.down = true; + } + Some(key_event) +} + +pub fn map_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Option { + match event.event_type { + EventType::KeyPress(..) => { + key_event.down = true; + } + EventType::KeyRelease(..) => { + key_event.down = false; + } + _ => 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" => { + // https://github.com/rustdesk/rustdesk/issues/1371 + // Filter scancodes that are greater than 255 and the hight word is not 0xE0. + if event.scan_code > 255 && (event.scan_code >> 8) != 0xE0 { + return None; + } + event.scan_code + } + "macos" => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::win_scancode_to_macos_iso_code(event.scan_code)? + } else { + rdev::win_scancode_to_macos_code(event.scan_code)? + } + } + _ => rdev::win_scancode_to_linux_code(event.scan_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 _)?, + }; + #[cfg(target_os = "linux")] + let keycode = match peer.as_str() { + "windows" => rdev::linux_code_to_win_scancode(event.code as _)?, + "macos" => { + if hbb_common::config::LocalConfig::get_kb_layout_type() == "ISO" { + rdev::linux_code_to_macos_iso_code(event.code as _)? + } else { + rdev::linux_code_to_macos_code(event.code as _)? + } + } + _ => event.code as _, + }; + #[cfg(any(target_os = "android", target_os = "ios"))] + let keycode = 0; + + key_event.set_chr(keycode); + Some(key_event) +} + +pub fn translate_keyboard_mode(_event: &Event, mut _key_event: KeyEvent) -> Option { + None +} diff --git a/src/lang.rs b/src/lang.rs index 9e6ea1b5b..f24d015e2 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -28,6 +28,10 @@ mod fa; mod ca; mod gr; mod sv; +mod sq; +mod sr; +mod th; +mod sl; lazy_static::lazy_static! { pub static ref LANGS: Value = @@ -58,6 +62,10 @@ lazy_static::lazy_static! { ("ca", "Català"), ("gr", "Ελληνικά"), ("sv", "Svenska"), + ("sq", "Shqip"), + ("sr", "Srpski"), + ("th", "ภาษาไทย"), + ("sl", "Slovenščina"), ("ro", "Română"), ]); } @@ -113,6 +121,10 @@ pub fn translate_locale(name: String, locale: &str) -> String { "ca" => ca::T.deref(), "gr" => gr::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(), }; diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 0dd21e168..72f55b44b 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -1,400 +1,437 @@ -lazy_static::lazy_static! { -pub static ref T: std::collections::HashMap<&'static str, &'static str> = - [ - ("Status", "Estat"), - ("Your Desktop", "EL teu escriptori"), - ("desk_tip", "Pots accedir al teu escriptori amb aquest ID i contrasenya."), - ("Password", "Contrasenya"), - ("Ready", "Llest"), - ("Established", "Establert"), - ("connecting_status", "Connexió a la xarxa RustDesk en progrés..."), - ("Enable Service", "Habilitar Servei"), - ("Start Service", "Iniciar Servei"), - ("Service is running", "El servei s'està executant"), - ("Service is not running", "El servei no s'està executant"), - ("not_ready_status", "No està llest. Comprova la teva connexió"), - ("Control Remote Desktop", "Controlar escriptori remot"), - ("Transfer File", "Transferir arxiu"), - ("Connect", "Connectar"), - ("Recent Sessions", "Sessions recents"), - ("Address Book", "Directori"), - ("Confirmation", "Confirmació"), - ("TCP Tunneling", "Túnel TCP"), - ("Remove", "Eliminar"), - ("Refresh random password", "Actualitzar contrasenya aleatòria"), - ("Set your own password", "Estableix la teva pròpia contrasenya"), - ("Enable Keyboard/Mouse", "Habilitar teclat/ratolí"), - ("Enable Clipboard", "Habilitar portapapers"), - ("Enable File Transfer", "Habilitar transferència d'arxius"), - ("Enable TCP Tunneling", "Habilitar túnel TCP"), - ("IP Whitelisting", "Direccions IP admeses"), - ("ID/Relay Server", "Servidor ID/Relay"), - ("Import Server Config", "Importar configuració de servidor"), - ("Export Server Config", "Exportar configuració del servidor"), - ("Import server configuration successfully", "Configuració de servidor importada amb èxit"), - ("Export server configuration successfully", "Configuració de servidor exportada con èxit"), - ("Invalid server configuration", "Configuració de servidor incorrecta"), - ("Clipboard is empty", "El portapapers està buit"), - ("Stop service", "Aturar servei"), - ("Change ID", "Canviar ID"), - ("Website", "Lloc web"), - ("About", "Sobre"), - ("Mute", "Silenciar"), - ("Audio Input", "Entrada d'àudio"), - ("Enhancements", "Millores"), - ("Hardware Codec", "Còdec de hardware"), - ("Adaptive Bitrate", "Tasa de bits adaptativa"), - ("ID Server", "Servidor de IDs"), - ("Relay Server", "Servidor Relay"), - ("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"), - ("Too frequent", "Massa comú"), - ("Cancel", "Cancel·lar"), - ("Skip", "Saltar"), - ("Close", "Tancar"), - ("Retry", "Reintentar"), - ("OK", ""), - ("Password Required", "Es necessita la contrasenya"), - ("Please enter your password", "Si us plau, introdueixi la seva contrasenya"), - ("Remember password", "Recordar contrasenya"), - ("Wrong Password", "Contrasenya incorrecta"), - ("Do you want to enter again?", "Vol tornar a entrar?"), - ("Connection Error", "Error de connexió"), - ("Error", ""), - ("Reset by the peer", "Reestablert pel peer"), - ("Connecting...", "Connectant..."), - ("Connection in progress. Please wait.", "Connexió en procés. Esperi."), - ("Please try 1 minute later", "Torni a provar-ho d'aquí un minut"), - ("Login Error", "Error d'inicio de sessió"), - ("Successful", "Exitós"), - ("Connected, waiting for image...", "Connectant, esperant imatge..."), - ("Name", "Nom"), - ("Type", "Tipus"), - ("Modified", "Modificat"), - ("Size", "Grandària"), - ("Show Hidden Files", "Mostrar arxius ocults"), - ("Receive", "Rebre"), - ("Send", "Enviar"), - ("Refresh File", "Actualitzar arxiu"), - ("Local", ""), - ("Remote", "Remot"), - ("Remote Computer", "Ordinador remot"), - ("Local Computer", "Ordinador local"), - ("Confirm Delete", "Confirma eliminació"), - ("Delete", "Eliminar"), - ("Properties", "Propietats"), - ("Multi Select", "Selecció múltiple"), - ("Select All", "Selecciona-ho Tot"), - ("Unselect All", "Deselecciona-ho Tot"), - ("Empty Directory", "Directori buit"), - ("Not an empty directory", "No és un directori buit"), - ("Are you sure you want to delete this file?", "Estàs segur que vols eliminar aquest arxiu?"), - ("Are you sure you want to delete this empty directory?", "Estàs segur que vols eliminar aquest directori buit?"), - ("Are you sure you want to delete the file of this directory?", "Estàs segur que vols eliminar aquest arxiu d'aquest directori?"), - ("Do this for all conflicts", "Fes això per a tots els conflictes"), - ("This is irreversible!", "Això és irreversible!"), - ("Deleting", "Eliminant"), - ("files", "arxius"), - ("Waiting", "Esperant"), - ("Finished", "Acabat"), - ("Speed", "Velocitat"), - ("Custom Image Quality", "Qualitat d'imatge personalitzada"), - ("Privacy mode", "Mode privat"), - ("Block user input", "Bloquejar entrada d'usuari"), - ("Unblock user input", "Desbloquejar entrada d'usuari"), - ("Adjust Window", "Ajustar finestra"), - ("Original", "Original"), - ("Shrink", "Reduir"), - ("Stretch", "Estirar"), - ("Scrollbar", "Barra de desplaçament"), - ("ScrollAuto", "Desplaçament automàtico"), - ("Good image quality", "Bona qualitat d'imatge"), - ("Balanced", "Equilibrat"), - ("Optimize reaction time", "Optimitzar el temps de reacció"), - ("Custom", "Personalitzat"), - ("Show remote cursor", "Mostrar cursor remot"), - ("Show quality monitor", "Mostrar qualitat del monitor"), - ("Disable clipboard", "Deshabilitar portapapers"), - ("Lock after session end", "Bloquejar després del final de la sessió"), - ("Insert", "Inserir"), - ("Insert Lock", "Inserir bloqueig"), - ("Refresh", "Actualitzar"), - ("ID does not exist", "L'ID no existeix"), - ("Failed to connect to rendezvous server", "No es pot connectar al servidor rendezvous"), - ("Please try later", "Siusplau provi-ho més tard"), - ("Remote desktop is offline", "L'escriptori remot està desconecctat"), - ("Key mismatch", "La clau no coincideix"), - ("Timeout", "Temps esgotat"), - ("Failed to connect to relay server", "No es pot connectar al servidor de relay"), - ("Failed to connect via rendezvous server", "No es pot connectar a través del servidor de rendezvous"), - ("Failed to connect via relay server", "No es pot connectar a través del servidor de relay"), - ("Failed to make direct connection to remote desktop", "No s'ha pogut establir una connexió directa amb l'escriptori remot"), - ("Set Password", "Configurar la contrasenya"), - ("OS Password", "contrasenya del sistema operatiu"), - ("install_tip", ""), - ("Click to upgrade", "Clicar per actualitzar"), - ("Click to download", "Clicar per descarregar"), - ("Click to update", "Clicar per refrescar"), - ("Configure", "Configurar"), - ("config_acc", ""), - ("config_screen", ""), - ("Installing ...", "Instal·lant ..."), - ("Install", "Instal·lar"), - ("Installation", "Instal·lació"), - ("Installation Path", "Ruta d'instal·lació"), - ("Create start menu shortcuts", "Crear accessos directes al menú d'inici"), - ("Create desktop icon", "Crear icona d'escriptori"), - ("agreement_tip", ""), - ("Accept and Install", "Acceptar i instal·lar"), - ("End-user license agreement", "Acord de llicència d'usuario final"), - ("Generating ...", "Generant ..."), - ("Your installation is lower version.", "La seva instal·lació és una versión inferior."), - ("not_close_tcp_tip", ""), - ("Listening ...", "Escoltant..."), - ("Remote Host", "Hoste remot"), - ("Remote Port", "Port remot"), - ("Action", "Acció"), - ("Add", "Afegirr"), - ("Local Port", "Port local"), - ("Local Address", "Adreça Local"), - ("Change Local Port", "Canviar Port Local"), - ("setup_server_tip", ""), - ("Too short, at least 6 characters.", "Massa curt, almenys 6 caràcters."), - ("The confirmation is not identical.", "La confirmación no coincideix."), - ("Permissions", "Permisos"), - ("Accept", "Acceptar"), - ("Dismiss", "Cancel·lar"), - ("Disconnect", "Desconnectar"), - ("Allow using keyboard and mouse", "Permetre l'ús del teclat i ratolí"), - ("Allow using clipboard", "Permetre usar portapapers"), - ("Allow hearing sound", "Permetre escoltar so"), - ("Allow file copy and paste", "Permetre copiar i enganxar arxius"), - ("Connected", "Connectat"), - ("Direct and encrypted connection", "Connexió directa i xifrada"), - ("Relayed and encrypted connection", "connexió retransmesa i xifrada"), - ("Direct and unencrypted connection", "connexió directa i sense xifrar"), - ("Relayed and unencrypted connection", "connexió retransmesa i sense xifrar"), - ("Enter Remote ID", "Introduixi l'ID remot"), - ("Enter your password", "Introdueixi la seva contrasenya"), - ("Logging in...", "Iniciant sessió..."), - ("Enable RDP session sharing", "Habilitar l'ús compartit de sessions RDP"), - ("Auto Login", "Inici de sessió automàtic"), - ("Enable Direct IP Access", "Habilitar accés IP directe"), - ("Rename", "Renombrar"), - ("Space", "Espai"), - ("Create Desktop Shortcut", "Crear accés directe a l'escriptori"), - ("Change Path", "Cnviar ruta"), - ("Create Folder", "Crear carpeta"), - ("Please enter the folder name", "Indiqui el nom de la carpeta"), - ("Fix it", "Soluciona-ho"), - ("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"), - ("x11 expected", "x11 necessari"), - ("Port", ""), - ("Settings", "Ajustaments"), - ("Username", " Nom d'usuari"), - ("Invalid port", "Port incorrecte"), - ("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"), - ("Always connect via relay", "Connecta sempre a través de relay"), - ("whitelist_tip", ""), - ("Login", "Inicia sessió"), - ("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"), - ("Unselect all tags", "Deseleccionar tots els tags"), - ("Network error", "Error de xarxa"), - ("Username missed", "Nom d'usuari oblidat"), - ("Password missed", "Contrasenya oblidada"), - ("Wrong credentials", "Credencials incorrectes"), - ("Edit Tag", "Editar tag"), - ("Unremember Password", "Contrasenya oblidada"), - ("Favorites", "Preferits"), - ("Add to Favorites", "Afegir a preferits"), - ("Remove from Favorites", "Treure de preferits"), - ("Empty", "Buit"), - ("Invalid folder name", "Nom de carpeta incorrecte"), - ("Socks5 Proxy", "Proxy Socks5"), - ("Hostname", ""), - ("Discovered", "Descobert"), - ("install_daemon_tip", ""), - ("Remote ID", "ID remot"), - ("Paste", "Enganxar"), - ("Paste here?", "Enganxar aquí?"), - ("Are you sure to close the connection?", "Estàs segur que vols tancar la connexió?"), - ("Download new version", "Descarregar nova versió"), - ("Touch mode", "Mode tàctil"), - ("Mouse mode", "Mode ratolí"), - ("One-Finger Tap", "Toqui amb un dit"), - ("Left Mouse", "Ratolí esquerra"), - ("One-Long Tap", "Toc llarg"), - ("Two-Finger Tap", "Toqui amb dos dits"), - ("Right Mouse", "Botó dret"), - ("One-Finger Move", "Moviment amb un dir"), - ("Double Tap & Move", "Toqui dos cops i mogui"), - ("Mouse Drag", "Arrastri amb el ratolí"), - ("Three-Finger vertically", "Tres dits verticalment"), - ("Mouse Wheel", "Roda del ratolí"), - ("Two-Finger Move", "Moviment amb dos dits"), - ("Canvas Move", "Moviment del llenç"), - ("Pinch to Zoom", "Pessiga per fer zoom"), - ("Canvas Zoom", "Ampliar llenç"), - ("Reset canvas", "Reestablir llenç"), - ("No permission of file transfer", "No tens permís de transferència de fitxers"), - ("Note", "Nota"), - ("Connection", "connexió"), - ("Share Screen", "Compartir pantalla"), - ("CLOSE", "TANCAR"), - ("OPEN", "OBRIR"), - ("Chat", "Xat"), - ("Total", "Total"), - ("items", "ítems"), - ("Selected", "Seleccionat"), - ("Screen Capture", "Captura de pantalla"), - ("Input Control", "Control d'entrada"), - ("Audio Capture", "Captura d'àudio"), - ("File Connection", "connexió d'arxius"), - ("Screen Connection", "connexió de pantalla"), - ("Do you accept?", "Acceptes?"), - ("Open System Setting", "Configuració del sistema obert"), - ("How to get Android input permission?", "Com obtenir el permís d'entrada d'Android?"), - ("android_input_permission_tip1", "Per a que un dispositiu remot controli el seu dispositiu Android amb el ratolí o tocs, cal permetre que RustDesk utilitzi el servei d' \"Accesibilitat\"."), - ("android_input_permission_tip2", "Vagi a la pàgina de [Serveis instal·lats], activi el servici [RustDesk Input]."), - ("android_new_connection_tip", "S'ha rebut una nova sol·licitud de control per al dispositiu actual."), - ("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."), - ("Account", "Compte"), - ("Overwrite", "Sobreescriure"), - ("This file exists, skip or overwrite this file?", "Aquest arxiu ja existeix, ometre o sobreescriure l'arxiu?"), - ("Quit", "Sortir"), - ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), - ("Help", "Ajuda"), - ("Failed", "Ha fallat"), - ("Succeeded", "Aconseguit"), - ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surti"), - ("Unsupported", "No suportat"), - ("Peer denied", "Peer denegat"), - ("Please install plugins", "Instal·li complements"), - ("Peer exit", "El peer ha sortit"), - ("Failed to turn off", "Error en apagar"), - ("Turned off", "Apagat"), - ("In privacy mode", "En mode de privacitat"), - ("Out privacy mode", "Fora del mode de privacitat"), - ("Language", "Idioma"), - ("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", ""), - ("Connection not allowed", "Connexió no disponible"), - ("Legacy mode", "Mode heretat"), - ("Map mode", "Mode mapa"), - ("Translate mode", "Mode traduit"), - ("Use permanent password", "Utilitzar contrasenya permament"), - ("Use both passwords", "Utilitzar ambdues contrasenyas"), - ("Set permanent password", "Establir contrasenya permament"), - ("Enable Remote Restart", "Activar reinici remot"), - ("Allow remote restart", "Permetre reinici remot"), - ("Restart Remote Device", "Reiniciar dispositiu"), - ("Are you sure you want to restart", "Està segur que vol reiniciar?"), - ("Restarting Remote Device", "Reiniciant dispositiu remot"), - ("remote_restarting_tip", "Dispositiu remot reiniciant, tanqui aquest missatge i tornis a connectar amb la contrasenya."), - ("Copied", "Copiat"), - ("Exit Fullscreen", "Sortir de la pantalla completa"), - ("Fullscreen", "Pantalla completa"), - ("Mobile Actions", "Accions mòbils"), - ("Select Monitor", "Seleccionar monitor"), - ("Control Actions", "Accions de control"), - ("Display Settings", "Configuració de pantalla"), - ("Ratio", "Relació"), - ("Image Quality", "Qualitat d'imatge"), - ("Scroll Style", "Estil de desplaçament"), - ("Show Menubar", "Mostra barra de menú"), - ("Hide Menubar", "Amaga barra de menú"), - ("Direct Connection", "Connexió directa"), - ("Relay Connection", "Connexió Relay"), - ("Secure Connection", "Connexió segura"), - ("Insecure Connection", "Connexió insegura"), - ("Scale original", "Escala original"), - ("Scale adaptive", "Escala adaptativa"), - ("General", ""), - ("Security", "Seguritat"), - ("Account", "Compte"), - ("Theme", "Tema"), - ("Dark Theme", "Tema Fosc"), - ("Dark", "Fosc"), - ("Light", "Clar"), - ("Follow System", "Tema del sistema"), - ("Enable hardware codec", "Habilitar còdec per hardware"), - ("Unlock Security Settings", "Desbloquejar ajustaments de seguritat"), - ("Enable Audio", "Habilitar àudio"), - ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), - ("Server", "Servidor"), - ("Direct IP Access", "Accés IP Directe"), - ("Proxy", ""), - ("Port", ""), - ("Apply", "Aplicar"), - ("Disconnect all devices?", "Desconnectar tots els dispositius?"), - ("Clear", "Netejar"), - ("Audio Input Device", "Dispositiu d'entrada d'àudio"), - ("Deny remote access", "Denegar accés remot"), - ("Use IP Whitelisting", "Utilitza llista de IPs admeses"), - ("Network", "Xarxa"), - ("Enable RDP", "Habilitar RDP"), - ("Pin menubar", "Bloqueja barra de menú"), - ("Unpin menubar", "Desbloquejar barra de menú"), - ("Recording", "Gravant"), - ("Directory", "Directori"), - ("Automatically record incoming sessions", "Gravació automàtica de sessions entrants"), - ("Change", "Canviar"), - ("Start session recording", "Començar gravació de sessió"), - ("Stop session recording", "Aturar gravació de sessió"), - ("Enable Recording Session", "Habilitar gravació de sessió"), - ("Allow recording session", "Permetre gravació de sessió"), - ("Enable LAN Discovery", "Habilitar descobriment de LAN"), - ("Deny LAN Discovery", "Denegar descobriment de LAN"), - ("Write a message", "Escriure un missatge"), - ("Prompt", ""), - ("elevation_prompt", ""), - ("uac_warning", ""), - ("elevated_foreground_window_warning", ""), - ("Disconnected", "Desconnectat"), - ("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."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Provi l'escriptori X11 o canvïi el seu sistema operatiu."), - ("JumpLink", "Veure"), - ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioni la pantalla que es compartirà (Operar al costat del peer)."), - ("Show RustDesk", "Mostrar RustDesk"), - ("This PC", "Aquest PC"), - ("or", "o"), - ("Continue with", "Continuar amb"), - ("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", ""), - ].iter().cloned().collect(); -} +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Estat"), + ("Your Desktop", "EL teu escriptori"), + ("desk_tip", "Pots accedir al teu escriptori amb aquest ID i contrasenya."), + ("Password", "Contrasenya"), + ("Ready", "Llest"), + ("Established", "Establert"), + ("connecting_status", "Connexió a la xarxa RustDesk en progrés..."), + ("Enable Service", "Habilitar Servei"), + ("Start Service", "Iniciar Servei"), + ("Service is running", "El servei s'està executant"), + ("Service is not running", "El servei no s'està executant"), + ("not_ready_status", "No està llest. Comprova la teva connexió"), + ("Control Remote Desktop", "Controlar escriptori remot"), + ("Transfer File", "Transferir arxiu"), + ("Connect", "Connectar"), + ("Recent Sessions", "Sessions recents"), + ("Address Book", "Directori"), + ("Confirmation", "Confirmació"), + ("TCP Tunneling", "Túnel TCP"), + ("Remove", "Eliminar"), + ("Refresh random password", "Actualitzar contrasenya aleatòria"), + ("Set your own password", "Estableix la teva pròpia contrasenya"), + ("Enable Keyboard/Mouse", "Habilitar teclat/ratolí"), + ("Enable Clipboard", "Habilitar portapapers"), + ("Enable File Transfer", "Habilitar transferència d'arxius"), + ("Enable TCP Tunneling", "Habilitar túnel TCP"), + ("IP Whitelisting", "Direccions IP admeses"), + ("ID/Relay Server", "Servidor ID/Relay"), + ("Import Server Config", "Importar configuració de servidor"), + ("Export Server Config", "Exportar configuració del servidor"), + ("Import server configuration successfully", "Configuració de servidor importada amb èxit"), + ("Export server configuration successfully", "Configuració de servidor exportada con èxit"), + ("Invalid server configuration", "Configuració de servidor incorrecta"), + ("Clipboard is empty", "El portapapers està buit"), + ("Stop service", "Aturar servei"), + ("Change ID", "Canviar ID"), + ("Website", "Lloc web"), + ("About", "Sobre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Silenciar"), + ("Audio Input", "Entrada d'àudio"), + ("Enhancements", "Millores"), + ("Hardware Codec", "Còdec de hardware"), + ("Adaptive Bitrate", "Tasa de bits adaptativa"), + ("ID Server", "Servidor de IDs"), + ("Relay Server", "Servidor Relay"), + ("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"), + ("Too frequent", "Massa comú"), + ("Cancel", "Cancel·lar"), + ("Skip", "Saltar"), + ("Close", "Tancar"), + ("Retry", "Reintentar"), + ("OK", ""), + ("Password Required", "Es necessita la contrasenya"), + ("Please enter your password", "Si us plau, introdueixi la seva contrasenya"), + ("Remember password", "Recordar contrasenya"), + ("Wrong Password", "Contrasenya incorrecta"), + ("Do you want to enter again?", "Vol tornar a entrar?"), + ("Connection Error", "Error de connexió"), + ("Error", ""), + ("Reset by the peer", "Reestablert pel peer"), + ("Connecting...", "Connectant..."), + ("Connection in progress. Please wait.", "Connexió en procés. Esperi."), + ("Please try 1 minute later", "Torni a provar-ho d'aquí un minut"), + ("Login Error", "Error d'inicio de sessió"), + ("Successful", "Exitós"), + ("Connected, waiting for image...", "Connectant, esperant imatge..."), + ("Name", "Nom"), + ("Type", "Tipus"), + ("Modified", "Modificat"), + ("Size", "Grandària"), + ("Show Hidden Files", "Mostrar arxius ocults"), + ("Receive", "Rebre"), + ("Send", "Enviar"), + ("Refresh File", "Actualitzar arxiu"), + ("Local", ""), + ("Remote", "Remot"), + ("Remote Computer", "Ordinador remot"), + ("Local Computer", "Ordinador local"), + ("Confirm Delete", "Confirma eliminació"), + ("Delete", "Eliminar"), + ("Properties", "Propietats"), + ("Multi Select", "Selecció múltiple"), + ("Select All", "Selecciona-ho Tot"), + ("Unselect All", "Deselecciona-ho Tot"), + ("Empty Directory", "Directori buit"), + ("Not an empty directory", "No és un directori buit"), + ("Are you sure you want to delete this file?", "Estàs segur que vols eliminar aquest arxiu?"), + ("Are you sure you want to delete this empty directory?", "Estàs segur que vols eliminar aquest directori buit?"), + ("Are you sure you want to delete the file of this directory?", "Estàs segur que vols eliminar aquest arxiu d'aquest directori?"), + ("Do this for all conflicts", "Fes això per a tots els conflictes"), + ("This is irreversible!", "Això és irreversible!"), + ("Deleting", "Eliminant"), + ("files", "arxius"), + ("Waiting", "Esperant"), + ("Finished", "Acabat"), + ("Speed", "Velocitat"), + ("Custom Image Quality", "Qualitat d'imatge personalitzada"), + ("Privacy mode", "Mode privat"), + ("Block user input", "Bloquejar entrada d'usuari"), + ("Unblock user input", "Desbloquejar entrada d'usuari"), + ("Adjust Window", "Ajustar finestra"), + ("Original", "Original"), + ("Shrink", "Reduir"), + ("Stretch", "Estirar"), + ("Scrollbar", "Barra de desplaçament"), + ("ScrollAuto", "Desplaçament automàtico"), + ("Good image quality", "Bona qualitat d'imatge"), + ("Balanced", "Equilibrat"), + ("Optimize reaction time", "Optimitzar el temps de reacció"), + ("Custom", "Personalitzat"), + ("Show remote cursor", "Mostrar cursor remot"), + ("Show quality monitor", "Mostrar qualitat del monitor"), + ("Disable clipboard", "Deshabilitar portapapers"), + ("Lock after session end", "Bloquejar després del final de la sessió"), + ("Insert", "Inserir"), + ("Insert Lock", "Inserir bloqueig"), + ("Refresh", "Actualitzar"), + ("ID does not exist", "L'ID no existeix"), + ("Failed to connect to rendezvous server", "No es pot connectar al servidor rendezvous"), + ("Please try later", "Siusplau provi-ho més tard"), + ("Remote desktop is offline", "L'escriptori remot està desconecctat"), + ("Key mismatch", "La clau no coincideix"), + ("Timeout", "Temps esgotat"), + ("Failed to connect to relay server", "No es pot connectar al servidor de relay"), + ("Failed to connect via rendezvous server", "No es pot connectar a través del servidor de rendezvous"), + ("Failed to connect via relay server", "No es pot connectar a través del servidor de relay"), + ("Failed to make direct connection to remote desktop", "No s'ha pogut establir una connexió directa amb l'escriptori remot"), + ("Set Password", "Configurar la contrasenya"), + ("OS Password", "contrasenya del sistema operatiu"), + ("install_tip", ""), + ("Click to upgrade", "Clicar per actualitzar"), + ("Click to download", "Clicar per descarregar"), + ("Click to update", "Clicar per refrescar"), + ("Configure", "Configurar"), + ("config_acc", ""), + ("config_screen", ""), + ("Installing ...", "Instal·lant ..."), + ("Install", "Instal·lar"), + ("Installation", "Instal·lació"), + ("Installation Path", "Ruta d'instal·lació"), + ("Create start menu shortcuts", "Crear accessos directes al menú d'inici"), + ("Create desktop icon", "Crear icona d'escriptori"), + ("agreement_tip", ""), + ("Accept and Install", "Acceptar i instal·lar"), + ("End-user license agreement", "Acord de llicència d'usuario final"), + ("Generating ...", "Generant ..."), + ("Your installation is lower version.", "La seva instal·lació és una versión inferior."), + ("not_close_tcp_tip", ""), + ("Listening ...", "Escoltant..."), + ("Remote Host", "Hoste remot"), + ("Remote Port", "Port remot"), + ("Action", "Acció"), + ("Add", "Afegirr"), + ("Local Port", "Port local"), + ("Local Address", "Adreça Local"), + ("Change Local Port", "Canviar Port Local"), + ("setup_server_tip", ""), + ("Too short, at least 6 characters.", "Massa curt, almenys 6 caràcters."), + ("The confirmation is not identical.", "La confirmación no coincideix."), + ("Permissions", "Permisos"), + ("Accept", "Acceptar"), + ("Dismiss", "Cancel·lar"), + ("Disconnect", "Desconnectar"), + ("Allow using keyboard and mouse", "Permetre l'ús del teclat i ratolí"), + ("Allow using clipboard", "Permetre usar portapapers"), + ("Allow hearing sound", "Permetre escoltar so"), + ("Allow file copy and paste", "Permetre copiar i enganxar arxius"), + ("Connected", "Connectat"), + ("Direct and encrypted connection", "Connexió directa i xifrada"), + ("Relayed and encrypted connection", "connexió retransmesa i xifrada"), + ("Direct and unencrypted connection", "connexió directa i sense xifrar"), + ("Relayed and unencrypted connection", "connexió retransmesa i sense xifrar"), + ("Enter Remote ID", "Introduixi l'ID remot"), + ("Enter your password", "Introdueixi la seva contrasenya"), + ("Logging in...", "Iniciant sessió..."), + ("Enable RDP session sharing", "Habilitar l'ús compartit de sessions RDP"), + ("Auto Login", "Inici de sessió automàtic"), + ("Enable Direct IP Access", "Habilitar accés IP directe"), + ("Rename", "Renombrar"), + ("Space", "Espai"), + ("Create Desktop Shortcut", "Crear accés directe a l'escriptori"), + ("Change Path", "Cnviar ruta"), + ("Create Folder", "Crear carpeta"), + ("Please enter the folder name", "Indiqui el nom de la carpeta"), + ("Fix it", "Soluciona-ho"), + ("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"), + ("x11 expected", "x11 necessari"), + ("Port", ""), + ("Settings", "Ajustaments"), + ("Username", " Nom d'usuari"), + ("Invalid port", "Port incorrecte"), + ("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"), + ("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"), + ("Unselect all tags", "Deseleccionar tots els tags"), + ("Network error", "Error de xarxa"), + ("Username missed", "Nom d'usuari oblidat"), + ("Password missed", "Contrasenya oblidada"), + ("Wrong credentials", "Credencials incorrectes"), + ("Edit Tag", "Editar tag"), + ("Unremember Password", "Contrasenya oblidada"), + ("Favorites", "Preferits"), + ("Add to Favorites", "Afegir a preferits"), + ("Remove from Favorites", "Treure de preferits"), + ("Empty", "Buit"), + ("Invalid folder name", "Nom de carpeta incorrecte"), + ("Socks5 Proxy", "Proxy Socks5"), + ("Hostname", ""), + ("Discovered", "Descobert"), + ("install_daemon_tip", ""), + ("Remote ID", "ID remot"), + ("Paste", "Enganxar"), + ("Paste here?", "Enganxar aquí?"), + ("Are you sure to close the connection?", "Estàs segur que vols tancar la connexió?"), + ("Download new version", "Descarregar nova versió"), + ("Touch mode", "Mode tàctil"), + ("Mouse mode", "Mode ratolí"), + ("One-Finger Tap", "Toqui amb un dit"), + ("Left Mouse", "Ratolí esquerra"), + ("One-Long Tap", "Toc llarg"), + ("Two-Finger Tap", "Toqui amb dos dits"), + ("Right Mouse", "Botó dret"), + ("One-Finger Move", "Moviment amb un dir"), + ("Double Tap & Move", "Toqui dos cops i mogui"), + ("Mouse Drag", "Arrastri amb el ratolí"), + ("Three-Finger vertically", "Tres dits verticalment"), + ("Mouse Wheel", "Roda del ratolí"), + ("Two-Finger Move", "Moviment amb dos dits"), + ("Canvas Move", "Moviment del llenç"), + ("Pinch to Zoom", "Pessiga per fer zoom"), + ("Canvas Zoom", "Ampliar llenç"), + ("Reset canvas", "Reestablir llenç"), + ("No permission of file transfer", "No tens permís de transferència de fitxers"), + ("Note", "Nota"), + ("Connection", "connexió"), + ("Share Screen", "Compartir pantalla"), + ("CLOSE", "TANCAR"), + ("OPEN", "OBRIR"), + ("Chat", "Xat"), + ("Total", "Total"), + ("items", "ítems"), + ("Selected", "Seleccionat"), + ("Screen Capture", "Captura de pantalla"), + ("Input Control", "Control d'entrada"), + ("Audio Capture", "Captura d'àudio"), + ("File Connection", "connexió d'arxius"), + ("Screen Connection", "connexió de pantalla"), + ("Do you accept?", "Acceptes?"), + ("Open System Setting", "Configuració del sistema obert"), + ("How to get Android input permission?", "Com obtenir el permís d'entrada d'Android?"), + ("android_input_permission_tip1", "Per a que un dispositiu remot controli el seu dispositiu Android amb el ratolí o tocs, cal permetre que RustDesk utilitzi el servei d' \"Accesibilitat\"."), + ("android_input_permission_tip2", "Vagi a la pàgina de [Serveis instal·lats], activi el servici [RustDesk Input]."), + ("android_new_connection_tip", "S'ha rebut una nova sol·licitud de control per al dispositiu actual."), + ("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."), + ("Account", "Compte"), + ("Overwrite", "Sobreescriure"), + ("This file exists, skip or overwrite this file?", "Aquest arxiu ja existeix, ometre o sobreescriure l'arxiu?"), + ("Quit", "Sortir"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Ajuda"), + ("Failed", "Ha fallat"), + ("Succeeded", "Aconseguit"), + ("Someone turns on privacy mode, exit", "Algú ha activat el mode de privacitat, surti"), + ("Unsupported", "No suportat"), + ("Peer denied", "Peer denegat"), + ("Please install plugins", "Instal·li complements"), + ("Peer exit", "El peer ha sortit"), + ("Failed to turn off", "Error en apagar"), + ("Turned off", "Apagat"), + ("In privacy mode", "En mode de privacitat"), + ("Out privacy mode", "Fora del mode de privacitat"), + ("Language", "Idioma"), + ("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", ""), + ("Connection not allowed", "Connexió no disponible"), + ("Legacy mode", "Mode heretat"), + ("Map mode", "Mode mapa"), + ("Translate mode", "Mode traduit"), + ("Use permanent password", "Utilitzar contrasenya permament"), + ("Use both passwords", "Utilitzar ambdues contrasenyas"), + ("Set permanent password", "Establir contrasenya permament"), + ("Enable Remote Restart", "Activar reinici remot"), + ("Allow remote restart", "Permetre reinici remot"), + ("Restart Remote Device", "Reiniciar dispositiu"), + ("Are you sure you want to restart", "Està segur que vol reiniciar?"), + ("Restarting Remote Device", "Reiniciant dispositiu remot"), + ("remote_restarting_tip", "Dispositiu remot reiniciant, tanqui aquest missatge i tornis a connectar amb la contrasenya."), + ("Copied", "Copiat"), + ("Exit Fullscreen", "Sortir de la pantalla completa"), + ("Fullscreen", "Pantalla completa"), + ("Mobile Actions", "Accions mòbils"), + ("Select Monitor", "Seleccionar monitor"), + ("Control Actions", "Accions de control"), + ("Display Settings", "Configuració de pantalla"), + ("Ratio", "Relació"), + ("Image Quality", "Qualitat d'imatge"), + ("Scroll Style", "Estil de desplaçament"), + ("Show Menubar", "Mostra barra de menú"), + ("Hide Menubar", "Amaga barra de menú"), + ("Direct Connection", "Connexió directa"), + ("Relay Connection", "Connexió Relay"), + ("Secure Connection", "Connexió segura"), + ("Insecure Connection", "Connexió insegura"), + ("Scale original", "Escala original"), + ("Scale adaptive", "Escala adaptativa"), + ("General", ""), + ("Security", "Seguretat"), + ("Theme", "Tema"), + ("Dark Theme", "Tema Fosc"), + ("Dark", "Fosc"), + ("Light", "Clar"), + ("Follow System", "Tema del sistema"), + ("Enable hardware codec", "Habilitar còdec per hardware"), + ("Unlock Security Settings", "Desbloquejar ajustaments de seguretat"), + ("Enable Audio", "Habilitar àudio"), + ("Unlock Network Settings", "Desbloquejar Ajustaments de Xarxa"), + ("Server", "Servidor"), + ("Direct IP Access", "Accés IP Directe"), + ("Proxy", ""), + ("Apply", "Aplicar"), + ("Disconnect all devices?", "Desconnectar tots els dispositius?"), + ("Clear", "Netejar"), + ("Audio Input Device", "Dispositiu d'entrada d'àudio"), + ("Deny remote access", "Denegar accés remot"), + ("Use IP Whitelisting", "Utilitza llista de IPs admeses"), + ("Network", "Xarxa"), + ("Enable RDP", "Habilitar RDP"), + ("Pin menubar", "Bloqueja barra de menú"), + ("Unpin menubar", "Desbloquejar barra de menú"), + ("Recording", "Gravant"), + ("Directory", "Directori"), + ("Automatically record incoming sessions", "Gravació automàtica de sessions entrants"), + ("Change", "Canviar"), + ("Start session recording", "Començar gravació de sessió"), + ("Stop session recording", "Aturar gravació de sessió"), + ("Enable Recording Session", "Habilitar gravació de sessió"), + ("Allow recording session", "Permetre gravació de sessió"), + ("Enable LAN Discovery", "Habilitar descobriment de LAN"), + ("Deny LAN Discovery", "Denegar descobriment de LAN"), + ("Write a message", "Escriure un missatge"), + ("Prompt", ""), + ("Please wait for confirmation of UAC...", ""), + ("elevated_foreground_window_tip", ""), + ("Disconnected", "Desconnectat"), + ("Other", "Altre"), + ("Confirm before closing multiple tabs", "Confirmar abans de tancar múltiples pestanyes"), + ("Keyboard Settings", "Ajustaments de teclat"), + ("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."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland requereix una versió superior de la distribución de Linux. Provi l'escriptori X11 o canvïi el seu sistema operatiu."), + ("JumpLink", "Veure"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seleccioni la pantalla que es compartirà (Operar al costat del peer)."), + ("Show RustDesk", "Mostrar RustDesk"), + ("This PC", "Aquest PC"), + ("or", "o"), + ("Continue with", "Continuar amb"), + ("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", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a3b3b47c8..14e8a463d 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -7,7 +7,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Password", "密码"), ("Ready", "就绪"), ("Established", "已建立"), - ("connecting_status", "正在接入RustDesk网络..."), + ("connecting_status", "正在接入 RustDesk 网络..."), ("Enable Service", "允许服务"), ("Start Service", "启动服务"), ("Service is running", "服务正在运行"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "改变ID"), ("Website", "网站"), ("About", "关于"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "静音"), ("Audio Input", "音频输入"), ("Enhancements", "增强功能"), @@ -116,7 +118,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "好画质"), ("Balanced", "一般画质"), ("Optimize reaction time", "优化反应时间"), - ("Custom", "自定义画质"), + ("Custom", "自定义"), ("Show remote cursor", "显示远程光标"), ("Show quality monitor", "显示质量监测"), ("Disable clipboard", "禁止剪贴板"), @@ -136,13 +138,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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\"屏幕录制\" 权限。"), + ("config_acc", "为了能够远程控制你的桌面, 请给予 RustDesk \"辅助功能\" 权限。"), + ("config_screen", "为了能够远程访问你的桌面, 请给予 RustDesk \"屏幕录制\" 权限。"), ("Installing ...", "安装 ..."), ("Install", "安装"), ("Installation", "安装"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "强制走中继连接"), ("whitelist_tip", "只有白名单里的ip才能访问我"), ("Login", "登录"), + ("Verify", "验证"), + ("Remember me", "记住我"), + ("Trust this device", "信任此设备"), + ("Verification code", "验证码"), + ("verification_tip", "检测到新设备登录,已向注册邮箱发送了登录验证码,输入验证码继续登录"), ("Logout", "登出"), ("Tags", "标签"), ("Search ID", "查找ID"), @@ -219,7 +226,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Network error", "网络错误"), ("Username missed", "用户名没有填写"), ("Password missed", "密码没有填写"), - ("Wrong credentials", "用户名或者密码错误"), + ("Wrong credentials", "提供的登入信息错误"), ("Edit Tag", "修改标签"), ("Unremember Password", "忘掉密码"), ("Favorites", "收藏"), @@ -271,14 +278,14 @@ 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", "账号"), + ("Account", "账户"), ("Overwrite", "覆盖"), ("This file exists, skip or overwrite this file?", "这个文件/文件夹已存在,跳过/覆盖?"), ("Quit", "退出"), @@ -296,9 +303,9 @@ 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 应用设置页面中,找到并进入 [电源] 页面,取消勾选 [不受限制]"), ("Connection not allowed", "对方不允许连接"), ("Legacy mode", "传统模式"), ("Map mode", "1:1传输"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "适应窗口"), ("General", "常规"), ("Security", "安全"), - ("Account", "账户"), ("Theme", "主题"), ("Dark Theme", "暗黑主题"), ("Dark", "黑暗"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "服务器"), ("Direct IP Access", "IP直接访问"), ("Proxy", "代理"), - ("Port", "端口"), ("Apply", "应用"), ("Disconnect all devices?", "断开所有远程连接?"), ("Clear", "清空"), @@ -374,14 +379,13 @@ 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 或更高版本。"), ("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", "使用"), @@ -397,5 +401,37 @@ 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支持处于实验阶段,如果你需要使用无人值守访问,请使用X11。"), + ("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", "请选择本地键盘类型"), + ("software_render_tip", "如果你使用英伟达显卡, 并且远程窗口在会话建立后会立刻关闭, 那么安装nouveau驱动并且选择使用软件渲染可能会有帮助。重启软件后生效。"), + ("Always use software rendering", "使用软件渲染"), + ("config_input", "为了能够通过键盘控制远程桌面, 请给予 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?", "请确认要让对方访问你的桌面?"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index 450f3971a..e2935770c 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Změnit identifikátor"), ("Website", "Webové stránky"), ("About", "O aplikaci"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Ztlumit"), ("Audio Input", "Vstup zvuku"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Měřítko adaptivní"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,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."), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index ea7263ac8..937990ea8 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ændre ID"), ("Website", "Hjemmeside"), ("About", "Omkring"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Sluk for mikrofonen"), ("Audio Input", "Lydindgang"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forbindelse via relæ-server"), ("whitelist_tip", "Kun IP'er på udgivelseslisten kan få adgang til mig"), ("Login", "Login"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "logger af"), ("Tags", "Nøgleord"), ("Search ID", "Søg ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skaler adaptiv"), ("General", "Generelt"), ("Security", "Sikkerhed"), - ("Account", "Konto"), ("Theme", "Thema"), ("Dark Theme", "Mørk Tema"), ("Dark", "Mørk"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Server"), ("Direct IP Access", "Direkte IP Adgang"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Anvend"), ("Disconnect all devices?", "Afbryd alle enheder?"), ("Clear", "Nulstil"), @@ -374,7 +379,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 kræver Ubuntu 21.04 eller nyere version."), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index c2290b95c..a567877a2 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -9,64 +9,66 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Established", "Verbunden"), ("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"), + ("not_ready_status", "Nicht bereit. Bitte überprüfen Sie Ihre Netzwerkverbindung."), + ("Control Remote Desktop", "Entfernten Desktop steuern"), ("Transfer File", "Datei übertragen"), ("Connect", "Verbinden"), ("Recent Sessions", "Letzte Sitzungen"), ("Address Book", "Adressbuch"), ("Confirmation", "Bestätigung"), - ("TCP Tunneling", "TCP Tunneln"), + ("TCP Tunneling", "TCP-Tunnelung"), ("Remove", "Entfernen"), ("Refresh random password", "Zufälliges Passwort erzeugen"), ("Set your own password", "Eigenes Passwort setzen"), - ("Enable Keyboard/Mouse", "Tastatur/Maus aktivieren"), + ("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"), ("Website", "Webseite"), ("About", "Über"), + ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), + ("Privacy Statement", "Datenschutz"), ("Mute", "Stummschalten"), ("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"), + ("server_not_support", "Diese Funktion wird noch nicht vom Server unterstützt."), ("Not available", "Nicht verfügbar"), ("Too frequent", "Zu häufig"), ("Cancel", "Abbrechen"), ("Skip", "Überspringen"), - ("Close", "Sitzung beenden"), + ("Close", "Schließen"), ("Retry", "Erneut versuchen"), ("OK", "OK"), ("Password Required", "Passwort erforderlich"), - ("Please enter your password", "Bitte geben Sie das Passwort der Gegenstelle ein"), + ("Please enter your password", "Bitte geben Sie Ihr Passwort ein"), ("Remember password", "Passwort merken"), ("Wrong Password", "Falsches Passwort"), ("Do you want to enter again?", "Erneut verbinden?"), ("Connection Error", "Verbindungsfehler"), ("Error", "Fehler"), - ("Reset by the peer", "Verbindung wurde von der Gegenstelle zurückgesetzt"), + ("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..."), ("Please try 1 minute later", "Bitte versuchen Sie es später erneut"), @@ -92,7 +94,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select All", "Alles auswählen"), ("Unselect All", "Alles abwählen"), ("Empty Directory", "Leerer Ordner"), - ("Not an empty directory", "Ordner ist nicht leer"), + ("Not an empty directory", "Ordner ist nicht leer."), ("Are you sure you want to delete this file?", "Sind Sie sicher, dass Sie diese Datei löschen wollen?"), ("Are you sure you want to delete this empty directory?", "Sind Sie sicher, dass Sie diesen leeren Ordner löschen möchten?"), ("Are you sure you want to delete the file of this directory?", "Sind Sie sicher, dass Sie die Datei dieses Ordners löschen möchten?"), @@ -111,7 +113,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Verkleinern"), ("Stretch", "Strecken"), - ("Scrollbar", "Scrollleiste"), + ("Scrollbar", "Scroll-Leiste"), ("ScrollAuto", "Automatisch scrollen"), ("Good image quality", "Hohe Bildqualität"), ("Balanced", "Ausgeglichen"), @@ -124,46 +126,46 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert", "Einfügen"), ("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"), - ("Please try later", "Bitte versuchen Sie es später erneut"), - ("Remote desktop is offline", "Entfernter PC ist offline"), - ("Key mismatch", "Schlüssel stimmt nicht überein"), + ("ID does not exist", "Diese ID existiert nicht."), + ("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 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, um RustDesk auf dem System zu installieren"), + ("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."), ("Click to upgrade", "Upgrade"), ("Click to download", "Zum Herunterladen klicken"), ("Click to update", "Update"), ("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 \"Bildschirm-Aufnahme\"-Berechtigung erteilen."), - ("Installing ...", "Installiere..."), + ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk die Berechtigung \"Bildschirmaufnahme\" erteilen."), + ("Installing ...", "Installieren..."), ("Install", "Installieren"), ("Installation", "Installation"), ("Installation Path", "Installationspfad"), ("Create start menu shortcuts", "Verknüpfung im Startmenü erstellen"), ("Create desktop icon", "Desktop-Verknüpfung erstellen"), - ("agreement_tip", "Durch die Installation akzeptieren Sie die Lizenzvereinbarung"), + ("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..."), ("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 ...", "Lausche..."), + ("Listening ...", "Lauschen..."), ("Remote Host", "Entfernter PC"), ("Remote Port", "Entfernter Port"), ("Action", "Aktion"), ("Add", "Hinzufügen"), ("Local Port", "Lokaler Port"), - ("Local Address", "Lokale Addresse"), + ("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 +185,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enter your password", "Geben Sie Ihr Passwort ein"), ("Logging in...", "Anmelden..."), ("Enable RDP session sharing", "RDP-Sitzungsfreigabe aktivieren"), - ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Sperren nach Sitzungsende\" aktiviert haben)"), + ("Auto Login", "Automatisch anmelden (nur gültig, wenn Sie \"Nach Sitzungsende sperren\" aktiviert haben)"), ("Enable Direct IP Access", "Direkten IP-Zugang aktivieren"), ("Rename", "Umbenennen"), ("Space", "Speicherplatz"), @@ -193,31 +195,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "Bitte geben Sie den Ordnernamen ein"), ("Fix it", "Reparieren"), ("Warning", "Warnung"), - ("Login screen using Wayland is not supported", "Anmeldebildschirm wird mit Wayland nicht unterstützt"), + ("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"), ("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"), ("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"), ("Always connect via relay", "Immer über Relay-Server verbinden"), - ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen"), + ("whitelist_tip", "Nur IPs auf der Whitelist können zugreifen."), ("Login", "Anmelden"), + ("Verify", "Überprüfen"), + ("Remember me", "Login speichern"), + ("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 weiter anzumelden."), ("Logout", "Abmelden"), ("Tags", "Schlagworte"), ("Search ID", "Suche ID"), - ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt"), + ("Current Wayland display server is not supported", "Der aktuelle Wayland-Anzeigeserver wird nicht unterstützt."), ("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"), + ("Username missed", "Benutzernamen vergessen"), ("Password missed", "Passwort vergessen"), ("Wrong credentials", "Falsche Anmeldedaten"), ("Edit Tag", "Schlagwort bearbeiten"), @@ -227,17 +234,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove from Favorites", "Aus Favoriten entfernen"), ("Empty", "Keine Einträge"), ("Invalid folder name", "Ungültiger Ordnername"), - ("Socks5 Proxy", "Socks5 Proxy"), + ("Socks5 Proxy", "SOCKS5-Proxy"), ("Hostname", "Hostname"), ("Discovered", "Im LAN erkannt"), - ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein"), + ("install_daemon_tip", "Um mit System zu starten, muss der Systemdienst installiert sein."), ("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?"), ("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"), @@ -253,8 +260,8 @@ 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."), @@ -271,8 +278,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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 und geben Sie [Installierte Dienste] ein, schalten Sie den Dienst [RustDesk Input] ein."), + ("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."), @@ -288,20 +295,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Succeeded", "Erfolgreich"), ("Someone turns on privacy mode, exit", "Jemand hat den Datenschutzmodus aktiviert, beende..."), ("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"), ("Out privacy mode", "Datenschutzmodus deaktivieren"), ("Language", "Sprache"), ("Keep RustDesk background service", "RustDesk im Hintergrund ausführen"), - ("Ignore Battery Optimizations", "Batterieoptimierung ignorieren"), - ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Batterieopimierung öffnen?"), + ("Ignore Battery Optimizations", "Akkuoptimierung ignorieren"), + ("android_open_battery_optimizations_tip", "Möchten Sie die Einstellungen zur Akkuoptimierung öffnen?"), ("Connection not allowed", "Verbindung abgelehnt"), ("Legacy mode", "Kompatibilitätsmodus"), - ("Map mode", ""), //Muss noch angepasst werden + ("Map mode", "Kartenmodus"), ("Translate mode", "Übersetzungsmodus"), ("Use permanent password", "Permanentes Passwort verwenden"), ("Use both passwords", "Beide Passwörter verwenden"), @@ -328,16 +335,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relaisverbindung"), ("Secure Connection", "Sichere Verbindung"), ("Insecure Connection", "Unsichere Verbindung"), - ("Scale original", "Keine Saklierung"), - ("Scale adaptive", "Automatische Saklierung"), + ("Scale original", "Keine Skalierung"), + ("Scale adaptive", "Automatische Skalierung"), ("General", "Allgemein"), ("Security", "Sicherheit"), - ("Account", "Konto"), ("Theme", "Farbgebung"), - ("Dark Theme", "dunkle Farbgebung"), + ("Dark Theme", "Dunkle Farbgebung"), ("Dark", "Dunkel"), ("Light", "Hell"), - ("Follow System", "System-Standard"), + ("Follow System", "Systemstandard"), ("Enable hardware codec", "Hardware-Codec aktivieren"), ("Unlock Security Settings", "Sicherheitseinstellungen entsperren"), ("Enable Audio", "Audio aktivieren"), @@ -345,7 +351,6 @@ 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"), @@ -358,7 +363,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin menubar", "Menüleiste lösen"), ("Recording", "Aufnahme"), ("Directory", "Verzeichnis"), - ("Automatically record incoming sessions", "Automatische Aufzeichnung eingehender Sitzungen"), + ("Automatically record incoming sessions", "Eingehende Sitzungen automatisch aufzeichnen"), ("Change", "Ändern"), ("Start session recording", "Sitzungsaufzeichnung starten"), ("Stop session recording", "Sitzungsaufzeichnung beenden"), @@ -367,14 +372,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "LAN-Erkennung aktivieren"), ("Deny LAN Discovery", "LAN-Erkennung verbieten"), ("Write a message", "Nachricht schreiben"), - ("Prompt", ""), //Aufforderung? + ("Prompt", "Meldung"), ("Please wait for confirmation of UAC...", "Bitte auf die Bestätigung des Nutzers warten..."), - ("elevated_foreground_window_tip", ""), + ("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."), ("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."), @@ -389,13 +393,45 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Zoom cursor", "Cursor zoomen"), ("Accept sessions via password", "Sitzung mit Passwort bestätigen"), ("Accept sessions via click", "Sitzung mit einem Klick bestätigen"), - ("Accept sessions via both", "Sitzung durch Klick und Passwort bestätigen"), - ("Please wait for the remote side to accept your session request...", "Bitte warten Sie auf die Gegenstelle, dass diese Ihre Sitzungsanfrage bestätigt..."), + ("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..."), ("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"), ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"), - ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff nur über ein permanentes Passwort erfolgt."), // Sehr unklar. Muss noch angepasst werden. Original: Allow hiding only if accepting sessions via password and using pernament password + ("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."), + ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"), + ("Skipped", "Übersprungen"), + ("Add to Address Book", "Zum Adressbuch hinzufügen"), + ("Group", "Gruppe"), + ("Search", "Suchen"), + ("Closed manually by the web console", "Manuell über die Webkonsole beendet"), + ("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 \"Input Monitoring\"-Rechte 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 OK 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, ob Sie Ihren Desktop freigeben möchten."), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index a661c17bc..6eed43a77 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."), @@ -34,5 +34,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("JumpLink", "View"), ("Stop service", "Stop Service"), ("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.") ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 797eb2bb6..839c69bbb 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Ŝanĝi identigilon"), ("Website", "Retejo"), ("About", "Pri"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Muta"), ("Audio Input", "Aŭdia enigo"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Skalo adapta"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,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."), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 514d21480..2b109c18f 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -17,7 +17,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Transfer File", "Transferir archivo"), ("Connect", "Conectar"), ("Recent Sessions", "Sesiones recientes"), - ("Address Book", "Directorio"), + ("Address Book", "Libreta de direcciones"), ("Confirmation", "Confirmación"), ("TCP Tunneling", "Túnel TCP"), ("Remove", "Quitar"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambiar ID"), ("Website", "Sitio web"), ("About", "Acerca de"), + ("Slogan_tip", "Hecho con corazón en este mundo caótico!"), + ("Privacy Statement", "Declaración de privacidad"), ("Mute", "Silenciar"), ("Audio Input", "Entrada de audio"), ("Enhancements", "Mejoras"), @@ -55,7 +57,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Not available", "No disponible"), ("Too frequent", "Demasiado frecuente"), ("Cancel", "Cancelar"), - ("Skip", "Saltar"), + ("Skip", "Omitir"), ("Close", "Cerrar"), ("Retry", "Reintentar"), ("OK", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -298,7 +305,7 @@ 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]"), ("Connection not allowed", "Conexión no disponible"), ("Legacy mode", "Modo heredado"), ("Map mode", "Modo mapa"), @@ -311,8 +318,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"), @@ -332,7 +339,6 @@ 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"), ("Dark", "Oscuro"), @@ -345,7 +351,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"), @@ -368,13 +373,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."), @@ -397,5 +401,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Solicitud de acceso a su dispositivo"), ("Hide connection management window", "Ocultar ventana de gestión de conexión"), ("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", "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"), + ("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\"."), + ("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"), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index ac4a8771b..b107bb91a 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -9,22 +9,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Established", "اتصال برقرار شد"), ("connecting_status", "...در حال برقراری ارتباط با سرور"), ("Enable Service", "فعالسازی سرویس"), - ("Start Service", "اجرا سرویس"), + ("Start Service", "اجرای سرویس"), ("Service is running", "سرویس در حال اجرا است"), ("Service is not running", "سرویس اجرا نشده"), ("not_ready_status", "ارتباط برقرار نشد. لطفا شبکه خود را بررسی کنید"), ("Control Remote Desktop", "کنترل دسکتاپ میزبان"), - ("Transfer File", "جابه جایی فایل"), + ("Transfer File", "انتقال فایل"), ("Connect", "اتصال"), ("Recent Sessions", "جلسات اخیر"), ("Address Book", "دفترچه آدرس"), ("Confirmation", "تایید"), ("TCP Tunneling", "TCP تانل"), ("Remove", "حذف"), - ("Refresh random password", "رمز عبور تصادفی را بروز کنید"), + ("Refresh random password", "بروزرسانی رمز عبور تصادفی"), ("Set your own password", "!رمز عبور دلخواه بگذارید"), - ("Enable Keyboard/Mouse", "Keyboard/Mouse فعالسازی"), - ("Enable Clipboard", "Clipboard فعالسازی"), + ("Enable Keyboard/Mouse", " فعالسازی ماوس/صفحه کلید"), + ("Enable Clipboard", "فعال سازی کلیپبورد"), ("Enable File Transfer", "انتقال فایل را فعال کنید"), ("Enable TCP Tunneling", "را فعال کنید TCP تانل"), ("IP Whitelisting", "های مجاز IP لیست"), @@ -34,11 +34,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Import server configuration successfully", "تنظیمات سرور با فایل کانفیگ با موفقیت انجام شد"), ("Export server configuration successfully", "ایجاد فایل کانفیگ از تنظیمات فعلی با موفقیت انجام شد"), ("Invalid server configuration", "تنظیمات سرور نامعتبر است"), - ("Clipboard is empty", "خالی است Clipboard"), + ("Clipboard is empty", "کلیپبورد خالی است"), ("Stop service", "توقف سرویس"), ("Change ID", "تعویض شناسه"), ("Website", "وب سایت"), ("About", "درباره"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "بستن صدا"), ("Audio Input", "ورودی صدا"), ("Enhancements", "بهبودها"), @@ -50,10 +52,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("invalid_http", "شروع شود http:// یا https:// باید با"), ("Invalid IP", "نامعتبر است IP آدرس"), ("id_change_tip", "شناسه باید طبق این شرایط باشد : حروف کوچک و بزرگ انگلیسی و اعداد از 0 تا 9، _ و همچنین حرف اول آن فقط حروف بزرگ یا کوچک انگلیسی و طول آن بین 6 الی 16 کاراکتر باشد"), - ("Invalid format", "فرمت نادرس است"), + ("Invalid format", "فرمت نادرست است"), ("server_not_support", "هنوز توسط سرور مورد نظر پشتیبانی نمی شود"), ("Not available", "در دسترسی نیست"), - ("Too frequent", "تعداد زیاد"), + ("Too frequent", "خیلی رایج"), ("Cancel", "لغو"), ("Skip", "رد کردن"), ("Close", "بستن"), @@ -72,7 +74,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please try 1 minute later", "لطفا بعد از 1 دقیقه مجددا تلاش کنید"), ("Login Error", "ورود ناموفق بود"), ("Successful", "ورود با موفقیت انجام شد"), - ("Connected, waiting for image...", "ارتباط وصل شد. برای دریافت تصویر دسکتاپ میزبان منتظر بمانید..."), + ("Connected, waiting for image...", "...ارتباط برقرار شد. انتظار برای دریافت تصاویر"), ("Name", "نام"), ("Type", "نوع فایل"), ("Modified", "تاریخ تغییر"), @@ -84,20 +86,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local", "محلی"), ("Remote", "از راه دور"), ("Remote Computer", "سیستم میزبان"), - ("Local Computer", "سیستم از راه دور"), - ("Confirm Delete", "حذف را تایید کنید"), + ("Local Computer", "سیستم راه دور"), + ("Confirm Delete", "تایید حذف"), ("Delete", "حذف"), - ("Properties", "Properties"), - ("Multi Select", "انتخاب همزمان"), + ("Properties", "مشخصات"), + ("Multi Select", "انتخاب دسته ای"), ("Select All", "انتخاب همه"), - ("Unselect 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!", "این برگشت ناپذیر است!"), + ("Do this for all conflicts", "این عمل برای همه ی تضادها انجام شود"), + ("This is irreversible!", "این اقدام برگشت ناپذیر است!"), ("Deleting", "در حال حذف"), ("files", "فایل ها"), ("Waiting", "انتظار"), @@ -105,21 +107,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Speed", "سرعت"), ("Custom Image Quality", "سفارشی سازی کیفیت تصاویر"), ("Privacy mode", "حالت حریم خصوصی"), - ("Block user input", "ورودی کاربر را مسدود کنید"), - ("Unblock user input", "قفل ورودی کاربر را باز کنید"), - ("Adjust Window", "پنجره را تنظیم کنید"), + ("Block user input", "بلاک کردن ورودی کاربر"), + ("Unblock user input", "آنبلاک کردن ورودی کاربر"), + ("Adjust Window", "تنظیم پنجره"), ("Original", "اصل"), - ("Shrink", ""), - ("Stretch", ""), - ("Scrollbar", ""), - ("ScrollAuto", ""), + ("Shrink", "کوچک کردن"), + ("Stretch", "کشیدن تصویر"), + ("Scrollbar", "اسکرول بار"), + ("ScrollAuto", "پیمایش/اسکرول خودکار"), ("Good image quality", "کیفیت خوب تصویر"), ("Balanced", "متعادل"), - ("Optimize reaction time", "زمان واکنش را بهینه کنید"), + ("Optimize reaction time", "بهینه سازی زمان واکنش"), ("Custom", "سفارشی"), ("Show remote cursor", "نمایش مکان نما موس میزبان"), ("Show quality monitor", "نمایش کیفیت مانیتور"), - ("Disable clipboard", "Clipboard غیرفعالسازی"), + ("Disable clipboard", " غیرفعالسازی کلیپبورد"), ("Lock after session end", "قفل کردن حساب کاربری سیستم عامل پس از پایان جلسه"), ("Insert", "افزودن"), ("Insert Lock", "افزودن قفل"), @@ -127,23 +129,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID does not exist", "شناسه وجود ندارد"), ("Failed to connect to rendezvous server", "اتصال به سرور تولید شناسه انجام نشد"), ("Please try later", "لطفا بعدا تلاش کنید"), - ("Remote desktop is offline", "دسکتاپ از راه دور خاموش است"), + ("Remote desktop is offline", "دسکتاپ راه دور آفلاین است"), ("Key mismatch", "عدم تطابق کلید"), ("Timeout", "زمان انتظار به پایان رسید"), ("Failed to connect to relay server", "سرور وصل نشد Relay به"), ("Failed to connect via rendezvous server", "اتصال از طریق سرور تولید شناسه انجام نشد"), ("Failed to connect via relay server", "انجام نشد Relay اتصال از طریق سرور"), - ("Failed to make direct connection to remote desktop", "اتصال مستقیم به دسکتاپ از راه دور با موفقیت انجام نشد"), - ("Set Password", "اختصاص رمزعبور"), + ("Failed to make direct connection to remote desktop", "اتصال مستقیم به دسکتاپ راه دور انجام نشد"), + ("Set Password", "تنظیم رمزعبور"), ("OS Password", "رمز عیور سیستم عامل"), ("install_tip", "لطفا برنامه را نصب کنید UAC و جلوگیری از خطای RustDesk برای راحتی در استفاده از نرم افزار"), ("Click to upgrade", "برای ارتقا کلیک کنید"), ("Click to download", "برای دانلود کلیک کنید"), ("Click to update", "برای به روز رسانی کلیک کنید"), ("Configure", "تنظیم"), - ("config_acc", "برای کنترل از راه دور دسکتاپ، باید به RustDesk مجوز \"access\" بدهید"), - ("config_screen", "برای دسترسی از راه دور به دسکتاپ خود، باید به RustDesk مجوزهای \"screenshot\" بدهید."), - ("Installing ...", "در حال نصب..."), + ("config_acc", "بدهید \"access\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), + ("config_screen", "بدهید \"screenshot\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), + ("Installing ...", "...در حال نصب"), ("Install", "نصب"), ("Installation", "نصب و راه اندازی"), ("Installation Path", "محل نصب"), @@ -152,28 +154,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("agreement_tip", "با شروع نصب، شرایط توافق نامه مجوز را می پذیرید"), ("Accept and Install", "قبول و شروع نصب"), ("End-user license agreement", "قرارداد مجوز کاربر نهایی"), - ("Generating ...", "پدید آوردن..."), - ("Your installation is lower version.", "نسخه قبلی نصب شده است"), + ("Generating ...", "...در حال تولید"), + ("Your installation is lower version.", "نسخه قدیمی تری نصب شده است"), ("not_close_tcp_tip", "هنگام استفاده از تونل این پنجره را نبندید"), - ("Listening ...", "انتظار..."), - ("Remote Host", "دستگاه از راه دور"), + ("Listening ...", "...انتظار"), + ("Remote Host", "هاست راه دور"), ("Remote Port", "پورت راه دور"), ("Action", "عملیات"), ("Add", "افزودن"), ("Local Port", "پورت محلی"), ("Local Address", "آدرس محلی"), ("Change Local Port", "تغییر پورت محلی"), - ("setup_server_tip", "برای اتصال سریعتر، سرور اتصال خود را راه اندازی کنید"), + ("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", "را بدهید Clipboard اجازه استفاده از"), - ("Allow hearing sound", "اجازه شنیدن صدا را بدهید"), - ("Allow file copy and paste", "اجازه کپی و چسباندن فایل را بدهید"), + ("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", "و رمزگذاری شده Relay اتصال از طریق"), @@ -181,7 +183,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relayed and unencrypted connection", "و رمزگذاری نشده Relay اتصال از طریق"), ("Enter Remote ID", "شناسه از راه دور را وارد کنید"), ("Enter your password", "زمر عبور خود را وارد کنید"), - ("Logging in...", "در حال ورود..."), + ("Logging in...", "...در حال ورود"), ("Enable RDP session sharing", "اشتراک گذاری جلسه RDP را فعال کنید"), ("Auto Login", "ورود خودکار"), ("Enable Direct IP Access", "دسترسی مستقیم IP را فعال کنید"), @@ -193,7 +195,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter the folder name", "نام پوشه را وارد کنید"), ("Fix it", "بازسازی"), ("Warning", "هشدار"), - ("Login screen using Wayland is not supported", "ورود به سیستم با استفاده از Wayland پشتیبانی نمی شود"), + ("Login screen using Wayland is not supported", "پشتیبانی نمی شود Wayland ورود به سیستم با استفاده از "), ("Reboot required", "راه اندازی مجدد مورد نیاز است"), ("Unsupported display server ", "سرور تصویر پشتیبانی نشده است"), ("x11 expected", ""), @@ -202,16 +204,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username", "نام کاربری"), ("Invalid port", "پورت نامعتبر است"), ("Closed manually by the peer", "به صورت دستی توسط میزبان بسته شد"), - ("Enable remote configuration modification", "تغییرات پیکربندی از راه دور را مجاز کنید"), + ("Enable remote configuration modification", "فعال بودن اعمال تغییرات پیکربندی از راه دور"), ("Run without install", "بدون نصب اجرا شود"), ("Always connected via relay", "متصل است Relay همیشه با"), - ("Always connect via relay", "برای اتصال استفاده کنید Relay از"), - ("whitelist_tip", "فقط آدرس های IP مجاز می توانند به این دسکتاپ متصل شوند"), + ("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 پشتیبانی نمی شود"), + ("Current Wayland display server is not supported", "پشتیبانی نمی شود Wayland سرور نمایش فعلی"), ("whitelist_sep", "با کاما، نقطه ویرگول، فاصله یا خط جدید از هم جدا می شوند"), ("Add ID", "افزودن شناسه"), ("Add Tag", "افزودن برچسب"), @@ -220,19 +227,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "نام کاربری وجود ندارد"), ("Password missed", "رمزعبور وجود ندارد"), ("Wrong credentials", "اعتبارنامه نادرست است"), - ("Edit Tag", "برچسب را تغییر دهید"), - ("Unremember Password", "رمز عبور را ذخیره نکنید"), - ("Favorites", "موارد دلخواه"), + ("Edit Tag", "ویرایش برچسب"), + ("Unremember Password", "رمز عبور ذخیره نشود"), + ("Favorites", "اتصالات دلخواه"), ("Add to Favorites", "افزودن به علاقه مندی ها"), ("Remove from Favorites", "از علاقه مندی ها حذف شود"), ("Empty", "موردی وجود ندارد"), ("Invalid folder name", "نام پوشه نامعتبر است"), ("Socks5 Proxy", "Socks5 Proxy"), - ("Hostname", "Hostname"), + ("Hostname", "نام هاست"), ("Discovered", "پیدا شده"), ("install_daemon_tip", "برای شروع در هنگام راه اندازی، باید سرویس سیستم را نصب کنید"), - ("Remote ID", "شناسه از راه دور"), - ("Paste", "درج کنید"), + ("Remote ID", "شناسه راه دور"), + ("Paste", "درج"), ("Paste here?", "اینجا درج شود؟"), ("Are you sure to close the connection?", "آیا مطمئن هستید که می خواهید اتصال را پایان دهید؟"), ("Download new version", "دانلود نسخه جدید"), @@ -241,7 +248,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-Finger Tap", "با یک انگشت لمس کنید"), ("Left Mouse", "دکمه سمت چپ ماوس"), ("One-Long Tap", "لمس طولانی با یک انگشت"), - ("Two-Finger Tap", "با دو انگشت لمس کنید"), + ("Two-Finger Tap", "لمس دو انگشتی"), ("Right Mouse", "دکمه سمت راست ماوس"), ("One-Finger Move", "با یک انگشت حرکت کنید"), ("Double Tap & Move", "دو ضربه سریع بزنید و حرکت دهید"), @@ -250,7 +257,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mouse Wheel", "چرخ ماوس"), ("Two-Finger Move", "با دو انگشت حرکت کنید"), ("Canvas Move", ""), - ("Pinch to Zoom", "زوم را کوچک کنید"), + ("Pinch to Zoom", "با دو انگشت بکشید تا زوم شود"), ("Canvas Zoom", ""), ("Reset canvas", ""), ("No permission of file transfer", "مجوز انتقال فایل داده نشده"), @@ -261,14 +268,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OPEN", "باز کردن"), ("Chat", "چت"), ("Total", "مجموع"), - ("items", "موارد"), + ("items", "آیتم ها"), ("Selected", "انتخاب شده"), ("Screen Capture", "ضبط صفحه"), ("Input Control", "کنترل ورودی"), ("Audio Capture", "ضبط صدا"), ("File Connection", "ارتباط فایل"), ("Screen Connection", "ارتباط صفحه"), - ("Do you accept?", "شما می پذیرید؟"), + ("Do you accept?", "آیا می پذیرید؟"), ("Open System Setting", "باز کردن تنظیمات سیستم"), ("How to get Android input permission?", "چگونه مجوز ورود به سیستم اندروید را دریافت کنیم؟"), ("android_input_permission_tip1", "برای اینکه یک دستگاه راه دور بتواند دستگاه Android شما را از طریق ماوس یا لمسی کنترل کند، باید به RustDesk اجازه دهید از ویژگی \"Accessibility\" استفاده کند."), @@ -278,9 +285,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_stop_service_tip", "با بستن سرویس، تمام اتصالات برقرار شده به طور خودکار بسته می شود"), ("android_version_audio_tip", "نسخه فعلی اندروید از ضبط صدا پشتیبانی نمی‌کند، لطفاً به اندروید 10 یا بالاتر به‌روزرسانی کنید"), ("android_start_service_tip", "برای شروع سرویس اشتراک‌گذاری صفحه، روی مجوز \"شروع مرحله‌بندی سرور\" یا OPEN \"Screen Capture\" کلیک کنید."), - ("Account", "حساب"), + ("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/en/manual/mac/#enable-permissions"), ("Help", "راهنما"), @@ -291,27 +298,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Peer denied", "توسط میزبان راه دور رد شد"), ("Please install plugins", "لطفا افزونه ها را نصب کنید"), ("Peer exit", "میزبان خارج شد"), - ("Failed to turn off", "خاموش کردن با موفقیت انجام نشد"), + ("Failed to turn off", "خاموش کردن انجام نشد"), ("Turned off", "خاموش شد"), ("In privacy mode", "در حالت حریم خصوصی"), ("Out privacy mode", "خارج از حالت حریم خصوصی"), ("Language", "زبان"), - ("Keep RustDesk background service", "سرویس RustDesk را در پس زمینه نگه دارید"), - ("Ignore Battery Optimizations", "بهینه سازی باتری را نادیده بگیرید"), + ("Keep RustDesk background service", "را در پس زمینه نگه دارید RustDesk سرویس"), + ("Ignore Battery Optimizations", "بهینه سازی باتری نادیده گرفته شود"), ("android_open_battery_optimizations_tip", "به صفحه تنظیمات بعدی بروید"), ("Connection not allowed", "اتصال مجاز نیست"), - ("Legacy mode", "پشتیبانی موارد قدیمی"), - ("Map mode", "حالت نقشه"), + ("Legacy mode", "legacy حالت"), + ("Map mode", "map حالت"), ("Translate mode", "حالت ترجمه"), - ("Use permanent password", "از رمز عبور دائمی استفاده کنید"), - ("Use both passwords", "از هر دو رمز عبور استفاده کنید"), - ("Set permanent password", "یک رمز عبور دائمی تنظیم کنید"), - ("Enable Remote Restart", "فعال کردن راه‌اندازی مجدد از راه دور"), - ("Allow remote restart", "اجازه راه اندازی مجدد از راه دور"), - ("Restart Remote Device", "راه‌اندازی مجدد دستگاه از راه دور"), + ("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", "دستگاه راه دور دوباره راه اندازی می شود. این پیام را ببندید و پس از مدتی با استفاده از یک رمز عبور دائمی دوباره وصل شوید."), + ("Restarting Remote Device", "در حال راه اندازی مجدد دستگاه راه دور"), + ("remote_restarting_tip", "دستگاه راه دور در حال راه اندازی مجدد است. این پیام را ببندید و پس از مدتی با استفاده از یک رمز عبور دائمی دوباره وصل شوید."), ("Copied", "کپی شده است"), ("Exit Fullscreen", "از حالت تمام صفحه خارج شوید"), ("Fullscreen", "تمام صفحه"), @@ -332,30 +339,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "مقیاس تطبیقی"), ("General", "عمومی"), ("Security", "امنیت"), - ("Account", "حساب کاربری"), ("Theme", "نمایه"), ("Dark Theme", "نمایه تیره"), ("Dark", "تیره"), ("Light", "روشن"), - ("Follow System", "سیستم را دنبال کنید"), - ("Enable hardware codec", "از کدک سخت افزاری استفاده کنید"), - ("Unlock Security Settings", "تنظیمات امنیتی را باز کنید"), - ("Enable Audio", "صدا را روشن کنید"), - ("Unlock Network Settings", "باز کردن قفل تنظیمات شبکه"), + ("Follow System", "پیروی از سیستم"), + ("Enable hardware codec", "فعال سازی کدک سخت افزاری"), + ("Unlock Security Settings", "آنلاک شدن تنظیمات امنیتی"), + ("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 را فعال کنید"), - ("Pin menubar", "نوار منو ثابت کنید"), - ("Unpin menubar", "پین نوار منو را بردارید"), + ("Enable RDP", "RDP فعال شدن"), + ("Pin menubar", "پین کردن نوار منو"), + ("Unpin menubar", "آنپین کردن نوار منو"), ("Recording", "در حال ضبط"), ("Directory", "مسیر"), ("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"), @@ -363,7 +368,7 @@ 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", "فعالسازی جستجو در شبکه"), ("Deny LAN Discovery", "غیر فعالسازی جستجو در شبکه"), ("Write a message", "یک پیام بنویسید"), @@ -371,30 +376,62 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please wait for confirmation of UAC...", ""), ("elevated_foreground_window_tip", ""), ("Disconnected", "قطع ارتباط"), - ("Other", "دیگر"), - ("Confirm before closing multiple tabs", "بستن چندین برگه را تأیید کنید"), + ("Other", "سایر"), + ("Confirm before closing multiple tabs", "تایید بستن دسته ای برگه ها"), ("Keyboard Settings", "تنظیمات صفحه کلید"), - ("Custom", "سفارشی"), ("Full Access", "دسترسی کامل"), ("Screen Share", "اشتراک گذاری صفحه"), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), - ("JumpLink", ""), + ("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", "چشم انداز"), ("Please Select the screen to be shared(Operate on the peer side).", "لطفاً صفحه‌ای را برای اشتراک‌گذاری انتخاب کنید (در سمت همتا به همتا کار کنید)."), - ("Show RustDesk", "RustDesk را نشان دهید"), + ("Show RustDesk", "RustDesk نمایش"), ("This PC", "This PC"), ("or", "یا"), ("Continue with", "ادامه با"), - ("Zoom cursor", "نشانگر بزرگنمایی"), + ("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...", "لطفا صبر کنید تا میزبان درخواست شما را قبول کند..."), + ("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", "نوع صفحه کلید محلی را انتخاب کنید"), + ("software_render_tip", "اگر کارت گرافیک Nvidia دارید و پنجره راه دور بلافاصله پس از اتصال بسته می شود، درایور nouveau را نصب نمایید و انتخاب گزینه استفاده از رندر نرم افزار می تواند کمک کننده باشد. راه اندازی مجدد نرم افزار مورد نیاز است."), + ("Always use software rendering", "همیشه از رندر نرم افزاری استفاده کنید"), + ("config_input", "برای کنترل دسکتاپ از راه دور با صفحه کلید، باید مجوز RustDesk \"Input Monitoring\" را بدهید."), + ("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?", "لطفاً تأیید کنید که آیا می خواهید دسکتاپ خود را به اشتراک بگذارید؟"), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 53c3b3bfa..ea2dbfede 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"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Changer d'ID"), ("Website", "Site Web"), ("About", "À propos de"), + ("Slogan_tip", "Fait avec cœur dans ce monde chaotique!"), + ("Privacy Statement", "Déclaration de confidentialité"), ("Mute", "Muet"), ("Audio Input", "Entrée audio"), ("Enhancements", "Améliorations"), @@ -172,7 +174,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"), @@ -181,7 +183,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"), @@ -194,7 +196,7 @@ 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"), + ("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"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Forcer la connexion relais"), ("whitelist_tip", "Seul l'IP dans 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"), @@ -217,11 +224,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -296,7 +303,7 @@ 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 service arrière plan"), ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), ("Connection not allowed", "Connexion non autorisée"), @@ -316,7 +323,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"), @@ -328,11 +335,10 @@ 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"), ("Dark", "Sombre"), @@ -345,7 +351,6 @@ 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"), @@ -360,8 +365,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Directory", "Répertoire"), ("Automatically record incoming sessions", "Enregistrement automatique des session 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"), @@ -374,7 +379,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."), @@ -397,5 +401,37 @@ 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", "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", "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\"."), + ("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 ?"), ].iter().cloned().collect(); } diff --git a/src/lang/gr.rs b/src/lang/gr.rs index 93b75d54e..6ec1152cd 100644 --- a/src/lang/gr.rs +++ b/src/lang/gr.rs @@ -1,401 +1,437 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Κατάσταση"), - ("Your Desktop", "Ο σταθμός εργασίας σας"), - ("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το αναγνωριστικό και τον κωδικό πρόσβασης."), - ("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 Tunneling"), - ("Remove", "Κατάργηση"), - ("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"), - ("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"), - ("Enable Keyboard/Mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"), - ("Enable Clipboard", "Ενεργοποίηση Προχείρου"), - ("Enable File Transfer", "Ενεργοποίηση μεταφοράς αρχείων"), - ("Enable TCP Tunneling", "Ενεργοποίηση TCP Tunneling"), - ("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", "Πληροφορίες"), - ("Mute", "Σίγαση"), - ("Audio Input", "Είσοδος ήχου"), - ("Enhancements", "Βελτιώσεις"), - ("Hardware Codec", "Κωδικοποιητής υλικού"), - ("Adaptive Bitrate", "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", "Αποτυχία σύνδεσης με διακομιστή"), - ("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 \"Screen Capture\"."), - ("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, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."), - ("android_input_permission_tip2", "Παρακαλώ μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."), - ("android_new_connection_tip", "θέλω να ελέγξω τη συσκευή σου."), - ("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."), - ("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/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", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"), - ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), - ("Connection not allowed", "Η σύνδεση απορρίφθηκε"), - ("Legacy mode", "Λειτουργία συμβατότητας"), - ("Map 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", "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", "Ενεργοποίηση εντοπισμού 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 απαιτεί υψηλότερη έκδοση του linux distro. Δοκιμάστε την επιφάνεια εργασίας 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", "Να επιτρέπεται η απόκρυψη, μόνο εάν αποδέχεστε συνδέσεις μέσω κωδικού πρόσβασης και χρησιμοποιείτε μόνιμο κωδικό πρόσβασης"), + ("Status", "Κατάσταση"), + ("Your Desktop", "Ο σταθμός εργασίας σας"), + ("desk_tip", "Η πρόσβαση στον σταθμό εργασίας σας είναι δυνατή με αυτό το αναγνωριστικό και τον κωδικό πρόσβασης."), + ("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 Tunneling"), + ("Remove", "Κατάργηση"), + ("Refresh random password", "Νέος τυχαίος κωδικός πρόσβασης"), + ("Set your own password", "Ορίστε τον δικό σας κωδικό πρόσβασης"), + ("Enable Keyboard/Mouse", "Ενεργοποίηση πληκτρολογίου/ποντικιού"), + ("Enable Clipboard", "Ενεργοποίηση Προχείρου"), + ("Enable File Transfer", "Ενεργοποίηση μεταφοράς αρχείων"), + ("Enable TCP Tunneling", "Ενεργοποίηση TCP Tunneling"), + ("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", "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", "Αποτυχία σύνδεσης με διακομιστή"), + ("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 \"Screen Capture\"."), + ("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", "Σύνδεση"), + ("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", "Προσθήκη ετικέτας"), + ("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, πρέπει να επιτρέψετε στο RustDesk να χρησιμοποιεί την υπηρεσία \"Προσβασιμότητα\"."), + ("android_input_permission_tip2", "Παρακαλώ μεταβείτε στην επόμενη σελίδα ρυθμίσεων συστήματος, βρείτε και πληκτρολογήστε [Εγκατεστημένες υπηρεσίες], ενεργοποιήστε την υπηρεσία [Είσοδος RustDesk]."), + ("android_new_connection_tip", "θέλω να ελέγξω τη συσκευή σου."), + ("android_service_will_start_tip", "Η ενεργοποίηση της κοινής χρήσης οθόνης θα ξεκινήσει αυτόματα την υπηρεσία, ώστε άλλες συσκευές να μπορούν να ελέγχουν αυτήν τη συσκευή Android."), + ("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/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", "Παράβλεψη βελτιστοποιήσεων μπαταρίας"), + ("android_open_battery_optimizations_tip", "Θέλετε να ανοίξετε τις ρυθμίσεις βελτιστοποίησης μπαταρίας;"), + ("Connection not allowed", "Η σύνδεση απορρίφθηκε"), + ("Legacy mode", "Λειτουργία συμβατότητας"), + ("Map 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", "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", "Σκούρο θέμα"), + ("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", "Ενεργοποίηση εντοπισμού 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 απαιτεί υψηλότερη έκδοση του linux distro. Δοκιμάστε την επιφάνεια εργασίας 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 the web console", "Κλειστό χειροκίνητα από την κονσόλα web"), + ("Local keyboard type", "Τύπος τοπικού πληκτρολογίου"), + ("Select local keyboard type", "Επιλογή τύπου τοπικού πληκτρολογίου"), + ("software_render_tip", "Εάν έχετε κάρτα γραφικών Nvidia και το παράθυρο σύνδεσης κλείνει αμέσως μετά τη σύνδεση, η εγκατάσταση του προγράμματος οδήγησης nouveau και η επιλογή χρήσης της επιτάχυνσης γραφικών μέσω λογισμικού μπορεί να βοηθήσει. Απαιτείται επανεκκίνηση."), + ("Always use software rendering", "Επιτάχυνση γραφικών μέσω λογισμικού"), + ("config_input", "Για να ελέγξετε την απομακρυσμένη επιφάνεια εργασίας με πληκτρολόγιο, πρέπει να εκχωρήσετε δικαιώματα στο 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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 417c83f45..295104a67 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Azonosító megváltoztatása"), ("Website", "Weboldal"), ("About", "Rólunk"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Némítás"), ("Audio Input", "Hangátvitel"), ("Enhancements", "Fejlesztések"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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..."), @@ -332,7 +339,6 @@ 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"), ("Dark", "Sötét"), @@ -345,7 +351,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"), @@ -374,7 +379,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."), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index b76bb687d..5604a0c52 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -15,7 +15,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("not_ready_status", "Belum siap. Silakan periksa koneksi Anda"), ("Control Remote Desktop", "Kontrol Remote Desktop"), ("Transfer File", "File Transfer"), - ("Connect", "Menghubung"), + ("Connect", "Terhubung"), ("Recent Sessions", "Sesi Terkini"), ("Address Book", "Buku Alamat"), ("Confirmation", "Konfirmasi"), @@ -30,15 +30,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "Daftar Putih IP"), ("ID/Relay Server", "ID/Relay Server"), ("Import Server Config", "Impor Konfigurasi Server"), - ("Export Server Config", ""), + ("Export Server Config", "Ekspor Konfigutasi Server"), ("Import server configuration successfully", "Impor konfigurasi server berhasil"), - ("Export server configuration successfully", ""), + ("Export server configuration successfully", "Ekspor konfigurasi server berhasil"), ("Invalid server configuration", "Konfigurasi server tidak valid"), ("Clipboard is empty", "Papan klip kosong"), ("Stop service", "Hentikan Layanan"), ("Change ID", "Ubah ID"), ("Website", "Website"), ("About", "Tentang"), + ("Slogan_tip", ""), + ("Privacy Statement", "Pernyataan Privasi"), ("Mute", "Bisukan"), ("Audio Input", "Masukkan Audio"), ("Enhancements", "Peningkatan"), @@ -58,16 +60,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Skip", "Lanjutkan"), ("Close", "Tutup"), ("Retry", "Ulangi"), - ("OK", "OK"), - ("Password Required", "Password dibutukan"), - ("Please enter your password", "Silahkan masukkan password anda"), + ("OK", "Oke"), + ("Password Required", "Kata sandi dibutuhkan"), + ("Please enter your password", "Silahkan masukkan kata sandi anda"), ("Remember password", "Ingat Password"), - ("Wrong Password", "Password Salah"), + ("Wrong Password", "Kata sandi Salah"), ("Do you want to enter again?", "Apakah anda ingin masuk lagi?"), ("Connection Error", "Kesalahan koneksi"), ("Error", "Kesalahan"), ("Reset by the peer", "Setel ulang oleh rekan"), - ("Connecting...", "Hubungkan..."), + ("Connecting...", "Menghubungkan..."), ("Connection in progress. Please wait.", "Koneksi sedang berlangsung. Mohon tunggu."), ("Please try 1 minute later", "Silahkan coba 1 menit lagi"), ("Login Error", "Kesalahan Login"), @@ -108,7 +110,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Block user input", "Blokir masukan pengguna"), ("Unblock user input", "Jangan blokir masukan pengguna"), ("Adjust Window", "Sesuaikan Jendela"), - ("Original", "Original"), + ("Original", "Asli"), ("Shrink", "Susutkan"), ("Stretch", "Regangkan"), ("Scrollbar", "Scroll bar"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -332,7 +339,6 @@ 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"), ("Dark", "Gelap"), @@ -345,7 +351,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", ""), @@ -374,28 +379,59 @@ 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."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."), ("JumpLink", "View"), ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan (Operasi di sisi rekan)."), - ("Show RustDesk", ""), - ("This PC", ""), - ("or", ""), - ("Continue with", ""), + ("Show RustDesk", "Tampilkan RustDesk"), + ("This PC", "PC ini"), + ("or", "atau"), + ("Continue with", "Lanjutkan dengan"), ("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", ""), + ("Accept sessions via password", "Izinkan sesi dengan kata sandi"), + ("Accept sessions via click", "Izinkan sesi dengan klik"), + ("Accept sessions via both", "Izinkan sesi dengan keduanya"), + ("Please wait for the remote side to accept your session request...", "Harap tunggu sisi jarak jauh untuk menerima permintaan sesi Anda..."), + ("One-time Password", "Kata sandi satu kali"), + ("Use one-time password", "Gunakan kata sandi satu kali"), ("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", "Pencarian"), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 523386eb5..7b979aff0 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"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Cambia ID"), ("Website", "Sito web"), ("About", "Informazioni"), + ("Slogan_tip", "Fatta con il cuore in questo mondo caotico!"), + ("Privacy Statement", "Informativa sulla privacy"), ("Mute", "Silenzia"), ("Audio Input", "Input audio"), ("Enhancements", "Miglioramenti"), @@ -116,7 +118,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Qualità immagine migliore"), ("Balanced", "Bilanciato"), ("Optimize reaction time", "Ottimizza il tempo di reazione"), - ("Custom", "Personalizza"), + ("Custom", "Personalizzato"), ("Show remote cursor", "Mostra il cursore remoto"), ("Show quality monitor", "Visualizza qualità video"), ("Disable clipboard", "Disabilita appunti"), @@ -183,7 +185,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"), @@ -193,21 +195,26 @@ 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"), ("x11 expected", "x11 necessario"), ("Port", "Porta"), ("Settings", "Impostazioni"), ("Username", " Nome utente"), - ("Invalid port", "Porta non valida"), + ("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"), + ("Run without install", "Esegui senza installare"), ("Always connected via relay", "Connesso sempre tramite relay"), - ("Always connect via relay", "Connetti sempre 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"), @@ -217,8 +224,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -278,7 +285,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), - ("Account", ""), + ("Account", "Account"), ("Overwrite", "Sovrascrivi"), ("This file exists, skip or overwrite this file?", "Questo file esiste, saltare o sovrascrivere questo file?"), ("Quit", "Esci"), @@ -325,14 +332,13 @@ 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"), ("Dark", "Scuro"), @@ -342,10 +348,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"), @@ -367,14 +372,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."), @@ -397,5 +401,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Richiedi l'accesso al tuo dispositivo"), ("Hide connection management window", "Nascondi la finestra di gestione delle connessioni"), ("hide_cm_tip", "Permetti di nascondere solo se si accettano sessioni con password permanente"), + ("wayland_experiment_tip", "Il supporto Wayland è in fase sperimentale, utilizza X11 se necessiti di un accesso stabile."), + ("Right click to select tabs", "Clic con il tasto destro per selezionare le schede"), + ("Skipped", "Saltato"), + ("Add to Address Book", "Aggiungi alla rubrica"), + ("Group", "Gruppo"), + ("Search", "Cerca"), + ("Closed manually by the 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\"."), + ("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?"), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 8d806416d..a280940c7 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "IDを変更"), ("Website", "公式サイト"), ("About", "情報"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "ミュート"), ("Audio Input", "音声入力デバイス"), ("Enhancements", "追加機能"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "常に中継サーバー経由で接続"), ("whitelist_tip", "ホワイトリストに登録されたIPからのみ接続を許可します"), ("Login", "ログイン"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "ログアウト"), ("Tags", "タグ"), ("Search ID", "IDを検索"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "フィットウィンドウ"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,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 以降のバージョンが必要です。"), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index 9f8027be7..1cdf529ce 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID 변경"), ("Website", "웹사이트"), ("About", "정보"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "음소거"), ("Audio Input", "오디오 입력"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "항상 relay를 통해 접속하기"), ("whitelist_tip", "화이트리스트에 있는 IP만 현 데스크탑에 접속 가능합니다"), ("Login", "로그인"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "로그아웃"), ("Tags", "태그"), ("Search ID", "ID 검색"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "맞는 창"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,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 이상 버전이 필요합니다."), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 3a8c27cf3..59d26135f 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID ауыстыру"), ("Website", "Web-сайт"), ("About", "Туралы"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Дыбыссыздандыру"), ("Audio Input", "Аудио Еңгізу"), ("Enhancements", "Жақсартулар"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "Әрқашан да релай сербері арқылы қосылу"), ("whitelist_tip", "Маған тек ақ-тізімделген IP қол жеткізе алады"), ("Login", "Кіру"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Шығу"), ("Tags", "Тақтар"), ("Search ID", "ID Іздеу"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Scale adaptive"), ("General", ""), ("Security", ""), - ("Account", "Есепкі"), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", "Порт"), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,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 немесе одан жоғары нұсқасын қажет етеді."), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index dae77ed88..ee4b45334 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", "Aby połaczyć się z tym urządzeniem należy użyć tego ID i hasła."), + ("desk_tip", "W celu zestawienia połączenia z tym urządzeniem należy poniższego ID i hasła."), ("Password", "Hasło"), ("Ready", "Gotowe"), ("Established", "Nawiązano"), @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Usługa uruchomiona"), ("Service is not running", "Usługa nie jest uruchomiona"), ("not_ready_status", "Brak gotowości"), - ("Control Remote Desktop", "Kontroluj zdalny pulpit"), + ("Control Remote Desktop", "Połącz się z"), ("Transfer File", "Transfer plików"), ("Connect", "Połącz"), ("Recent Sessions", "Ostatnie sesje"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Zmień ID"), ("Website", "Strona internetowa"), ("About", "O"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Wycisz"), ("Audio Input", "Wejście audio"), ("Enhancements", "Ulepszenia"), @@ -118,7 +120,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Optimize reaction time", "Zoptymalizuj czas reakcji"), ("Custom", "Własne"), ("Show remote cursor", "Pokazuj zdalny kursor"), - ("Show quality monitor", "Pokazuj jakość monitora"), + ("Show quality monitor", "Parametry połączenia"), ("Disable clipboard", "Wyłącz schowek"), ("Lock after session end", "Zablokuj po zakończeniu sesji"), ("Insert", "Wyślij"), @@ -137,9 +139,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), - ("Click to upgrade", "Kliknij, aby zaktualizować (upgrade)"), - ("Click to download", "Kliknij, aby pobrać"), - ("Click to update", "Kliknij, aby zaktualizować (update)"), + ("Click to upgrade", "Zaktualizuj"), + ("Click to download", "Pobierz"), + ("Click to update", "Uaktualinij"), ("Configure", "Konfiguruj"), ("config_acc", "Konfiguracja konta"), ("config_screen", "Konfiguracja ekranu"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), ("Login", "Zaloguj"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "Wyloguj"), ("Tags", "Tagi"), ("Search ID", "Szukaj ID"), @@ -314,25 +321,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("remote_restarting_tip", "Trwa ponownie uruchomienie zdalnego urządzenia, zamknij ten komunikat i ponownie nawiąż za chwilę połączenie używając hasła permanentnego"), ("Copied", "Skopiowano"), ("Exit Fullscreen", "Wyłączyć tryb pełnoekranowy"), - ("Fullscreen", "Pełny ekran"), - ("Mobile Actions", "Działania mobilne"), - ("Select Monitor", "Wybierz Monitor"), - ("Control Actions", "Działania kontrolne"), + ("Fullscreen", "Tryb pełnoekranowy"), + ("Mobile Actions", "Dostępne mobilne polecenia"), + ("Select Monitor", "Wybierz ekran"), + ("Control Actions", "Dostępne polecenia"), ("Display Settings", "Ustawienia wyświetlania"), ("Ratio", "Proporcje"), ("Image Quality", "Jakość obrazu"), ("Scroll Style", "Styl przewijania"), ("Show Menubar", "Pokaż pasek menu"), ("Hide Menubar", "Ukryj pasek menu"), - ("Direct Connection", "Połącznie Bezpośrednie"), - ("Relay Connection", "Połączenie Pośrednie"), - ("Secure Connection", "Połączenie Bezpieczne"), - ("Insecure Connection", "Połączenie Niebezpieczne"), + ("Direct Connection", "Połącznie bezpośrednie"), + ("Relay Connection", "Połączenie przez bramkę"), + ("Secure Connection", "Połączenie szyfrowane"), + ("Insecure Connection", "Połączenie nieszyfrowane"), ("Scale original", "Skaluj oryginalnie"), ("Scale adaptive", "Skaluj adaptacyjnie"), ("General", "Ogólne"), ("Security", "Zabezpieczenia"), - ("Account", "Konto"), ("Theme", "Motyw"), ("Dark Theme", "Ciemny motyw"), ("Dark", "Ciemny"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Serwer"), ("Direct IP Access", "Bezpośredni Adres IP"), ("Proxy", "Proxy"), - ("Port", "Port"), ("Apply", "Zastosuj"), ("Disconnect all devices?", "Czy rozłączyć wszystkie urządzenia?"), ("Clear", "Wyczyść"), @@ -364,17 +369,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop session recording", "Zatrzymaj nagrywanie sesji"), ("Enable Recording Session", "Włącz Nagrywanie Sesji"), ("Allow recording session", "Zezwól na nagrywanie sesji"), - ("Enable LAN Discovery", "Włącz Wykrywanie LAN"), - ("Deny LAN Discovery", "Zablokuj Wykrywanie LAN"), + ("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...", ""), + ("Please wait for confirmation of UAC...", "Oczekuje potwierdzenia ustawień UAC"), ("elevated_foreground_window_tip", ""), ("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."), @@ -385,17 +389,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("This PC", "Ten komputer"), ("or", "albo"), ("Continue with", "Kontynuuj z"), - ("Elevate", "Podwyższ"), + ("Elevate", "Uzyskaj uprawnienia"), ("Zoom cursor", "Zoom kursora"), - ("Accept sessions via password", "Akceptuj sesje używając hasła"), - ("Accept sessions via click", "Akceptuj sesję klikając"), - ("Accept sessions via both", "Akceptuj sesjęna dwa sposoby"), - ("Please wait for the remote side to accept your session request...", "Proszę czekać aż zdalny host zaakceptuje Twoją prośbę..."), + ("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"), + ("Please wait for the remote side to accept your session request...", "Oczekiwanie, na zatwierdzenie sesji przez host zdalny..."), ("One-time Password", "Hasło jednorazowe"), ("Use one-time password", "Użyj hasła jednorazowego"), ("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", ""), + ("Hide connection management window", "Ukryj okno zarządzania połączeniem"), ("hide_cm_tip", ""), + ("wayland_experiment_tip", ""), + ("Right click to select tabs", ""), + ("Skipped", ""), + ("Add to Address Book", "Dodaj do Książki Adresowej"), + ("Group", "Grypy"), + ("Search", "Szukaj"), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index bc5fbbdfd..66373a5e9 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Silenciar"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), @@ -116,7 +118,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Qualidade visual boa"), ("Balanced", "Equilibrada"), ("Optimize reaction time", "Optimizar tempo de reacção"), - ("Custom", "Personalizado"), + ("Custom", ""), ("Show remote cursor", "Mostrar cursor remoto"), ("Show quality monitor", ""), ("Disable clipboard", "Desabilitar área de transferência"), @@ -197,7 +199,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Reboot required", "Reinicialização necessária"), ("Unsupported display server ", "Servidor de display não suportado"), ("x11 expected", "x11 em falha"), - ("Port", "Porta"), + ("Port", ""), ("Settings", "Configurações"), ("Username", "Nome de utilizador"), ("Invalid port", "Porta inválida"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Escala adaptável"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,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."), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index 0d77eb905..5a137f391 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Alterar ID"), ("Website", "Website"), ("About", "Sobre"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Desativar som"), ("Audio Input", "Entrada de Áudio"), ("Enhancements", "Melhorias"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -332,7 +339,6 @@ 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"), ("Dark", "Escuro"), @@ -345,7 +351,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"), @@ -374,7 +379,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."), @@ -397,5 +401,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Solicitar acesso ao seu dispositivo"), ("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", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 81a427d51..8103ae3a3 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -3,7 +3,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Статус"), ("Your Desktop", "Ваш рабочий стол"), - ("desk_tip", "Ваш рабочий стол доступен с этим идентификатором и паролем"), + ("desk_tip", "Ваш рабочий стол доступен с этим ID и паролем"), ("Password", "Пароль"), ("Ready", "Готово"), ("Established", "Установлено"), @@ -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", "Подключиться"), @@ -33,12 +33,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Export Server Config", "Экспортировать конфигурацию сервера"), ("Import server configuration successfully", "Конфигурация сервера успешно импортирована"), ("Export server configuration successfully", "Конфигурация сервера успешно экспортирована"), - ("Invalid server configuration", "Недопустимая конфигурация сервера"), + ("Invalid server configuration", "Неправильная конфигурация сервера"), ("Clipboard is empty", "Буфер обмена пуст"), ("Stop service", "Остановить службу"), ("Change ID", "Изменить ID"), ("Website", "Сайт"), ("About", "О программе"), + ("Slogan_tip", "Сделано с душой в этом безумном мире!"), + ("Privacy Statement", "Заявление о конфиденциальности"), ("Mute", "Отключить звук"), ("Audio Input", "Аудиовход"), ("Enhancements", "Улучшения"), @@ -48,30 +50,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", "Неверный формат"), + ("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", "Попробовать снова"), + ("Retry", "Повторить"), ("OK", "ОК"), ("Password Required", "Требуется пароль"), - ("Please enter your password", "Пожалуйста, введите пароль"), + ("Please enter your password", "Введите пароль"), ("Remember password", "Запомнить пароль"), - ("Wrong Password", "Неверный пароль"), - ("Do you want to enter again?", "Хотите снова войти?"), + ("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 минуту"), + ("Connection in progress. Please wait.", "Выполняется подключение. Подождите."), + ("Please try 1 minute later", "Попробуйте через минуту"), ("Login Error", "Ошибка входа"), - ("Successful", "Операция успешна"), + ("Successful", "Успешно"), ("Connected, waiting for image...", "Подключено, ожидание изображения..."), ("Name", "Имя"), ("Type", "Тип"), @@ -93,10 +95,10 @@ 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?", "Вы уверены, что хотите удалить файл из этой папки?"), - ("Do this for all conflicts", "Это относится ко всем конфликтам"), + ("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", "файлы"), @@ -125,21 +127,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Insert Lock", "Установить замок"), ("Refresh", "Обновить"), ("ID does not exist", "ID не существует"), - ("Failed to connect to rendezvous server", "Не удалось подключиться к промежуточному серверу"), + ("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", "Не удалось установить прямое подключение к удалённому рабочему столу"), + ("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", "Нажмите, чтобы обновить"), + ("Click to upgrade", "Нажмите для проверки обновлений"), + ("Click to download", "Нажмите для скачивания"), + ("Click to update", "Нажмите для обновления"), ("Configure", "Настроить"), ("config_acc", "Чтобы удалённо управлять своим рабочим столом, вы должны предоставить RustDesk права \"доступа\""), ("config_screen", "Для удалённого доступа к рабочему столу вы должны предоставить RustDesk права \"снимок экрана\""), @@ -156,7 +158,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Your installation is lower version.", "Установлена более ранняя версия"), ("not_close_tcp_tip", "Не закрывать это окно при использовании туннеля"), ("Listening ...", "Ожидание..."), - ("Remote Host", "Удалённая машина"), + ("Remote Host", "Удалённый узел"), ("Remote Port", "Удалённый порт"), ("Action", "Действие"), ("Add", "Добавить"), @@ -190,24 +192,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create Desktop Shortcut", "Создать ярлык на рабочем столе"), ("Change Path", "Изменить путь"), ("Create Folder", "Создать папку"), - ("Please enter the folder name", "Пожалуйста, введите имя папки"), + ("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", "Настройки"), - ("Username", "Имя пользователя"), - ("Invalid port", "Неверный порт"), + ("Username", "Пользователь"), + ("Invalid port", "Неправильный порт"), ("Closed manually by the peer", "Закрыто удалённым узлом вручную"), ("Enable remote configuration modification", "Разрешить удалённое изменение конфигурации"), ("Run without install", "Запустить без установки"), - ("Always connected via relay", "Всегда подключён через ретрансляционный сервер"), + ("Always connected 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"), @@ -215,11 +222,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("whitelist_sep", "Раздельно запятой, точкой с запятой, пробелом или новой строкой"), ("Add ID", "Добавить ID"), ("Add Tag", "Добавить ключевое слово"), - ("Unselect all tags", "Отменить выбор всех тегов"), + ("Unselect all tags", "Отменить выбор всех меток"), ("Network error", "Ошибка сети"), ("Username missed", "Имя пользователя отсутствует"), ("Password missed", "Забыли пароль"), - ("Wrong credentials", "Неверные учётные данные"), + ("Wrong credentials", "Неправильные учётные данные"), ("Edit Tag", "Изменить метку"), ("Unremember Password", "Не сохранять пароль"), ("Favorites", "Избранное"), @@ -227,14 +234,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove from Favorites", "Удалить из избранного"), ("Empty", "Пусто"), ("Invalid folder name", "Недопустимое имя папки"), - ("Socks5 Proxy", "Socks5-прокси"), - ("Hostname", "Имя"), + ("Socks5 Proxy", "SOCKS5-прокси"), + ("Hostname", "Узел"), ("Discovered", "Найдено"), ("install_daemon_tip", "Для запуска при загрузке необходимо установить системную службу"), - ("Remote ID", "Удалённый идентификатор"), + ("Remote ID", "Удалённый ID"), ("Paste", "Вставить"), ("Paste here?", "Вставить сюда?"), - ("Are you sure to close the connection?", "Вы уверены, что хотите завершить подключение?"), + ("Are you sure to close the connection?", "Завершить подключение?"), ("Download new version", "Скачать новую версию"), ("Touch mode", "Сенсорный режим"), ("Mouse mode", "Режим мыши"), @@ -289,9 +296,9 @@ 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", "Не удалось отключить"), + ("Failed to turn off", "Невозможно отключить"), ("Turned off", "Отключён"), ("In privacy mode", "В режиме конфиденциальности"), ("Out privacy mode", "Выход из режима конфиденциальности"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Адаптивный масштаб"), ("General", "Общие"), ("Security", "Безопасность"), - ("Account", "Аккаунт"), ("Theme", "Тема"), ("Dark Theme", "Тёмная тема"), ("Dark", "Тёмная"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "Сервер"), ("Direct IP Access", "Прямой IP-доступ"), ("Proxy", "Прокси"), - ("Port", "Порт"), ("Apply", "Применить"), ("Disconnect all devices?", "Отключить все устройства?"), ("Clear", "Очистить"), @@ -372,15 +377,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("elevated_foreground_window_tip", "Текущее окно удалённого рабочего стола требует более высоких привилегий для работы, поэтому временно невозможно использовать мышь и клавиатуру. Можно попросить удалённого пользователя свернуть текущее окно или нажать кнопку повышения прав в окне управления подключением. Чтобы избежать этой проблемы в дальнейшем, рекомендуется выполнить установку программного обеспечения на удалённом устройстве."), ("Disconnected", "Отключено"), ("Other", "Другое"), - ("Confirm before closing multiple tabs", "Подтверждение закрытия несколько вкладок"), + ("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).", "Пожалуйста, выберите экран для совместного использования (работайте на одноранговой стороне)."), + ("Please Select the screen to be shared(Operate on the peer side).", "Выберите экран для совместного использования (работайте на одноранговой стороне)."), ("Show RustDesk", "Показать RustDesk"), ("This PC", "Этот компьютер"), ("or", "или"), @@ -397,5 +401,37 @@ 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 находится на экспериментальной стадии, используйте 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", "Выберите тип локальной клавиатуры"), + ("software_render_tip", "Если у вас видеокарта Nvidia и удалённое окно закрывается сразу после подключения, может помочь установка драйвера Nouveau и выбор использования программной визуализации. Потребуется перезапуск."), + ("Always use software rendering", "Использовать программную визуализацию"), + ("config_input", "Чтобы управлять удалённым рабочим столом с помощью клавиатуры, необходимо предоставить 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?", "Подтвердите, что хотите поделиться своим рабочим столом?"), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 33f2be7ab..c735cb28c 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Zmeniť ID"), ("Website", "Webová stránka"), ("About", "O RustDesk"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Stíšiť"), ("Audio Input", "Zvukový vstup"), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Prispôsobivá mierka"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,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."), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs new file mode 100755 index 000000000..6a17cc906 --- /dev/null +++ b/src/lang/sl.rs @@ -0,0 +1,437 @@ +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"), + ("Website", "Spletna stran"), + ("About", "O programu"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Izklopi zvok"), + ("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"), + ("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."), + ("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"), + ("Always connected via relay", "Vedno povezan preko posrednika"), + ("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"), + ("Current Wayland display server is not supported", "Trenutni Wayland zaslonski strežnik ni podprt"), + ("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"), + ("CLOSE", "ZAPRI"), + ("OPEN", "ODPRI"), + ("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", "Tapnite »Zaženi storitev« ali »ODPRI« pri dovoljenju za zajem zaslona da zaženete storitev deljenja zaslona."), + ("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«"), + ("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"), + ("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 the 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", ""), + ("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?", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/sq.rs b/src/lang/sq.rs new file mode 100644 index 000000000..ebb43f6b7 --- /dev/null +++ b/src/lang/sq.rs @@ -0,0 +1,437 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Statusi"), + ("Your Desktop", "Desktopi juaj"), + ("desk_tip", "Desktopi juaj mund të aksesohet me këtë ID dhe fjalëkalim."), + ("Password", "fjalëkalimi"), + ("Ready", "Gati"), + ("Established", "I themeluar"), + ("connecting_status", "statusi_i_lidhjes"), + ("Enable Service", "Aktivizo Shërbimin"), + ("Start Service", "Nis Shërbimin"), + ("Service is running", "Shërbimi është duke funksionuar"), + ("Service is not running", "Shërbimi nuk është duke funksionuar"), + ("not_ready_status", "Jo gati.Ju lutem kontolloni lidhjen tuaj."), + ("Control Remote Desktop", "Kontrolli i desktopit në distancë"), + ("Transfer File", "Transfero dosje"), + ("Connect", "Lidh"), + ("Recent Sessions", "Sessioni i fundit"), + ("Address Book", "Libër adresash"), + ("Confirmation", "Konfirmimi"), + ("TCP Tunneling", "TCP tunel"), + ("Remove", "Hiqni"), + ("Refresh random password", "Rifreskoni fjalëkalimin e rastësishëm"), + ("Set your own password", "Vendosni fjalëkalimin tuaj"), + ("Enable Keyboard/Mouse", "Aktivizoni Tastierën/Mousin"), + ("Enable Clipboard", "Aktivizo"), + ("Enable File Transfer", "Aktivizoni transferimin e skedarëve"), + ("Enable TCP Tunneling", "Aktivizoni TCP Tunneling"), + ("IP Whitelisting", ""), + ("ID/Relay Server", "ID/server rele"), + ("Import Server Config", "Konfigurimi i severit të importit"), + ("Export Server Config", "Konfigurimi i severit të eksportit"), + ("Import server configuration successfully", "Konfigurimi i severit të importit i suksesshëm"), + ("Export server configuration successfully", "Konfigurimi i severit të eksprotit i suksesshëm"), + ("Invalid server configuration", "Konfigurim i pavlefshëm i serverit"), + ("Clipboard is empty", "Clipboard është bosh"), + ("Stop service", "Ndaloni shërbimin"), + ("Change ID", "Ndryshoni ID"), + ("Website", "Faqe ëebi"), + ("About", "Rreth"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Pa zë"), + ("Audio Input", "Inputi zërit"), + ("Enhancements", "Përmirësimet"), + ("Hardware Codec", "Kodeku Harduerik"), + ("Adaptive Bitrate", "Shpejtësia adaptive e biteve"), + ("ID Server", "ID e serverit"), + ("Relay Server", "Serveri rele"), + ("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"), + ("Too frequent", "Shumë i përdorur"), + ("Cancel", "Anullo"), + ("Skip", "Kalo"), + ("Close", "Mbyll"), + ("Retry", "Riprovo"), + ("OK", "OK"), + ("Password Required", "Fjalëkalimi i detyrueshëm"), + ("Please enter your password", "Ju lutem vendosni fjalëkalimin tuaj"), + ("Remember password", "Mbani mend fjalëkalimin"), + ("Wrong Password", "Fjalëkalim i gabuar"), + ("Do you want to enter again?", "Dëshironi të vendosni përsëri"), + ("Connection Error", "Gabim në lidhje"), + ("Error", "Gabim"), + ("Reset by the peer", "Riseto nga peer"), + ("Connecting...", "Duke u lidhur"), + ("Connection in progress. Please wait.", "Lidhja në progres. Ju lutem prisni"), + ("Please try 1 minute later", "Ju lutemi provoni 1 minut më vonë"), + ("Login Error", "Gabim në login"), + ("Successful", "E suksesshme"), + ("Connected, waiting for image...", "E lidhur , prisni për imazhin..."), + ("Name", "Emri"), + ("Type", "Shkruaj"), + ("Modified", "E modifikuar"), + ("Size", "Madhesia"), + ("Show Hidden Files", "Shfaq skedarët e fshehur"), + ("Receive", "Merr"), + ("Send", "Dërgo"), + ("Refresh File", "Rifreskoni skedarët"), + ("Local", "Lokal"), + ("Remote", "Në distancë"), + ("Remote Computer", "Kompjuter në distancë"), + ("Local Computer", "Kompjuter Lokal"), + ("Confirm Delete", "Konfirmoni fshirjen"), + ("Delete", "Fshij"), + ("Properties", "Karakteristikat"), + ("Multi Select", "Shumë përzgjedhje"), + ("Select All", "Selektoni të gjitha"), + ("Unselect All", "Ç'selektoni të gjitha"), + ("Empty Directory", "Direktori boshe"), + ("Not an empty directory", "Jo një direktori boshe"), + ("Are you sure you want to delete this file?", "Jeni të sigurtë që doni të fshini këtë skedarë"), + ("Are you sure you want to delete this empty directory?", "Jeni të sigurtë që dëshironi të fshini këtë direktori boshe"), + ("Are you sure you want to delete the file of this directory?", "Jeni të sigurtë që dëshironi te fshini skedarin e kësaj direktorie"), + ("Do this for all conflicts", "Bëjeni këtë për të gjitha konfliktet"), + ("This is irreversible!", "Kjo është e pakthyeshme"), + ("Deleting", "Duke i fshirë"), + ("files", "Skedarë"), + ("Waiting", "Në pritje"), + ("Finished", "Përfunduar"), + ("Speed", "Shpejtësia"), + ("Custom Image Quality", "Cilësi e personalizuar imazhi"), + ("Privacy mode", "Modaliteti i Privatësisë"), + ("Block user input", "Blloko inputin e përdorusesit"), + ("Unblock user input", "Zhblloko inputin e përdorusesit"), + ("Adjust Window", "Rregulloni dritaren"), + ("Original", "Origjinal"), + ("Shrink", "Shkurtim"), + ("Stretch", "Shtrirje"), + ("Scrollbar", "Shiriti i lëvizjes"), + ("ScrollAuto", "Levizje automatikisht"), + ("Good image quality", "Cilësi e mirë imazhi"), + ("Balanced", "E balancuar"), + ("Optimize reaction time", "Optimizo kohën e reagimit"), + ("Custom", "Personalizuar"), + ("Show remote cursor", "Shfaq kursorin në distancë"), + ("Show quality monitor", "Shaq cilësinë e monitorit"), + ("Disable clipboard", "Ç'aktivizo clipboard"), + ("Lock after session end", "Kyç pasi sesioni të përfundoj"), + ("Insert", "Fut"), + ("Insert Lock", "Fut bllokimin"), + ("Refresh", "Rifresko"), + ("ID does not exist", "ID nuk ekziston"), + ("Failed to connect to rendezvous server", "Dështoj të lidhet me serverin e takimit"), + ("Please try later", "Ju lutemi provoni më vonë"), + ("Remote desktop is offline", "Desktopi në distancë nuk është në linjë"), + ("Key mismatch", "Mospërputhje kryesore"), + ("Timeout", "Koha mbaroi"), + ("Failed to connect to relay server", "Lidhja me serverin transmetues dështoi"), + ("Failed to connect via rendezvous server", "Lidhja nëpërmjet serverit të takimit dështoi"), + ("Failed to connect via relay server", "Lidhja nëpërmjet serverit të transmetimit dështoi"), + ("Failed to make direct connection to remote desktop", "Lidhja direkte me desktopin në distancë dështoi"), + ("Set Password", "Vendosni fjalëkalimin"), + ("OS Password", "OS fjalëkalim"), + ("install_tip", "Për shkak të UAC, Rustdesk nuk mund të punoj sic duhet si nje remote në distancë në disa raste. Për të shamngur UAC, ju lutem klikoni butonin më poshtë për të instaluar RustDesk në sistem."), + ("Click to upgrade", "Klikoni për përmirësim"), + ("Click to download", "Klikoni për tu shkarkuar"), + ("Click to update", "Klikoni për përditësim"), + ("Configure", "Koniguro"), + ("config_acc", "Për të kontrolluar Desktopin tuaj nga distanca, duhet të jepni leje RustDesk \"Aksesueshmëri\"."), + ("config_screen", "Për të aksesuar Desktopin tuaj nga distanca, duhet ti jepni lejet RustDesk \"Regjistrimin e ekranit\"."), + ("Installing ...", "Duke u instaluar"), + ("Install", "Instalo"), + ("Installation", "Instalimi"), + ("Installation Path", "Rruga instalimit"), + ("Create start menu shortcuts", "Krijoni shortcuts për menunë e fillimit"), + ("Create desktop icon", "Krijoni ikonën e desktopit"), + ("agreement_tip", "Duke filluar instalimin, ju pranoni marrëveshjen e licencës"), + ("Accept and Install", "Pranoni dhe instaloni"), + ("End-user license agreement", "Marrëeveshja e licencës së perdoruesit fundor"), + ("Generating ...", "Duke gjeneruar"), + ("Your installation is lower version.", "Instalimi juaj është version i ulët"), + ("not_close_tcp_tip", "Mos e mbyll këtë dritare ndërsa jeni duke përdorur tunelin"), + ("Listening ...", "Duke dëgjuar"), + ("Remote Host", "Host në distancë"), + ("Remote Port", "Port në distancë"), + ("Action", "Veprim"), + ("Add", "Shto"), + ("Local Port", "Portë Lokale"), + ("Local Address", "Adresë Lokale"), + ("Change Local Port", "Ndryshoni portën lokale"), + ("setup_server_tip", "Për lidhje më të shpejtë, ju lutemi konfiguroni serverin tuaj"), + ("Too short, at least 6 characters.", "Shumë e shkurtër , nevojiten të paktën 6 karaktere"), + ("The confirmation is not identical.", "Konfirmimi nuk është identik"), + ("Permissions", "Leje"), + ("Accept", "Prano"), + ("Dismiss", "Hiq"), + ("Disconnect", "Shkëput"), + ("Allow using keyboard and mouse", "Lejoni përdorimin e Tastierës dhe Mousit"), + ("Allow using clipboard", "Lejoni përdorimin e clipboard"), + ("Allow hearing sound", "Lejoni dëgjimin e zërit"), + ("Allow file copy and paste", "Lejoni kopjimin dhe pastimin e skedarëve"), + ("Connected", "I lidhur"), + ("Direct and encrypted connection", "Lidhje direkte dhe enkriptuar"), + ("Relayed and encrypted connection", "Lidhje transmetuese dhe e enkriptuar"), + ("Direct and unencrypted connection", "Lidhje direkte dhe jo e enkriptuar"), + ("Relayed and unencrypted connection", "Lidhje transmetuese dhe jo e enkriptuar"), + ("Enter Remote ID", "Vendosni ID në distancë"), + ("Enter your password", "Vendosni fjalëkalimin tuaj"), + ("Logging in...", "Duke u loguar"), + ("Enable RDP session sharing", "Aktivizoni shpërndarjen e sesionit RDP"), + ("Auto Login", "Hyrje automatike"), + ("Enable Direct IP Access", "Aktivizoni aksesimin e IP direkte"), + ("Rename", "Riemërto"), + ("Space", "Hapërsirë"), + ("Create Desktop Shortcut", "Krijoni shortcut desktop"), + ("Change Path", "Ndrysho rrugëzimin"), + ("Create Folder", "Krijoni një folder"), + ("Please enter the folder name", "Ju lutem vendosni emrin e folderit"), + ("Fix it", "Rregulloni ate"), + ("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"), + ("x11 expected", "Pritet x11"), + ("Port", "Port"), + ("Settings", "Cilësimet"), + ("Username", "Emri i përdoruesit"), + ("Invalid port", "Port e pavlefshme"), + ("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"), + ("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"), + ("Unselect all tags", "Hiq selektimin e te gjithë tageve"), + ("Network error", "Gabim në rrjet"), + ("Username missed", "Mungon përdorusesi"), + ("Password missed", "Mungon fjalëkalimi"), + ("Wrong credentials", "Kredinciale të gabuara"), + ("Edit Tag", "Edito tagun"), + ("Unremember Password", "Fjalëkalim jo i kujtueshëm"), + ("Favorites", "Te preferuarat"), + ("Add to Favorites", "Shto te të preferuarat"), + ("Remove from Favorites", "Hiq nga të preferuarat"), + ("Empty", "Bosh"), + ("Invalid folder name", "Emri i dosjes i pavlefshëm"), + ("Socks5 Proxy", "Socks5 Proxy"), + ("Hostname", "Emri Hostit"), + ("Discovered", "I pambuluar"), + ("install_daemon_tip", "Për të nisur në boot, duhet të instaloni shërbimin e sistemit"), + ("Remote ID", "ID në distancë"), + ("Paste", "Ngjit"), + ("Paste here?", "Ngjit këtu"), + ("Are you sure to close the connection?", "Jeni të sigurtë të mbyllni lidhjen"), + ("Download new version", "Shkarko versionin e ri"), + ("Touch mode", "Metoda me prekje"), + ("Mouse mode", "Modaliteti mausit"), + ("One-Finger Tap", "Prekja Një gisht"), + ("Left Mouse", "Mausi majt"), + ("One-Long Tap", "Prekja nje-gjate"), + ("Two-Finger Tap", "Prekja dy-gishta"), + ("Right Mouse", "Mausi i djathtë"), + ("One-Finger Move", "Lëvizja një-gisht"), + ("Double Tap & Move", "Prekja dhe lëvizja e dyfishtë"), + ("Mouse Drag", "Zhvendosja e mausit"), + ("Three-Finger vertically", "Tre-Gishta vertikalisht"), + ("Mouse Wheel", "Rrota mausit"), + ("Two-Finger Move", "Lëvizja Dy-Gishta"), + ("Canvas Move", "Lëvizja Canvas"), + ("Pinch to Zoom", "Prekni për të zmadhuar"), + ("Canvas Zoom", "Zmadhimi Canavas"), + ("Reset canvas", "Riseto canvas"), + ("No permission of file transfer", "Nuk ka leje për transferimin e dosjesve"), + ("Note", "Shënime"), + ("Connection", "Lidhja"), + ("Share Screen", "Ndaj ekranin"), + ("CLOSE", "Mbyll"), + ("OPEN", "Hap"), + ("Chat", "Biseda"), + ("Total", "Total"), + ("items", "artikuj"), + ("Selected", "E zgjedhur"), + ("Screen Capture", "Kapja e ekranit"), + ("Input Control", "Kontrollo inputin"), + ("Audio Capture", "Kapja e zërit"), + ("File Connection", "Lidhja e skedarëve"), + ("Screen Connection", "Lidhja e ekranit"), + ("Do you accept?", "E pranoni"), + ("Open System Setting", "Hapni cilësimet e sistemit"), + ("How to get Android input permission?", "Si të merrni leje e inputit të Android"), + ("android_input_permission_tip1", "Në mënyrë që një pajisje në distancë të kontrollojë pajisjen tuaj Android nëpërmjet mausit ose prekjes, duhet të lejoni RustDesk të përdorë shërbimin."), + ("android_input_permission_tip2", "Ju lutemi shkoni në faqen tjetër të cilësimeve të sistemit, gjeni dhe shtypni [Shërbimet e Instaluara], aktivizoni shërbimin [RustDesk Input]"), + ("android_new_connection_tip", "Është marrë një kërkesë e re kontrolli, e cila dëshiron të kontrollojë pajisjen tuaj aktuale."), + ("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."), + ("Account", "Llogaria"), + ("Overwrite", "Përshkruaj"), + ("This file exists, skip or overwrite this file?", "Ky skedar ekziston , tejkalo ose përshkruaj këtë skedarë"), + ("Quit", "Hiq"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Ndihmë"), + ("Failed", "Deshtoi"), + ("Succeeded", "Sukses"), + ("Someone turns on privacy mode, exit", "Dikush ka ndezur menyrën e privatësisë , largohu"), + ("Unsupported", "Nuk mbështetet"), + ("Peer denied", "Peer mohohet"), + ("Please install plugins", "Ju lutemi instaloni shtojcat"), + ("Peer exit", "Dalje peer"), + ("Failed to turn off", "Dështoi të fiket"), + ("Turned off", "I fikur"), + ("In privacy mode", "Në modalitetin e privatësisë"), + ("Out privacy mode", "Jashtë modaliteti i privatësisë"), + ("Language", "Gjuha"), + ("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]"), + ("Connection not allowed", "Lidhja nuk lejohet"), + ("Legacy mode", "Modaliteti i trashëgimisë"), + ("Map mode", "Modaliteti i hartës"), + ("Translate mode", "Modaliteti i përkthimit"), + ("Use permanent password", "Përdor fjalëkalim të përhershëm"), + ("Use both passwords", "Përdor të dy fjalëkalimet"), + ("Set permanent password", "Vendos fjalëkalimin e përhershëm"), + ("Enable Remote Restart", "Aktivizo rinisjen në distancë"), + ("Allow remote restart", "Lejo rinisjen në distancë"), + ("Restart Remote Device", "Rinisni pajisjen në distancë"), + ("Are you sure you want to restart", "A jeni i sigurt që dëshironi të rinisni"), + ("Restarting Remote Device", "Rinisja e pajisjes në distancë"), + ("remote_restarting_tip", "Pajisja në distancë po riniset, ju lutemi mbyllni këtë kuti mesazhi dhe lidheni përsëri me fjalëkalim të përhershëm pas një kohe"), + ("Copied", "Kopjuar"), + ("Exit Fullscreen", "Dil nga ekrani i plotë"), + ("Fullscreen", "Ekran i plotë"), + ("Mobile Actions", "Veprimet celulare"), + ("Select Monitor", "Zgjidh Monitor"), + ("Control Actions", "Veprimet e kontrollit"), + ("Display Settings", "Cilësimet e ekranit"), + ("Ratio", "Raport"), + ("Image Quality", "Cilësia e imazhit"), + ("Scroll Style", "Stili i lëvizjes"), + ("Show Menubar", "Shfaq shiritin e menusë"), + ("Hide Menubar", "Fshih menunë"), + ("Direct Connection", "Lidhja e drejtpërdrejtë"), + ("Relay Connection", "Lidhja rele"), + ("Secure Connection", "Lidhje e sigurt"), + ("Insecure Connection", "Lidhje e pasigurt"), + ("Scale original", "Shkalla origjinale"), + ("Scale adaptive", " E përsjhtatshme në shkallë"), + ("General", "Gjeneral"), + ("Security", "Siguria"), + ("Theme", "Theme"), + ("Dark Theme", "Theme e errët"), + ("Dark", "E errët"), + ("Light", "Drita"), + ("Follow System", "Ndiq sistemin"), + ("Enable hardware codec", "Aktivizo kodekun e harduerit"), + ("Unlock Security Settings", "Zhbllokoni cilësimet e sigurisë"), + ("Enable Audio", "Aktivizo audio"), + ("Unlock Network Settings", "Zhbllokoni cilësimet e rrjetit"), + ("Server", "Server"), + ("Direct IP Access", "Qasje e drejtpërdrejtë IP"), + ("Proxy", "Proxy"), + ("Apply", "Apliko"), + ("Disconnect all devices?", "Shkyç të gjitha pajisjet?"), + ("Clear", "Pastro"), + ("Audio Input Device", "Pajisja e hyrjes audio"), + ("Deny remote access", "Mohoni qasjen në distancë"), + ("Use IP Whitelisting", "Përdor listën e bardhë IP"), + ("Network", "Rrjeti"), + ("Enable RDP", "Aktivizo RDP"), + ("Pin menubar", "Pin menubar"), + ("Unpin menubar", "Zgjidh shiritin e menusë"), + ("Recording", "Regjistrimi"), + ("Directory", "Direktoria"), + ("Automatically record incoming sessions", "Regjistro automatikisht seancat hyrëse"), + ("Change", "Ndrysho"), + ("Start session recording", "Fillo regjistrimin e sesionit"), + ("Stop session recording", "Ndalo regjistrimin e sesionit"), + ("Enable Recording Session", "Aktivizo seancën e regjistrimit"), + ("Allow recording session", "Lejo regjistrimin e sesionit"), + ("Enable LAN Discovery", "Aktivizo zbulimin e LAN"), + ("Deny LAN Discovery", "Mohoni zbulimin e LAN"), + ("Write a message", "Shkruani një mesazh"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Ju lutemi prisni për konfirmimin e UAC..."), + ("elevated_foreground_window_tip", "Përkohësisht është e pamundur për të përdorur mausin dhe tastierën, për shkak se dritarja aktuale e desktopit në distancë kërkon privilegj më të lartë për të vepruar,ju mund t'i kërkoni përdoruesit në distancë të minimizojë dritaren aktuale. Për të shmangur këtë problem, rekomandohet të instaloni softuerin në pajisjen në distancë ose ekzekutoni atë me privilegje administratori."), + ("Disconnected", "Shkyçur"), + ("Other", "Tjetër"), + ("Confirm before closing multiple tabs", "Konfirmo përpara se të mbyllësh shumë skeda"), + ("Keyboard Settings", "Cilësimet e tastierës"), + ("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ë"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland kërkon një version më të lartë të shpërndarjes linux. Ju lutemi provoni desktopin X11 ose ndryshoni OS."), + ("JumpLink", "JumpLink"), + ("Please Select the screen to be shared(Operate on the peer side).", "Ju lutemi zgjidhni ekranin që do të ndahet (Vepro në anën e kolegëve"), + ("Show RustDesk", "Shfaq RustDesk"), + ("This PC", "Ky PC"), + ("or", "ose"), + ("Continue with", "Vazhdo me"), + ("Elevate", "Ngritja"), + ("Zoom cursor", "Zmadho kursorin"), + ("Accept sessions via password", "Prano sesionin nëpërmjet fjalëkalimit"), + ("Accept sessions via click", "Prano sesionet nëpërmjet klikimit"), + ("Accept sessions via both", "Prano sesionet nëpërmjet të dyjave"), + ("Please wait for the remote side to accept your session request...", "Ju lutem prisni që ana në distancë të pranoj kërkësen tuaj"), + ("One-time Password", "Fjalëkalim Një-herë"), + ("Use one-time password", "Përdorni fjalëkalim Një-herë"), + ("One-time password length", "Gjatësia e fjalëkalimit një herë"), + ("Request access to your device", "Kërko akses në pajisjejn tuaj"), + ("Hide connection management window", "Fshih dritaren e menaxhimit të lidhjes"), + ("hide_cm_tip", "Kjo është e mundur vetëm nëse aksesi bëhet nëpërmjet një fjalëkalimi të përhershëm"), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/sr.rs b/src/lang/sr.rs new file mode 100644 index 000000000..d9463318d --- /dev/null +++ b/src/lang/sr.rs @@ -0,0 +1,437 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Status"), + ("Your Desktop", "Vaša radna površina"), + ("desk_tip", "Vašoj radnoj površini se može pristupiti ovim ID i lozinkom."), + ("Password", "Lozinka"), + ("Ready", "Spremno"), + ("Established", "Uspostavljeno"), + ("connecting_status", "Spajanje na RustDesk mrežu..."), + ("Enable Service", "Dozvoli servis"), + ("Start Service", "Pokreni servis"), + ("Service is running", "Servis je pokrenut"), + ("Service is not running", "Servis nije pokrenut"), + ("not_ready_status", "Nije spremno. Proverite konekciju."), + ("Control Remote Desktop", "Upravljanje udaljenom radnom površinom"), + ("Transfer File", "Prenos fajla"), + ("Connect", "Spajanje"), + ("Recent Sessions", "Poslednje sesije"), + ("Address Book", "Adresar"), + ("Confirmation", "Potvrda"), + ("TCP Tunneling", "TCP tunel"), + ("Remove", "Ukloni"), + ("Refresh random password", "Osveži slučajnu lozinku"), + ("Set your own password", "Postavi lozinku"), + ("Enable Keyboard/Mouse", "Dozvoli tastaturu/miša"), + ("Enable Clipboard", "Dozvoli clipboard"), + ("Enable File Transfer", "Dozvoli prenos fajlova"), + ("Enable TCP Tunneling", "Dozvoli TCP tunel"), + ("IP Whitelisting", "IP pouzdana lista"), + ("ID/Relay Server", "ID/Posredni server"), + ("Import Server Config", "Import server konfiguracije"), + ("Export Server Config", "Eksport server konfiguracije"), + ("Import server configuration successfully", "Import server konfiguracije uspešan"), + ("Export server configuration successfully", "Eksport server konfiguracije uspešan"), + ("Invalid server configuration", "Pogrešna konfiguracija servera"), + ("Clipboard is empty", "Clipboard je prazan"), + ("Stop service", "Stopiraj servis"), + ("Change ID", "Promeni ID"), + ("Website", "Web sajt"), + ("About", "O programu"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), + ("Mute", "Utišaj"), + ("Audio Input", "Audio ulaz"), + ("Enhancements", "Proširenja"), + ("Hardware Codec", "Hardverski kodek"), + ("Adaptive Bitrate", "Prilagodljiva gustina podataka"), + ("ID Server", "ID server"), + ("Relay Server", "Posredni server"), + ("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"), + ("Too frequent", "Previše često"), + ("Cancel", "Otkaži"), + ("Skip", "Preskoči"), + ("Close", "Zatvori"), + ("Retry", "Ponovi"), + ("OK", "Ok"), + ("Password Required", "Potrebna lozinka"), + ("Please enter your password", "Molimo unesite svoju lozinku"), + ("Remember password", "Zapamti lozinku"), + ("Wrong Password", "Pogrešna lozinka"), + ("Do you want to enter again?", "Želite li da unesete ponovo?"), + ("Connection Error", "Greška u konekciji"), + ("Error", "Greška"), + ("Reset by the peer", "Prekinuto sa druge strane"), + ("Connecting...", "Povezivanje..."), + ("Connection in progress. Please wait.", "Povezivanje u toku. Molimo sačekajte."), + ("Please try 1 minute later", "Pokušajte minut kasnije"), + ("Login Error", "Greška u prijavljivanju"), + ("Successful", "Uspešno"), + ("Connected, waiting for image...", "Spojeno, sačekajte sliku..."), + ("Name", "Ime"), + ("Type", "Tip"), + ("Modified", "Izmenjeno"), + ("Size", "Veličina"), + ("Show Hidden Files", "Prikaži skrivene datoteke"), + ("Receive", "Prijem"), + ("Send", "Slanje"), + ("Refresh File", "Osveži datoteku"), + ("Local", "Lokalno"), + ("Remote", "Udaljeno"), + ("Remote Computer", "Udaljeni računar"), + ("Local Computer", "Lokalni računar"), + ("Confirm Delete", "Potvrdite brisanje"), + ("Delete", "Brisanje"), + ("Properties", "Osobine"), + ("Multi Select", "Višestruko selektovanje"), + ("Select All", "Selektuj sve"), + ("Unselect All", "Deselektuj sve"), + ("Empty Directory", "Prazan direktorijum"), + ("Not an empty directory", "Nije prazan direktorijum"), + ("Are you sure you want to delete this file?", "Da li ste sigurni da želite da obrišete ovu datoteku?"), + ("Are you sure you want to delete this empty directory?", "Da li ste sigurni da želite da obrišete ovaj prazan direktorijum?"), + ("Are you sure you want to delete the file of this directory?", "Da li ste sigurni da želite da obrišete datoteku ovog direktorijuma?"), + ("Do this for all conflicts", "Uradi ovo za sve konflikte"), + ("This is irreversible!", "Ovo je nepovratno"), + ("Deleting", "Brisanje"), + ("files", "datoteke"), + ("Waiting", "Čekanje"), + ("Finished", "Završeno"), + ("Speed", "Brzina"), + ("Custom Image Quality", "Korisnički kvalitet slike"), + ("Privacy mode", "Mod privatnosti"), + ("Block user input", "Blokiraj korisnikov unos"), + ("Unblock user input", "Odblokiraj korisnikov unos"), + ("Adjust Window", "Podesi prozor"), + ("Original", "Original"), + ("Shrink", "Skupi"), + ("Stretch", "Raširi"), + ("Scrollbar", "Skrol linija"), + ("ScrollAuto", "Auto skrol"), + ("Good image quality", "Dobar kvalitet slike"), + ("Balanced", "Balansirano"), + ("Optimize reaction time", "Optimizuj vreme reakcije"), + ("Custom", "Korisnički"), + ("Show remote cursor", "Prikaži udaljeni kursor"), + ("Show quality monitor", "Prikaži monitor kvaliteta"), + ("Disable clipboard", "Zabrani clipboard"), + ("Lock after session end", "Zaključaj po završetku sesije"), + ("Insert", "Umetni"), + ("Insert Lock", "Zaključaj umetanje"), + ("Refresh", "Osveži"), + ("ID does not exist", "ID ne postoji"), + ("Failed to connect to rendezvous server", "Greška u spajanju na server za povezivanje"), + ("Please try later", "Molimo pokušajte kasnije"), + ("Remote desktop is offline", "Udaljeni ekran je isključen"), + ("Key mismatch", "Pogrešan ključ"), + ("Timeout", "Isteklo vreme"), + ("Failed to connect to relay server", "Greška u spajanju na posredni server"), + ("Failed to connect via rendezvous server", "Greška u spajanju preko servera za povezivanje"), + ("Failed to connect via relay server", "Greška u spajanju preko posrednog servera"), + ("Failed to make direct connection to remote desktop", "Greška u direktnom spajanju na udaljenu radnu površinu"), + ("Set Password", "Postavi lozinku"), + ("OS Password", "OS lozinka"), + ("install_tip", "Zbog UAC RustDesk ne može raditi pravilno u nekim slučajevima. Da biste prevazišli UAC, kliknite taster ispod da instalirate RustDesk na sistem."), + ("Click to upgrade", "Klik za nadogradnju"), + ("Click to download", "Klik za preuzimanje"), + ("Click to update", "Klik za ažuriranje"), + ("Configure", "Konfigurisanje"), + ("config_acc", "Da biste daljinski kontrolisali radnu površinu, RustDesk-u treba da dodelite \"Accessibility\" prava."), + ("config_screen", "Da biste daljinski pristupili radnoj površini, RustDesk-u treba da dodelite \"Screen Recording\" prava."), + ("Installing ...", "Instaliranje..."), + ("Install", "Instaliraj"), + ("Installation", "Instalacija"), + ("Installation Path", "Putanja za instalaciju"), + ("Create start menu shortcuts", "Kreiraj prečice u meniju"), + ("Create desktop icon", "Kreiraj ikonicu na radnoj površini"), + ("agreement_tip", "Pokretanjem instalacije prihvatate ugovor o licenciranju."), + ("Accept and Install", "Prihvati i instaliraj"), + ("End-user license agreement", "Ugovor sa krajnjim korisnikom"), + ("Generating ...", "Generisanje..."), + ("Your installation is lower version.", "Vaša instalacija je niže verzije"), + ("not_close_tcp_tip", "Ne zatvarajte ovaj prozor dok koristite tunel"), + ("Listening ...", "Na slušanju..."), + ("Remote Host", "Adresa udaljenog uređaja"), + ("Remote Port", "Udaljeni port"), + ("Action", "Akcija"), + ("Add", "Dodaj"), + ("Local Port", "Lokalni port"), + ("Local Address", "Lokalna adresa"), + ("Change Local Port", "Promeni lokalni port"), + ("setup_server_tip", "Za brže spajanje, molimo da koristite svoj server"), + ("Too short, at least 6 characters.", "Prekratko, najmanje 6 znakova."), + ("The confirmation is not identical.", "Potvrda nije identična"), + ("Permissions", "Dozvole"), + ("Accept", "Prihvati"), + ("Dismiss", "Odbaci"), + ("Disconnect", "Raskini konekciju"), + ("Allow using keyboard and mouse", "Dozvoli korišćenje tastature i miša"), + ("Allow using clipboard", "Dozvoli korišćenje clipboard-a"), + ("Allow hearing sound", "Dozvoli da se čuje zvuk"), + ("Allow file copy and paste", "Dozvoli kopiranje i lepljenje fajlova"), + ("Connected", "Spojeno"), + ("Direct and encrypted connection", "Direktna i kriptovana konekcija"), + ("Relayed and encrypted connection", "Posredna i kriptovana konekcija"), + ("Direct and unencrypted connection", "Direktna i nekriptovana konekcija"), + ("Relayed and unencrypted connection", "Posredna i nekriptovana konekcija"), + ("Enter Remote ID", "Unesite ID udaljenog uređaja"), + ("Enter your password", "Unesite svoju lozinku"), + ("Logging in...", "Prijava..."), + ("Enable RDP session sharing", "Dozvoli deljenje RDP sesije"), + ("Auto Login", "Auto prijavljivanje (Važeće samo ako ste postavili \"Lock after session end\")"), + ("Enable Direct IP Access", "Dozvoli direktan pristup preko IP"), + ("Rename", "Preimenuj"), + ("Space", "Prazno"), + ("Create Desktop Shortcut", "Kreiraj prečicu na radnoj površini"), + ("Change Path", "Promeni putanju"), + ("Create Folder", "Kreiraj direktorijum"), + ("Please enter the folder name", "Unesite ime direktorijuma"), + ("Fix it", "Popravi ga"), + ("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"), + ("x11 expected", "x11 očekivan"), + ("Port", "Port"), + ("Settings", "Postavke"), + ("Username", "Korisničko ime"), + ("Invalid port", "Pogrešan port"), + ("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"), + ("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"), + ("Unselect all tags", "Odselektuj sve oznake"), + ("Network error", "Greška na mreži"), + ("Username missed", "Korisničko ime promašeno"), + ("Password missed", "Lozinka promašena"), + ("Wrong credentials", "Pogrešno korisničko ime ili lozinka"), + ("Edit Tag", "Izmeni oznaku"), + ("Unremember Password", "Zaboravi lozinku"), + ("Favorites", "Favoriti"), + ("Add to Favorites", "Dodaj u favorite"), + ("Remove from Favorites", "Izbaci iz favorita"), + ("Empty", "Prazno"), + ("Invalid folder name", "Pogrešno ime direktorijuma"), + ("Socks5 Proxy", "Socks5 proksi"), + ("Hostname", "Ime uređaja"), + ("Discovered", "Otkriveno"), + ("install_daemon_tip", "Za pokretanje pri startu sistema, treba da instalirate sistemski servis."), + ("Remote ID", "Udaljeni ID"), + ("Paste", "Nalepi"), + ("Paste here?", "Nalepi ovde?"), + ("Are you sure to close the connection?", "Da li ste sigurni da želite da zatvorite konekciju?"), + ("Download new version", "Preuzmi novu verziju"), + ("Touch mode", "Mod na dodir"), + ("Mouse mode", "Miš mod"), + ("One-Finger Tap", "Pritisak jednim prstom"), + ("Left Mouse", "Levi miš"), + ("One-Long Tap", "Dugi pritisak"), + ("Two-Finger Tap", "Pritisak sa dva prsta"), + ("Right Mouse", "Desni miš"), + ("One-Finger Move", "Pomeranje jednim prstom"), + ("Double Tap & Move", "Dupli pritisak i pomeranje"), + ("Mouse Drag", "Prevlačenje mišem"), + ("Three-Finger vertically", "Sa tri prsta vertikalno"), + ("Mouse Wheel", "Točkić miša"), + ("Two-Finger Move", "Pomeranje sa dva prsta"), + ("Canvas Move", "Pomeranje pozadine"), + ("Pinch to Zoom", "Stisnite za zumiranje"), + ("Canvas Zoom", "Zumiranje pozadine"), + ("Reset canvas", "Resetuj pozadinu"), + ("No permission of file transfer", "Nemate pravo prenosa datoteka"), + ("Note", "Primedba"), + ("Connection", "Konekcija"), + ("Share Screen", "Podeli ekran"), + ("CLOSE", "ZATVORI"), + ("OPEN", "OTVORI"), + ("Chat", "Dopisivanje"), + ("Total", "Ukupno"), + ("items", "stavki"), + ("Selected", "Izabrano"), + ("Screen Capture", "Snimanje ekrana"), + ("Input Control", "Kontrola unosa"), + ("Audio Capture", "Snimanje zvuka"), + ("File Connection", "Spajanje preko datoteke"), + ("Screen Connection", "Podeli konekciju"), + ("Do you accept?", "Prihvatate?"), + ("Open System Setting", "Postavke otvorenog sistema"), + ("How to get Android input permission?", "Kako dobiti pristup za Android unos?"), + ("android_input_permission_tip1", "Da bi daljinski uređaj kontrolisao vaš Android uređaj preko miša ili na dodir, treba da dozvolite RustDesk-u da koristi \"Accessibility\" servis."), + ("android_input_permission_tip2", "Molimo pređite na sledeću stranicu sistemskih podešavanja, pronađite i unesite [Installed Services], uključite [RustDesk Input] servis."), + ("android_new_connection_tip", "Primljen je novi zahtev za upravljanje, koji želi da upravlja ovim vašim uređajem."), + ("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."), + ("Account", "Nalog"), + ("Overwrite", "Prepiši preko"), + ("This file exists, skip or overwrite this file?", "Ova datoteka postoji, preskoči ili prepiši preko?"), + ("Quit", "Izlaz"), + ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"), + ("Help", "Pomoć"), + ("Failed", "Greška"), + ("Succeeded", "Uspešno"), + ("Someone turns on privacy mode, exit", "Neko je uključio mod privatnosti, izlaz."), + ("Unsupported", "Nepodržano"), + ("Peer denied", "Klijent zabranjen"), + ("Please install plugins", "Molimo instalirajte dodatke"), + ("Peer exit", "Klijent izašao"), + ("Failed to turn off", "Greška kod isključenja"), + ("Turned off", "Isključeno"), + ("In privacy mode", "U modu privatnosti"), + ("Out privacy mode", "Van moda privatnosti"), + ("Language", "Jezik"), + ("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]"), + ("Connection not allowed", "Konekcija nije dozvoljena"), + ("Legacy mode", "Zastareli mod"), + ("Map mode", "Mod mapiranja"), + ("Translate mode", "Mod prevođenja"), + ("Use permanent password", "Koristi trajnu lozinku"), + ("Use both passwords", "Koristi obe lozinke"), + ("Set permanent password", "Postavi trajnu lozinku"), + ("Enable Remote Restart", "Omogući daljinsko restartovanje"), + ("Allow remote restart", "Dozvoli daljinsko restartovanje"), + ("Restart Remote Device", "Restartuj daljinski uređaj"), + ("Are you sure you want to restart", "Da li ste sigurni da želite restart"), + ("Restarting Remote Device", "Restartovanje daljinskog uređaja"), + ("remote_restarting_tip", "Udaljeni uređaj se restartuje, molimo zatvorite ovu poruku i ponovo se kasnije povežite trajnom šifrom"), + ("Copied", "Kopirano"), + ("Exit Fullscreen", "Napusti mod celog ekrana"), + ("Fullscreen", "Mod celog ekrana"), + ("Mobile Actions", "Mobilne akcije"), + ("Select Monitor", "Izbor monitora"), + ("Control Actions", "Upravljačke akcije"), + ("Display Settings", "Postavke prikaza"), + ("Ratio", "Odnos"), + ("Image Quality", "Kvalitet slike"), + ("Scroll Style", "Stil skrolovanja"), + ("Show Menubar", "Prikaži meni"), + ("Hide Menubar", "Sakrij meni"), + ("Direct Connection", "Direktna konekcija"), + ("Relay Connection", "Posredna konekcija"), + ("Secure Connection", "Bezbedna konekcija"), + ("Insecure Connection", "Nebezbedna konekcija"), + ("Scale original", "Skaliraj original"), + ("Scale adaptive", "Adaptivno skaliranje"), + ("General", "Uopšteno"), + ("Security", "Bezbednost"), + ("Theme", "Tema"), + ("Dark Theme", "Tamna tema"), + ("Dark", "Tamno"), + ("Light", "Svetlo"), + ("Follow System", "Prema sistemu"), + ("Enable hardware codec", "Omogući hardverski kodek"), + ("Unlock Security Settings", "Otključaj postavke bezbednosti"), + ("Enable Audio", "Dozvoli zvuk"), + ("Unlock Network Settings", "Otključaj postavke mreže"), + ("Server", "Server"), + ("Direct IP Access", "Direktan IP pristup"), + ("Proxy", "Proksi"), + ("Apply", "Primeni"), + ("Disconnect all devices?", "Otkači sve uređaju?"), + ("Clear", "Obriši"), + ("Audio Input Device", "Uređaj za ulaz zvuka"), + ("Deny remote access", "Zabrani daljinski pristup"), + ("Use IP Whitelisting", "Koristi listu pouzdanih IP"), + ("Network", "Mreža"), + ("Enable RDP", "Dozvoli RDP"), + ("Pin menubar", "Zakači meni"), + ("Unpin menubar", "Otkači meni"), + ("Recording", "Snimanje"), + ("Directory", "Direktorijum"), + ("Automatically record incoming sessions", "Automatski snimaj dolazne sesije"), + ("Change", "Promeni"), + ("Start session recording", "Započni snimanje sesije"), + ("Stop session recording", "Zaustavi snimanje sesije"), + ("Enable Recording Session", "Omogući snimanje sesije"), + ("Allow recording session", "Dozvoli snimanje sesije"), + ("Enable LAN Discovery", "Omogući LAN otkrivanje"), + ("Deny LAN Discovery", "Zabrani LAN otkrivanje"), + ("Write a message", "Napiši poruku"), + ("Prompt", "Prompt"), + ("Please wait for confirmation of UAC...", "Molimo sačekajte UAC potvrdu..."), + ("elevated_foreground_window_tip", "Tekući prozor udaljene radne površine zahteva veću privilegiju za rad, tako da trenutno nije moguće koristiti miša i tastaturu. Možete zahtevati od udaljenog korisnika da minimizira aktivni prozor, ili kliknuti na taster za podizanje privilegija u prozoru za rad sa konekcijom. Da biste prevazišli ovaj problem, preporučljivo je da instalirate softver na udaljeni uređaj."), + ("Disconnected", "Odspojeno"), + ("Other", "Ostalo"), + ("Confirm before closing multiple tabs", "Potvrda pre zatvaranja više kartica"), + ("Keyboard Settings", "Postavke tastature"), + ("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"), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland zahteva veću verziju Linux distribucije. Molimo pokušajte X11 ili promenite OS."), + ("JumpLink", "Vidi"), + ("Please Select the screen to be shared(Operate on the peer side).", "Molimo izaberite ekran koji će biti podeljen (Za rad na klijent strani)"), + ("Show RustDesk", "Prikazi RustDesk"), + ("This PC", "Ovaj PC"), + ("or", "ili"), + ("Continue with", "Nastavi sa"), + ("Elevate", "Izdigni"), + ("Zoom cursor", "Zumiraj kursor"), + ("Accept sessions via password", "Prihvati sesije preko lozinke"), + ("Accept sessions via click", "Prihvati sesije preko klika"), + ("Accept sessions via both", "Prihvati sesije preko oboje"), + ("Please wait for the remote side to accept your session request...", "Molimo sačekajte da udaljena strana prihvati vaš zahtev za sesijom..."), + ("One-time Password", "Jednokratna lozinka"), + ("Use one-time password", "Koristi jednokratnu lozinku"), + ("One-time password length", "Dužina jednokratne lozinke"), + ("Request access to your device", "Zahtev za pristup vašem uređaju"), + ("Hide connection management window", "Sakrij prozor za uređivanje konekcije"), + ("hide_cm_tip", "Skrivanje dozvoljeno samo prihvatanjem sesije preko lozinke i korišćenjem trajne lozinke"), + ("wayland_experiment_tip", "Wayland eksperiment savet"), + ("Right click to select tabs", "Desni klik za izbor kartica"), + ("Skipped", ""), + ("Add to Address Book", "Dodaj u adresar"), + ("Group", "Grupa"), + ("Search", "Pretraga"), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/sv.rs b/src/lang/sv.rs index 52faa8fed..146e60f9a 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Byt ID"), ("Website", "Hemsida"), ("About", "Om"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Tyst"), ("Audio Input", "Ljud input"), ("Enhancements", "Förbättringar"), @@ -116,7 +118,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Bra bildkvalitet"), ("Balanced", "Balanserad"), ("Optimize reaction time", "Optimera reaktionstid"), - ("Custom", "Anpassad"), + ("Custom", "Anpassat"), ("Show remote cursor", "Visa fjärrmus"), ("Show quality monitor", "Visa bildkvalitet"), ("Disable clipboard", "Stäng av urklipp"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -231,7 +238,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Hostname", "Hostname"), ("Discovered", "Upptäckt"), ("install_daemon_tip", "För att starta efter boot måste du installera systemtjänsten."), - ("android_input_permission_tip1", "För att kontrollera din Android-enhet med mus eller touch, måste du tillåta RustDesk att använda \"Tillgänglighets\" tjänsten."), ("Remote ID", "Fjärr ID"), ("Paste", "Klistra in"), ("Paste here?", "Klistra in här?"), @@ -333,7 +339,6 @@ 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"), ("Dark", "Mörk"), @@ -346,7 +351,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"), @@ -375,7 +379,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."), @@ -398,5 +401,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Begär åtkomst till din enhet"), ("Hide connection management window", "Göm hanteringsfönster"), ("hide_cm_tip", "Tillåt att gömma endast om accepterande sessioner med lösenord och permanenta lösenord"), - ].iter().cloned().collect(); + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), + ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index 8f855d96a..729932973 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", ""), ("Website", ""), ("About", ""), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", ""), ("Audio Input", ""), ("Enhancements", ""), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", ""), ("whitelist_tip", ""), ("Login", ""), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", ""), ("Tags", ""), ("Search ID", ""), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", ""), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,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.", ""), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs new file mode 100644 index 000000000..a78509e59 --- /dev/null +++ b/src/lang/th.rs @@ -0,0 +1,437 @@ +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", "เข้าสู่ระบบ"), + ("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", "คั่นโดยเครื่องหมาย 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", "ความปลอดภัย"), + ("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", "พรอกซี"), + ("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 the web console", "ถูกปิดโดยเว็บคอนโซล"), + ("Local keyboard type", "ประเภทคีย์บอร์ด"), + ("Select local keyboard type", "เลือกประเภทคีย์บอร์ด"), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 0f5dd42ba..483ee67e3 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "ID Değiştir"), ("Website", "Website"), ("About", "Hakkında"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Sustur"), ("Audio Input", "Ses Girişi"), ("Enhancements", "Geliştirmeler"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -332,7 +339,6 @@ 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"), ("Dark", "Koyu"), @@ -345,7 +351,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"), @@ -374,7 +379,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."), @@ -397,5 +401,37 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Request access to your device", "Cihazınıza erişim talep edin"), ("Hide connection management window", "Bağlantı yönetimi penceresini gizle"), ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 4945fd511..459c517ff 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "更改 ID"), ("Website", "網站"), ("About", "關於"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "靜音"), ("Audio Input", "音訊輸入"), ("Enhancements", "增強功能"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Always connect via relay", "一律透過轉送連線"), ("whitelist_tip", "只有白名單中的 IP 可以存取"), ("Login", "登入"), + ("Verify", ""), + ("Remember me", ""), + ("Trust this device", ""), + ("Verification code", ""), + ("verification_tip", ""), ("Logout", "登出"), ("Tags", "標籤"), ("Search ID", "搜尋 ID"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "適應窗口"), ("General", "常規"), ("Security", "安全"), - ("Account", "賬戶"), ("Theme", "主題"), ("Dark Theme", "暗黑主題"), ("Dark", "黑暗"), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", "服務器"), ("Direct IP Access", "IP直接訪問"), ("Proxy", "代理"), - ("Port", "端口"), ("Apply", "應用"), ("Disconnect all devices?", "斷開所有遠程連接?"), ("Clear", "清空"), @@ -374,7 +379,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 或更高版本。"), @@ -397,5 +401,37 @@ 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", ""), + ("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", "請選擇本地鍵盤類型"), + ("software_render_tip", "如果你使用英偉達顯卡, 並且遠程窗口在會話建立後會立刻關閉, 那麼安裝nouveau驅動並且選擇使用軟件渲染可能會有幫助。重啟軟件後生效。"), + ("Always use software rendering", "使用軟件渲染"), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ua.rs b/src/lang/ua.rs index 3861f0598..ca99be12e 100644 --- a/src/lang/ua.rs +++ b/src/lang/ua.rs @@ -2,8 +2,8 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Статус"), - ("Your Desktop", "Ваш робочий стіл"), - ("desk_tip", "Ваш робочий стіл доступний з цим ідентифікатором і паролем"), + ("Your Desktop", "Ваша стільниця"), + ("desk_tip", "Ваша стільниця доступна з цим ідентифікатором і паролем"), ("Password", "Пароль"), ("Ready", "Готово"), ("Established", "Встановлено"), @@ -13,7 +13,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service is running", "Служба працює"), ("Service is not running", "Служба не запущена"), ("not_ready_status", "Не готово. Будь ласка, перевірте підключення"), - ("Control Remote Desktop", "Управління віддаленим робочим столом"), + ("Control Remote Desktop", "Керування віддаленою стільницею"), ("Transfer File", "Передати файл"), ("Connect", "Підключитися"), ("Recent Sessions", "Останні сеанси"), @@ -30,7 +30,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("IP Whitelisting", "Список дозволених IP-адрес"), ("ID/Relay Server", "ID/Сервер ретрансляції"), ("Import Server Config", "Імпортувати конфігурацію сервера"), - ("Export Server Config", ""), + ("Export Server Config", "Експортувати конфігурацію сервера"), ("Import server configuration successfully", "Конфігурацію сервера успішно імпортовано"), ("Export server configuration successfully", ""), ("Invalid server configuration", "Недійсна конфігурація сервера"), @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Змінити ID"), ("Website", "Веб-сайт"), ("About", "Про RustDesk"), + ("Slogan_tip", "Створено з душею в цьому хаотичному світі!"), + ("Privacy Statement", "Декларація про конфіденційність"), ("Mute", "Вимкнути звук"), ("Audio Input", "Аудіовхід"), ("Enhancements", "Покращення"), @@ -89,8 +91,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "Видалити"), ("Properties", "Властивості"), ("Multi Select", "Багатоелементний вибір"), - ("Select All", ""), - ("Unselect All", ""), + ("Select All", "Вибрати все"), + ("Unselect All", "Скасувати вибір"), ("Empty Directory", "Порожня папка"), ("Not an empty directory", "Папка не порожня"), ("Are you sure you want to delete this file?", "Ви впевнені, що хочете видалити цей файл?"), @@ -116,8 +118,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Хороша якість зображення"), ("Balanced", "Збалансований"), ("Optimize reaction time", "Оптимізувати час реакції"), - ("Custom", ""), - ("Show remote cursor", "Показати віддалений курсор"), + ("Custom", "Користувацькі"), + ("Show remote cursor", "Показати віддалений вказівник"), ("Show quality monitor", "Показати якість"), ("Disable clipboard", "Відключити буфер обміну"), ("Lock after session end", "Вихід з облікового запису після завершення сеансу"), @@ -127,13 +129,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID does not exist", "ID не існує"), ("Failed to connect to rendezvous server", "Не вдалося підключитися до проміжного сервера"), ("Please try later", "Будь ласка, спробуйте пізніше"), - ("Remote desktop is offline", "Віддалений робочий стіл не в мережі"), + ("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", "Не вдалося встановити пряме підключення до віддаленого робочого столу"), + ("Failed to make direct connection to remote desktop", "Не вдалося встановити пряме підключення до віддаленої стільниці"), ("Set Password", "Встановити пароль"), ("OS Password", "Пароль ОС"), ("install_tip", "У деяких випадках через UAC RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натисніть кнопку нижче, щоб встановити RustDesk у системі"), @@ -141,16 +143,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Click to download", "Натисніть, щоб завантажити"), ("Click to update", "Натисніть, щоб оновити"), ("Configure", "Налаштувати"), - ("config_acc", "Щоб віддалено керувати своїм робочим столом, ви повинні надати RustDesk права \"доступу\""), - ("config_screen", "Для віддаленого доступу до робочого столу ви повинні надати RustDesk права \"знімок екрану\""), + ("config_acc", "Щоб віддалено керувати своєю стільницею, ви повинні надати RustDesk права \"доступності\""), + ("config_screen", "Для віддаленого доступу до стільниці ви повинні надати RustDesk права для \"запису екрану\""), ("Installing ...", "Встановлюється..."), ("Install", "Встановити"), ("Installation", "Установка"), ("Installation Path", "Шлях встановлення"), ("Create start menu shortcuts", "Створити ярлики меню \"Пуск\""), - ("Create desktop icon", "Створити значок на робочому столі"), + ("Create desktop icon", "Створити значок на стільниці"), ("agreement_tip", "Починаючи установку, ви приймаєте умови ліцензійної угоди"), - ("Accept and Install", "Прийняти і встановити"), + ("Accept and Install", "Прийняти та встановити"), ("End-user license agreement", "Ліцензійна угода з кінцевим користувачем"), ("Generating ...", "Генерація..."), ("Your installation is lower version.", "Ваша установка більш ранньої версії"), @@ -161,8 +163,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Action", "Дія"), ("Add", "Додати"), ("Local Port", "Локальний порт"), - ("Local Address", ""), - ("Change Local Port", ""), + ("Local Address", "Локальна адреса"), + ("Change Local Port", "Змінити локальний порт"), ("setup_server_tip", "Для більш швидкого підключення налаштуйте свій власний сервер підключення"), ("Too short, at least 6 characters.", "Занадто коротко, мінімум 6 символів"), ("The confirmation is not identical.", "Підтвердження не збігається"), @@ -170,15 +172,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept", "Прийняти"), ("Dismiss", "Відхилити"), ("Disconnect", "Відключити"), - ("Allow using keyboard and mouse", "Дозволити використання клавіатури і миші"), + ("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", "Ретрансльоване і зашифроване з'єднання"), - ("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...", "Вхід..."), @@ -187,7 +189,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable Direct IP Access", "Увімкнути прямий IP-доступ"), ("Rename", "Перейменувати"), ("Space", "Місце"), - ("Create Desktop Shortcut", "Створити ярлик на робочому столі"), + ("Create Desktop Shortcut", "Створити ярлик на стільниці"), ("Change Path", "Змінити шлях"), ("Create Folder", "Створити папку"), ("Please enter the folder name", "Будь ласка, введіть ім'я папки"), @@ -195,7 +197,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", "Налаштування"), @@ -208,11 +210,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", "Окремо комою, крапкою з комою, пропуском або новим рядком"), + ("Current Wayland display server is not supported", "Поточний графічний сервер Wayland не підтримується"), + ("whitelist_sep", "Розділені комою, крапкою з комою, пробілом або новим рядком"), ("Add ID", "Додати ID"), ("Add Tag", "Додати ключове слово"), ("Unselect all tags", "Скасувати вибір усіх тегів"), @@ -244,7 +251,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Two-Finger Tap", "Дотик двома пальцями"), ("Right Mouse", "Права миша"), ("One-Finger Move", "Рух одним пальцем"), - ("Double Tap & Move", "Подвійне натискання і переміщення"), + ("Double Tap & Move", "Подвійне натискання та переміщення"), ("Mouse Drag", "Перетягування мишею"), ("Three-Finger vertically", "Трьома пальцями по вертикалі"), ("Mouse Wheel", "Коліщатко миші"), @@ -272,8 +279,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Open System Setting", "Відкрити налаштування системи"), ("How to get Android input permission?", "Як отримати дозвіл на введення Android?"), ("android_input_permission_tip1", "Щоб віддалений пристрій міг керувати вашим Android-пристроєм за допомогою миші або торкання, вам необхідно дозволити RustDesk використовувати службу \"Спеціальні можливості\"."), - ("android_input_permission_tip2", "Перейдіть на наступну сторінку системних налаштувань, знайдіть і увійдіть у [Встановлені служби], увімкніть службу [RustDesk Input]."), - ("android_new_connection_tip", "Отримано новий запит на управління вашим поточним пристроєм."), + ("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 або вище."), @@ -300,9 +307,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ignore Battery Optimizations", "Ігнорувати оптимізацію батареї"), ("android_open_battery_optimizations_tip", "Перейдіть на наступну сторінку налаштувань"), ("Connection not allowed", "Підключення не дозволено"), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), + ("Legacy mode", "Застарілий режим"), + ("Map mode", "Режим карти"), + ("Translate mode", "Режим перекладу"), ("Use permanent password", "Використовувати постійний пароль"), ("Use both passwords", "Використовувати обидва паролі"), ("Set permanent password", "Встановити постійний пароль"), @@ -311,13 +318,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Restart Remote Device", "Перезапустити віддалений пристрій"), ("Are you sure you want to restart", "Ви впевнені, що хочете виконати перезапуск?"), ("Restarting Remote Device", "Перезавантаження віддаленого пристрою"), - ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення і через деякий час перепідключіться, використовуючи постійний пароль."), + ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення та через деякий час перепідключіться, використовуючи постійний пароль."), ("Copied", ""), ("Exit Fullscreen", "Вийти з повноекранного режиму"), ("Fullscreen", "Повноекранний"), ("Mobile Actions", "Мобільні дії"), ("Select Monitor", "Виберіть монітор"), - ("Control Actions", "Дії з управління"), + ("Control Actions", "Дії для керування"), ("Display Settings", "Налаштування відображення"), ("Ratio", "Співвідношення"), ("Image Quality", "Якість зображення"), @@ -332,20 +339,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Масштаб адаптивний"), ("General", "Загальні"), ("Security", "Безпека"), - ("Account", "Акаунт"), ("Theme", "Тема"), ("Dark Theme", "Темна тема"), ("Dark", "Темна"), ("Light", "Світла"), - ("Follow System", "Використовувати системну"), + ("Follow System", "Як у системі"), ("Enable hardware codec", "Увімкнути апаратний кодек"), ("Unlock Security Settings", "Розблокувати налаштування безпеки"), - ("Enable Audio", "Вімкнути аудіо"), + ("Enable Audio", "Увімкнути аудіо"), ("Unlock Network Settings", "Розблокувати мережеві налаштування"), ("Server", "Сервер"), ("Direct IP Access", "Прямий IP доступ"), ("Proxy", "Проксі"), - ("Port", "Порт"), ("Apply", "Застосувати"), ("Disconnect all devices?", "Відключити всі прилади?"), ("Clear", "Очистити"), @@ -367,35 +372,66 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Enable LAN Discovery", "Увімкнути пошук локальної мережі"), ("Deny LAN Discovery", "Заборонити виявлення локальної мережі"), ("Write a message", "Написати повідомлення"), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", ""), - ("Disconnected", ""), - ("Other", ""), + ("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", ""), + ("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", "View"), + ("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", ""), + ("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", "Додати IP до Адресної книги"), + ("Group", "Група"), + ("Search", "Пошук"), + ("Closed manually by the web console", ""), + ("Local keyboard type", ""), + ("Select local keyboard type", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 8ddeadfca..53de4e67c 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -39,6 +39,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change ID", "Thay đổi ID"), ("Website", "Trang web"), ("About", "About"), + ("Slogan_tip", ""), + ("Privacy Statement", ""), ("Mute", "Tắt tiếng"), ("Audio Input", "Đầu vào âm thanh"), ("Enhancements", "Các tiện itchs"), @@ -208,6 +210,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -332,7 +339,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Scale adaptive", "Quy mô thích ứng"), ("General", ""), ("Security", ""), - ("Account", ""), ("Theme", ""), ("Dark Theme", ""), ("Dark", ""), @@ -345,7 +351,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Server", ""), ("Direct IP Access", ""), ("Proxy", ""), - ("Port", ""), ("Apply", ""), ("Disconnect all devices?", ""), ("Clear", ""), @@ -374,7 +379,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."), @@ -397,5 +401,37 @@ 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", ""), + ("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", ""), + ("software_render_tip", ""), + ("Always use software rendering", ""), + ("config_input", ""), + ("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?", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index eb8a876ec..7b94c8a2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #[cfg(not(any(target_os = "ios")))] /// cbindgen:ignore pub mod platform; +mod keyboard; #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use platform::{get_cursor, get_cursor_data, get_cursor_pos, start_os_service}; #[cfg(not(any(target_os = "ios")))] diff --git a/src/main.rs b/src/main.rs index 9c7170309..6500a8e4a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,8 +36,9 @@ fn main() { use hbb_common::log; let args = format!( "-p, --port-forward=[PORT-FORWARD-OPTIONS] 'Format: remote-id:local-port:remote-port[:remote-host]' + -c, --connect=[REMOTE_ID] 'test only' -k, --key=[KEY] '' - -s, --server... 'Start server'", + -s, --server=[] 'Start server'", ); let matches = App::new("rustdesk") .version(crate::VERSION) @@ -71,6 +72,8 @@ fn main() { if options.len() > 3 { remote_host = options[3].clone(); } + common::test_rendezvous_server(); + common::test_nat_type(); let key = matches.value_of("key").unwrap_or("").to_owned(); let token = LocalConfig::get_option("access_token"); cli::start_one_port_forward( @@ -81,6 +84,15 @@ fn main() { key, token, ); + } else if let Some(p) = matches.value_of("connect") { + common::test_rendezvous_server(); + common::test_nat_type(); + let key = matches.value_of("key").unwrap_or("").to_owned(); + 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/platform/linux.rs b/src/platform/linux.rs index c8abe432e..34276426d 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -4,6 +4,7 @@ use hbb_common::{allow_err, bail, log}; use libc::{c_char, c_int, c_void}; use std::{ cell::RefCell, + collections::HashMap, path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, @@ -179,7 +180,8 @@ fn set_x11_env(uid: &str) { log::info!("uid of seat0: {}", uid); let gdm = format!("/run/user/{}/gdm/Xauthority", uid); let mut auth = get_env_tries("XAUTHORITY", uid, 10); - if auth.is_empty() { + // 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() { gdm } else { @@ -416,9 +418,9 @@ fn get_display() -> String { pub fn is_login_wayland() -> bool { if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") { - contents.contains("#WaylandEnable=false") + contents.contains("#WaylandEnable=false") || contents.contains("WaylandEnable=true") } else if let Ok(contents) = std::fs::read_to_string("/etc/gdm/custom.conf") { - contents.contains("#WaylandEnable=false") + contents.contains("#WaylandEnable=false") || contents.contains("WaylandEnable=true") } else { false } @@ -705,9 +707,9 @@ 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, @@ -715,3 +717,86 @@ pub fn get_double_click_time() -> u32 { double_click_time } } + +/// forever: may not work +pub fn system_message(title: &str, msg: &str, forever: bool) -> 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 std::process::Command::new(k).args(v).spawn().is_ok() { + return Ok(()); + } + } + bail!("failed to post system message"); +} + +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") + }) { + hbb_common::config::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() { + system_message( + "RustDesk", + &format!("Got signal {} and exit.{}", sig, info), + true, + ) + .ok(); + } + std::process::exit(0); +} + +pub fn register_breakdown_handler() { + unsafe { + libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); + } +} diff --git a/src/platform/macos.mm b/src/platform/macos.mm new file mode 100644 index 000000000..789404cb6 --- /dev/null +++ b/src/platform/macos.mm @@ -0,0 +1,42 @@ +#import +#import +#import + +// https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm + +extern "C" bool InputMonitoringAuthStatus(bool prompt) { + 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; +} + +extern "C" float BackingScaleFactor() { + NSScreen* s = [NSScreen mainScreen]; + if (s) return [s backingScaleFactor]; + return 1; +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index edb2aadb1..c7dbd9b73 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}, }; @@ -32,6 +33,7 @@ extern "C" { fn CGEventGetLocation(e: *const c_void) -> CGPoint; static kAXTrustedCheckOptionPrompt: CFStringRef; fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL; + fn InputMonitoringAuthStatus(_: BOOL) -> BOOL; } pub fn is_process_trusted(prompt: bool) -> bool { @@ -47,6 +49,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 @@ -322,7 +331,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 +440,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(&[ @@ -500,7 +509,7 @@ pub fn start_os_service() { Err(err) => { log::error!("Failed to start server: {}", err); } - _ => { /*no hapen*/ } + _ => { /*no happen*/ } } } std::thread::sleep(std::time::Duration::from_millis(super::SERVICE_INTERVAL)); @@ -532,7 +541,6 @@ pub fn is_installed() -> bool { } pub fn quit_gui() { - use cocoa::appkit::NSApp; unsafe { let () = msg_send!(NSApp(), terminate: nil); }; @@ -542,3 +550,29 @@ 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); + } +} + +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/mod.rs b/src/platform/mod.rs index f6b79da59..ed5fcfaa1 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -75,3 +75,4 @@ mod tests { } } } + diff --git a/src/windows.cc b/src/platform/windows.cc similarity index 93% rename from src/windows.cc rename to src/platform/windows.cc index dd3fa2e9e..c4286ebdd 100644 --- a/src/windows.cc +++ b/src/platform/windows.cc @@ -95,7 +95,7 @@ extern "C" CreateEnvironmentBlock(&lpEnvironment, // Environment block hToken, // New token - TRUE); // Inheritence + TRUE); // Inheritance } if (lpEnvironment) { @@ -588,4 +588,44 @@ extern "C" stop_system_key_propagate = v; } + // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user + BOOL is_local_system() + { + HANDLE hToken; + UCHAR bTokenUser[sizeof(TOKEN_USER) + 8 + 4 * SID_MAX_SUB_AUTHORITIES]; + PTOKEN_USER pTokenUser = (PTOKEN_USER)bTokenUser; + ULONG cbTokenUser; + SID_IDENTIFIER_AUTHORITY siaNT = SECURITY_NT_AUTHORITY; + PSID pSystemSid; + BOOL bSystem; + + // open process token + if (!OpenProcessToken(GetCurrentProcess(), + TOKEN_QUERY, + &hToken)) + return FALSE; + + // retrieve user SID + if (!GetTokenInformation(hToken, TokenUser, pTokenUser, + sizeof(bTokenUser), &cbTokenUser)) + { + CloseHandle(hToken); + return FALSE; + } + + CloseHandle(hToken); + + // allocate LocalSystem well-known SID + if (!AllocateAndInitializeSid(&siaNT, 1, SECURITY_LOCAL_SYSTEM_RID, + 0, 0, 0, 0, 0, 0, 0, &pSystemSid)) + return FALSE; + + // compare the user SID from the token with the LocalSystem SID + bSystem = EqualSid(pTokenUser->User.Sid, pSystemSid); + + FreeSid(pSystemSid); + + return bSystem; + } + } // end of extern "C" \ No newline at end of file diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 075f7ed08..190834eb8 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -24,7 +24,7 @@ use winapi::{ minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, - OpenProcessToken, + OpenProcessToken, PROCESS_INFORMATION, STARTUPINFOW, }, securitybaseapi::GetTokenInformation, shellapi::ShellExecuteW, @@ -439,6 +439,7 @@ extern "C" { fn win32_disable_lowlevel_keyboard(hwnd: HWND); fn win_stop_system_key_propagate(v: BOOL); fn is_win_down() -> BOOL; + fn is_local_system() -> BOOL; } extern "system" { @@ -718,10 +719,10 @@ pub fn set_share_rdp(enable: bool) { } pub fn get_active_username() -> String { - let name = crate::username(); - if name != "SYSTEM" { - return name; + if !is_root() { + return crate::username(); } + extern "C" { fn get_active_user(path: *mut u16, n: u32, rdp: BOOL) -> u32; } @@ -757,7 +758,8 @@ pub fn is_prelogin() -> bool { } pub fn is_root() -> bool { - crate::username() == "SYSTEM" + // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user + unsafe { is_local_system() == TRUE } } pub fn lock_screen() { @@ -1366,22 +1368,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)); - }); - } } } @@ -1712,3 +1698,42 @@ pub fn send_message_to_hnwd( } return true; } + +pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> { + unsafe { + let wuser = wide_string(user); + let wpc = wide_string(""); + 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, + ) + { + bail!("CreateProcessWithLogonW failed, errno={}", GetLastError()); + } + } + return Ok(()); +} diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 9350085c4..8b7dae1ba 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -18,13 +18,14 @@ use hbb_common::{ log, protobuf::Message as _, rendezvous_proto::*, - sleep, socket_client, + sleep, + socket_client::{self, is_ipv4}, tokio::{ self, select, time::{interval, Duration}, }, udp::FramedSocket, - AddrMangle, IntoTargetAddr, ResultType, TargetAddr, + AddrMangle, ResultType, }; use crate::server::{check_zombie, new as new_server, ServerPtr}; @@ -38,7 +39,7 @@ static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); #[derive(Clone)] pub struct RendezvousMediator { - addr: TargetAddr<'static>, + addr: hbb_common::tokio_socks::TargetAddr<'static>, host: String, host_prefix: String, last_id_pk_registry: String, @@ -110,17 +111,15 @@ impl RendezvousMediator { } }) .unwrap_or(host.to_owned()); + let host = crate::check_port(&host, RENDEZVOUS_PORT); + let (mut socket, addr) = socket_client::new_udp_for(&host, RENDEZVOUS_TIMEOUT).await?; let mut rz = Self { - addr: Config::get_any_listen_addr().into_target_addr()?, + addr: addr, host: host.clone(), host_prefix, last_id_pk_registry: "".to_owned(), }; - rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?; - let any_addr = Config::get_any_listen_addr(); - let mut socket = socket_client::new_udp(any_addr, RENDEZVOUS_TIMEOUT).await?; - const TIMER_OUT: Duration = Duration::from_secs(1); let mut timer = interval(TIMER_OUT); let mut last_timer: Option = None; @@ -250,11 +249,11 @@ impl RendezvousMediator { Config::update_latency(&host, -1); old_latency = 0; if last_dns_check.elapsed().as_millis() as i64 > DNS_INTERVAL { - rz.addr = socket_client::get_target_addr(&crate::check_port(&host, RENDEZVOUS_PORT))?; // in some case of network reconnect (dial IP network), // old UDP socket not work any more after network recover - if let Some(s) = socket_client::rebind_udp(any_addr).await? { + if let Some((s, addr)) = socket_client::rebind_udp_for(&rz.host).await? { socket = s; + rz.addr = addr; } last_dns_check = Instant::now(); } @@ -293,19 +292,14 @@ impl RendezvousMediator { ) -> ResultType<()> { let peer_addr = AddrMangle::decode(&socket_addr); log::info!( - "create_relay requested from from {:?}, relay_server: {}, uuid: {}, secure: {}", + "create_relay requested from {:?}, relay_server: {}, uuid: {}, secure: {}", peer_addr, relay_server, uuid, secure, ); - let mut socket = socket_client::connect_tcp( - self.addr.to_owned(), - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await?; + let mut socket = socket_client::connect_tcp(&*self.host, RENDEZVOUS_TIMEOUT).await?; let mut msg_out = Message::new(); let mut rr = RelayResponse { @@ -320,24 +314,41 @@ impl RendezvousMediator { } msg_out.set_relay_response(rr); socket.send(&msg_out).await?; - crate::create_relay_connection(server, relay_server, uuid, peer_addr, secure).await; + crate::create_relay_connection( + server, + relay_server, + uuid, + peer_addr, + secure, + is_ipv4(&self.addr), + ) + .await; Ok(()) } async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { + let relay_server = self.get_relay_server(fla.relay_server); + if !is_ipv4(&self.addr) { + // nat64, go relay directly, because current hbbs will crash if demangle ipv6 address + let uuid = Uuid::new_v4().to_string(); + return self + .create_relay( + fla.socket_addr.into(), + relay_server, + uuid, + server, + true, + true, + ) + .await; + } let peer_addr = AddrMangle::decode(&fla.socket_addr); log::debug!("Handle intranet from {:?}", peer_addr); - let mut socket = socket_client::connect_tcp( - self.addr.to_owned(), - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await?; + let mut socket = socket_client::connect_tcp(&*self.host, RENDEZVOUS_TIMEOUT).await?; let local_addr = socket.local_addr(); let local_addr: SocketAddr = format!("{}:{}", local_addr.ip(), local_addr.port()).parse()?; let mut msg_out = Message::new(); - let relay_server = self.get_relay_server(fla.relay_server); msg_out.set_local_addr(LocalAddr { id: Config::get_id(), socket_addr: AddrMangle::encode(peer_addr).into(), @@ -372,14 +383,11 @@ impl RendezvousMediator { let peer_addr = AddrMangle::decode(&ph.socket_addr); log::debug!("Punch hole to {:?}", peer_addr); let mut socket = { - let socket = socket_client::connect_tcp( - self.addr.to_owned(), - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await?; + let socket = socket_client::connect_tcp(&*self.host, RENDEZVOUS_TIMEOUT).await?; let local_addr = socket.local_addr(); - allow_err!(socket_client::connect_tcp(peer_addr, local_addr, 300).await); + // key important here for punch hole to tell my gateway incoming peer is safe. + // it can not be async here, because local_addr can not be reused, we must close the connection before use it again. + allow_err!(socket_client::connect_tcp_local(peer_addr, Some(local_addr), 30).await); socket }; let mut msg_out = Message::new(); @@ -461,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 } @@ -498,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!( @@ -510,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 { @@ -532,7 +531,9 @@ async fn direct_server(server: ServerPtr) { if let Ok(Ok((stream, addr))) = hbb_common::timeout(1000, l.accept()).await { stream.set_nodelay(true).ok(); log::info!("direct access from {}", addr); - let local_addr = stream.local_addr().unwrap_or(Config::get_any_listen_addr()); + let local_addr = stream + .local_addr() + .unwrap_or(Config::get_any_listen_addr(true)); let server = server.clone(); tokio::spawn(async move { allow_err!( @@ -650,13 +651,7 @@ async fn create_online_stream() -> ResultType { bail!("Invalid server address: {}", rendezvous_server); } let online_server = format!("{}:{}", tmp[0], port - 1); - let server_addr = socket_client::get_target_addr(&online_server)?; - socket_client::connect_tcp( - server_addr, - Config::get_any_listen_addr(), - RENDEZVOUS_TIMEOUT, - ) - .await + socket_client::connect_tcp(online_server, RENDEZVOUS_TIMEOUT).await } async fn query_online_states_( diff --git a/src/server.rs b/src/server.rs index d08dd2672..381e3df90 100644 --- a/src/server.rs +++ b/src/server.rs @@ -85,7 +85,7 @@ pub fn new() -> ServerPtr { #[cfg(not(any(target_os = "android", target_os = "ios")))] { server.add_service(Box::new(clipboard_service::new())); - if !video_service::capture_cursor_embeded() { + if !video_service::capture_cursor_embedded() { server.add_service(Box::new(input_service::new_cursor())); server.add_service(Box::new(input_service::new_pos())); } @@ -194,6 +194,16 @@ pub async fn create_tcp_connection( } } + #[cfg(target_os = "macos")] + { + use std::process::Command; + 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; Ok(()) } @@ -215,9 +225,10 @@ pub async fn create_relay_connection( uuid: String, peer_addr: SocketAddr, secure: bool, + ipv4: bool, ) { if let Err(err) = - create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure).await + create_relay_connection_(server, relay_server, uuid.clone(), peer_addr, secure, ipv4).await { log::error!( "Failed to create relay connection for {} with uuid {}: {}", @@ -234,10 +245,10 @@ async fn create_relay_connection_( uuid: String, peer_addr: SocketAddr, secure: bool, + ipv4: bool, ) -> ResultType<()> { let mut stream = socket_client::connect_tcp( - crate::check_port(relay_server, RELAY_PORT), - Config::get_any_listen_addr(), + socket_client::ipv4_to_ipv6(crate::check_port(relay_server, RELAY_PORT), ipv4), CONNECT_TIMEOUT, ) .await?; @@ -379,6 +390,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); diff --git a/src/server/connection.rs b/src/server/connection.rs index fb281adde..cd5bd8cfa 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -39,6 +39,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); @@ -72,6 +74,7 @@ pub struct Connection { hash: Hash, read_jobs: Vec, timer: Interval, + file_timer: Interval, file_transfer: Option<(String, bool)>, port_forward_socket: Option>, port_forward_address: String, @@ -93,11 +96,15 @@ pub struct Connection { tx_input: std_mpsc::Sender, // handle input messages video_ack_required: bool, peer_info: (String, String), - api_server: String, + server_audit_conn: String, + server_audit_file: String, lr: LoginRequest, last_recv_time: Arc>, chat_unanswered: bool, close_manually: bool, + #[allow(unused)] + elevation_requested: bool, + from_switch: bool, } impl Subscriber for ConnInner { @@ -130,6 +137,7 @@ const MILLI1: Duration = Duration::from_millis(1); const SEND_TIMEOUT_VIDEO: u64 = 12_000; const SEND_TIMEOUT_OTHER: u64 = SEND_TIMEOUT_VIDEO * 10; const SESSION_TIMEOUT: Duration = Duration::from_secs(30); +const SWITCH_SIDES_TIMEOUT: Duration = Duration::from_secs(10); impl Connection { pub async fn start( @@ -143,13 +151,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 { @@ -163,6 +173,7 @@ impl Connection { hash, read_jobs: Vec::new(), timer: time::interval(SEC30), + file_timer: time::interval(SEC30), file_transfer: None, port_forward_socket: None, port_forward_address: "".to_owned(), @@ -184,11 +195,14 @@ impl Connection { tx_input, video_ack_required: false, peer_info: Default::default(), - api_server: "".to_owned(), + server_audit_conn: "".to_owned(), + server_audit_file: "".to_owned(), lr: Default::default(), last_recv_time: Arc::new(Mutex::new(Instant::now())), chat_unanswered: false, close_manually: false, + elevation_requested: false, + from_switch: false, }; #[cfg(not(any(target_os = "android", target_os = "ios")))] tokio::spawn(async move { @@ -240,11 +254,13 @@ impl Connection { #[cfg(windows)] let mut last_foreground_window_elevated = false; #[cfg(windows)] + let mut last_portable_service_running = false; + #[cfg(windows)] let is_installed = crate::platform::is_installed(); loop { tokio::select! { - // biased; // video has higher priority // causing test_delay_timer failed while transfering big file + // biased; // video has higher priority // causing test_delay_timer failed while transferring big file Some(data) = rx_from_cm.recv() => { match data { @@ -255,7 +271,7 @@ impl Connection { } } ipc::Data::Close => { - conn.on_close_manually("connection manager").await; + conn.on_close_manually("connection manager", "peer").await; break; } ipc::Data::ChatMessage{text} => { @@ -311,7 +327,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); } @@ -346,10 +362,18 @@ impl Connection { } #[cfg(windows)] ipc::Data::DataPortableService(ipc::DataPortableService::RequestStart) => { - 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 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; + } _ => {} } }, @@ -375,20 +399,25 @@ impl Connection { break; } }, - _ = conn.timer.tick() => { + _ = conn.file_timer.tick() => { if !conn.read_jobs.is_empty() { if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await { conn.on_close(&err.to_string(), false).await; break; } } else { - conn.timer = time::interval_at(Instant::now() + SEC30, SEC30); + conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30); } - conn.post_audit(json!({})); // heartbeat - }, + } + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&id) { + conn.on_close_manually("web console", "web console").await; + break; + } + } Some((instant, value)) = rx_video.recv() => { if !conn.video_ack_required { - video_service::notify_video_frame_feched(id, Some(instant.into())); + video_service::notify_video_frame_fetched(id, Some(instant.into())); } if let Err(err) = conn.stream.send(&value as &Message).await { conn.on_close(&err.to_string(), false).await; @@ -412,7 +441,7 @@ impl Connection { Some(message::Union::Misc(m)) => { match &m.union { Some(misc::Union::StopService(_)) => { - conn.on_close_manually("stop service").await; + conn.on_close_manually("stop service", "peer").await; break; } _ => {}, @@ -428,8 +457,18 @@ impl Connection { _ = second_timer.tick() => { #[cfg(windows)] { - if !is_installed { - let portable_service_running = crate::portable_service::client::PORTABLE_SERVICE_RUNNING.lock().unwrap().clone(); + if !is_installed && conn.file_transfer.is_none() && conn.port_forward_socket.is_none(){ + let portable_service_running = crate::portable_service::client::running(); + if portable_service_running != last_portable_service_running { + last_portable_service_running = portable_service_running; + if portable_service_running && conn.elevation_requested { + let mut misc = Misc::new(); + misc.set_portable_service_running(portable_service_running); + let mut msg = Message::new(); + msg.set_misc(misc); + conn.inner.send(msg.into()); + } + } let uac = crate::video_service::IS_UAC_RUNNING.lock().unwrap().clone(); if last_uac != uac { last_uac = uac; @@ -487,7 +526,7 @@ impl Connection { } else if video_privacy_conn_id == 0 { let _ = privacy_mode::turn_off_privacy(0); } - video_service::notify_video_frame_feched(id, None); + video_service::notify_video_frame_fetched(id, None); scrap::codec::Encoder::update_video_encoder(id, scrap::codec::EncoderUpdate::Remove); video_service::VIDEO_QOS.lock().unwrap().reset(); if conn.authorized { @@ -497,16 +536,23 @@ impl Connection { conn.on_close(&err.to_string(), false).await; } - conn.post_audit(json!({ + conn.post_conn_audit(json!({ "action": "close", })); + ALIVE_CONNS.lock().unwrap().retain(|&c| c != id); log::info!("#{} connection loop exited", id); } #[cfg(not(any(target_os = "android", target_os = "ios")))] fn handle_input(receiver: std_mpsc::Receiver, tx: Sender) { let mut block_input_mode = false; - + #[cfg(target_os = "windows")] + { + rdev::set_dw_mouse_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); + rdev::set_dw_keyboard_extra_info(enigo::ENIGO_INPUT_EXTRA_VALUE); + } + #[cfg(target_os = "macos")] + reset_input_ondisconn(); loop { match receiver.recv_timeout(std::time::Duration::from_millis(500)) { Ok(v) => match v { @@ -567,12 +613,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"); } _ => {} } @@ -597,7 +644,12 @@ impl Connection { if last_recv_time.elapsed() >= H1 { bail!("Timeout"); } - self.post_audit(json!({})); // heartbeat + } + Ok(conns) = hbbs_rx.recv() => { + if conns.contains(&self.inner.id) { + // todo: check reconnect + bail!("Closed manually by the web console"); + } } } } @@ -638,6 +690,13 @@ impl Connection { { self.send_login_error("Your ip is blocked by the peer") .await; + Self::post_alarm_audit( + AlarmAuditType::IpWhitelist, //"ip whitelist", + true, + json!({ + "ip":addr.ip(), + }), + ); sleep(1.).await; return false; } @@ -646,7 +705,7 @@ impl Connection { msg_out.set_hash(self.hash.clone()); self.send(msg_out).await; self.get_api_server(); - self.post_audit(json!({ + self.post_conn_audit(json!({ "ip": addr.ip(), "action": "new", })); @@ -654,30 +713,90 @@ impl Connection { } fn get_api_server(&mut self) { - self.api_server = crate::get_audit_server( + self.server_audit_conn = crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), + "conn".to_owned(), + ); + self.server_audit_file = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + "file".to_owned(), ); } - fn post_audit(&self, v: Value) { - if self.api_server.is_empty() { + fn post_conn_audit(&self, v: Value) { + if self.server_audit_conn.is_empty() { return; } - let url = self.api_server.clone(); + 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["Id"] = json!(self.inner.id); + v["conn_id"] = json!(self.inner.id); + tokio::spawn(async move { + allow_err!(Self::post_audit_async(url, v).await); + }); + } + + fn post_file_audit( + &self, + r#type: FileAuditType, + path: &str, + files: Vec<(String, i64)>, + info: Value, + ) { + if self.server_audit_file.is_empty() { + return; + } + let url = self.server_audit_file.clone(); + let file_num = files.len(); + let mut files = files; + files.sort_by(|a, b| b.1.cmp(&a.1)); + files.truncate(10); + let is_file = files.len() == 1 && files[0].0.is_empty(); + let mut info = info; + info["ip"] = json!(self.ip.clone()); + info["name"] = json!(self.lr.my_name.clone()); + info["num"] = json!(file_num); + info["files"] = json!(files); + let v = json!({ + "id":json!(Config::get_id()), + "uuid":json!(base64::encode(hbb_common::get_uuid())), + "peer_id":json!(self.lr.my_id), + "type": r#type as i8, + "path":path, + "is_file":is_file, + "info":json!(info).to_string(), + }); + tokio::spawn(async move { + allow_err!(Self::post_audit_async(url, v).await); + }); + } + + pub fn post_alarm_audit(typ: AlarmAuditType, from_remote: bool, info: Value) { + let url = crate::get_audit_server( + Config::get_option("api-server"), + Config::get_option("custom-rendezvous-server"), + "alarm".to_owned(), + ); + if url.is_empty() { + return; + } + let mut v = Value::default(); + v["id"] = json!(Config::get_id()); + v["uuid"] = json!(base64::encode(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()); tokio::spawn(async move { allow_err!(Self::post_audit_async(url, v).await); }); } #[inline] - async fn post_audit_async(url: String, v: Value) -> ResultType<()> { - crate::post_request(url, v.to_string(), "").await?; - Ok(()) + async fn post_audit_async(url: String, v: Value) -> ResultType { + crate::post_request(url, v.to_string(), "").await } async fn send_logon_response(&mut self) { @@ -691,7 +810,7 @@ impl Connection { } else { 0 }; - self.post_audit(json!({"peer": self.peer_info, "Type": conn_type})); + self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type})); #[allow(unused_mut)] let mut username = crate::platform::get_active_username(); let mut res = LoginResponse::new(); @@ -847,6 +966,7 @@ impl Connection { file_transfer_enabled: self.file_transfer_enabled(), restart: self.restart, recording: self.recording, + from_switch: self.from_switch, }); } @@ -926,18 +1046,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(), @@ -968,29 +1091,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.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, ); } - 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; } @@ -1046,7 +1173,7 @@ impl Connection { } _ => {} } - if !crate::is_ip(&lr.username) && lr.username != Config::get_id() { + if !hbb_common::is_ipv4_str(&lr.username) && lr.username != Config::get_id() { self.send_login_error("Offline").await; } else if password::approve_mode() == ApproveMode::Click || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() @@ -1063,7 +1190,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() { @@ -1082,8 +1209,22 @@ impl Connection { if failure.2 > 30 { self.send_login_error("Too many wrong password attempts") .await; + Self::post_alarm_audit( + AlarmAuditType::ManyWrongPassword, + true, + json!({ + "ip":self.ip, + }), + ); } else if time == failure.0 && failure.1 > 6 { self.send_login_error("Please try 1 minute later").await; + Self::post_alarm_audit( + AlarmAuditType::FrequentAttempt, + true, + json!({ + "ip":self.ip, + }), + ); } else if !self.validate_password() { if failure.0 == time { failure.1 += 1; @@ -1123,6 +1264,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() < SWITCH_SIDES_TIMEOUT); + 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)) => { @@ -1180,7 +1340,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)) } } } @@ -1221,8 +1381,18 @@ impl Connection { Ok(job) => { self.send(fs::new_dir(id, path, job.files().to_vec())) .await; + let mut files = job.files().to_owned(); self.read_jobs.push(job); - self.timer = time::interval(MILLI1); + self.file_timer = time::interval(MILLI1); + self.post_file_audit( + FileAuditType::RemoteSend, + &s.path, + files + .drain(..) + .map(|f| (f.name, f.size as _)) + .collect(), + json!({}), + ); } } } @@ -1233,7 +1403,7 @@ impl Connection { &self.lr.version, )); self.send_fs(ipc::FS::NewWrite { - path: r.path, + path: r.path.clone(), id: r.id, file_num: r.file_num, files: r @@ -1244,6 +1414,16 @@ impl Connection { .collect(), overwrite_detection: od, }); + self.post_file_audit( + FileAuditType::RemoteReceive, + &r.path, + r.files + .to_vec() + .drain(..) + .map(|f| (f.name, f.size as _)) + .collect(), + json!({}), + ); } Some(file_action::Union::RemoveDir(d)) => { self.send_fs(ipc::FS::RemoveDir { @@ -1300,6 +1480,13 @@ impl Connection { last_modified: d.last_modified, is_upload: true, }), + Some(file_response::Union::Error(e)) => { + self.send_fs(ipc::FS::WriteError { + id: e.id, + file_num: e.file_num, + err: e.error, + }); + } _ => {} }, Some(message::Union::Misc(misc)) => match misc.union { @@ -1319,7 +1506,7 @@ impl Connection { } } Some(misc::Union::VideoReceived(_)) => { - video_service::notify_video_frame_feched( + video_service::notify_video_frame_fetched( self.inner.id, Some(Instant::now().into()), ); @@ -1340,6 +1527,65 @@ 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() + && !crate::portable_service::client::running() + { + use crate::portable_service::client; + err = client::start_portable_service(client::StartPara::Direct) + .err() + .map_or("".to_string(), |e| e.to_string()); + } + self.elevation_requested = err.is_empty(); + 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() + && !crate::portable_service::client::running() + { + use crate::portable_service::client; + err = client::start_portable_service(client::StartPara::Logon( + _r.username, + _r.password, + )) + .err() + .map_or("".to_string(), |e| e.to_string()); + } + self.elevation_requested = err.is_empty(); + let mut misc = Misc::new(); + misc.set_elevation_response(err); + let mut msg = Message::new(); + msg.set_misc(misc); + self.send(msg).await; + } + } + _ => {} + }, + #[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(); + return false; + } + } _ => {} }, _ => {} @@ -1530,10 +1776,10 @@ impl Connection { self.port_forward_socket.take(); } - async fn on_close_manually(&mut self, close_from: &str) { + async fn on_close_manually(&mut self, close_from: &str, close_by: &str) { self.close_manually = true; let mut misc = Misc::new(); - misc.set_close_reason("Closed manually by the peer".into()); + misc.set_close_reason(format!("Closed manually by the {}", close_by)); let mut msg_out = Message::new(); msg_out.set_misc(misc); self.send(msg_out).await; @@ -1554,6 +1800,17 @@ 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() + } +} + +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")))] @@ -1701,8 +1958,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::ui::win_privacy::turn_on_privacy(_conn_id)?; + Ok(plugin_exist) } #[cfg(not(windows))] { @@ -1710,3 +1967,14 @@ mod privacy_mode { } } } + +pub enum AlarmAuditType { + IpWhitelist = 0, + ManyWrongPassword = 1, + FrequentAttempt = 2, +} + +pub enum FileAuditType { + RemoteSend = 0, + RemoteReceive = 1, +} 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 b465658bb..2715a2643 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -5,7 +5,10 @@ use crate::common::IS_X11; use dispatch::Queue; use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable}; use hbb_common::{config::COMPRESS_LEVEL, get_time, protobuf::EnumOrUnknown}; -use rdev::{simulate, EventType, Key as RdevKey}; +use rdev::{self, EventType, Key as RdevKey, RawKey}; +#[cfg(target_os = "macos")] +use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput}; +use std::time::Duration; use std::{ convert::TryFrom, ops::Sub, @@ -68,6 +71,7 @@ struct Input { y: i32, } +const KEY_RDEV_START: u64 = 999; const KEY_CHAR_START: u64 = 9999; #[derive(Clone, Default)] @@ -219,9 +223,14 @@ lazy_static::lazy_static! { static ref IS_SERVER: bool = std::env::args().nth(1) == Some("--server".to_owned()); } +#[cfg(target_os = "macos")] +static mut VIRTUAL_INPUT_MTX: Mutex<()> = Mutex::new(()); +#[cfg(target_os = "macos")] +static mut VIRTUAL_INPUT: Option = None; + // First call set_uinput() will create keyboard and mouse clients. // The clients are ipc connections that must live shorter than tokio runtime. -// Thus this funtion must not be called in a temporary runtime. +// Thus this function must not be called in a temporary runtime. #[cfg(target_os = "linux")] pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { // Keyboard and mouse both open /dev/uinput @@ -245,16 +254,18 @@ pub async fn setup_uinput(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultT pub async fn update_mouse_resolution(minx: i32, maxx: i32, miny: i32, maxy: i32) -> ResultType<()> { set_uinput_resolution(minx, maxx, miny, maxy).await?; - if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { - if let Some(mouse) = mouse - .as_mut_any() - .downcast_mut::() - { - allow_err!(mouse.send_refresh()); - } else { - log::error!("failed downcast uinput mouse"); + std::thread::spawn(|| { + if let Some(mouse) = ENIGO.lock().unwrap().get_custom_mouse() { + if let Some(mouse) = mouse + .as_mut_any() + .downcast_mut::() + { + allow_err!(mouse.send_refresh()); + } else { + log::error!("failed downcast uinput mouse"); + } } - } + }); Ok(()) } @@ -277,13 +288,18 @@ pub fn mouse_move_relative(x: i32, y: i32) { en.mouse_move_relative(x, y); } -#[cfg(not(target_os = "macos"))] +#[cfg(windows)] fn modifier_sleep() { // sleep for a while, this is only for keying in rdp in peer so far - #[cfg(windows)] std::thread::sleep(std::time::Duration::from_nanos(1)); } +#[inline] +#[cfg(not(target_os = "macos"))] +fn is_pressed(key: &Key, en: &mut Enigo) -> bool { + get_modifier_state(key.clone(), en) +} + #[inline] fn get_modifier_state(key: Key, en: &mut Enigo) -> bool { // https://github.com/rustdesk/rustdesk/issues/332 @@ -332,7 +348,7 @@ pub fn handle_mouse(evt: &MouseEvent, conn: i32) { pub fn fix_key_down_timeout_loop() { std::thread::spawn(move || loop { - std::thread::sleep(std::time::Duration::from_millis(1_000)); + std::thread::sleep(std::time::Duration::from_millis(10_000)); fix_key_down_timeout(false); }); if let Err(err) = ctrlc::set_handler(move || { @@ -353,38 +369,63 @@ pub fn fix_key_down_timeout_at_exit() { } #[inline] -fn get_layout(key: u32) -> Key { - Key::Layout(std::char::from_u32(key).unwrap_or('\0')) +fn record_key_is_control_key(record_key: u64) -> bool { + record_key < KEY_CHAR_START +} + +#[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 +} + +#[inline] +fn record_key_to_key(record_key: u64) -> Option { + if record_key_is_control_key(record_key) { + control_key_value_to_key(record_key as _) + } else if record_key_is_chr(record_key) { + let chr: u32 = (record_key - KEY_CHAR_START) as _; + Some(char_value_to_key(chr)) + } else { + None + } +} + +#[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); + } + }; + + #[cfg(target_os = "macos")] + QUEUE.exec_async(func); + #[cfg(not(target_os = "macos"))] + func(); } fn fix_key_down_timeout(force: bool) { - if KEYS_DOWN.lock().unwrap().is_empty() { + let key_down = KEYS_DOWN.lock().unwrap(); + if key_down.is_empty() { return; } - let cloned = (*KEYS_DOWN.lock().unwrap()).clone(); - for (key, value) in cloned.into_iter() { - if force || value.elapsed().as_millis() >= 360_000 { - KEYS_DOWN.lock().unwrap().remove(&key); - let key = if key < KEY_CHAR_START { - if let Some(key) = KEY_MAP.get(&(key as _)) { - Some(*key) - } else { - None - } - } else { - Some(get_layout((key - KEY_CHAR_START) as _)) - }; - if let Some(key) = key { - let func = move || { - let mut en = ENIGO.lock().unwrap(); - en.key_up(key); - log::debug!("Fixed {:?} timeout", key); - }; - #[cfg(target_os = "macos")] - QUEUE.exec_async(func); - #[cfg(not(target_os = "macos"))] - func(); - } + let cloned = (*key_down).clone(); + drop(key_down); + + for (record_key, time) in cloned.into_iter() { + if force || time.elapsed().as_millis() >= 360_000 { + record_pressed_key(record_key, false); + release_record_key(record_key); } } } @@ -446,7 +487,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() { @@ -455,7 +496,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 { @@ -464,7 +505,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 { @@ -502,6 +543,7 @@ pub fn handle_mouse_(evt: &MouseEvent) { if key != &Key::CapsLock && key != &Key::NumLock { if !get_modifier_state(key.clone(), &mut en) { en.key_down(key.clone()).ok(); + #[cfg(windows)] modifier_sleep(); to_release.push(key); } @@ -514,27 +556,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 => { @@ -640,7 +694,423 @@ pub async fn lock_screen() { super::video_service::switch_to_primary().await; } +pub fn handle_key(evt: &KeyEvent) { + #[cfg(target_os = "macos")] + if !*IS_SERVER { + // having GUI, run main GUI thread, otherwise crash + let evt = evt.clone(); + QUEUE.exec_async(move || handle_key_(&evt)); + std::thread::sleep(Duration::from_millis(20)); + return; + } + #[cfg(windows)] + crate::portable_service::client::handle_key(evt); + #[cfg(not(windows))] + handle_key_(evt); + #[cfg(target_os = "macos")] + std::thread::sleep(Duration::from_millis(20)); +} + +#[cfg(target_os = "macos")] +#[inline] +fn reset_input() { + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + VIRTUAL_INPUT = VirtualInput::new( + CGEventSourceStateID::Private, + CGEventTapLocation::AnnotatedSession, + ) + .ok(); + } +} + +#[cfg(target_os = "macos")] +pub fn reset_input_ondisconn() { + if !*IS_SERVER { + QUEUE.exec_async(reset_input); + } else { + reset_input(); + } +} + +fn sim_rdev_rawkey(code: u32, keydown: bool) { + #[cfg(target_os = "windows")] + let rawkey = RawKey::ScanCode(code); + #[cfg(target_os = "linux")] + let rawkey = RawKey::LinuxXorgKeycode(code); + // // to-do: test android + // #[cfg(target_os = "android")] + // let rawkey = RawKey::LinuxConsoleKeycode(code); + #[cfg(target_os = "macos")] + let rawkey = RawKey::MacVirtualKeycode(code); + + let event_type = if keydown { + EventType::KeyPress(RdevKey::RawKey(rawkey)) + } else { + EventType::KeyRelease(RdevKey::RawKey(rawkey)) + }; + simulate_(&event_type); +} + +#[cfg(target_os = "macos")] +#[inline] +fn simulate_(event_type: &EventType) { + unsafe { + let _lock = VIRTUAL_INPUT_MTX.lock(); + if let Some(virtual_input) = &VIRTUAL_INPUT { + let _ = virtual_input.simulate(&event_type); + } + } +} + +#[cfg(not(target_os = "macos"))] +#[inline] +fn simulate_(event_type: &EventType) { + match rdev::simulate(&event_type) { + Ok(()) => (), + Err(_simulate_error) => { + log::error!("Could not send {:?}", &event_type); + } + } +} + +fn is_modifier_in_key_event(control_key: ControlKey, key_event: &KeyEvent) -> bool { + key_event + .modifiers + .iter() + .position(|&m| m == control_key.into()) + .is_some() +} + +#[inline] +fn control_key_value_to_key(value: i32) -> Option { + KEY_MAP.get(&value).and_then(|k| Some(*k)) +} + +#[inline] +fn char_value_to_key(value: u32) -> Key { + Key::Layout(std::char::from_u32(value).unwrap_or('\0')) +} + +fn is_not_same_status(client_locking: bool, remote_locking: bool) -> bool { + client_locking != remote_locking +} + +#[cfg(target_os = "windows")] +fn has_numpad_key(key_event: &KeyEvent) -> bool { + key_event + .modifiers + .iter() + .filter(|&&ck| NUMPAD_KEY_MAP.get(&ck.value()).is_some()) + .count() + != 0 +} + +#[cfg(target_os = "windows")] +fn is_rdev_numpad_key(key_event: &KeyEvent) -> bool { + let code = key_event.chr(); + let key = rdev::get_win_key(code, 0); + match key { + RdevKey::Home + | RdevKey::UpArrow + | RdevKey::PageUp + | RdevKey::LeftArrow + | RdevKey::RightArrow + | RdevKey::End + | RdevKey::DownArrow + | RdevKey::PageDown + | RdevKey::Insert + | RdevKey::Delete => true, + _ => false, + } +} + +#[cfg(target_os = "windows")] +fn is_numlock_disabled(key_event: &KeyEvent) -> bool { + // disable numlock if press home etc when numlock is on, + // because we will get numpad value (7,8,9 etc) if not + match key_event.mode.unwrap() { + KeyboardMode::Map => is_rdev_numpad_key(key_event), + _ => has_numpad_key(key_event), + } +} + +fn click_capslock(en: &mut Enigo) { + #[cfg(not(targe_os = "macos"))] + en.key_click(enigo::Key::CapsLock); + #[cfg(target_os = "macos")] + let _ = en.key_down(enigo::Key::CapsLock); +} + +fn click_numlock(_en: &mut Enigo) { + // without numlock in macos + #[cfg(not(target_os = "macos"))] + _en.key_click(enigo::Key::NumLock); +} + +fn sync_numlock_capslock_status(key_event: &KeyEvent) { + let mut en = ENIGO.lock().unwrap(); + + let client_caps_locking = is_modifier_in_key_event(ControlKey::CapsLock, key_event); + let client_num_locking = is_modifier_in_key_event(ControlKey::NumLock, key_event); + let remote_caps_locking = en.get_key_state(enigo::Key::CapsLock); + let remote_num_locking = en.get_key_state(enigo::Key::NumLock); + + let need_click_capslock = is_not_same_status(client_caps_locking, remote_caps_locking); + let need_click_numlock = is_not_same_status(client_num_locking, remote_num_locking); + + #[cfg(not(target_os = "windows"))] + let disable_numlock = false; + #[cfg(target_os = "windows")] + let disable_numlock = is_numlock_disabled(key_event); + + if need_click_capslock { + click_capslock(&mut en); + } + + if need_click_numlock && !disable_numlock { + click_numlock(&mut en); + } +} + +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 { + let mut en = ENIGO.lock().unwrap(); + let code = evt.chr() as u16; + + if evt.down { + en.key_down(enigo::Key::Raw(code)).ok(); + } else { + en.key_up(enigo::Key::Raw(code)); + } + return; + } + + sim_rdev_rawkey(evt.chr(), evt.down); +} + +#[cfg(target_os = "macos")] +fn add_flags_to_enigo(en: &mut Enigo, key_event: &KeyEvent) { + // When long-pressed the command key, then press and release + // the Tab key, there should be CGEventFlagCommand in the flag. + en.reset_flag(); + for ck in key_event.modifiers.iter() { + if let Some(key) = KEY_MAP.get(&ck.value()) { + en.add_flag(key); + } + } +} + +fn get_control_key_value(key_event: &KeyEvent) -> i32 { + if let Some(key_event::Union::ControlKey(ck)) = key_event.union { + ck.value() + } else { + -1 + } +} + +fn release_unpressed_modifiers(en: &mut Enigo, key_event: &KeyEvent) { + let ck_value = get_control_key_value(key_event); + fix_modifiers(&key_event.modifiers[..], en, ck_value); +} + +#[cfg(target_os = "linux")] +fn is_altgr_pressed() -> bool { + KEYS_DOWN + .lock() + .unwrap() + .get(&(ControlKey::RAlt.value() as _)) + .is_some() +} + +#[cfg(not(target_os = "macos"))] +fn press_modifiers(en: &mut Enigo, key_event: &KeyEvent, to_release: &mut Vec) { + for ref ck in key_event.modifiers.iter() { + if let Some(key) = control_key_value_to_key(ck.value()) { + if !is_pressed(&key, en) { + #[cfg(target_os = "linux")] + if key == Key::Alt && is_altgr_pressed() { + continue; + } + en.key_down(key.clone()).ok(); + to_release.push(key.clone()); + #[cfg(windows)] + modifier_sleep(); + } + } + } +} + +fn sync_modifiers(en: &mut Enigo, key_event: &KeyEvent, _to_release: &mut Vec) { + #[cfg(target_os = "macos")] + add_flags_to_enigo(en, key_event); + + if key_event.down { + release_unpressed_modifiers(en, key_event); + #[cfg(not(target_os = "macos"))] + press_modifiers(en, key_event, _to_release); + } +} + +fn process_control_key(en: &mut Enigo, ck: &EnumOrUnknown, down: bool) { + if let Some(key) = control_key_value_to_key(ck.value()) { + if down { + en.key_down(key).ok(); + } else { + en.key_up(key); + } + } +} + +#[inline] +fn need_to_uppercase(en: &mut Enigo) -> bool { + get_modifier_state(Key::Shift, en) || get_modifier_state(Key::CapsLock, en) +} + +fn process_chr(en: &mut Enigo, chr: u32, down: bool) { + let key = char_value_to_key(chr); + + if down { + if en.key_down(key).is_ok() { + } else { + if let Ok(chr) = char::try_from(chr) { + let mut s = chr.to_string(); + if need_to_uppercase(en) { + s = s.to_uppercase(); + } + en.key_sequence(&s); + }; + } + } else { + en.key_up(key); + } +} + +fn process_unicode(en: &mut Enigo, chr: u32) { + if let Ok(chr) = char::try_from(chr) { + en.key_sequence(&chr.to_string()); + } +} + +fn process_seq(en: &mut Enigo, sequence: &str) { + en.key_sequence(&sequence); +} + +#[cfg(not(target_os = "macos"))] +fn release_keys(en: &mut Enigo, to_release: &Vec) { + for key in to_release { + en.key_up(key.clone()); + } +} + +fn record_pressed_key(record_key: u64, down: bool) { + let mut key_down = KEYS_DOWN.lock().unwrap(); + if down { + key_down.insert(record_key, Instant::now()); + } else { + key_down.remove(&record_key); + } +} + +fn is_function_key(ck: &EnumOrUnknown) -> bool { + let mut res = false; + if ck.value() == ControlKey::CtrlAltDel.value() { + // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. + std::thread::spawn(|| { + allow_err!(send_sas()); + }); + res = true; + } else if ck.value() == ControlKey::LockScreen.value() { + lock_screen_2(); + res = true; + } + return res; +} + +fn legacy_keyboard_mode(evt: &KeyEvent) { + #[cfg(windows)] + crate::platform::windows::try_change_desktop(); + let mut to_release: Vec = Vec::new(); + + let mut en = ENIGO.lock().unwrap(); + sync_modifiers(&mut en, &evt, &mut to_release); + + let down = evt.down; + match evt.union { + Some(key_event::Union::ControlKey(ck)) => { + if is_function_key(&ck) { + return; + } + let record_key = ck.value() as u64; + record_pressed_key(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); + process_chr(&mut en, chr, down) + } + Some(key_event::Union::Unicode(chr)) => process_unicode(&mut en, chr), + Some(key_event::Union::Seq(ref seq)) => process_seq(&mut en, seq), + _ => {} + } + + #[cfg(not(target_os = "macos"))] + release_keys(&mut en, &to_release); +} + +pub fn handle_key_(evt: &KeyEvent) { + if EXITING.load(Ordering::SeqCst) { + return; + } + + if evt.down { + sync_numlock_capslock_status(evt) + } + match evt.mode.unwrap() { + KeyboardMode::Map => { + map_keyboard_mode(evt); + } + KeyboardMode::Translate => { + legacy_keyboard_mode(evt); + } + _ => { + legacy_keyboard_mode(evt); + } + } +} + +#[tokio::main(flavor = "current_thread")] +async fn lock_screen_2() { + lock_screen().await; +} + +#[tokio::main(flavor = "current_thread")] +async fn send_sas() -> ResultType<()> { + let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; + timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; + Ok(()) +} + lazy_static::lazy_static! { + static ref MODIFIER_MAP: HashMap = [ + (ControlKey::Alt, Key::Alt), + (ControlKey::RAlt, Key::RightAlt), + (ControlKey::Control, Key::Control), + (ControlKey::RControl, Key::RightControl), + (ControlKey::Shift, Key::Shift), + (ControlKey::RShift, Key::RightShift), + (ControlKey::Meta, Key::Meta), + (ControlKey::RWin, Key::RWin), + ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); static ref KEY_MAP: HashMap = [ (ControlKey::Alt, Key::Alt), @@ -733,347 +1203,3 @@ lazy_static::lazy_static! { (ControlKey::Delete, true), ].iter().map(|(a, b)| (a.value(), b.clone())).collect(); } - -pub fn handle_key(evt: &KeyEvent) { - #[cfg(target_os = "macos")] - if !*IS_SERVER { - // having GUI, run main GUI thread, otherwise crash - let evt = evt.clone(); - QUEUE.exec_async(move || handle_key_(&evt)); - return; - } - #[cfg(windows)] - crate::portable_service::client::handle_key(evt); - #[cfg(not(windows))] - handle_key_(evt); -} - -fn rdev_key_down_or_up(key: RdevKey, down_or_up: bool) { - let event_type = match down_or_up { - true => EventType::KeyPress(key), - false => EventType::KeyRelease(key), - }; - let delay = std::time::Duration::from_millis(20); - match simulate(&event_type) { - Ok(()) => (), - Err(_simulate_error) => { - log::error!("Could not send {:?}", &event_type); - } - } - // Let ths OS catchup (at least MacOS) - std::thread::sleep(delay); -} - -fn rdev_key_click(key: RdevKey) { - rdev_key_down_or_up(key, true); - rdev_key_down_or_up(key, false); -} - -fn sync_status(evt: &KeyEvent) -> (bool, bool) { - /* todo! Shift+delete */ - let mut en = ENIGO.lock().unwrap(); - - // remote caps status - let caps_locking = evt - .modifiers - .iter() - .position(|&r| r == ControlKey::CapsLock.into()) - .is_some(); - // remote numpad status - let num_locking = evt - .modifiers - .iter() - .position(|&r| r == ControlKey::NumLock.into()) - .is_some(); - - let click_capslock = (caps_locking && !en.get_key_state(enigo::Key::CapsLock)) - || (!caps_locking && en.get_key_state(enigo::Key::CapsLock)); - let click_numlock = (num_locking && !en.get_key_state(enigo::Key::NumLock)) - || (!num_locking && en.get_key_state(enigo::Key::NumLock)); - #[cfg(windows)] - let click_numlock = { - let code = evt.chr(); - let key = rdev::get_win_key(code, 0); - match key { - RdevKey::Home - | RdevKey::UpArrow - | RdevKey::PageUp - | RdevKey::LeftArrow - | RdevKey::RightArrow - | RdevKey::End - | RdevKey::DownArrow - | RdevKey::PageDown - | RdevKey::Insert - | RdevKey::Delete => en.get_key_state(enigo::Key::NumLock), - _ => click_numlock, - } - }; - return (click_capslock, click_numlock); -} - -fn map_keyboard_mode(evt: &KeyEvent) { - // map mode(1): Send keycode according to the peer platform. - #[cfg(windows)] - crate::platform::windows::try_change_desktop(); - - let (click_capslock, click_numlock) = sync_status(evt); - - // Wayland - #[cfg(target_os = "linux")] - if !*IS_X11.lock().unwrap() { - let mut en = ENIGO.lock().unwrap(); - let code = evt.chr() as u16; - - #[cfg(not(target_os = "macos"))] - if click_capslock { - en.key_click(enigo::Key::CapsLock); - } - #[cfg(not(target_os = "macos"))] - if click_numlock { - en.key_click(enigo::Key::NumLock); - } - #[cfg(target_os = "macos")] - en.key_down(enigo::Key::CapsLock); - - if evt.down { - en.key_down(enigo::Key::Raw(code)).ok(); - } else { - en.key_up(enigo::Key::Raw(code)); - } - return; - } - - #[cfg(not(target_os = "macos"))] - if click_capslock { - rdev_key_click(RdevKey::CapsLock); - } - #[cfg(not(target_os = "macos"))] - if click_numlock { - rdev_key_click(RdevKey::NumLock); - } - #[cfg(target_os = "macos")] - if evt.down && click_capslock { - rdev_key_down_or_up(RdevKey::CapsLock, evt.down); - } - - rdev_key_down_or_up(RdevKey::Unknown(evt.chr()), evt.down); - return; -} - -fn legacy_keyboard_mode(evt: &KeyEvent) { - let (click_capslock, click_numlock) = sync_status(evt); - - #[cfg(windows)] - crate::platform::windows::try_change_desktop(); - let mut en = ENIGO.lock().unwrap(); - if click_capslock { - en.key_click(Key::CapsLock); - } - if click_numlock { - en.key_click(Key::NumLock); - } - // disable numlock if press home etc when numlock is on, - // because we will get numpad value (7,8,9 etc) if not - #[cfg(windows)] - let mut _disable_numlock = false; - #[cfg(target_os = "macos")] - en.reset_flag(); - // When long-pressed the command key, then press and release - // the Tab key, there should be CGEventFlagCommand in the flag. - #[cfg(target_os = "macos")] - for ck in evt.modifiers.iter() { - if let Some(key) = KEY_MAP.get(&ck.value()) { - en.add_flag(key); - } - } - #[cfg(not(target_os = "macos"))] - let mut to_release = Vec::new(); - - if evt.down { - let ck = if let Some(key_event::Union::ControlKey(ck)) = evt.union { - ck.value() - } else { - -1 - }; - fix_modifiers(&evt.modifiers[..], &mut en, ck); - for ref ck in evt.modifiers.iter() { - if let Some(key) = KEY_MAP.get(&ck.value()) { - #[cfg(target_os = "linux")] - if key == &Key::Alt && !get_modifier_state(key.clone(), &mut en) { - // for AltGr on Linux - if KEYS_DOWN - .lock() - .unwrap() - .get(&(ControlKey::RAlt.value() as _)) - .is_some() - { - continue; - } - } - #[cfg(not(target_os = "macos"))] - if !get_modifier_state(key.clone(), &mut en) { - en.key_down(key.clone()).ok(); - modifier_sleep(); - to_release.push(key); - } - } - } - } - - match evt.union { - Some(key_event::Union::ControlKey(ck)) => { - if let Some(key) = KEY_MAP.get(&ck.value()) { - #[cfg(windows)] - if let Some(_) = NUMPAD_KEY_MAP.get(&ck.value()) { - _disable_numlock = en.get_key_state(Key::NumLock); - if _disable_numlock { - en.key_down(Key::NumLock).ok(); - en.key_up(Key::NumLock); - } - } - if evt.down { - en.key_down(key.clone()).ok(); - KEYS_DOWN - .lock() - .unwrap() - .insert(ck.value() as _, Instant::now()); - } else { - en.key_up(key.clone()); - KEYS_DOWN.lock().unwrap().remove(&(ck.value() as _)); - } - } else if ck.value() == ControlKey::CtrlAltDel.value() { - // have to spawn new thread because send_sas is tokio_main, the caller can not be tokio_main. - std::thread::spawn(|| { - allow_err!(send_sas()); - }); - } else if ck.value() == ControlKey::LockScreen.value() { - lock_screen_2(); - } - } - Some(key_event::Union::Chr(chr)) => { - if evt.down { - if en.key_down(get_layout(chr)).is_ok() { - KEYS_DOWN - .lock() - .unwrap() - .insert(chr as u64 + KEY_CHAR_START, Instant::now()); - } else { - if let Ok(chr) = char::try_from(chr) { - let mut x = chr.to_string(); - if get_modifier_state(Key::Shift, &mut en) - || get_modifier_state(Key::CapsLock, &mut en) - { - x = x.to_uppercase(); - } - en.key_sequence(&x); - } - } - KEYS_DOWN - .lock() - .unwrap() - .insert(chr as u64 + KEY_CHAR_START, Instant::now()); - } else { - en.key_up(get_layout(chr)); - KEYS_DOWN - .lock() - .unwrap() - .remove(&(chr as u64 + KEY_CHAR_START)); - } - } - Some(key_event::Union::Unicode(chr)) => { - if let Ok(chr) = char::try_from(chr) { - en.key_sequence(&chr.to_string()); - } - } - Some(key_event::Union::Seq(ref seq)) => { - en.key_sequence(&seq); - } - _ => {} - } - #[cfg(not(target_os = "macos"))] - for key in to_release { - en.key_up(key.clone()); - } -} - -pub fn handle_key_(evt: &KeyEvent) { - if EXITING.load(Ordering::SeqCst) { - return; - } - - match evt.mode.unwrap() { - KeyboardMode::Legacy => { - legacy_keyboard_mode(evt); - } - KeyboardMode::Map => { - map_keyboard_mode(evt); - } - KeyboardMode::Translate => { - legacy_keyboard_mode(evt); - } - _ => { - legacy_keyboard_mode(evt); - } - } -} - -#[tokio::main(flavor = "current_thread")] -async fn lock_screen_2() { - lock_screen().await; -} - -#[tokio::main(flavor = "current_thread")] -async fn send_sas() -> ResultType<()> { - let mut stream = crate::ipc::connect(1000, crate::POSTFIX_SERVICE).await?; - timeout(1000, stream.send(&crate::ipc::Data::SAS)).await??; - Ok(()) -} - -#[cfg(test)] -mod test { - use super::*; - use rdev::{listen, Event, EventType, Key}; - use std::sync::mpsc; - - #[test] - fn test_handle_key() { - // listen - let (tx, rx) = mpsc::channel(); - std::thread::spawn(move || { - std::env::set_var("KEYBOARD_ONLY", "y"); - let func = move |event: Event| { - tx.send(event).ok(); - }; - if let Err(error) = listen(func) { - println!("Error: {:?}", error); - } - }); - // set key/char base on char - let mut evt = KeyEvent::new(); - evt.set_chr(66); - evt.mode = KeyboardMode::Legacy.into(); - - evt.modifiers.push(ControlKey::CapsLock.into()); - - // press - evt.down = true; - handle_key(&evt); - if let Ok(listen_evt) = rx.recv() { - assert_eq!(listen_evt.event_type, EventType::KeyPress(Key::Num1)) - } - // release - evt.down = false; - handle_key(&evt); - if let Ok(listen_evt) = rx.recv() { - assert_eq!(listen_evt.event_type, EventType::KeyRelease(Key::Num1)) - } - } - #[test] - fn test_get_key_state() { - let mut en = ENIGO.lock().unwrap(); - println!( - "[*] test_get_key_state: {:?}", - en.get_key_state(enigo::Key::NumLock) - ); - } -} diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index ace70e1bd..0651fd4ce 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -44,7 +44,7 @@ const ADDR_CAPTURE_FRAME_COUNTER: usize = ADDR_CAPTURE_WOULDBLOCK + size_of:: { @@ -407,7 +407,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; } @@ -451,18 +451,24 @@ pub mod server { // functions called in main process. pub mod client { use hbb_common::anyhow::Context; + use std::path::PathBuf; 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()); } - 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 +497,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_dir_permission(&PathBuf::from(dir)) { + *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_dir_permission(&dir) { + 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 +561,16 @@ pub mod client { *SHMEM.lock().unwrap() = None; } + fn set_dir_permission(dir: &PathBuf) -> bool { + // // give Everyone RX permission + std::process::Command::new("icacls") + .arg(dir.as_os_str()) + .arg("/grant") + .arg("Everyone:(OI)(CI)RX") + .arg("/T") + .spawn() + .is_ok() + } pub struct CapturerPortable; impl CapturerPortable { @@ -622,7 +684,7 @@ pub mod client { async fn start_ipc_server_async(rx: mpsc::UnboundedReceiver) { use DataPortableService::*; let rx = Arc::new(tokio::sync::Mutex::new(rx)); - let postfix = IPC_PROFIX; + let postfix = IPC_SUFFIX; #[cfg(feature = "flutter")] let quick_support = { let args: Vec<_> = std::env::args().collect(); @@ -668,7 +730,7 @@ pub mod client { } Pong => { nack = 0; - *PORTABLE_SERVICE_RUNNING.lock().unwrap() = true; + *RUNNING.lock().unwrap() = true; }, ConnCount(None) => { if !quick_support { @@ -699,7 +761,7 @@ pub mod client { } } } - *PORTABLE_SERVICE_RUNNING.lock().unwrap() = false; + *RUNNING.lock().unwrap() = false; }); } Err(err) => { @@ -752,11 +814,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 +829,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 +837,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 +845,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/uinput.rs b/src/server/uinput.rs index 78b22c562..a2e91e57b 100644 --- a/src/server/uinput.rs +++ b/src/server/uinput.rs @@ -4,7 +4,7 @@ use evdev::{ uinput::{VirtualDevice, VirtualDeviceBuilder}, AttributeSet, EventType, InputEvent, }; -use hbb_common::{allow_err, bail, log, tokio, ResultType}; +use hbb_common::{allow_err, bail, log, tokio::{self, runtime::Runtime}, ResultType}; static IPC_CONN_TIMEOUT: u64 = 1000; static IPC_REQUEST_TIMEOUT: u64 = 1000; @@ -17,24 +17,24 @@ pub mod client { pub struct UInputKeyboard { conn: Connection, + rt: Runtime, } impl UInputKeyboard { pub async fn new() -> ResultType { let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_KEYBOARD).await?; - Ok(Self { conn }) + let rt = Runtime::new()?; + Ok(Self { conn, rt }) } - #[tokio::main(flavor = "current_thread")] - async fn send(&mut self, data: Data) -> ResultType<()> { - self.conn.send(&data).await + fn send(&mut self, data: Data) -> ResultType<()> { + self.rt.block_on(self.conn.send(&data)) } - #[tokio::main(flavor = "current_thread")] - async fn send_get_key_state(&mut self, data: Data) -> ResultType { - self.conn.send(&data).await?; + fn send_get_key_state(&mut self, data: Data) -> ResultType { + self.rt.block_on(self.conn.send(&data))?; - match self.conn.next_timeout(IPC_REQUEST_TIMEOUT).await { + match self.rt.block_on(self.conn.next_timeout(IPC_REQUEST_TIMEOUT)) { Ok(Some(Data::KeyboardResponse(ipc::DataKeyboardResponse::GetKeyState(state)))) => { Ok(state) } @@ -101,17 +101,18 @@ pub mod client { pub struct UInputMouse { conn: Connection, + rt: Runtime, } impl UInputMouse { pub async fn new() -> ResultType { let conn = ipc::connect(IPC_CONN_TIMEOUT, IPC_POSTFIX_MOUSE).await?; - Ok(Self { conn }) + let rt = Runtime::new()?; + Ok(Self { conn, rt }) } - #[tokio::main(flavor = "current_thread")] - async fn send(&mut self, data: Data) -> ResultType<()> { - self.conn.send(&data).await + fn send(&mut self, data: Data) -> ResultType<()> { + self.rt.block_on(self.conn.send(&data)) } pub fn send_refresh(&mut self) -> ResultType<()> { @@ -586,6 +587,16 @@ pub mod service { match data { Data::Mouse(data) => { if let DataMouse::Refresh = data { + let resolution = RESOLUTION.lock().unwrap(); + let rng_x = resolution.0.clone(); + let rng_y = resolution.1.clone(); + log::info!( + "Refresh uinput mouce with rng_x: ({}, {}), rng_y: ({}, {})", + rng_x.0, + rng_x.1, + rng_y.0, + rng_y.1 + ); mouse = match mouce::Mouse::new_uinput(rng_x, rng_y) { Ok(mouse) => mouse, Err(e) => { diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index d75596157..47bf49707 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -198,7 +198,7 @@ impl VideoQoS { #[cfg(target_os = "android")] { - // fix when andorid screen shrinks + // fix when android screen shrinks let fix = scrap::Display::fix_quality() as u32; log::debug!("Android screen, fix quality:{}", fix); let base_bitrate = base_bitrate * fix; diff --git a/src/server/video_service.rs b/src/server/video_service.rs index c8f59d60d..599dfbd54 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -20,7 +20,7 @@ use super::{video_qos::VideoQoS, *}; #[cfg(windows)] -use crate::portable_service::client::PORTABLE_SERVICE_RUNNING; +use hbb_common::get_version_number; use hbb_common::tokio::sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, Mutex as TokioMutex, @@ -74,11 +74,11 @@ fn is_capturer_mag_supported() -> bool { false } -pub fn capture_cursor_embeded() -> bool { - scrap::is_cursor_embeded() +pub fn capture_cursor_embedded() -> bool { + scrap::is_cursor_embedded() } -pub fn notify_video_frame_feched(conn_id: i32, frame_tm: Option) { +pub fn notify_video_frame_fetched(conn_id: i32, frame_tm: Option) { FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap() } @@ -92,7 +92,8 @@ pub fn get_privacy_mode_conn_id() -> i32 { pub fn is_privacy_mode_supported() -> bool { #[cfg(windows)] - return *IS_CAPTURER_MAGNIFIER_SUPPORTED; + return *IS_CAPTURER_MAGNIFIER_SUPPORTED + && get_version_number(&crate::VERSION) > get_version_number("1.1.9"); #[cfg(not(windows))] return false; } @@ -141,7 +142,7 @@ impl VideoFrameController { fetched_conn_ids.insert(id); } Ok(None) => { - // this branch would nerver be reached + // this branch would never be reached } } } @@ -157,7 +158,7 @@ fn check_display_changed( last_n: usize, last_current: usize, last_width: usize, - last_hegiht: usize, + last_height: usize, ) -> bool { #[cfg(target_os = "linux")] { @@ -182,7 +183,7 @@ fn check_display_changed( if i != last_current { return true; }; - if d.width() != last_width || d.height() != last_hegiht { + if d.width() != last_width || d.height() != last_height { return true; }; } @@ -244,7 +245,7 @@ fn create_capturer( PRIVACY_WINDOW_NAME ); } - log::debug!("Create maginifier capture for {}", privacy_mode_id); + log::debug!("Create magnifier capture for {}", privacy_mode_id); c = Some(Box::new(c1)); } Err(e) => { @@ -304,9 +305,9 @@ 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 { +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::ui::win_privacy::is_process_consent_running()? { bail!("consent.exe is running"); } @@ -325,7 +326,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, } @@ -366,27 +367,29 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType ResultType ResultType<()> { #[cfg(windows)] ensure_close_virtual_device()?; - // ensure_inited() is needed because release_resouce() may be called. + // ensure_inited() is needed because release_resource() may be called. #[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; @@ -459,7 +462,7 @@ fn run(sp: GenericService) -> ResultType<()> { y: c.origin.1 as _, width: c.width as _, height: c.height as _, - cursor_embeded: capture_cursor_embeded(), + cursor_embedded: capture_cursor_embedded(), ..Default::default() }); let mut msg_out = Message::new(); @@ -477,22 +480,7 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(windows)] log::info!("gdi: {}", c.is_gdi()); let codec_name = Encoder::current_hw_encoder_name(); - #[cfg(not(target_os = "ios"))] - let recorder = if !Config::get_option("allow-auto-record-incoming").is_empty() { - Recorder::new(RecorderContext { - id: "local".to_owned(), - default_dir: crate::ui_interface::default_video_save_directory(), - filename: "".to_owned(), - width: c.width, - height: c.height, - codec_id: scrap::record::RecordCodecID::VP9, - }) - .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) - } else { - Default::default() - }; - #[cfg(target_os = "ios")] - let recorder: Arc>> = Default::default(); + let recorder = get_recorder(c.width, c.height, &codec_name); #[cfg(windows)] start_uac_elevation_check(); @@ -501,7 +489,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() { @@ -526,14 +514,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"); } @@ -609,8 +597,8 @@ fn run(sp: GenericService) -> ResultType<()> { would_block_count += 1; if !scrap::is_x11() { if would_block_count >= 100 { - super::wayland::release_resouce(); - bail!("Wayland capturer none 100 times, try restart captuere"); + super::wayland::release_resource(); + bail!("Wayland capturer none 100 times, try restart capture"); } } } @@ -645,7 +633,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() { @@ -663,12 +651,59 @@ fn run(sp: GenericService) -> ResultType<()> { #[cfg(target_os = "linux")] if !scrap::is_x11() { - super::wayland::release_resouce(); + super::wayland::release_resource(); } Ok(()) } +fn get_recorder( + width: usize, + height: usize, + codec_name: &Option, +) -> Arc>> { + #[cfg(not(target_os = "ios"))] + let recorder = if !Config::get_option("allow-auto-record-incoming").is_empty() { + use crate::hbbs_http::record_upload; + use scrap::record::RecordCodecID::*; + + let tx = if record_upload::is_enable() { + let (tx, rx) = std::sync::mpsc::channel(); + record_upload::run(rx); + Some(tx) + } else { + None + }; + let codec_id = match codec_name { + Some(name) => { + if name.contains("264") { + H264 + } else { + H265 + } + } + None => VP9, + }; + Recorder::new(RecorderContext { + server: true, + id: Config::get_id(), + default_dir: crate::ui_interface::default_video_save_directory(), + filename: "".to_owned(), + width, + height, + codec_id, + tx, + }) + .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))) + } else { + Default::default() + }; + #[cfg(target_os = "ios")] + let recorder: Arc>> = Default::default(); + + recorder +} + fn check_privacy_mode_changed(sp: &GenericService, privacy_mode_id: i32) -> ResultType<()> { let privacy_mode_id_2 = *PRIVACY_MODE_CONN_ID.lock().unwrap(); if privacy_mode_id != privacy_mode_id_2 { @@ -784,7 +819,7 @@ pub(super) fn get_displays_2(all: &Vec) -> (usize, Vec) { height: d.height() as _, name: d.name(), online: d.is_online(), - cursor_embeded: false, + cursor_embedded: false, ..Default::default() }); } diff --git a/src/server/wayland.rs b/src/server/wayland.rs index fdf9bccec..68b9c37cf 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -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_embeded = true; + display.cursor_embedded = true; } let mut rects: Vec<((i32, i32), usize, usize)> = Vec::new(); @@ -244,7 +244,7 @@ pub(super) fn get_display_num() -> ResultType { } #[allow(dead_code)] -pub(super) fn release_resouce() { +pub(super) fn release_resource() { if scrap::is_x11() { return; } @@ -276,7 +276,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 3658739a4..e41a616de 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -1,3 +1,4 @@ +#[cfg(any(target_os = "linux", target_os = "windows"))] use super::ui_interface::get_option_opt; #[cfg(target_os = "linux")] use hbb_common::log::{debug, error, info}; @@ -44,7 +45,7 @@ pub fn start_tray() { } else { *control_flow = ControlFlow::Wait; } - let stopped = is_service_stoped(); + let stopped = is_service_stopped(); let state = if stopped { 2 } else { 1 }; let old = *old_state.lock().unwrap(); if state != old { @@ -88,6 +89,9 @@ pub fn start_tray() { /// 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"); @@ -98,7 +102,7 @@ pub fn start_tray() { } if let Some(mut appindicator) = get_default_app_indicator() { let mut menu = gtk::Menu::new(); - let stoped = is_service_stoped(); + let stoped = is_service_stopped(); // start/stop service let label = if stoped { crate::client::translate("Start Service".to_owned()) @@ -106,9 +110,9 @@ pub fn start_tray() { crate::client::translate("Stop service".to_owned()) }; let menu_item_service = gtk::MenuItem::with_label(label.as_str()); - menu_item_service.connect_activate(move |item| { + menu_item_service.connect_activate(move |_| { let _lock = crate::ui_interface::SENDER.lock().unwrap(); - update_tray_service_item(item); + change_service_state(); }); menu.append(&menu_item_service); // show tray item @@ -116,6 +120,16 @@ pub fn start_tray() { 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"); @@ -123,20 +137,28 @@ pub fn start_tray() { } #[cfg(target_os = "linux")] -fn update_tray_service_item(item: >k::MenuItem) { - use gtk::traits::GtkMenuItemExt; - - if is_service_stoped() { +fn change_service_state() { + if is_service_stopped() { debug!("Now try to start service"); - item.set_label(&crate::client::translate("Stop service".to_owned())); crate::ipc::set_option("stop-service", ""); } else { debug!("Now try to stop service"); - item.set_label(&crate::client::translate("Start Service".to_owned())); 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; @@ -173,7 +195,8 @@ fn get_default_app_indicator() -> Option { /// Check if service is stoped. /// Return [`true`] if service is stoped, [`false`] otherwise. #[inline] -fn is_service_stoped() -> bool { +#[cfg(any(target_os = "linux", target_os = "windows"))] +fn is_service_stopped() -> bool { if let Some(v) = get_option_opt("stop-service") { v == "Y" } else { @@ -183,17 +206,31 @@ fn is_service_stoped() -> bool { #[cfg(target_os = "macos")] pub fn make_tray() { + extern "C" { + fn BackingScaleFactor() -> f32; + } + let f = unsafe { BackingScaleFactor() }; use tray_item::TrayItem; let mode = dark_light::detect(); - let mut icon_path = ""; - match mode { + let icon_path = match mode { dark_light::Mode::Dark => { - icon_path = "mac-tray-light.png"; - }, + // still show big overflow icon in my test, so still use x1 png. + // let's do it with objc with svg support later. + // or use another tray crate, or find out in tauri (it has tray support) + if f > 2. { + "mac-tray-light-x2.png" + } else { + "mac-tray-light.png" + } + } dark_light::Mode::Light => { - icon_path = "mac-tray-dark.png"; - }, - } + if f > 2. { + "mac-tray-dark-x2.png" + } else { + "mac-tray-dark.png" + } + } + }; if let Ok(mut tray) = TrayItem::new(&crate::get_app_name(), icon_path) { tray.add_label(&format!( "{} {}", @@ -211,4 +248,3 @@ pub fn make_tray() { } } } - diff --git a/src/ui.rs b/src/ui.rs index e282d19c4..b8473072d 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -9,18 +9,13 @@ use sciter::Value; use hbb_common::{ allow_err, - config::{self, Config, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, - futures::future::join_all, + config::{self, PeerConfig}, log, - protobuf::Message as _, - rendezvous_proto::*, - tcp::FramedStream, - tokio, }; -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")] @@ -31,8 +26,6 @@ pub mod remote; #[cfg(target_os = "windows")] pub mod win_privacy; -type Message = RendezvousMessage; - pub type Children = Arc)>>; #[allow(dead_code)] type Status = (i32, bool, i64, String); @@ -42,6 +35,11 @@ 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(); +} + struct UIHostHandler; pub fn start(args: &mut [String]) { @@ -124,12 +122,13 @@ pub fn start(args: &mut [String]) { let args: Vec = iter.map(|x| x.clone()).collect(); frame.set_title(&id); frame.register_behavior("native-remote", move || { - Box::new(remote::SciterSession::new( - cmd.clone(), - id.clone(), - pass.clone(), - args.clone(), - )) + let handler = + remote::SciterSession::new(cmd.clone(), id.clone(), pass.clone(), args.clone()); + #[cfg(not(any(feature = "flutter", feature = "cli")))] + { + *CUR_SESSION.lock().unwrap() = Some(handler.inner()); + } + Box::new(handler) }); page = "remote.html"; } else { @@ -215,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() } @@ -510,7 +505,7 @@ impl UI { fn change_id(&self, id: String) { let old_id = self.get_id(); - change_id(id, old_id); + change_id_shared(id, old_id); } fn post_request(&self, url: String, body: String, header: String) { @@ -606,7 +601,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(); @@ -690,101 +684,6 @@ fn get_sound_inputs() -> Vec { .collect() } -const INVALID_FORMAT: &'static str = "Invalid format"; -const UNKNOWN_ERROR: &'static str = "Unknown error"; - -#[tokio::main(flavor = "current_thread")] -async fn change_id(id: String, old_id: String) -> &'static str { - if !hbb_common::is_valid_custom_id(&id) { - return INVALID_FORMAT; - } - let uuid = machine_uid::get().unwrap_or("".to_owned()); - if uuid.is_empty() { - return UNKNOWN_ERROR; - } - let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await; - let mut futs = Vec::new(); - let err: Arc> = Default::default(); - for rendezvous_server in rendezvous_servers { - let err = err.clone(); - let id = id.to_owned(); - let uuid = uuid.clone(); - let old_id = old_id.clone(); - futs.push(tokio::spawn(async move { - let tmp = check_id(rendezvous_server, old_id, id, uuid).await; - if !tmp.is_empty() { - *err.lock().unwrap() = tmp; - } - })); - } - join_all(futs).await; - let err = *err.lock().unwrap(); - if err.is_empty() { - crate::ipc::set_config_async("id", id.to_owned()).await.ok(); - } - err -} - -async fn check_id( - rendezvous_server: String, - old_id: String, - id: String, - uuid: String, -) -> &'static str { - let any_addr = Config::get_any_listen_addr(); - if let Ok(mut socket) = FramedStream::new( - crate::check_port(rendezvous_server, RENDEZVOUS_PORT), - any_addr, - RENDEZVOUS_TIMEOUT, - ) - .await - { - let mut msg_out = Message::new(); - msg_out.set_register_pk(RegisterPk { - old_id, - id, - uuid: uuid.into(), - ..Default::default() - }); - let mut ok = false; - if socket.send(&msg_out).await.is_ok() { - if let Some(Ok(bytes)) = socket.next_timeout(3_000).await { - if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { - match msg_in.union { - Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => { - match rpr.result.enum_value_or_default() { - register_pk_response::Result::OK => { - ok = true; - } - register_pk_response::Result::ID_EXISTS => { - return "Not available"; - } - register_pk_response::Result::TOO_FREQUENT => { - return "Too frequent"; - } - register_pk_response::Result::NOT_SUPPORT => { - return "server_not_support"; - } - register_pk_response::Result::INVALID_ID_FORMAT => { - return INVALID_FORMAT; - } - _ => {} - } - } - _ => {} - } - } - } - } - if !ok { - return UNKNOWN_ERROR; - } - } else { - return "Failed to connect to rendezvous server"; - } - "" -} - // sacrifice some memory pub fn value_crash_workaround(values: &[Value]) -> Arc> { let persist = Arc::new(values.to_vec()); diff --git a/src/ui/cm.css b/src/ui/cm.css index ff4d422e4..960c8b567 100644 --- a/src/ui/cm.css +++ b/src/ui/cm.css @@ -45,7 +45,7 @@ div.right-panel { div.icon-and-id { flow: horizontal; - border-spacing: 1em; + border-spacing: 1em; } div.icon { @@ -64,7 +64,7 @@ div.id { div.permissions { flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; } div.permissions > div { @@ -141,7 +141,7 @@ button.elevate>span { } button.elevate>span>span { - margin-left:*; + margin-left:*; margin-right:*; } diff --git a/src/ui/cm.html b/src/ui/cm.html index 4edb4a762..aabaa0294 100644 --- a/src/ui/cm.html +++ b/src/ui/cm.html @@ -4,7 +4,7 @@ @import url(common.css); @import url(cm.css); - diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 2cfc14bf1..4e46e217f 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -29,9 +29,9 @@ class Body: Reactor.Component }; var right_style = show_chat ? "" : "display: none"; var disconnected = c.disconnected; - var show_elevation_btn = handler.can_elevate() && show_elevation; + 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 alreayd 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/common.css b/src/ui/common.css index 1814ad32d..0fb9afcb1 100644 --- a/src/ui/common.css +++ b/src/ui/common.css @@ -56,7 +56,7 @@ button[type=checkbox], button[type=checkbox]:active { button.outline { border: color(border) solid 1px; - background: transparent; + background: transparent; color: color(text); } @@ -115,7 +115,7 @@ textarea:empty { .base:disabled { background: transparent; } .slider:hover { background: grey; } .slider:active { background: grey; } - .base { size: 16px; } + .base { size: 16px; } .corner { background: white; } } @@ -185,7 +185,7 @@ header div.window-icon icon { header caption { size: *; -} +} @media platform != "OSX" { button.window { diff --git a/src/ui/file_transfer.css b/src/ui/file_transfer.css index 9b45ea2b7..7fd4ac7a8 100644 --- a/src/ui/file_transfer.css +++ b/src/ui/file_transfer.css @@ -12,22 +12,22 @@ div#file-transfer { } table -{ +{ font: system; border: 1px solid color(border); flow: table-fixed; prototype: Grid; size: *; padding:0; - border-spacing: 0; + border-spacing: 0; overflow-x: auto; overflow-y: hidden; } - -table > thead { + +table > thead { behavior: column-resizer; border-bottom: color(border) solid 1px; -} +} table > tbody { behavior: select-multiple; @@ -41,20 +41,20 @@ table th { } table th -{ +{ padding: 4px; foreground-repeat: no-repeat; foreground-position: 50% 3px auto auto; border-left: color(border) solid 1px; -} +} -table th.sortable[sort=asc] -{ +table th.sortable[sort=asc] +{ foreground-image: url(stock:arrow-down); -} +} table th.sortable[sort=desc] -{ +{ foreground-image: url(stock:arrow-up); } @@ -81,10 +81,10 @@ table.has_current thead th:current { table tr:nth-child(odd) { background-color: white; } /* each odd row */ table tr:nth-child(even) { background-color: #F4F5F6; } /* each even row */ -table.has_current tr:current /* current row */ -{ - background-color: color(accent); -} +table.has_current tr:current /* current row */ +{ + background-color: color(accent); +} table.has_current tbody tr:checked { @@ -95,9 +95,9 @@ table.has_current tbody tr:checked td { color: highlighttext; } -table td -{ - padding: 4px; +table td +{ + padding: 4px; text-align: left; font-size: 1em; height: 1.4em; @@ -124,11 +124,11 @@ table td:nth-child(4) { section { size: *; margin: 1em; - border-spacing: 0.5em; + border-spacing: 0.5em; } table td:nth-child(1) { - foreground-repeat: no-repeat; + foreground-repeat: no-repeat; foreground-position: 50% 50% } @@ -160,11 +160,11 @@ div.toolbar > div.button:hover { div.toolbar > div.send { flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; } div.remote > div.send svg { - transform: scale(-1, 1); + transform: scale(-1, 1); } div.navbar { @@ -207,7 +207,7 @@ table.job-table tr td { padding: 0.5em 1em; border-bottom: color(border) 1px solid; flow: horizontal; - border-spacing: 1em; + border-spacing: 1em; height: 3em; overflow-x: hidden; } @@ -217,11 +217,11 @@ table.job-table tr svg { } table.job-table tr.is_remote svg { - transform: scale(-1, 1); + transform: scale(-1, 1); } table.job-table tr.is_remote div.svg_continue svg { - transform: scale(1, 1); + transform: scale(1, 1); } table.job-table tr td div.text { @@ -246,7 +246,7 @@ table#port-forward thead tr th { table#port-forward tr td { height: 3em; - text-align: left; + text-align: left; } table#port-forward input[type=text], table#port-forward input[type=number] { diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 451117403..f69f6d323 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -245,7 +245,13 @@ class JobTable: Reactor.Component { var percent = job.total_size == 0 ? 100 : (100. * job.finished_size / job.total_size).toInteger(); // (100. * i / (n || 1)).toInteger(); if (job.finished) percent = '100'; if (percent) res += ", " + percent + "%"; - if (job.finished) res = translate("Finished") + " " + res; + if (job.finished) { + if (job.err == "skipped") { + res = translate("Skipped") + " " + res; + } else { + res = translate("Finished") + " " + res; + } + } if (job.speed) res += ", " + getSize(0, job.speed) + "/s"; return res; } @@ -268,9 +274,10 @@ class JobTable: Reactor.Component { if (file_num < job.file_num) return; job.file_num = file_num; var n = job.num_entries || job.entries.length; - job.finished = job.file_num >= n - 1 || err == "cancel"; + job.finished = job.file_num >= n - 1 || err == "cancel" || err == "skipped"; job.finished_size = finished_size; job.speed = speed || 0; + job.err = err; this.updateJob(job); if (job.type == "del-dir") { if (job.finished) { diff --git a/src/ui/header.css b/src/ui/header.css index e248b46d5..8fe408612 100644 --- a/src/ui/header.css +++ b/src/ui/header.css @@ -8,7 +8,7 @@ header #screens { height: 22px; border-radius: 4px; flow: horizontal; - border-spacing: 0.5em; + border-spacing: 0.5em; padding-right: 1em; position: relative; } diff --git a/src/ui/header.tis b/src/ui/header.tis index 086696726..dd0b35541 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -164,10 +164,10 @@ class Header: Reactor.Component { var codecs = handler.supported_hwcodec(); var show_codec = handler.has_hwcodec() && (codecs[0] || codecs[1]); - var cursor_embeded = false; + var cursor_embedded = false; if ((pi.displays || []).length > 0) { if (pi.displays.length > pi.current_display) { - cursor_embeded = pi.displays[pi.current_display].cursor_embeded; + cursor_embedded = pi.displays[pi.current_display].cursor_embedded; } } @@ -191,7 +191,7 @@ class Header: Reactor.Component { {codecs[1] ?
  • {svg_checkmark}H265
  • : ""}
    : ""}
    - {!cursor_embeded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • } + {!cursor_embedded &&
  • {svg_checkmark}{translate('Show remote cursor')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} {is_win && pi.platform == 'Windows' && file_enabled ?
  • {svg_checkmark}{translate('Allow file copy and paste')}
  • : ""} @@ -208,7 +208,7 @@ class Header: Reactor.Component { {keyboard_enabled ?
  • {translate('OS Password')}
  • : ""}
  • {translate('Transfer File')}
  • {translate('TCP Tunneling')}
  • - {handler.get_audit_server() &&
  • {translate('Note')}
  • } + {handler.get_audit_server("conn") &&
  • {translate('Note')}
  • }
    {keyboard_enabled && (pi.platform == "Linux" || pi.sas_enabled) ?
  • {translate('Insert')} Ctrl + Alt + Del
  • : ""} {restart_enabled && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS") ?
  • {translate('Restart Remote Device')}
  • : ""} diff --git a/src/ui/index.tis b/src/ui/index.tis index 9dcd4f4c4..2d77b1eec 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]; @@ -310,7 +309,6 @@ class MyIdMenu: Reactor.Component { {handler.is_rdp_service_open() ? : ""} {false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connected via relay')}
  • } - {handler.has_rendezvous_service() ?
  • {translate(rendezvous_service_stopped ? "Start ID/relay service" : "Stop ID/relay service")}
  • : ""} {handler.is_ok_change_id() ?
    : ""} {username ?
  • {translate('Logout')} ({username})
  • : @@ -361,7 +359,7 @@ class MyIdMenu: Reactor.Component { function showAbout() { var name = handler.get_app_name(); - msgbox("custom-nocancel-nook-hasclose", "About " + name, "
    \ + msgbox("custom-nocancel-nook-hasclose", translate("About") + " " + name, "
    \
    Version: " + handler.get_version() + " \
    Privacy Statement
    \
    Website
    \ @@ -468,8 +466,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') + "
    \ @@ -1121,11 +1117,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/macos.rs b/src/ui/macos.rs index 488d1afc8..7daef8eab 100644 --- a/src/ui/macos.rs +++ b/src/ui/macos.rs @@ -13,7 +13,6 @@ use objc::{ }; use sciter::{make_args, Host}; use std::{ffi::c_void, rc::Rc}; -use dark_light; static APP_HANDLER_IVAR: &str = "GoDeskAppHandler"; @@ -43,7 +42,7 @@ impl DelegateState { } } -static mut LAUCHED: bool = false; +static mut LAUNCHED: bool = false; impl AppHandler for Rc { fn command(&mut self, cmd: u32) { @@ -110,7 +109,7 @@ unsafe fn set_delegate(handler: Option>) { 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]; @@ -123,12 +122,12 @@ 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::check_main_window(); } let inner: *mut c_void = *this.get_ivar(APP_HANDLER_IVAR); let inner = &mut *(inner as *mut DelegateState); @@ -234,24 +233,4 @@ pub fn make_tray() { 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(); -} +} \ No newline at end of file diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 3d209a71c..21504d20d 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -79,8 +79,8 @@ impl InvokeUiSession for SciterHandler { } } - fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embeded: bool) { - self.call("setDisplay", &make_args!(x, y, w, h, cursor_embeded)); + fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embedded: bool) { + self.call("setDisplay", &make_args!(x, y, w, h, cursor_embedded)); // https://sciter.com/forums/topic/color_spaceiyuv-crash // Nothing spectacular in decoder – done on CPU side. // So if you can do BGRA translation on your side – the better. @@ -223,7 +223,7 @@ impl InvokeUiSession for SciterHandler { display.set_item("y", d.y); display.set_item("width", d.width); display.set_item("height", d.height); - display.set_item("cursor_embeded", d.cursor_embeded); + display.set_item("cursor_embedded", d.cursor_embedded); displays.push(display); } pi_sciter.set_item("displays", displays); @@ -231,6 +231,17 @@ impl InvokeUiSession for SciterHandler { self.call("updatePi", &make_args!(pi_sciter)); } + fn on_connected(&self, conn_type: ConnType) { + match conn_type { + ConnType::RDP => {} + ConnType::PORT_FORWARD => {} + ConnType::FILE_TRANSFER => {} + ConnType::DEFAULT_CONN => { + crate::keyboard::client::start_grab_loop(); + } + } + } + fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str, retry: bool) { self.call2( "msgbox_retry", @@ -253,6 +264,8 @@ impl InvokeUiSession for SciterHandler { fn update_block_input_state(&self, on: bool) { self.call("updateBlockInputState", &make_args!(on)); } + + fn switch_back(&self, _id: &str) {} } pub struct SciterSession(Session); @@ -337,7 +350,7 @@ impl sciter::EventHandler for SciterSession { } sciter::dispatch_script_call! { - fn get_audit_server(); + fn get_audit_server(String); fn send_note(String); fn is_xfce(); fn get_id(); @@ -429,11 +442,15 @@ impl SciterSession { ConnType::DEFAULT_CONN }; - session.lc.write().unwrap().initialize(id, conn_type); + session.lc.write().unwrap().initialize(id, conn_type, None); Self(session) } + pub fn inner(&self) -> Session { + self.0.clone() + } + fn get_custom_image_quality(&mut self) -> Value { let mut v = Value::array(0); for x in self.lc.read().unwrap().custom_image_quality.iter() { diff --git a/src/ui/remote.tis b/src/ui/remote.tis index 012205abc..5c828689d 100644 --- a/src/ui/remote.tis +++ b/src/ui/remote.tis @@ -6,7 +6,7 @@ var display_width = 0; var display_height = 0; var display_origin_x = 0; var display_origin_y = 0; -var display_cursor_embeded = false; +var display_cursor_embedded = false; var display_scale = 1; var keyboard_enabled = true; // server side var clipboard_enabled = true; // server side @@ -16,17 +16,17 @@ var restart_enabled = true; // server side var recording_enabled = true; // server side var scroll_body = $(body); -handler.setDisplay = function(x, y, w, h, cursor_embeded) { +handler.setDisplay = function(x, y, w, h, cursor_embedded) { display_width = w; display_height = h; display_origin_x = x; display_origin_y = y; - display_cursor_embeded = cursor_embeded; + display_cursor_embedded = cursor_embedded; adaptDisplay(); if (recording) handler.record_screen(true, w, h); } -// in case toolbar not shown correclty +// in case toolbar not shown correctly view.windowMinSize = (scaleIt(500), scaleIt(300)); function adaptDisplay() { @@ -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; @@ -197,7 +197,7 @@ function handler.onMouse(evt) dragging = false; break; case Event.MOUSE_MOVE: - if (display_cursor_embeded) { + if (display_cursor_embedded) { break; } if (cursor_img.style#display != "none" && keyboard_enabled) { @@ -365,7 +365,7 @@ function updateCursor(system=false) { } function refreshCursor() { - if (display_cursor_embeded) { + if (display_cursor_embedded) { cursor_img.style#display = "none"; return; } diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index 26e5e4077..ea3553c8a 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -48,6 +48,7 @@ pub struct Client { pub file: bool, pub restart: bool, pub recording: bool, + pub from_switch: bool, #[serde(skip)] tx: UnboundedSender, } @@ -118,6 +119,7 @@ impl ConnectionManager { file: bool, restart: bool, recording: bool, + from_switch: bool, tx: mpsc::UnboundedSender, ) { let client = Client { @@ -134,6 +136,7 @@ impl ConnectionManager { file, restart, recording, + from_switch, tx, }; CLIENTS @@ -241,6 +244,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 +264,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 +299,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 +319,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 +365,7 @@ impl IpcTaskRunner { } } #[cfg(windows)] - Data::ClipbaordFile(_clip) => { + Data::ClipboardFile(_clip) => { #[cfg(windows)] { let conn_id = self.conn_id; @@ -394,7 +405,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 => { // @@ -498,6 +509,7 @@ pub async fn start_listen( file, restart, recording, + from_switch, .. }) => { current_id = id; @@ -514,6 +526,7 @@ pub async fn start_listen( file, restart, recording, + from_switch, tx.clone(), ); } @@ -596,6 +609,12 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec, tx: &Unbo fs::remove_job(id, write_jobs); } } + ipc::FS::WriteError { id, file_num, err } => { + if let Some(job) = fs::get_job(id, write_jobs) { + send_raw(fs::new_error(job.id(), err, file_num), tx); + fs::remove_job(job.id(), write_jobs); + } + } ipc::FS::WriteBlock { id, file_num, @@ -774,11 +793,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() && !crate::portable_service::client::running(); } #[cfg(not(windows))] return false; diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 41f22a563..ebaf8c317 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -14,20 +14,17 @@ use hbb_common::{ tokio::{self, sync::mpsc, time}, }; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] use hbb_common::{ config::{RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, futures::future::join_all, protobuf::Message as _, rendezvous_proto::*, - tcp::FramedStream, }; #[cfg(feature = "flutter")] use crate::hbbs_http::account; use crate::{common::SOFTWARE_UPDATE_URL, ipc}; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] type Message = RendezvousMessage; pub type Children = Arc)>>; @@ -137,13 +134,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)] @@ -161,6 +151,7 @@ pub fn get_license() -> String { } #[inline] +#[cfg(any(target_os = "linux", target_os = "windows"))] pub fn get_option_opt(key: &str) -> Option { OPTIONS.lock().unwrap().get(key).map(|x| x.clone()) } @@ -202,6 +193,18 @@ pub fn set_local_flutter_config(key: String, value: String) { LocalConfig::set_flutter_config(key, value); } +#[cfg(feature = "flutter")] +#[inline] +pub fn get_kb_layout_type() -> String { + LocalConfig::get_kb_layout_type() +} + +#[cfg(feature = "flutter")] +#[inline] +pub fn set_kb_layout_type(kb_layout_type: String) { + LocalConfig::set_kb_layout_type(kb_layout_type); +} + #[inline] pub fn peer_has_password(id: String) -> bool { !PeerConfig::load(&id).password.is_empty() @@ -233,7 +236,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").is_none() + && crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() } #[inline] @@ -572,6 +576,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")))] @@ -685,6 +698,20 @@ pub fn discover() { }); } +#[cfg(feature = "flutter")] +pub fn peer_to_map(id: String, p: PeerConfig) -> HashMap<&'static str, String> { + HashMap::<&str, String>::from_iter([ + ("id", id), + ("username", p.info.username.clone()), + ("hostname", p.info.hostname.clone()), + ("platform", p.info.platform.clone()), + ( + "alias", + p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(), + ), + ]) +} + #[inline] pub fn get_lan_peers() -> Vec> { config::LanPeers::load() @@ -728,7 +755,7 @@ pub fn change_id(id: String) { *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); let old_id = get_id(); std::thread::spawn(move || { - *ASYNC_JOB_STATUS.lock().unwrap() = change_id_(id, old_id).to_owned(); + *ASYNC_JOB_STATUS.lock().unwrap() = change_id_shared(id, old_id).to_owned(); }); } @@ -904,7 +931,7 @@ pub fn account_auth_result() -> String { serde_json::to_string(&account::OidcSession::get_result()).unwrap_or_default() } -// notice: avoiding create ipc connecton repeatly, +// 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) { @@ -985,14 +1012,11 @@ pub(crate) async fn send_to_cm(data: &ipc::Data) { } } -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] const INVALID_FORMAT: &'static str = "Invalid format"; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] const UNKNOWN_ERROR: &'static str = "Unknown error"; -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] #[tokio::main(flavor = "current_thread")] -async fn change_id_(id: String, old_id: String) -> &'static str { +pub async fn change_id_shared(id: String, old_id: String) -> &'static str { if !hbb_common::is_valid_custom_id(&id) { return INVALID_FORMAT; } @@ -1040,17 +1064,14 @@ async fn change_id_(id: String, old_id: String) -> &'static str { err } -#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] async fn check_id( rendezvous_server: String, old_id: String, id: String, uuid: String, ) -> &'static str { - let any_addr = Config::get_any_listen_addr(); - if let Ok(mut socket) = FramedStream::new( + if let Ok(mut socket) = hbb_common::socket_client::connect_tcp( crate::check_port(rendezvous_server, RENDEZVOUS_PORT), - any_addr, RENDEZVOUS_TIMEOUT, ) .await diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 6f8820e87..3fb3f2621 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -1,67 +1,27 @@ use crate::client::io_loop::Remote; use crate::client::{ - check_if_retry, handle_hash, handle_login_from_ui, handle_test_delay, input_os_password, - load_config, send_mouse, start_video_audio_threads, FileManager, Key, LoginConfigHandler, - QualityStatus, KEY_MAP, + 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, }; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::client::{get_key_state, SERVER_KEYBOARD_ENABLED}; -#[cfg(target_os = "linux")] -use crate::common::IS_X11; +use crate::common::{self, GrabState}; +use crate::keyboard; use crate::{client::Data, client::Interface}; use async_trait::async_trait; +use bytes::Bytes; 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, EventType::*, Key as RdevKey}; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use rdev::{Keyboard as RdevKeyboard, KeyboardState}; -use std::collections::{HashMap, HashSet}; +use rdev::{Event, EventType::*}; +use std::collections::HashMap; use std::ops::{Deref, DerefMut}; +use std::str::FromStr; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex, RwLock}; - -/// IS_IN KEYBOARD_HOOKED sciter only +use uuid::Uuid; pub static IS_IN: AtomicBool = AtomicBool::new(false); -pub static KEYBOARD_HOOKED: AtomicBool = AtomicBool::new(false); -pub static HOTKEY_HOOKED: AtomicBool = AtomicBool::new(false); -#[cfg(windows)] -static mut IS_ALT_GR: bool = false; -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::flutter::FlutterHandler; - -lazy_static::lazy_static! { - static ref TO_RELEASE: Arc>> = Arc::new(Mutex::new(HashSet::::new())); -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -lazy_static::lazy_static! { - static ref KEYBOARD: Arc> = Arc::new(Mutex::new(RdevKeyboard::new().unwrap())); -} - -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -lazy_static::lazy_static! { - pub static ref CUR_SESSION: Arc>>> = Default::default(); -} - -lazy_static::lazy_static! { - static ref MUTEX_SPECIAL_KEYS: Mutex> = { - let mut m = HashMap::new(); - m.insert(RdevKey::ShiftLeft, false); - m.insert(RdevKey::ShiftRight, false); - m.insert(RdevKey::ControlLeft, false); - m.insert(RdevKey::ControlRight, false); - m.insert(RdevKey::Alt, false); - m.insert(RdevKey::AltGr, false); - m.insert(RdevKey::MetaLeft, false); - m.insert(RdevKey::MetaRight, false); - Mutex::new(m) - }; -} #[derive(Clone, Default)] pub struct Session { @@ -75,6 +35,32 @@ pub struct Session { } impl Session { + pub fn is_file_transfer(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::FILE_TRANSFER) + } + + pub fn is_port_forward(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::PORT_FORWARD) + } + + pub fn is_rdp(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) + } + + pub fn set_connection_info(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.direct = Some(direct); + lc.received = received; + } + pub fn get_view_style(&self) -> String { self.lc.read().unwrap().view_style.clone() } @@ -91,12 +77,16 @@ impl Session { self.lc.read().unwrap().custom_image_quality.clone() } - pub fn get_keyboard_mode(&self) -> String { - global_get_keyboard_mode() + pub fn get_peer_version(&self) -> i64 { + self.lc.read().unwrap().version.clone() } - pub fn save_keyboard_mode(&self, value: String) { - global_save_keyboard_mode(value); + pub fn get_keyboard_mode(&self) -> String { + self.lc.read().unwrap().keyboard_mode.clone() + } + + pub fn save_keyboard_mode(&mut self, value: String) { + self.lc.write().unwrap().save_keyboard_mode(value); } pub fn save_view_style(&mut self, value: String) { @@ -200,7 +190,7 @@ impl Session { h265 = h265 && encoding_265; return (h264, h265); } - #[allow(dead_code)] + #[allow(unreachable_code)] (false, false) } @@ -216,7 +206,7 @@ impl Session { self.send(Data::Message(msg)); } - pub fn get_audit_server(&self) -> String { + pub fn get_audit_server(&self, typ: String) -> String { if self.lc.read().unwrap().conn_id <= 0 || LocalConfig::get_option("access_token").is_empty() { @@ -225,11 +215,12 @@ impl Session { crate::get_audit_server( Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"), + typ, ) } pub fn send_note(&self, note: String) { - let url = self.get_audit_server(); + let url = self.get_audit_server("conn".to_string()); let id = self.id.clone(); let conn_id = self.lc.read().unwrap().conn_id; std::thread::spawn(move || { @@ -241,6 +232,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 @@ -307,439 +303,6 @@ impl Session { self.lc.read().unwrap().info.platform.clone() } - pub fn ctrl_alt_del(&self) { - if self.peer_platform() == "Windows" { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::CtrlAltDel); - // todo - key_event.down = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } else { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::Delete); - self.legacy_modifiers(&mut key_event, true, true, false, false); - // todo - key_event.press = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } - } - - fn send_key_event(&self, mut evt: KeyEvent, keyboard_mode: KeyboardMode) { - // mode: legacy(0), map(1), translate(2), auto(3) - evt.mode = keyboard_mode.into(); - let mut msg_out = Message::new(); - msg_out.set_key_event(evt); - self.send(Data::Message(msg_out)); - } - - #[allow(dead_code)] - fn convert_numpad_keys(&self, key: RdevKey) -> RdevKey { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::NumLock) { - return key; - } - match key { - RdevKey::Kp0 => RdevKey::Insert, - RdevKey::KpDecimal => RdevKey::Delete, - RdevKey::Kp1 => RdevKey::End, - RdevKey::Kp2 => RdevKey::DownArrow, - RdevKey::Kp3 => RdevKey::PageDown, - RdevKey::Kp4 => RdevKey::LeftArrow, - RdevKey::Kp5 => RdevKey::Clear, - RdevKey::Kp6 => RdevKey::RightArrow, - RdevKey::Kp7 => RdevKey::Home, - RdevKey::Kp8 => RdevKey::UpArrow, - RdevKey::Kp9 => RdevKey::PageUp, - _ => key, - } - } - - fn map_keyboard_mode(&self, down_or_up: bool, key: RdevKey, _evt: Option) { - // map mode(1): Send keycode according to the peer platform. - #[cfg(target_os = "windows")] - let key = if let Some(e) = _evt { - rdev::get_win_key(e.code.into(), e.scan_code) - } else { - key - }; - - let peer = self.peer_platform(); - let mut key_event = KeyEvent::new(); - // According to peer platform. - let keycode: u32 = if peer == "Linux" { - rdev::linux_keycode_from_key(key).unwrap_or_default().into() - } else if peer == "Windows" { - rdev::win_keycode_from_key(key).unwrap_or_default().into() - } else { - // Without Clear Key on Mac OS - if key == rdev::Key::Clear { - return; - } - rdev::macos_keycode_from_key(key).unwrap_or_default().into() - }; - - key_event.set_chr(keycode); - key_event.down = down_or_up; - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - self.send_key_event(key_event, KeyboardMode::Map); - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn translate_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { - // translate mode(2): locally generated characters are send to the peer. - - // get char - let string = match KEYBOARD.lock() { - Ok(mut keyboard) => { - let string = keyboard.add(&evt.event_type).unwrap_or_default(); - if keyboard.is_dead() && string == "" && down_or_up == true { - return; - } - string - } - Err(_) => "".to_owned(), - }; - - // maybe two string - let chars = if string == "" { - None - } else { - let chars: Vec = string.chars().collect(); - Some(chars) - }; - - if let Some(chars) = chars { - for chr in chars { - let mut key_event = KeyEvent::new(); - key_event.set_chr(chr as _); - key_event.down = true; - key_event.press = false; - - self.send_key_event(key_event, KeyboardMode::Translate); - } - } else { - let success = if down_or_up == true { - TO_RELEASE.lock().unwrap().insert(key) - } else { - TO_RELEASE.lock().unwrap().remove(&key) - }; - - // AltGr && LeftControl(SpecialKey) without action - if key == RdevKey::AltGr || evt.scan_code == 541 { - return; - } - if success { - self.map_keyboard_mode(down_or_up, key, None); - } - } - } - - fn legacy_modifiers( - &self, - key_event: &mut KeyEvent, - alt: bool, - ctrl: bool, - shift: bool, - command: bool, - ) { - if alt - && !crate::is_control_key(&key_event, &ControlKey::Alt) - && !crate::is_control_key(&key_event, &ControlKey::RAlt) - { - key_event.modifiers.push(ControlKey::Alt.into()); - } - if shift - && !crate::is_control_key(&key_event, &ControlKey::Shift) - && !crate::is_control_key(&key_event, &ControlKey::RShift) - { - key_event.modifiers.push(ControlKey::Shift.into()); - } - if ctrl - && !crate::is_control_key(&key_event, &ControlKey::Control) - && !crate::is_control_key(&key_event, &ControlKey::RControl) - { - key_event.modifiers.push(ControlKey::Control.into()); - } - if command - && !crate::is_control_key(&key_event, &ControlKey::Meta) - && !crate::is_control_key(&key_event, &ControlKey::RWin) - { - key_event.modifiers.push(ControlKey::Meta.into()); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if get_key_state(enigo::Key::CapsLock) { - key_event.modifiers.push(ControlKey::CapsLock.into()); - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if self.peer_platform() != "Mac OS" { - if get_key_state(enigo::Key::NumLock) { - key_event.modifiers.push(ControlKey::NumLock.into()); - } - } - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - fn legacy_keyboard_mode(&self, down_or_up: bool, key: RdevKey, evt: Event) { - // legacy mode(0): Generate characters locally, look for keycode on other side. - let peer = self.peer_platform(); - let is_win = peer == "Windows"; - - let alt = get_key_state(enigo::Key::Alt); - #[cfg(windows)] - let ctrl = { - let mut tmp = - get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - unsafe { - if IS_ALT_GR { - if alt || key == RdevKey::AltGr { - if tmp { - tmp = false; - } - } else { - IS_ALT_GR = false; - } - } - } - tmp - }; - #[cfg(not(windows))] - let ctrl = get_key_state(enigo::Key::Control) || get_key_state(enigo::Key::RightControl); - let shift = get_key_state(enigo::Key::Shift) || get_key_state(enigo::Key::RightShift); - #[cfg(windows)] - let command = crate::platform::windows::get_win_key_state(); - #[cfg(not(windows))] - let command = get_key_state(enigo::Key::Meta); - let control_key = match key { - RdevKey::Alt => Some(ControlKey::Alt), - RdevKey::AltGr => Some(ControlKey::RAlt), - RdevKey::Backspace => Some(ControlKey::Backspace), - RdevKey::ControlLeft => { - // when pressing AltGr, an extra VK_LCONTROL with a special - // scancode with bit 9 set is sent, let's ignore this. - #[cfg(windows)] - if evt.scan_code & 0x200 != 0 { - unsafe { - IS_ALT_GR = true; - } - return; - } - Some(ControlKey::Control) - } - RdevKey::ControlRight => Some(ControlKey::RControl), - RdevKey::DownArrow => Some(ControlKey::DownArrow), - RdevKey::Escape => Some(ControlKey::Escape), - RdevKey::F1 => Some(ControlKey::F1), - RdevKey::F10 => Some(ControlKey::F10), - RdevKey::F11 => Some(ControlKey::F11), - RdevKey::F12 => Some(ControlKey::F12), - RdevKey::F2 => Some(ControlKey::F2), - RdevKey::F3 => Some(ControlKey::F3), - RdevKey::F4 => Some(ControlKey::F4), - RdevKey::F5 => Some(ControlKey::F5), - RdevKey::F6 => Some(ControlKey::F6), - RdevKey::F7 => Some(ControlKey::F7), - RdevKey::F8 => Some(ControlKey::F8), - RdevKey::F9 => Some(ControlKey::F9), - RdevKey::LeftArrow => Some(ControlKey::LeftArrow), - RdevKey::MetaLeft => Some(ControlKey::Meta), - RdevKey::MetaRight => Some(ControlKey::RWin), - RdevKey::Return => Some(ControlKey::Return), - RdevKey::RightArrow => Some(ControlKey::RightArrow), - RdevKey::ShiftLeft => Some(ControlKey::Shift), - RdevKey::ShiftRight => Some(ControlKey::RShift), - RdevKey::Space => Some(ControlKey::Space), - RdevKey::Tab => Some(ControlKey::Tab), - RdevKey::UpArrow => Some(ControlKey::UpArrow), - RdevKey::Delete => { - if is_win && ctrl && alt { - self.ctrl_alt_del(); - return; - } - Some(ControlKey::Delete) - } - RdevKey::Apps => Some(ControlKey::Apps), - RdevKey::Cancel => Some(ControlKey::Cancel), - RdevKey::Clear => Some(ControlKey::Clear), - RdevKey::Kana => Some(ControlKey::Kana), - RdevKey::Hangul => Some(ControlKey::Hangul), - RdevKey::Junja => Some(ControlKey::Junja), - RdevKey::Final => Some(ControlKey::Final), - RdevKey::Hanja => Some(ControlKey::Hanja), - RdevKey::Hanji => Some(ControlKey::Hanja), - RdevKey::Convert => Some(ControlKey::Convert), - RdevKey::Print => Some(ControlKey::Print), - RdevKey::Select => Some(ControlKey::Select), - RdevKey::Execute => Some(ControlKey::Execute), - RdevKey::PrintScreen => Some(ControlKey::Snapshot), - RdevKey::Help => Some(ControlKey::Help), - RdevKey::Sleep => Some(ControlKey::Sleep), - RdevKey::Separator => Some(ControlKey::Separator), - RdevKey::KpReturn => Some(ControlKey::NumpadEnter), - RdevKey::Kp0 => Some(ControlKey::Numpad0), - RdevKey::Kp1 => Some(ControlKey::Numpad1), - RdevKey::Kp2 => Some(ControlKey::Numpad2), - RdevKey::Kp3 => Some(ControlKey::Numpad3), - RdevKey::Kp4 => Some(ControlKey::Numpad4), - RdevKey::Kp5 => Some(ControlKey::Numpad5), - RdevKey::Kp6 => Some(ControlKey::Numpad6), - RdevKey::Kp7 => Some(ControlKey::Numpad7), - RdevKey::Kp8 => Some(ControlKey::Numpad8), - RdevKey::Kp9 => Some(ControlKey::Numpad9), - RdevKey::KpDivide => Some(ControlKey::Divide), - RdevKey::KpMultiply => Some(ControlKey::Multiply), - RdevKey::KpDecimal => Some(ControlKey::Decimal), - RdevKey::KpMinus => Some(ControlKey::Subtract), - RdevKey::KpPlus => Some(ControlKey::Add), - RdevKey::CapsLock | RdevKey::NumLock | RdevKey::ScrollLock => { - return; - } - RdevKey::Home => Some(ControlKey::Home), - RdevKey::End => Some(ControlKey::End), - RdevKey::Insert => Some(ControlKey::Insert), - RdevKey::PageUp => Some(ControlKey::PageUp), - RdevKey::PageDown => Some(ControlKey::PageDown), - RdevKey::Pause => Some(ControlKey::Pause), - _ => None, - }; - let mut key_event = KeyEvent::new(); - if let Some(k) = control_key { - key_event.set_control_key(k); - } else { - let mut chr = match evt.name { - Some(ref s) => { - if s.len() <= 2 { - // exclude chinese characters - s.chars().next().unwrap_or('\0') - } else { - '\0' - } - } - _ => '\0', - }; - if chr == '·' { - // special for Chinese - chr = '`'; - } - if chr == '\0' { - chr = match key { - RdevKey::Num1 => '1', - RdevKey::Num2 => '2', - RdevKey::Num3 => '3', - RdevKey::Num4 => '4', - RdevKey::Num5 => '5', - RdevKey::Num6 => '6', - RdevKey::Num7 => '7', - RdevKey::Num8 => '8', - RdevKey::Num9 => '9', - RdevKey::Num0 => '0', - RdevKey::KeyA => 'a', - RdevKey::KeyB => 'b', - RdevKey::KeyC => 'c', - RdevKey::KeyD => 'd', - RdevKey::KeyE => 'e', - RdevKey::KeyF => 'f', - RdevKey::KeyG => 'g', - RdevKey::KeyH => 'h', - RdevKey::KeyI => 'i', - RdevKey::KeyJ => 'j', - RdevKey::KeyK => 'k', - RdevKey::KeyL => 'l', - RdevKey::KeyM => 'm', - RdevKey::KeyN => 'n', - RdevKey::KeyO => 'o', - RdevKey::KeyP => 'p', - RdevKey::KeyQ => 'q', - RdevKey::KeyR => 'r', - RdevKey::KeyS => 's', - RdevKey::KeyT => 't', - RdevKey::KeyU => 'u', - RdevKey::KeyV => 'v', - RdevKey::KeyW => 'w', - RdevKey::KeyX => 'x', - RdevKey::KeyY => 'y', - RdevKey::KeyZ => 'z', - RdevKey::Comma => ',', - RdevKey::Dot => '.', - RdevKey::SemiColon => ';', - RdevKey::Quote => '\'', - RdevKey::LeftBracket => '[', - RdevKey::RightBracket => ']', - RdevKey::Slash => '/', - RdevKey::BackSlash => '\\', - RdevKey::Minus => '-', - RdevKey::Equal => '=', - RdevKey::BackQuote => '`', - _ => '\0', - } - } - if chr != '\0' { - if chr == 'l' && is_win && command { - self.lock_screen(); - return; - } - key_event.set_chr(chr as _); - } else { - log::error!("Unknown key {:?}", evt); - return; - } - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); - self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); - - if down_or_up == true { - key_event.down = true; - } - self.send_key_event(key_event, KeyboardMode::Legacy) - } - - fn key_down_or_up(&self, down_or_up: bool, key: RdevKey, evt: Event) { - // Call different functions according to keyboard mode. - let mode = match self.get_keyboard_mode().as_str() { - "map" => KeyboardMode::Map, - "legacy" => KeyboardMode::Legacy, - "translate" => KeyboardMode::Translate, - _ => KeyboardMode::Legacy, - }; - - #[cfg(not(windows))] - let key = self.convert_numpad_keys(key); - - let mut to_release = TO_RELEASE.lock().unwrap(); - match mode { - KeyboardMode::Map => { - if down_or_up == true { - to_release.insert(key); - } else { - to_release.remove(&key); - } - self.map_keyboard_mode(down_or_up, key, Some(evt)); - } - KeyboardMode::Legacy => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.legacy_keyboard_mode(down_or_up, key, evt) - } - KeyboardMode::Translate => { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.translate_keyboard_mode(down_or_up, key, evt); - } - _ => - { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.legacy_keyboard_mode(down_or_up, key, evt) - } - } - } - pub fn get_platform(&self, is_remote: bool) -> String { if is_remote { self.peer_platform() @@ -768,6 +331,13 @@ impl Session { return "".to_owned(); } + pub fn send_key_event(&self, evt: &KeyEvent) { + // mode: legacy(0), map(1), translate(2), auto(3) + let mut msg_out = Message::new(); + msg_out.set_key_event(evt.clone()); + self.send(Data::Message(msg_out)); + } + pub fn send_chat(&self, text: String) { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { @@ -790,77 +360,14 @@ impl Session { self.send(Data::Message(msg_out)); } - pub fn lock_screen(&self) { - let mut key_event = KeyEvent::new(); - key_event.set_control_key(ControlKey::LockScreen); - // todo - key_event.down = true; - self.send_key_event(key_event, KeyboardMode::Legacy); - } - pub fn enter(&self) { IS_IN.store(true, Ordering::SeqCst); - #[cfg(target_os = "linux")] - self.grab_hotkeys(true); - - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(true); + keyboard::client::change_grab_status(GrabState::Run); } pub fn leave(&self) { IS_IN.store(false, Ordering::SeqCst); - #[cfg(target_os = "linux")] - self.grab_hotkeys(false); - - for key in TO_RELEASE.lock().unwrap().iter() { - self.map_keyboard_mode(false, *key, None) - } - #[cfg(windows)] - crate::platform::windows::stop_system_key_propagate(false); - } - - #[cfg(target_os = "linux")] - pub fn grab_hotkeys(&self, _grab: bool) { - if _grab { - rdev::enable_grab().ok(); - } else { - rdev::disable_grab().ok(); - } - } - - pub fn handle_flutter_key_event( - &self, - name: &str, - keycode: i32, - scancode: i32, - down_or_up: bool, - ) { - if scancode < 0 || keycode < 0 { - return; - } - let keycode: u32 = keycode as u32; - let scancode: u32 = scancode as u32; - - #[cfg(not(target_os = "windows"))] - let key = rdev::key_from_scancode(scancode) as RdevKey; - // Windows requires special handling - #[cfg(target_os = "windows")] - let key = rdev::get_win_key(keycode, scancode); - - let event_type = if down_or_up { - KeyPress(key) - } else { - KeyRelease(key) - }; - let evt = Event { - time: std::time::SystemTime::now(), - name: Option::Some(name.to_owned()), - code: keycode as _, - scan_code: scancode as _, - event_type: event_type, - }; - - self.key_down_or_up(down_or_up, key, evt) + keyboard::client::change_grab_status(GrabState::Wait); } // flutter only TODO new input @@ -874,9 +381,6 @@ impl Session { shift: bool, command: bool, ) { - if HOTKEY_HOOKED.load(Ordering::SeqCst) { - return; - } let chars: Vec = name.chars().collect(); if chars.len() == 1 { let key = Key::_Raw(chars[0] as _); @@ -897,6 +401,41 @@ impl Session { self.send(Data::Message(msg_out)); } + pub fn handle_flutter_key_event( + &self, + name: &str, + keycode: i32, + scancode: i32, + lock_modes: i32, + down_or_up: bool, + ) { + if scancode < 0 || keycode < 0 { + return; + } + let keycode: u32 = keycode as u32; + let scancode: u32 = scancode as u32; + + #[cfg(not(target_os = "windows"))] + let key = rdev::key_from_code(keycode) as rdev::Key; + // Windows requires special handling + #[cfg(target_os = "windows")] + let key = rdev::get_win_key(keycode, scancode); + + let event_type = if down_or_up { + KeyPress(key) + } else { + KeyRelease(key) + }; + let event = Event { + time: std::time::SystemTime::now(), + name: Option::Some(name.to_owned()), + code: keycode as _, + scan_code: scancode as _, + event_type: event_type, + }; + keyboard::client::process_event(&event, Some(lock_modes)); + } + // flutter only TODO new input fn _input_key( &self, @@ -921,25 +460,6 @@ impl Session { key_event.set_chr(chr); } Key::ControlKey(key) => { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let key = if !get_key_state(enigo::Key::NumLock) { - match key { - ControlKey::Numpad0 => ControlKey::Insert, - ControlKey::Decimal => ControlKey::Delete, - ControlKey::Numpad1 => ControlKey::End, - ControlKey::Numpad2 => ControlKey::DownArrow, - ControlKey::Numpad3 => ControlKey::PageDown, - ControlKey::Numpad4 => ControlKey::LeftArrow, - ControlKey::Numpad5 => ControlKey::Clear, - ControlKey::Numpad6 => ControlKey::RightArrow, - ControlKey::Numpad7 => ControlKey::Home, - ControlKey::Numpad8 => ControlKey::UpArrow, - ControlKey::Numpad9 => ControlKey::PageUp, - _ => key, - } - } else { - key - }; key_event.set_control_key(key.clone()); } Key::_Raw(raw) => { @@ -947,17 +467,15 @@ impl Session { } } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); - - self.legacy_modifiers(&mut key_event, alt, ctrl, shift, command); if v == 1 { key_event.down = true; } else if v == 3 { key_event.press = true; } + keyboard::client::legacy_modifiers(&mut key_event, alt, ctrl, shift, command); + key_event.mode = KeyboardMode::Legacy.into(); - self.send_key_event(key_event, KeyboardMode::Legacy); + self.send_key_event(&key_event); } pub fn send_mouse( @@ -979,8 +497,9 @@ impl Session { } } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let (alt, ctrl, shift, command) = get_all_hotkey_state(alt, ctrl, shift, command); + // #[cfg(not(any(target_os = "android", target_os = "ios")))] + let (alt, ctrl, shift, command) = + keyboard::client::get_modifiers_state(alt, ctrl, shift, command); send_mouse(mask, x, y, alt, ctrl, shift, command, self); // on macos, ctrl + left button down = right button down, up won't emit, so we need to @@ -1092,15 +611,58 @@ 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 trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_cursor_data(&self, cd: CursorData); fn set_cursor_id(&self, id: String); fn set_cursor_position(&self, cp: CursorPosition); - fn set_display(&self, x: i32, y: i32, w: i32, h: i32, cursor_embeded: bool); + 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 on_connected(&self, conn_type: ConnType); fn update_privacy_mode(&self); fn set_permission(&self, name: &str, value: bool); fn close_success(&self); @@ -1130,6 +692,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { #[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); } impl Deref for Session { @@ -1150,39 +713,26 @@ impl FileManager for Session {} #[async_trait] impl Interface for Session { + fn get_login_config_handler(&self) -> Arc> { + return self.lc.clone(); + } + fn send(&self, data: Data) { if let Some(sender) = self.sender.read().unwrap().as_ref() { sender.send(data).ok(); } } - fn is_file_transfer(&self) -> bool { - self.lc - .read() - .unwrap() - .conn_type - .eq(&ConnType::FILE_TRANSFER) - } - - fn is_port_forward(&self) -> bool { - self.lc - .read() - .unwrap() - .conn_type - .eq(&ConnType::PORT_FORWARD) - } - - fn is_rdp(&self) -> bool { - self.lc.read().unwrap().conn_type.eq(&ConnType::RDP) - } - fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) { - let retry = check_if_retry(msgtype, title, text); + let direct = self.lc.read().unwrap().direct.unwrap_or_default(); + let received = self.lc.read().unwrap().received; + let retry_for_relay = direct && !received; + let retry = check_if_retry(msgtype, title, text, retry_for_relay); self.ui_handler.msgbox(msgtype, title, text, link, retry); } fn handle_login_error(&mut self, err: &str) -> bool { - self.lc.write().unwrap().handle_login_error(err, self) + handle_login_error(self.lc.clone(), err, self) } fn handle_peer_info(&mut self, mut pi: PeerInfo) { @@ -1211,7 +761,13 @@ impl Interface for Session { input_os_password(p, true, self.clone()); } let current = &pi.displays[pi.current_display as usize]; - self.set_display(current.x, current.y, current.width, current.height, current.cursor_embeded); + self.set_display( + current.x, + current.y, + current.width, + current.height, + current.cursor_embedded, + ); } self.update_privacy_mode(); // Save recent peers, then push event to flutter. So flutter can refresh peer page. @@ -1227,6 +783,7 @@ impl Interface for Session { "", ); } + self.on_connected(self.lc.read().unwrap().conn_type); #[cfg(windows)] { let mut path = std::env::temp_dir(); @@ -1237,23 +794,6 @@ impl Interface for Session { crate::platform::windows::add_recent_document(&path); } } - // only run in sciter - #[cfg(not(feature = "flutter"))] - { - // rdev::grab and rdev::listen use the same api in macOS & Windows - /* todo! Unused */ - #[cfg(not(any( - target_os = "android", - target_os = "ios", - target_os = "macos", - target_os = "windows", - target_os = "linux", - )))] - self.start_keyboard_hook(); - /* todo! (sciter) Only one device can be connected at the same time in linux */ - #[cfg(not(any(target_os = "android", target_os = "ios")))] - self.start_grab_hotkey(); - } } async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { @@ -1274,133 +814,14 @@ impl Interface for Session { handle_test_delay(t, peer).await; } } - - fn set_force_relay(&mut self, direct: bool, received: bool) { - let mut lc = self.lc.write().unwrap(); - lc.force_relay = false; - if direct && !received { - let errno = errno::errno().0; - log::info!("errno is {}", errno); - // TODO: check mac and ios - if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { - lc.force_relay = true; - lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); - } - } - } - - fn is_force_relay(&self) -> bool { - self.lc.read().unwrap().force_relay - } } -#[cfg(not(any(target_os = "android", target_os = "ios")))] impl Session { - fn handle_hotkey_event(&self, event: Event) { - // if is long press, don't do anything. - if is_long_press(&event) { - return; - } - - let (key, down) = match event.event_type { - EventType::KeyPress(key) => (key, true), - EventType::KeyRelease(key) => (key, false), - _ => return, - }; - - self.key_down_or_up(down, key, event); + pub fn lock_screen(&self) { + self.send_key_event(&crate::keyboard::client::event_lock_screen()); } - - #[allow(dead_code)] - fn start_grab_hotkey(&self) { - if self.is_port_forward() || self.is_file_transfer() { - return; - } - #[cfg(target_os = "linux")] - if !*IS_X11.lock().unwrap() { - return; - } - if HOTKEY_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - - log::info!("starting grab hotkeys"); - let me = self.clone(); - - #[cfg(target_os = "linux")] - { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(_key) | EventType::KeyRelease(_key) => { - me.handle_hotkey_event(event); - None - } - _ => Some(event), - }; - rdev::start_grab_listen(func) - } - #[cfg(any(target_os = "windows", target_os = "macos"))] - std::thread::spawn(move || { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(..) | EventType::KeyRelease(..) => { - // grab all keys - if !IS_IN.load(Ordering::SeqCst) - || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return Some(event); - } else { - me.handle_hotkey_event(event); - return None; - } - } - _ => Some(event), - }; - if let Err(error) = rdev::grab(func) { - log::error!("Error: {:?}", error) - } - }); - } - - #[allow(dead_code)] - fn start_keyboard_hook(&self) { - // only run in sciter - if self.is_port_forward() || self.is_file_transfer() { - return; - } - if KEYBOARD_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("keyboard hooked"); - - let me = self.clone(); - #[cfg(windows)] - crate::platform::windows::enable_lowlevel_keyboard(std::ptr::null_mut() as _); - std::thread::spawn(move || { - // This will block. - std::env::set_var("KEYBOARD_ONLY", "y"); - - let func = move |evt: Event| { - /* todo! IS_IN can't determine if the user is focused on remote page */ - if !IS_IN.load(Ordering::SeqCst) || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) - { - return; - } - if is_long_press(&evt) { - return; - } - let (key, down) = match evt.event_type { - EventType::KeyPress(key) => (key, true), - EventType::KeyRelease(key) => (key, false), - _ => return, - }; - me.key_down_or_up(down, key, evt); - }; - /* todo!: Shift + a -> AA in sciter - * rdev::listen and rdev::grab both send a - */ - if let Err(error) = rdev::listen(func) { - log::error!("rdev: {:?}", error); - } - }); + pub fn ctrl_alt_del(&self) { + self.send_key_event(&crate::keyboard::client::event_ctrl_alt_del()); } } @@ -1554,107 +975,3 @@ async fn send_note(url: String, id: String, conn_id: i32, note: String) { let body = serde_json::json!({ "id": id, "Id": conn_id, "note": note }); allow_err!(crate::post_request(url, body.to_string(), "").await); } - -fn get_hotkey_state(key: RdevKey) -> bool { - if let Some(&state) = MUTEX_SPECIAL_KEYS.lock().unwrap().get(&key) { - return state; - } else { - return false; - } -} - -fn get_all_hotkey_state( - alt: bool, - ctrl: bool, - shift: bool, - command: bool, -) -> (bool, bool, bool, bool) { - let ctrl = - get_hotkey_state(RdevKey::ControlLeft) || get_hotkey_state(RdevKey::ControlRight) || ctrl; - let shift = - get_hotkey_state(RdevKey::ShiftLeft) || get_hotkey_state(RdevKey::ShiftRight) || shift; - let command = - get_hotkey_state(RdevKey::MetaLeft) || get_hotkey_state(RdevKey::MetaRight) || command; - let alt = get_hotkey_state(RdevKey::Alt) || get_hotkey_state(RdevKey::AltGr) || alt; - - (alt, ctrl, shift, command) -} - -#[cfg(feature = "flutter")] -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn send_key_event_to_session(event: rdev::Event) { - if let Some(handler) = CUR_SESSION.lock().unwrap().as_ref() { - handler.handle_hotkey_event(event); - } -} - -#[cfg(feature = "flutter")] -pub fn global_grab_keyboard() { - if HOTKEY_HOOKED.swap(true, Ordering::SeqCst) { - return; - } - log::info!("starting global grab keyboard"); - - #[cfg(target_os = "linux")] - { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(_key) | EventType::KeyRelease(_key) => { - send_key_event_to_session(event); - None - } - _ => Some(event), - }; - rdev::start_grab_listen(func) - } - - #[cfg(any(target_os = "windows", target_os = "macos"))] - std::thread::spawn(move || { - let func = move |event: Event| match event.event_type { - EventType::KeyPress(..) | EventType::KeyRelease(..) => { - // grab all keys - if !IS_IN.load(Ordering::SeqCst) { - return Some(event); - } else { - send_key_event_to_session(event); - return None; - } - } - _ => Some(event), - }; - if let Err(error) = rdev::grab(func) { - log::error!("Error: {:?}", error) - } - }); -} - -pub fn global_get_keyboard_mode() -> String { - return std::env::var("KEYBOARD_MODE") - .unwrap_or(String::from("map")) - .to_lowercase(); -} - -pub fn global_save_keyboard_mode(value: String) { - std::env::set_var("KEYBOARD_MODE", value); -} - -fn is_long_press(event: &Event) -> bool { - let mut keys = MUTEX_SPECIAL_KEYS.lock().unwrap(); - match event.event_type { - EventType::KeyPress(k) => { - if let Some(&state) = keys.get(&k) { - if state == true { - return true; - } else { - keys.insert(k, true); - } - } - } - EventType::KeyRelease(k) => { - if keys.contains_key(&k) { - keys.insert(k, false); - } - } - _ => {} - }; - return false; -}