diff --git a/.claude/commands/reflection.md b/.claude/commands/reflection.md new file mode 100644 index 00000000000..9628fc157e4 --- /dev/null +++ b/.claude/commands/reflection.md @@ -0,0 +1,56 @@ +You are an expert in prompt engineering, specializing in optimizing AI code assistant instructions. Your task is to analyze and improve the instructions for Claude Code. +Follow these steps carefully: + +1. Analysis Phase: +Review the chat history in your context window. + +Then, examine the current Claude instructions, commands and config + +/CLAUDE.md +/.claude/commands/* +**/CLAUDE.md +.claude/settings.json +.claude/settings.local.json + + +Analyze the chat history, instructions, commands and config to identify areas that could be improved. Look for: +- Inconsistencies in Claude's responses +- Misunderstandings of user requests +- Areas where Claude could provide more detailed or accurate information +- Opportunities to enhance Claude's ability to handle specific types of queries or tasks +- New commands or improvements to a commands name, function or response +- Permissions and MCPs we've approved locally that we should add to the config, especially if we've added new tools or require them for the command to work + +2. Interaction Phase: +Present your findings and improvement ideas to the human. For each suggestion: +a) Explain the current issue you've identified +b) Propose a specific change or addition to the instructions +c) Describe how this change would improve Claude's performance + +Wait for feedback from the human on each suggestion before proceeding. If the human approves a change, move it to the implementation phase. If not, refine your suggestion or move on to the next idea. + +3. Implementation Phase: +For each approved change: +a) Clearly state the section of the instructions you're modifying +b) Present the new or modified text for that section +c) Explain how this change addresses the issue identified in the analysis phase + +4. Output Format: +Present your final output in the following structure: + + +[List the issues identified and potential improvements] + + + +[For each approved improvement: +1. Section being modified +2. New or modified instruction text +3. Explanation of how this addresses the identified issue] + + + +[Present the complete, updated set of instructions for Claude, incorporating all approved changes] + + +Remember, your goal is to enhance Claude's performance and consistency while maintaining the core functionality and purpose of the AI assistant. Be thorough in your analysis, clear in your explanations, and precise in your implementations. \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..56258e4e0a9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "gitsubmodule" + directory: "/" + target-branch: "master" + schedule: + interval: "daily" + commit-message: + prefix: "Git submodule" + labels: + - "dependencies" diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml index c8e963f096a..1913132e2ce 100644 --- a/.github/workflows/bridge.yml +++ b/.github/workflows/bridge.yml @@ -6,6 +6,7 @@ on: workflow_call: env: + CARGO_EXPAND_VERSION: "1.0.95" FLUTTER_VERSION: "3.22.3" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 @@ -25,6 +26,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Install prerequisites run: | @@ -37,9 +40,9 @@ jobs: gcc \ git \ g++ \ - libclang-11-dev \ + libclang-dev \ libgtk-3-dev \ - llvm-11-dev \ + llvm-dev \ nasm \ ninja-build \ pkg-config \ @@ -73,6 +76,7 @@ jobs: - name: Install flutter rust bridge deps shell: bash run: | + cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e910ef8645..e2169e2a20a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,8 @@ env: # MIN_SUPPORTED_RUST_VERSION: "1.46.0" # CICD_INTERMEDIATES_DIR: "_cicd-intermediates" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" - # vcpkg version: 2024.06.15 # for multiarch gcc compatibility - VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" + VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" on: workflow_dispatch: @@ -45,6 +44,8 @@ jobs: # steps: # - name: Checkout source code # uses: actions/checkout@v3 + # with: + # submodules: recursive # - name: Install rust toolchain (v${{ env.MIN_SUPPORTED_RUST_VERSION }}) # uses: actions-rs/toolchain@v1 @@ -80,154 +81,156 @@ jobs: # - { target: x86_64-apple-darwin , os: macos-10.15 } # - { target: x86_64-pc-windows-gnu , os: windows-2022 } # - { target: x86_64-pc-windows-msvc , os: windows-2022 } - - { target: x86_64-unknown-linux-gnu, os: ubuntu-20.04 } + - { target: x86_64-unknown-linux-gnu , os: ubuntu-22.04 } # - { target: x86_64-unknown-linux-musl , os: ubuntu-20.04, use-cross: true } steps: - - name: Export GitHub Actions cache environment variables - uses: actions/github-script@v6 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Install prerequisites - shell: bash - run: | - case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) - sudo apt-get -y update - sudo apt-get install -y \ - clang \ - cmake \ - curl \ - gcc \ - git \ - g++ \ - libpam0g-dev \ - libasound2-dev \ - libunwind-dev \ - libgstreamer1.0-dev \ - libgstreamer-plugins-base1.0-dev \ - libgtk-3-dev \ - libpulse-dev \ - libva-dev \ - libvdpau-dev \ - libxcb-randr0-dev \ - libxcb-shape0-dev \ - libxcb-xfixes0-dev \ - libxdo-dev \ - libxfixes-dev \ - nasm \ - wget - ;; - # 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: Setup vcpkg with Github Actions binary cache - uses: lukka/run-vcpkg@v11 - with: - vcpkgDirectory: /opt/artifacts/vcpkg - vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} - - - name: Install vcpkg dependencies - run: | - $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed" - shell: bash - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@v1 - with: - toolchain: stable - targets: ${{ matrix.job.target }} - components: "" - - - name: Show version information (Rust, cargo, GCC) - shell: bash - run: | - gcc --version || true - rustup -V - rustup toolchain list - rustup default - cargo -V - rustc -V - - - uses: Swatinem/rust-cache@v2 - - - name: Build - uses: actions-rs/cargo@v1 - with: - use-cross: ${{ matrix.job.use-cross }} - command: build - args: --locked --target=${{ matrix.job.target }} - - - name: clean - shell: bash - run: | - cargo clean - - # - name: Strip debug information from executable - # id: strip - # shell: bash - # run: | - # # Figure out suffix of binary - # EXE_suffix="" - # case ${{ matrix.job.target }} in - # *-pc-windows-*) EXE_suffix=".exe" ;; - # esac; - - # # Figure out what strip tool to use if any - # STRIP="strip" - # case ${{ matrix.job.target }} in - # arm-unknown-linux-*) STRIP="arm-linux-gnueabihf-strip" ;; - # aarch64-unknown-linux-gnu) STRIP="aarch64-linux-gnu-strip" ;; - # *-pc-windows-msvc) STRIP="" ;; - # esac; - - # # Setup paths - # BIN_DIR="${{ env.CICD_INTERMEDIATES_DIR }}/stripped-release-bin/" - # mkdir -p "${BIN_DIR}" - # BIN_NAME="${{ env.PROJECT_NAME }}${EXE_suffix}" - # BIN_PATH="${BIN_DIR}/${BIN_NAME}" - - # # Copy the release build binary to the result location - # cp "target/${{ matrix.job.target }}/release/${BIN_NAME}" "${BIN_DIR}" - - # # Also strip if possible - # if [ -n "${STRIP}" ]; then - # "${STRIP}" "${BIN_PATH}" - # fi - - # # Let subsequent steps know where to find the (stripped) bin - # echo ::set-output name=BIN_PATH::${BIN_PATH} - # echo ::set-output name=BIN_NAME::${BIN_NAME} - - - name: Set testing options - id: test-options - shell: bash - run: | - # test only library unit tests and binary for arm-type targets - unset CARGO_TEST_OPTIONS - - case ${{ matrix.job.target }} in - arm-* | aarch64-*) - CARGO_TEST_OPTIONS="--lib --bin ${PROJECT_NAME}" - ;; - *) - CARGO_TEST_OPTIONS="--workspace --no-fail-fast -- --skip test_get_cursor_pos --skip test_get_key_state" - ;; - esac; - - #deprecated echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} - echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_ENV - echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT - - - name: Run tests - uses: actions-rs/cargo@v1 - with: - use-cross: ${{ matrix.job.use-cross }} - command: test - args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}} + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install prerequisites + shell: bash + run: | + case ${{ matrix.job.target }} in + x86_64-unknown-linux-gnu) + sudo apt-get -y update + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + libpam0g-dev \ + libasound2-dev \ + libunwind-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpulse-dev \ + libva-dev \ + libvdpau-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + nasm \ + wget + ;; + # 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: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + run: | + $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed" + shell: bash + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + targets: ${{ matrix.job.target }} + components: '' + + - name: Show version information (Rust, cargo, GCC) + shell: bash + run: | + gcc --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - uses: Swatinem/rust-cache@v2 + + - name: Build + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.job.use-cross }} + command: build + args: --locked --target=${{ matrix.job.target }} + + - name: clean + shell: bash + run: | + cargo clean + + # - name: Strip debug information from executable + # id: strip + # shell: bash + # run: | + # # Figure out suffix of binary + # EXE_suffix="" + # case ${{ matrix.job.target }} in + # *-pc-windows-*) EXE_suffix=".exe" ;; + # esac; + + # # Figure out what strip tool to use if any + # STRIP="strip" + # case ${{ matrix.job.target }} in + # arm-unknown-linux-*) STRIP="arm-linux-gnueabihf-strip" ;; + # aarch64-unknown-linux-gnu) STRIP="aarch64-linux-gnu-strip" ;; + # *-pc-windows-msvc) STRIP="" ;; + # esac; + + # # Setup paths + # BIN_DIR="${{ env.CICD_INTERMEDIATES_DIR }}/stripped-release-bin/" + # mkdir -p "${BIN_DIR}" + # BIN_NAME="${{ env.PROJECT_NAME }}${EXE_suffix}" + # BIN_PATH="${BIN_DIR}/${BIN_NAME}" + + # # Copy the release build binary to the result location + # cp "target/${{ matrix.job.target }}/release/${BIN_NAME}" "${BIN_DIR}" + + # # Also strip if possible + # if [ -n "${STRIP}" ]; then + # "${STRIP}" "${BIN_PATH}" + # fi + + # # Let subsequent steps know where to find the (stripped) bin + # echo ::set-output name=BIN_PATH::${BIN_PATH} + # echo ::set-output name=BIN_NAME::${BIN_NAME} + + - name: Set testing options + id: test-options + shell: bash + run: | + # test only library unit tests and binary for arm-type targets + unset CARGO_TEST_OPTIONS + + case ${{ matrix.job.target }} in + arm-* | aarch64-*) + CARGO_TEST_OPTIONS="--lib --bin ${PROJECT_NAME}" + ;; + *) + CARGO_TEST_OPTIONS="--workspace --no-fail-fast -- --skip test_get_cursor_pos --skip test_get_key_state" + ;; + esac; + + #deprecated echo ::set-output name=CARGO_TEST_OPTIONS::${CARGO_TEST_OPTIONS} + echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_ENV + echo "CARGO_TEST_OPTIONS=${CARGO_TEST_OPTIONS}" >> $GITHUB_OUTPUT + + - name: Run tests + uses: actions-rs/cargo@v1 + with: + use-cross: ${{ matrix.job.use-cross }} + command: test + args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}} diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml index b57a84fbb88..a25e6943b5b 100644 --- a/.github/workflows/flutter-build.yml +++ b/.github/workflows/flutter-build.yml @@ -23,7 +23,7 @@ env: MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81 CARGO_NDK_VERSION: "3.1.2" SCITER_ARMV7_CMAKE_VERSION: "3.29.7" - SCITER_NASM_DEBVERSION: "2.14-1" + SCITER_NASM_DEBVERSION: "2.15.05-1" LLVM_VERSION: "15.0.6" FLUTTER_VERSION: "3.24.5" ANDROID_FLUTTER_VERSION: "3.24.5" @@ -31,17 +31,18 @@ env: FLUTTER_ELINUX_VERSION: "3.16.9" TAG_NAME: "${{ inputs.upload-tag }}" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" - # vcpkg version: 2024.07.12 - VCPKG_COMMIT_ID: "1de2026f28ead93ff1773e6e680387643e914ea1" - VERSION: "1.3.5" + # vcpkg version: 2025.01.13 + # If we change the `VCPKG COMMIT_ID`, please remember: + # 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`. + # Or we may face build issue like + # https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174 + # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. + VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "1.4.1" NDK_VERSION: "r27c" #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 }}" - API_SERVER: "${{ secrets.API_SERVER }}" UPLOAD_ARTIFACT: "${{ inputs.upload-artifact }}" SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" @@ -89,6 +90,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Restore bridge files uses: actions/download-artifact@master @@ -163,14 +166,44 @@ jobs: - name: Build rustdesk run: | + # Windows: build RustDesk + python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack + mv ./flutter/build/windows/x64/runner/Release ./rustdesk + + # Download usbmmidd_v2.zip and extract it to ./rustdesk Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip Expand-Archive usbmmidd_v2.zip -DestinationPath . - python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack Remove-Item -Path usbmmidd_v2\Win32 -Recurse Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" - mv ./flutter/build/windows/x64/runner/Release ./rustdesk mv -Force .\usbmmidd_v2 ./rustdesk + # Download printer driver files and extract them to ./rustdesk + try { + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_driver_adapter.zip + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/sha256sums -OutFile sha256sums + + # Check and move the files + $checksum_driver = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*rustdesk_printer_driver_v4-1.4\.zip$').Matches.Groups[1].Value + $downloadsum_driver = Get-FileHash -Path rustdesk_printer_driver_v4-1.4.zip -Algorithm SHA256 + $checksum_adapter = (Select-String -Path .\sha256sums -Pattern '^([a-fA-F0-9]{64}) \*printer_driver_adapter\.zip$').Matches.Groups[1].Value + $downloadsum_adapter = Get-FileHash -Path printer_driver_adapter.zip -Algorithm SHA256 + if ($checksum_driver -eq $downloadsum_driver.Hash -and $checksum_adapter -eq $downloadsum_adapter.Hash) { + Write-Output "rustdesk_printer_driver_v4-1.4, checksums match, extract the file." + Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath . + mkdir ./rustdesk/drivers + mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver + Expand-Archive printer_driver_adapter.zip -DestinationPath . + mv -Force .\printer_driver_adapter.dll ./rustdesk + } elseif ($checksum_driver -ne $downloadsum_driver.Hash) { + Write-Output "rustdesk_printer_driver_v4-1.4, checksums do not match, ignore the file." + } else { + Write-Output "printer_driver_adapter.dll, checksums do not match, ignore the file." + } + } catch { + Write-Host "Ingore the printer driver error." + } + - name: find Runner.res # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res # Runner.rc does not contain actual version, but Runner.res does @@ -279,6 +312,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Install LLVM and Clang uses: rustdesk-org/install-llvm-action-32bit@master @@ -392,78 +427,6 @@ jobs: files: | ./SignOutput/rustdesk-*.exe - build-for-macOS-arm64-selfhost: - # use build-for-macOS instead - if: false - runs-on: [self-hosted, macOS, ARM64] - needs: [generate-bridge] - steps: - - name: Export GitHub Actions cache environment variables - uses: actions/github-script@v6 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - - - name: Checkout source code - uses: actions/checkout@v4 - - - name: Restore bridge files - uses: actions/download-artifact@master - with: - name: bridge-artifact - path: ./ - - - name: Build rustdesk - run: | - ./build.py --flutter --hwcodec - - - name: create unsigned dmg - if: env.UPLOAD_ARTIFACT == 'true' - run: | - CREATE_DMG="$(command -v create-dmg)" - CREATE_DMG="$(readlink -f "$CREATE_DMG")" - sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG" - create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}-arm64.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app - - - name: Upload unsigned macOS app - if: env.UPLOAD_ARTIFACT == 'true' - uses: actions/upload-artifact@master - with: - name: rustdesk-unsigned-macos-arm64 - path: rustdesk-${{ env.VERSION }}-arm64.dmg # can not upload the directory directly or tar.gz file, which destroy the link structure, causing the codesign failed - - - name: Codesign app and create signed dmg - if: env.MACOS_P12_BASE64 != null && env.UPLOAD_ARTIFACT == 'true' - run: | - # Patch create-dmg to give more attempts to unmount image - CREATE_DMG="$(command -v create-dmg)" - CREATE_DMG="$(readlink -f "$CREATE_DMG")" - sed -i -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG" - # start sign the rustdesk.app and dmg - rm -rf *.dmg || true - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv - create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-${{ env.VERSION }}.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app - codesign --force --options runtime -s ${{ secrets.MACOS_CODESIGN_IDENTITY }} --deep --strict rustdesk-${{ env.VERSION }}.dmg -vvv - # notarize the rustdesk-${{ env.VERSION }}.dmg - rcodesign notary-submit --api-key-path ~/.p12/api-key.json --staple rustdesk-${{ env.VERSION }}.dmg - - - name: Rename rustdesk - if: env.UPLOAD_ARTIFACT == 'true' - run: | - for name in rustdesk*??.dmg; do - mv "$name" "${name%%.dmg}-aarch64.dmg" - done - - - name: Publish DMG package - if: env.UPLOAD_ARTIFACT == 'true' - uses: softprops/action-gh-release@v1 - with: - prerelease: true - tag_name: ${{ env.TAG_NAME }} - files: | - rustdesk*-aarch64.dmg - build-rustdesk-ios: if: false # if: ${{ inputs.upload-artifact }} @@ -493,6 +456,9 @@ jobs: brew install nasm yasm - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install flutter uses: subosito/flutter-action@v2 with: @@ -551,63 +517,13 @@ jobs: rustup target add ${{ matrix.job.target }} cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib - - name: Build rustdesk - shell: bash - run: | - pushd flutter - # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign - # for easy debugging - flutter build ipa --release --no-codesign - - # - name: Upload Artifacts - # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' - # uses: actions/upload-artifact@master - # with: - # name: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk - # path: flutter/build/ios/ipa/*.ipa - - # - name: Publish ipa package - # # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true' - # uses: softprops/action-gh-release@v1 - # with: - # prerelease: true - # tag_name: ${{ env.TAG_NAME }} - # files: | - # flutter/build/ios/ipa/*.ipa - - build-rustdesk-ios-selfhost: - #if: ${{ inputs.upload-artifact }} - if: false - runs-on: [self-hosted, macOS, ARM64] - needs: [generate-bridge] - strategy: - fail-fast: false - steps: - - name: Export GitHub Actions cache environment variables - uses: actions/github-script@v6 - with: - script: | - core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); - core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - - - name: Checkout source code - uses: actions/checkout@v4 - - # $VCPKG_ROOT/vcpkg install --triplet arm64-ios --x-install-root="$VCPKG_ROOT/installed" - - - name: Restore bridge files - uses: actions/download-artifact@master + - name: Upload liblibrustdesk.a Artifacts + uses: actions/upload-artifact@master with: - name: bridge-artifact - path: ./ - - - name: Build rustdesk lib - run: | - cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib + name: liblibrustdesk.a + path: target/aarch64-apple-ios/release/liblibrustdesk.a - name: Build rustdesk - # ios sdk not installed on this machine, I will install it later after I am back home - if: false shell: bash run: | pushd flutter @@ -649,7 +565,7 @@ jobs: } - { target: aarch64-apple-darwin, - os: macos-latest, + os: macos-14, # extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296 extra-build-args: "--screencapturekit", arch: aarch64, @@ -665,6 +581,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Import the codesign cert if: env.MACOS_P12_BASE64 != null @@ -724,7 +642,7 @@ jobs: shell: bash run: | cd "$(dirname "$(which flutter)")" - # https://github.com/flutter/flutter/issues/1.3.53 + # https://github.com/flutter/flutter/issues/133533 sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart @@ -786,7 +704,7 @@ jobs: sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj fi - ./build.py --flutter --hwcodec ${{ matrix.job.extra-build-args }} + ./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }} - name: create unsigned dmg if: env.UPLOAD_ARTIFACT == 'true' @@ -886,21 +804,21 @@ jobs: - { arch: aarch64, target: aarch64-linux-android, - os: ubuntu-22.04, + os: ubuntu-24.04, reltype: release, suffix: "", } - { arch: armv7, target: armv7-linux-androideabi, - os: ubuntu-22.04, + os: ubuntu-24.04, reltype: release, suffix: "", } - { arch: x86_64, target: x86_64-linux-android, - os: ubuntu-22.04, + os: ubuntu-24.04, reltype: release, suffix: "", } @@ -937,7 +855,7 @@ jobs: libayatana-appindicator3-dev \ libasound2-dev \ libc6-dev \ - libclang-11-dev \ + libclang-dev \ libunwind-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ @@ -950,7 +868,7 @@ jobs: libxcb-xfixes0-dev \ libxdo-dev \ libxfixes-dev \ - llvm-11-dev \ + llvm-dev \ nasm \ ninja-build \ openjdk-17-jdk-headless \ @@ -960,6 +878,9 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install flutter uses: subosito/flutter-action@v2 with: @@ -1029,16 +950,6 @@ jobs: prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated key: ${{ matrix.job.target }} - - name: fix android for flutter 3.13 - if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} - run: | - cd flutter - sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml - sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml - flutter pub get - cd lib - find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' - - name: Build rustdesk lib env: ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} @@ -1173,11 +1084,11 @@ jobs: signed-apk/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}.apk build-rustdesk-android-universal: + if: false needs: [build-rustdesk-android] name: build rustdesk android universal apk - if: false # if: ${{ inputs.upload-artifact }} - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: reltype: release x86_target: "" # can be ",android-x86" @@ -1215,7 +1126,7 @@ jobs: libayatana-appindicator3-dev \ libasound2-dev \ libc6-dev \ - libclang-11-dev \ + libclang-dev \ libunwind-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ @@ -1228,7 +1139,7 @@ jobs: libxcb-xfixes0-dev \ libxdo-dev \ libxfixes-dev \ - llvm-11-dev \ + llvm-dev \ nasm \ ninja-build \ openjdk-17-jdk-headless \ @@ -1238,6 +1149,9 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install flutter uses: subosito/flutter-action@v2 with: @@ -1280,16 +1194,6 @@ jobs: name: librustdesk.so.i686-linux-android path: ./flutter/android/app/src/main/jniLibs/x86 - - name: fix android for flutter 3.13 - if: ${{ env.ANDROID_FLUTTER_VERSION == '3.13.9' }} - run: | - cd flutter - sed -i 's/uni_links_desktop/#uni_links_desktop/g' pubspec.yaml - sed -i 's/extended_text: .*/extended_text: 11.1.0/' pubspec.yaml - flutter pub get - cd lib - find . | grep dart | xargs sed -i 's/textScaler: TextScaler.linear(\(.*\)),/textScaleFactor: \1,/g' - - name: Build rustdesk shell: bash env: @@ -1388,16 +1292,20 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Maximize build space - if: ${{ matrix.job.arch == 'x86_64' }} run: | sudo rm -rf /opt/ghc sudo rm -rf /usr/local/lib/android sudo rm -rf /usr/share/dotnet sudo apt-get update -y - sudo apt-get install -y nasm qemu-user-static + sudo apt-get install -y nasm + if [[ "${{ matrix.job.arch }}" == "x86_64" ]]; then + sudo apt-get install -y qemu-user-static + fi - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Set Swap Space if: ${{ matrix.job.arch == 'x86_64' }} @@ -1551,7 +1459,7 @@ jobs: export JOBS="" fi echo $JOBS - cargo build --lib $JOBS --features hwcodec,flutter --release + cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release rm -rf target/release/deps target/release/build rm -rf ~/.cargo @@ -1688,7 +1596,6 @@ jobs: build-rustdesk-linux-sciter: if: ${{ inputs.upload-artifact }} - needs: build-rustdesk-linux # not for dep, just make it run later for parallelism runs-on: ${{ matrix.job.on }} name: build-rustdesk-linux-sciter ${{ matrix.job.target }} strategy: @@ -1704,7 +1611,7 @@ jobs: deb_arch: amd64, sciter_arch: x64, vcpkg-triplet: x64-linux, - extra_features: ",hwcodec", + extra_features: ",hwcodec,unix-file-copy-paste", } steps: - name: Export GitHub Actions cache environment variables @@ -1716,6 +1623,8 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Free Space run: | @@ -1844,6 +1753,8 @@ jobs: cat ~/.cargo/config # install dependencies from vcpkg export VCPKG_ROOT=/opt/artifacts/vcpkg + # remove this when support higher version + export USE_AOM_391=1 if ! $VCPKG_ROOT/vcpkg install --triplet ${{ matrix.job.vcpkg-triplet }} --x-install-root="$VCPKG_ROOT/installed"; then find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do echo "$_1:" @@ -1903,6 +1814,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Download Binary uses: actions/download-artifact@master @@ -1919,13 +1832,11 @@ jobs: run: | # install libarchive-tools for bsdtar command used in AppImageBuilder.yml sudo apt-get update -y + # https://github.com/AppImage/AppImageKit/wiki/FUSE sudo apt-get install -y libarchive-tools libfuse2 # 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 + # https://github.com/AppImage/AppImageKit/issues/1395 + sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git # run appimage-builder pushd appimage sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml @@ -1945,23 +1856,23 @@ jobs: - build-rustdesk-linux - build-rustdesk-linux-sciter runs-on: ${{ matrix.job.on }} - # if: ${{ inputs.upload-artifact }} if: false - # disable for now, because the job runs forever in some situations + # if: ${{ inputs.upload-artifact }} strategy: fail-fast: false matrix: job: - { target: x86_64-unknown-linux-gnu, - distro: ubuntu18.04, + # https://github.com/ostreedev/ostree/commit/4bac96a8c817beda37448f9b8c662162bb619981 + distro: ubuntu22.04, on: ubuntu-22.04, arch: x86_64, suffix: "", } - { target: x86_64-unknown-linux-gnu, - distro: ubuntu18.04, + distro: ubuntu22.04, on: ubuntu-22.04, arch: x86_64, suffix: "-sciter", @@ -1969,6 +1880,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Download Binary uses: actions/download-artifact@master @@ -1994,36 +1907,17 @@ jobs: shell: /bin/bash install: | apt-get update -y - apt-get install -y \ - curl \ - git \ - rpm \ - wget + apt-get install -y git flatpak flatpak-builder run: | # disable git safe.directory git config --global --add safe.directory "*" pushd /workspace - # install - apt-get update -y - apt-get install -y \ - cmake \ - curl \ - flatpak \ - flatpak-builder \ - gcc \ - git \ - g++ \ - libgtk-3-dev \ - nasm \ - wget # flatpak deps - flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo - flatpak --user install -y flathub org.freedesktop.Platform/${{ matrix.job.arch }}/23.08 - flatpak --user install -y flathub org.freedesktop.Sdk/${{ matrix.job.arch }}/23.08 + flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo # package pushd flatpak git clone https://github.com/flathub/shared-modules.git --depth=1 - flatpak-builder --user --force-clean --repo=repo ./build ./rustdesk.json + flatpak-builder --user --install-deps-from=flathub -y --force-clean --repo=repo ./build ./rustdesk.json flatpak build-bundle ./repo rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak com.rustdesk.RustDesk - name: Publish flatpak package @@ -2035,9 +1929,11 @@ jobs: flatpak/rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.flatpak build-rustdesk-web: - if: False + if: false name: build-rustdesk-web runs-on: ubuntu-22.04 + permissions: + contents: read strategy: fail-fast: false env: @@ -2045,6 +1941,8 @@ jobs: steps: - name: Checkout source code uses: actions/checkout@v4 + with: + submodules: recursive - name: Prepare env run: | diff --git a/.github/workflows/playground.yml b/.github/workflows/playground.yml index 64b56611e7b..b1c1c2b306a 100644 --- a/.github/workflows/playground.yml +++ b/.github/workflows/playground.yml @@ -16,9 +16,8 @@ env: FLUTTER_ELINUX_VERSION: "3.16.9" TAG_NAME: "nightly" VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" - # vcpkg version: 2024.06.15 - VCPKG_COMMIT_ID: "f7423ee180c4b7f40d43402c2feb3859161ef625" - VERSION: "1.3.5" + VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "1.4.1" NDK_VERSION: "r26d" #signing keys env variable checks ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" @@ -91,6 +90,7 @@ jobs: uses: actions/checkout@v3 with: ref: ${{ matrix.job.ref }} + submodules: recursive - name: Import the codesign cert if: env.MACOS_P12_BASE64 != null @@ -250,6 +250,7 @@ jobs: uses: actions/checkout@v3 with: ref: ${{ matrix.job.ref }} + submodules: recursive - name: Install dependencies run: | @@ -265,7 +266,7 @@ jobs: libayatana-appindicator3-dev\ libasound2-dev \ libc6-dev \ - libclang-11-dev \ + libclang-dev \ libunwind-dev \ libgstreamer1.0-dev \ libgstreamer-plugins-base1.0-dev \ @@ -279,7 +280,7 @@ jobs: libxcb-xfixes0-dev \ libxdo-dev \ libxfixes-dev \ - llvm-11-dev \ + llvm-dev \ nasm \ yasm \ ninja-build \ diff --git a/.github/workflows/third-party-RustDeskTempTopMostWindow.yml b/.github/workflows/third-party-RustDeskTempTopMostWindow.yml index d35ae5aaf4c..ff8560950fc 100644 --- a/.github/workflows/third-party-RustDeskTempTopMostWindow.yml +++ b/.github/workflows/third-party-RustDeskTempTopMostWindow.yml @@ -3,29 +3,29 @@ name: build RustDeskTempTopMostWindow on: workflow_call: inputs: - upload-artifact: - type: boolean - default: true - target: - description: 'Target' - required: true - type: string - default: 'windows-2022' - configuration: - description: 'Configuration' - required: true - type: string - default: 'Release' - platform: - description: 'Platform' - required: true - type: string - default: 'x64' - target_version: - description: 'TargetVersion' - required: true - type: string - default: 'Windows10' + upload-artifact: + type: boolean + default: true + target: + description: "Target" + required: true + type: string + default: "windows-2022" + configuration: + description: "Configuration" + required: true + type: string + default: "Release" + platform: + description: "Platform" + required: true + type: string + default: "x64" + target_version: + description: "TargetVersion" + required: true + type: string + default: "Windows10" env: project_path: WindowInjection/WindowInjection.vcxproj @@ -35,7 +35,7 @@ jobs: if: false runs-on: ${{ inputs.target }} strategy: - fail-fast: false + fail-fast: false env: build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }} steps: @@ -56,6 +56,6 @@ jobs: uses: actions/upload-artifact@master if: ${{ inputs.upload-artifact }} with: - name: topmostwindow-artifacts - path: | - ./${{ env.build_output_dir }}/WindowInjection.dll + name: topmostwindow-artifacts + path: | + ./${{ env.build_output_dir }}/WindowInjection.dll diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..d80e69aa84a --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "libs/hbb_common"] + path = libs/hbb_common + url = https://github.com/rustdesk/hbb_common diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000000..8d46e1fa17b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,91 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +### Build Commands +- `cargo run` - Build and run the desktop application (requires libsciter library) +- `python3 build.py --flutter` - Build Flutter version (desktop) +- `python3 build.py --flutter --release` - Build Flutter version in release mode +- `python3 build.py --hwcodec` - Build with hardware codec support +- `python3 build.py --vram` - Build with VRAM feature (Windows only) +- `cargo build --release` - Build Rust binary in release mode +- `cargo build --features hwcodec` - Build with specific features + +### Flutter Mobile Commands +- `cd flutter && flutter build android` - Build Android APK +- `cd flutter && flutter build ios` - Build iOS app +- `cd flutter && flutter run` - Run Flutter app in development mode +- `cd flutter && flutter test` - Run Flutter tests + +### Testing +- `cargo test` - Run Rust tests +- `cd flutter && flutter test` - Run Flutter tests + +### Platform-Specific Build Scripts +- `flutter/build_android.sh` - Android build script +- `flutter/build_ios.sh` - iOS build script +- `flutter/build_fdroid.sh` - F-Droid build script + +## Project Architecture + +### Directory Structure +- **`src/`** - Main Rust application code + - `src/ui/` - Legacy Sciter UI (deprecated, use Flutter instead) + - `src/server/` - Audio/clipboard/input/video services and network connections + - `src/client.rs` - Peer connection handling + - `src/platform/` - Platform-specific code +- **`flutter/`** - Flutter UI code for desktop and mobile +- **`libs/`** - Core libraries + - `libs/hbb_common/` - Video codec, config, network wrapper, protobuf, file transfer utilities + - `libs/scrap/` - Screen capture functionality + - `libs/enigo/` - Platform-specific keyboard/mouse control + - `libs/clipboard/` - Cross-platform clipboard implementation + +### Key Components +- **Remote Desktop Protocol**: Custom protocol implemented in `src/rendezvous_mediator.rs` for communicating with rustdesk-server +- **Screen Capture**: Platform-specific screen capture in `libs/scrap/` +- **Input Handling**: Cross-platform input simulation in `libs/enigo/` +- **Audio/Video Services**: Real-time audio/video streaming in `src/server/` +- **File Transfer**: Secure file transfer implementation in `libs/hbb_common/` + +### UI Architecture +- **Legacy UI**: Sciter-based (deprecated) - files in `src/ui/` +- **Modern UI**: Flutter-based - files in `flutter/` + - Desktop: `flutter/lib/desktop/` + - Mobile: `flutter/lib/mobile/` + - Shared: `flutter/lib/common/` and `flutter/lib/models/` + +## Important Build Notes + +### Dependencies +- Requires vcpkg for C++ dependencies: `libvpx`, `libyuv`, `opus`, `aom` +- Set `VCPKG_ROOT` environment variable +- Download appropriate Sciter library for legacy UI support + +### Ignore Patterns +When working with files, ignore these directories: +- `target/` - Rust build artifacts +- `flutter/build/` - Flutter build output +- `flutter/.dart_tool/` - Flutter tooling files + +### Cross-Platform Considerations +- Windows builds require additional DLLs and virtual display drivers +- macOS builds need proper signing and notarization for distribution +- Linux builds support multiple package formats (deb, rpm, AppImage) +- Mobile builds require platform-specific toolchains (Android SDK, Xcode) + +### Feature Flags +- `hwcodec` - Hardware video encoding/decoding +- `vram` - VRAM optimization (Windows only) +- `flutter` - Enable Flutter UI +- `unix-file-copy-paste` - Unix file clipboard support +- `screencapturekit` - macOS ScreenCaptureKit (macOS only) + +### Config +All configurations or options are under `libs/hbb_common/src/config.rs` file, 4 types: +- Settings +- Local +- Display +- Built-in diff --git a/Cargo.lock b/Cargo.lock index 4f1e695bc7d..7487b5c5207 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,23 +34,11 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom", + "getrandom 0.2.15", "once_cell", "version_check", ] -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if 1.0.0", - "once_cell", - "version_check", - "zerocopy 0.7.34", -] - [[package]] name = "aho-corasick" version = "1.1.3" @@ -87,12 +75,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - [[package]] name = "alsa" version = "0.9.0" @@ -100,7 +82,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" dependencies = [ "alsa-sys", - "bitflags 2.6.0", + "bitflags 2.9.1", "libc", ] @@ -125,7 +107,7 @@ name = "android-wakelock" version = "0.1.0" source = "git+https://github.com/rustdesk-org/android-wakelock#d0292e5a367e627c4fa6f1ca6bdfad005dca7d90" dependencies = [ - "jni 0.21.1", + "jni", "log", "ndk-context", ] @@ -217,9 +199,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" [[package]] name = "arboard" @@ -242,6 +224,12 @@ dependencies = [ "x11rb 0.13.1", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.5.1" @@ -384,9 +372,9 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -419,9 +407,9 @@ version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -470,6 +458,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + [[package]] name = "autocfg" version = "0.1.8" @@ -539,10 +538,10 @@ dependencies = [ "lazycell", "log", "peeking_take_while", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "which", ] @@ -561,12 +560,12 @@ dependencies = [ "log", "peeking_take_while", "prettyplease", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", - "syn 2.0.68", + "syn 2.0.98", "which", ] @@ -576,18 +575,36 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "cexpr", "clang-sys", "itertools 0.12.1", "lazy_static", "lazycell", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", + "quote 1.0.36", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.98", +] + +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", - "rustc-hash", + "rustc-hash 2.1.1", "shlex", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -604,9 +621,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" dependencies = [ "serde 1.0.203", ] @@ -618,7 +635,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afb15541e888071f64592c0b4364fdff21b7cb0a247f984296699351963a8721" dependencies = [ "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -722,6 +739,16 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecodec" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf4c9d0bbf32eea58d7c0f812058138ee8edaf0f2802b6d03561b504729a325" +dependencies = [ + "byteorder", + "trackable 0.2.24", +] + [[package]] name = "bytemuck" version = "1.21.0" @@ -736,9 +763,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" dependencies = [ "serde 1.0.203", ] @@ -788,12 +815,12 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "cairo-sys-rs", "glib 0.18.5", "libc", "once_cell", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -809,13 +836,13 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.102" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779e6b7d17797c0b42023d417228c02889300190e700cb074c3438d9c541d332" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -869,16 +896,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits 0.2.19", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-link", ] [[package]] @@ -977,21 +1004,28 @@ version = "0.1.0" dependencies = [ "cacao", "cc", - "dashmap", + "dashmap 5.5.3", + "dirs 5.0.1", + "fsevent", "fuser", "hbb_common", "lazy_static", "libc", + "objc2 0.5.2", + "objc2-app-kit", + "objc2-foundation", "once_cell", "parking_lot", "percent-encoding", "rand 0.8.5", "serde 1.0.203", "serde_derive", - "thiserror", + "thiserror 1.0.61", "utf16string", + "uuid", "x11-clipboard 0.8.1", "x11rb 0.12.0", + "xattr", ] [[package]] @@ -1038,6 +1072,21 @@ dependencies = [ "cc", ] +[[package]] +name = "cocoa" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c49e86fc36d5704151f5996b7b3795385f50ce09e3be0f47a0cfde869681cf8" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.7.0", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "libc", + "objc", +] + [[package]] name = "cocoa" version = "0.24.1" @@ -1122,7 +1171,7 @@ source = "git+https://github.com/rustdesk-org/confy#83db9ec19a2f97e9718aef69e4fc dependencies = [ "directories-next", "serde 1.0.203", - "thiserror", + "thiserror 1.0.61", "toml 0.5.11", ] @@ -1157,7 +1206,7 @@ version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "unicode-xid 0.2.4", ] @@ -1174,12 +1223,22 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + [[package]] name = "core-foundation" version = "0.9.3" source = "git+https://github.com/madsmtm/core-foundation-rs.git?rev=7d593d016175755e492a92ef89edca68ac3bd5cd#7d593d016175755e492a92ef89edca68ac3bd5cd" dependencies = [ - "core-foundation-sys 0.8.6 (git+https://github.com/madsmtm/core-foundation-rs.git?rev=7d593d016175755e492a92ef89edca68ac3bd5cd)", + "core-foundation-sys 0.8.6", "libc", ] @@ -1189,15 +1248,25 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys 0.8.7", "libc", ] [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" [[package]] name = "core-foundation-sys" @@ -1207,6 +1276,24 @@ dependencies = [ "objc2-encode 2.0.0-pre.2", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.7.0", + "foreign-types 0.3.2", + "libc", +] + [[package]] name = "core-graphics" version = "0.22.3" @@ -1268,6 +1355,31 @@ dependencies = [ "libc", ] +[[package]] +name = "core-media-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273bf3fc5bf51fd06a7766a84788c1540b6527130a0bce39e00567d6ab9f31f1" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-video-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ecad23610ad9757664d644e369246edde1803fcb43ed72876565098a5d3828" +dependencies = [ + "cfg-if 0.1.10", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "libc", + "metal", + "objc", +] + [[package]] name = "coreaudio-rs" version = "0.11.3" @@ -1275,7 +1387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" dependencies = [ "bitflags 1.3.2", - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.8.7", "coreaudio-sys", ] @@ -1295,10 +1407,10 @@ source = "git+https://github.com/rustdesk-org/cpal?branch=osx-screencapturekit#6 dependencies = [ "alsa", "cidre", - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.8.7", "coreaudio-rs", "dasp_sample", - "jni 0.21.1", + "jni", "js-sys", "libc", "mach2", @@ -1320,6 +1432,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -1420,6 +1547,20 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "dasp" version = "0.11.0" @@ -1539,6 +1680,12 @@ dependencies = [ "dasp_sample", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "dbus" version = "0.9.7" @@ -1582,6 +1729,16 @@ dependencies = [ "windows 0.32.0", ] +[[package]] +name = "default_net" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/default_net#78f8f70cd85151a3a2c4a3230d80d5272703c02e" +dependencies = [ + "anyhow", + "regex", + "winapi 0.3.9", +] + [[package]] name = "deranged" version = "0.3.11" @@ -1597,7 +1754,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -1633,6 +1790,15 @@ dependencies = [ "dirs-sys 0.3.7", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1731,7 +1897,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a09ac8bb8c16a282264c379dffba707b9c998afc7506009137f3c6136888078" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -1783,6 +1949,12 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0" +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dylib_virtual_display" version = "0.1.0" @@ -1792,7 +1964,7 @@ dependencies = [ "lazy_static", "serde 1.0.203", "serde_derive", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -1810,15 +1982,6 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" -[[package]] -name = "encoding_rs" -version = "0.8.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" -dependencies = [ - "cfg-if 1.0.0", -] - [[package]] name = "enigo" version = "0.0.14" @@ -1842,7 +2005,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06c36cb11dbde389f4096111698d8b567c0720e3452fd5ac3e6b4e47e1939932" dependencies = [ - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -1860,9 +2023,9 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -1881,9 +2044,19 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", +] + +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", ] [[package]] @@ -1905,11 +2078,21 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" dependencies = [ - "humantime", - "is-terminal", "log", "regex", - "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", ] [[package]] @@ -1918,7 +2101,7 @@ version = "4.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74351c3392ea1ff6cd2628e0042d268ac2371cb613252ff383b6dfa50d22fa79" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "libc", ] @@ -2042,6 +2225,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" +dependencies = [ + "libc", + "thiserror 1.0.61", + "winapi 0.3.9", +] + [[package]] name = "filetime" version = "0.2.23" @@ -2083,9 +2276,9 @@ dependencies = [ "is-terminal", "lazy_static", "log", - "nu-ansi-term", + "nu-ansi-term 0.49.0", "regex", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2094,6 +2287,9 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ + "futures-core", + "futures-sink", + "nanorand", "spin", ] @@ -2169,9 +2365,9 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2208,6 +2404,25 @@ dependencies = [ "time 0.1.45", ] +[[package]] +name = "fsevent" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8836d1f147a0a195bf517a5fd211ea7023d19ced903135faf6c4504f2cf8775f" +dependencies = [ + "bitflags 1.3.2", + "fsevent-sys", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -2222,17 +2437,17 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "fuser" -version = "0.13.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21370f84640642c8ea36dfb2a6bfc4c55941f476fcf431f6fef25a5ddcf0169b" +checksum = "53274f494609e77794b627b1a3cddfe45d675a6b2e9ba9c0fdc8d8eee2184369" dependencies = [ "libc", "log", "memchr", + "nix 0.29.0", "page_size", - "pkg-config", "smallvec", - "zerocopy 0.6.6", + "zerocopy 0.8.14", ] [[package]] @@ -2317,9 +2532,9 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2474,8 +2689,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if 1.0.0", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", + "wasm-bindgen", ] [[package]] @@ -2510,7 +2741,7 @@ dependencies = [ "once_cell", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2564,7 +2795,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "futures-channel", "futures-core", "futures-executor", @@ -2578,7 +2809,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2592,7 +2823,7 @@ dependencies = [ "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -2606,9 +2837,9 @@ dependencies = [ "heck 0.4.1", "proc-macro-crate 2.0.2", "proc-macro-error", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -2680,7 +2911,7 @@ dependencies = [ "once_cell", "paste", "pretty-hex", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -2839,28 +3070,9 @@ checksum = "c6063efb63db582968fb7df72e1ae68aa6360dcfb0a75143f34fc7d616bad75e" dependencies = [ "proc-macro-crate 1.3.1", "proc-macro-error", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", -] - -[[package]] -name = "h2" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", + "syn 2.0.98", ] [[package]] @@ -2879,7 +3091,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.8", + "ahash", ] [[package]] @@ -2887,10 +3099,12 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.11", - "allocator-api2", -] + +[[package]] +name = "hashbrown" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "hbb_common" @@ -2902,10 +3116,11 @@ dependencies = [ "bytes", "chrono", "confy", + "default_net", "directories-next", "dirs-next", "dlopen", - "env_logger 0.10.2", + "env_logger 0.11.6", "filetime", "flexi_logger", "futures", @@ -2926,18 +3141,22 @@ dependencies = [ "serde 1.0.203", "serde_derive", "serde_json 1.0.118", + "sha2", "socket2 0.3.19", "sodiumoxide", "sysinfo", - "thiserror", + "thiserror 1.0.61", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.0", - "tokio-socks 0.5.2-1", + "tokio-rustls", + "tokio-socks 0.5.2-3", + "tokio-tungstenite", "tokio-util", "toml 0.7.8", + "tungstenite", "url", "uuid", + "whoami", "winapi 0.3.9", "zstd 0.13.1", ] @@ -2984,6 +3203,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hermit-abi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e" + [[package]] name = "hex" version = "0.4.3" @@ -3025,9 +3250,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.12" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", @@ -3036,26 +3261,32 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", - "pin-project-lite", ] [[package]] -name = "httparse" -version = "1.9.4" +name = "http-body-util" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] [[package]] -name = "httpdate" -version = "1.0.3" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "humantime" @@ -3066,7 +3297,7 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.7.1" -source = "git+https://github.com/rustdesk-org/hwcodec#0ea7e709d3c48bb6446e33a9cc8fd0e0da5709b9" +source = "git+https://github.com/rustdesk-org/hwcodec#17c1dbb38450fe4a64aeba78fb50bec32f364a16" dependencies = [ "bindgen 0.59.2", "cc", @@ -3078,53 +3309,75 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.29" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f361cde2f109281a220d4307746cdfd5ee3f410da58a70377762396775634b33" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", - "h2", "http", "http-body", "httparse", - "httpdate", "itoa 1.0.11", "pin-project-lite", - "socket2 0.5.7", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.27.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ - "futures-util", "http", "hyper", - "rustls 0.21.12", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.0", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", ] [[package]] @@ -3134,7 +3387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.8.7", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", @@ -3214,7 +3467,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", ] @@ -3277,6 +3530,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ioctl-rs" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7970510895cee30b3e9128319f2cefd4bde883a39f38baa279567ba3a7eb97d" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -3285,11 +3547,11 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi 0.5.0", "libc", "windows-sys 0.52.0", ] @@ -3336,20 +3598,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" -[[package]] -name = "jni" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - [[package]] name = "jni" version = "0.21.1" @@ -3361,7 +3609,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.61", "walkdir", "windows-sys 0.45.0", ] @@ -3392,13 +3640,37 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] +[[package]] +name = "kcp-sys" +version = "0.1.0" +source = "git+https://github.com/rustdesk-org/kcp-sys#32a6c09fc6223f54aea83981a6aa8995931d29be" +dependencies = [ + "anyhow", + "auto_impl", + "bindgen 0.71.1", + "bitflags 2.9.1", + "bytes", + "cc", + "dashmap 6.1.0", + "log", + "parking_lot", + "rand 0.8.5", + "thiserror 2.0.11", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "zerocopy 0.7.34", +] + [[package]] name = "keepawake" version = "0.4.3" @@ -3429,7 +3701,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "serde 1.0.203", "unicode-segmentation", ] @@ -3478,9 +3750,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.155" +version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libdbus-sys" @@ -3583,7 +3855,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "libc", ] @@ -3673,6 +3945,12 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac_address" version = "1.1.7" @@ -3766,6 +4044,21 @@ dependencies = [ "autocfg 1.3.0", ] +[[package]] +name = "metal" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e198a0ee42bdbe9ef2c09d0b9426f3b2b47d90d93a4a9b0395c4cea605e92dc0" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa 0.20.2", + "core-graphics 0.19.2", + "foreign-types 0.3.2", + "log", + "objc", +] + [[package]] name = "mime" version = "0.3.17" @@ -3800,6 +4093,42 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "mozjpeg" +version = "0.10.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55571bce4f12d80ceb4296526e7614f796df72daaaac85f265ab732fa47b7bc9" +dependencies = [ + "arrayvec", + "bytemuck", + "libc", + "mozjpeg-sys", + "rgb", +] + +[[package]] +name = "mozjpeg-sys" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad3626d7942d5b56cc6d47b1c59724c0a976b786fca059c5aaa904aef6324d55" +dependencies = [ + "cc", + "dunce", + "libc", + "nasm-rs", +] + [[package]] name = "muda" version = "0.13.5" @@ -3815,7 +4144,7 @@ dependencies = [ "objc", "once_cell", "png", - "thiserror", + "thiserror 1.0.61", "windows-sys 0.52.0", ] @@ -3825,6 +4154,24 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0419348c027fa7be448d2ae7ea0e4e04c2334c31dc4e74ab29f00a2a7ca69204" +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "nasm-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fcfa1bd49e0342ec1d07ed2be83b59963e7acbeb9310e1bb2c07b69dadd959" +dependencies = [ + "jobserver", +] + [[package]] name = "native-tls" version = "0.2.12" @@ -3837,7 +4184,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.10.0", "security-framework-sys", "tempfile", ] @@ -3865,7 +4212,7 @@ dependencies = [ "ndk-sys 0.4.1+23.1.7779620", "num_enum 0.5.11", "raw-window-handle 0.5.2", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -3874,12 +4221,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "jni-sys", "log", "ndk-sys 0.5.0+25.2.9519653", "num_enum 0.7.2", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -3941,7 +4288,7 @@ dependencies = [ "anyhow", "byteorder", "paste", - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -3968,6 +4315,20 @@ dependencies = [ "memoffset 0.6.5", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg 1.3.0", + "bitflags 1.3.2", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", + "pin-utils", +] + [[package]] name = "nix" version = "0.26.4" @@ -3986,7 +4347,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "cfg-if 1.0.0", "cfg_aliases 0.1.1", "libc", @@ -3999,12 +4360,75 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "cfg-if 1.0.0", "cfg_aliases 0.2.1", "libc", ] +[[package]] +name = "nokhwa" +version = "0.10.7" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "flume", + "image 0.25.1", + "nokhwa-bindings-linux", + "nokhwa-bindings-macos", + "nokhwa-bindings-windows", + "nokhwa-core", + "paste", + "thiserror 2.0.11", +] + +[[package]] +name = "nokhwa-bindings-linux" +version = "0.1.1" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "nokhwa-core", + "v4l", +] + +[[package]] +name = "nokhwa-bindings-macos" +version = "0.2.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-media-sys", + "core-video-sys", + "flume", + "nokhwa-core", + "objc", + "once_cell", +] + +[[package]] +name = "nokhwa-bindings-windows" +version = "0.4.2" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "dlopen", + "lazy_static", + "nokhwa-core", + "once_cell", + "windows 0.43.0", +] + +[[package]] +name = "nokhwa-core" +version = "0.1.5" +source = "git+https://github.com/rustdesk-org/nokhwa.git?branch=fix_from_raw_parts#c2f74662b6ce117f7f94301693fdfadc0b1ec91a" +dependencies = [ + "bytes", + "image 0.25.1", + "mozjpeg", + "thiserror 2.0.11", +] + [[package]] name = "nom" version = "7.1.3" @@ -4024,6 +4448,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi 0.3.9", +] + [[package]] name = "nu-ansi-term" version = "0.49.0" @@ -4064,7 +4498,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -4075,9 +4509,9 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4153,7 +4587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -4165,9 +4599,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro-crate 2.0.2", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4239,7 +4673,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4255,7 +4689,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation", @@ -4294,7 +4728,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4306,7 +4740,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation", @@ -4318,7 +4752,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation", @@ -4358,7 +4792,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" dependencies = [ - "jni 0.21.1", + "jni", "ndk 0.8.0", "ndk-context", "num-derive 0.4.2", @@ -4387,7 +4821,7 @@ version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "cfg-if 1.0.0", "foreign-types 0.3.2", "libc", @@ -4402,9 +4836,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4495,11 +4929,17 @@ dependencies = [ "serde_json 1.0.118", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "page_size" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b7663cbd190cfd818d08efa8497f6cd383076688c49a391ef7c0d03cd12b561" +checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" dependencies = [ "libc", "winapi 0.3.9", @@ -4522,7 +4962,7 @@ version = "0.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c94f3b9b97df3c6d4e51a14916639b24e02c7d15d1dba686ce9b1118277cb811" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -4563,8 +5003,8 @@ dependencies = [ [[package]] name = "parity-tokio-ipc" -version = "0.7.3-4" -source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#3623ec9ebef50c9b118e03b03df831008a4d1441" +version = "0.7.3-5" +source = "git+https://github.com/rustdesk-org/parity-tokio-ipc#c8c8bbcbabf9be1201c53afb0269b92b9b02d291" dependencies = [ "futures", "libc", @@ -4660,7 +5100,16 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" dependencies = [ - "phf_shared", + "phf_shared 0.7.24", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", ] [[package]] @@ -4669,8 +5118,18 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" dependencies = [ - "phf_generator", - "phf_shared", + "phf_generator 0.7.24", + "phf_shared 0.7.24", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", ] [[package]] @@ -4679,17 +5138,36 @@ version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" dependencies = [ - "phf_shared", + "phf_shared 0.7.24", "rand 0.6.5", ] +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + [[package]] name = "phf_shared" version = "0.7.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" dependencies = [ - "siphasher", + "siphasher 0.2.3", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", ] [[package]] @@ -4707,9 +5185,9 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -4799,6 +5277,26 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "portable-pty" +version = "0.8.1" +source = "git+https://github.com/rustdesk-org/wezterm?branch=rustdesk/pty_based_0.8.1#80174f8009f41565f0fa8c66dab90d4f9211ae16" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix 0.25.1", + "serial", + "shared_library", + "shell-words", + "winapi 0.3.9", + "winreg 0.10.1", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -4823,8 +5321,8 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ - "proc-macro2 1.0.86", - "syn 2.0.68", + "proc-macro2 1.0.93", + "syn 2.0.98", ] [[package]] @@ -4872,7 +5370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", "version_check", @@ -4884,7 +5382,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "version_check", ] @@ -4900,30 +5398,30 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "protobuf" -version = "3.5.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df67496db1a89596beaced1579212e9b7c53c22dca1d9745de00ead76573d514" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" dependencies = [ "bytes", "once_cell", "protobuf-support", - "thiserror", + "thiserror 1.0.61", ] [[package]] name = "protobuf-codegen" -version = "3.5.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab09155fad2d39333d3796f67845d43e29b266eea74f7bc93f153f707f126dc" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" dependencies = [ "anyhow", "once_cell", @@ -4931,14 +5429,14 @@ dependencies = [ "protobuf-parse", "regex", "tempfile", - "thiserror", + "thiserror 1.0.61", ] [[package]] name = "protobuf-parse" -version = "3.5.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a16027030d4ec33e423385f73bb559821827e9ec18c50e7874e4d6de5a4e96f" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" dependencies = [ "anyhow", "indexmap", @@ -4946,17 +5444,17 @@ dependencies = [ "protobuf", "protobuf-support", "tempfile", - "thiserror", + "thiserror 1.0.61", "which", ] [[package]] name = "protobuf-support" -version = "3.5.0" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e2d30ab1878b2e72d1e2fc23ff5517799c9929e2cf81a8516f9f4dcf2b9cf3" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" dependencies = [ - "thiserror", + "thiserror 1.0.61", ] [[package]] @@ -4994,7 +5492,7 @@ dependencies = [ "cfg-if 0.1.10", "rpassword 2.1.0", "tempfile", - "termios", + "termios 0.3.3", "winapi 0.3.9", ] @@ -5026,25 +5524,86 @@ dependencies = [ ] [[package]] -name = "quote" -version = "0.6.13" +name = "quinn" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" dependencies = [ - "proc-macro2 0.4.30", + "bytes", + "cfg_aliases 0.2.1", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls", + "socket2 0.5.10", + "thiserror 2.0.11", + "tokio", + "tracing", + "web-time", ] [[package]] -name = "quote" -version = "1.0.36" +name = "quinn-proto" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" dependencies = [ - "proc-macro2 1.0.86", -] - -[[package]] -name = "radium" + "bytes", + "getrandom 0.3.2", + "lru-slab", + "rand 0.9.0", + "ring", + "rustc-hash 2.1.1", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.11", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2 1.0.93", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "radium" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" @@ -5079,6 +5638,17 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", + "zerocopy 0.8.14", +] + [[package]] name = "rand_chacha" version = "0.1.1" @@ -5099,6 +5669,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + [[package]] name = "rand_core" version = "0.3.1" @@ -5120,7 +5700,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.15", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.2", ] [[package]] @@ -5224,7 +5813,7 @@ source = "git+https://github.com/rustdesk-org/rdev#f9b60b1dd0f3300a1b797d7a74c11 dependencies = [ "cocoa 0.24.1", "core-foundation 0.9.4", - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.8.7", "core-graphics 0.22.3", "dispatch", "enum-map", @@ -5233,7 +5822,7 @@ dependencies = [ "lazy_static", "libc", "log", - "mio", + "mio 0.8.11", "strum 0.24.1", "strum_macros 0.24.3", "widestring", @@ -5274,7 +5863,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", ] [[package]] @@ -5283,16 +5872,16 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom", + "getrandom 0.2.15", "libredox", - "thiserror", + "thiserror 1.0.61", ] [[package]] name = "regex" -version = "1.10.5" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -5302,9 +5891,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -5313,9 +5902,18 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "remote_printer" +version = "0.1.0" +dependencies = [ + "hbb_common", + "winapi 0.3.9", + "windows-strings 0.3.1", +] [[package]] name = "repng" @@ -5329,21 +5927,22 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.23" -source = "git+https://github.com/rustdesk-org/reqwest#9cb758c9fb2f4edc62eb790acfd45a6a3da21ed3" +version = "0.12.15" +source = "git+https://github.com/rustdesk-org/reqwest#9e859438203a71eb86ddc294fbebfde14cba7f7c" dependencies = [ "async-compression", - "base64 0.21.7", + "base64 0.22.1", "bytes", - "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2", "http", "http-body", + "http-body-util", "hyper", "hyper-rustls", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -5352,39 +5951,49 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", - "rustls-pemfile 1.0.4", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", "serde 1.0.203", "serde_json 1.0.118", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls 0.24.1", - "tokio-socks 0.5.1", + "tokio-rustls", + "tokio-socks 0.5.2", "tokio-util", + "tower", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.4", - "winreg 0.50.0", + "webpki-roots 0.26.9", + "windows-registry", +] + +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +dependencies = [ + "bytemuck", ] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if 1.0.0", - "getrandom", + "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -5467,7 +6076,7 @@ dependencies = [ [[package]] name = "rust-pulsectl" version = "0.2.12" -source = "git+https://github.com/open-trade/pulsectl#5e68f4c2b7c644fa321984688602d71e8ad0bba3" +source = "git+https://github.com/rustdesk-org/pulsectl#aa34dde499aa912a3abc5289cc0b547bd07dd6e2" dependencies = [ "libpulse-binding", ] @@ -5484,6 +6093,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustc_version" version = "0.4.0" @@ -5495,7 +6110,7 @@ dependencies = [ [[package]] name = "rustdesk" -version = "1.3.5" +version = "1.4.1" dependencies = [ "android-wakelock", "android_logger", @@ -5521,6 +6136,7 @@ dependencies = [ "dbus-crossroads", "default-net", "dispatch", + "docopt", "enigo", "errno", "evdev", @@ -5534,7 +6150,8 @@ dependencies = [ "image 0.24.9", "impersonate_system", "include_dir", - "jni 0.21.1", + "jni", + "kcp-sys", "keepawake", "lazy_static", "libloading 0.8.4", @@ -5551,8 +6168,10 @@ dependencies = [ "pam", "parity-tokio-ipc", "percent-encoding", + "portable-pty", "qrcode-generator", "rdev", + "remote_printer", "repng", "reqwest", "ringbuf", @@ -5570,11 +6189,13 @@ dependencies = [ "sha2", "shared_memory", "shutdown_hooks", + "stunclient", "sys-locale", "system_shutdown", "tao", "tauri-winrt-notification", - "termios", + "terminfo", + "termios 0.3.3", "totp-rs", "tray-icon", "url", @@ -5582,8 +6203,8 @@ dependencies = [ "uuid", "virtual_display", "wallpaper", - "whoami", "winapi 0.3.9", + "windows 0.61.1", "windows-service", "winreg 0.11.0", "winres", @@ -5595,7 +6216,7 @@ dependencies = [ [[package]] name = "rustdesk-portable-packer" -version = "1.3.5" +version = "1.4.1" dependencies = [ "brotli", "dirs 5.0.1", @@ -5640,7 +6261,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys 0.4.14", @@ -5649,100 +6270,68 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.23.10" +version = "0.23.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "df51b5869f3a441595eac5e8ff14d486ff285f7b8c0df8770e49c3b56351f0f0" dependencies = [ "log", "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.4", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-native-certs" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.2", "rustls-pki-types", "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", + "security-framework 3.2.0", ] [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] [[package]] name = "rustls-platform-verifier" -version = "0.3.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e3beb939bcd33c269f4bf946cc829fcd336370267c4a927ac0399c84a3151a1" +checksum = "4a5467026f437b4cb2a533865eaa73eb840019a0916f4b9ec563c6e617e086c9" dependencies = [ - "core-foundation 0.9.4", - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", - "jni 0.19.0", + "core-foundation 0.10.0", + "core-foundation-sys 0.8.7", + "jni", "log", "once_cell", - "rustls 0.23.10", - "rustls-native-certs 0.7.0", + "rustls", + "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.102.4", - "security-framework", + "rustls-webpki", + "security-framework 3.2.0", "security-framework-sys", - "webpki-roots 0.26.3", - "winapi 0.3.9", + "webpki-root-certs", + "windows-sys 0.52.0", ] [[package]] @@ -5753,19 +6342,9 @@ checksum = "84e217e7fdc8466b5b35d30f8c0a30febd29173df4a3a0c2115d306b9c4117ad" [[package]] name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.4" +version = "0.103.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" dependencies = [ "ring", "rustls-pki-types", @@ -5814,7 +6393,7 @@ dependencies = [ [[package]] name = "sciter-rs" version = "0.5.57" -source = "git+https://github.com/open-trade/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821" +source = "git+https://github.com/rustdesk-org/rust-sciter?branch=dyn#5322f3a755a0e6bf999fbc60d1efc35246c0f821" dependencies = [ "lazy_static", "libc", @@ -5849,11 +6428,12 @@ dependencies = [ "gstreamer-video", "hbb_common", "hwcodec", - "jni 0.21.1", + "jni", "lazy_static", "log", "ndk 0.7.0", "ndk-context", + "nokhwa", "num_cpus", "pkg-config", "quest", @@ -5867,36 +6447,38 @@ dependencies = [ ] [[package]] -name = "sct" -version = "0.7.1" +name = "security-framework" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ - "ring", - "untrusted", + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-foundation-sys 0.8.7", + "libc", + "security-framework-sys", ] [[package]] name = "security-framework" -version = "2.10.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 2.9.1", + "core-foundation 0.10.0", + "core-foundation-sys 0.8.7", "libc", - "num-bigint", "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.8.7", "libc", ] @@ -5927,9 +6509,9 @@ version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -5961,9 +6543,9 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -5987,6 +6569,48 @@ dependencies = [ "serde 1.0.203", ] +[[package]] +name = "serial" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1237a96570fc377c13baa1b88c7589ab66edced652e43ffb17088f003db3e86" +dependencies = [ + "serial-core", + "serial-unix", + "serial-windows", +] + +[[package]] +name = "serial-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f46209b345401737ae2125fe5b19a77acce90cd53e1658cda928e4fe9a64581" +dependencies = [ + "libc", +] + +[[package]] +name = "serial-unix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f03fbca4c9d866e24a459cbca71283f545a37f8e3e002ad8c70593871453cab7" +dependencies = [ + "ioctl-rs", + "libc", + "serial-core", + "termios 0.2.2", +] + +[[package]] +name = "serial-windows" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c6d3b776267a75d31bbdfd5d36c0ca051251caafc285827052bc53bcdc8162" +dependencies = [ + "libc", + "serial-core", +] + [[package]] name = "sha1" version = "0.10.6" @@ -6022,6 +6646,25 @@ dependencies = [ "tzdb 0.5.10", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + [[package]] name = "shared_memory" version = "0.12.4" @@ -6035,6 +6678,12 @@ dependencies = [ "win-sys", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.3.0" @@ -6074,6 +6723,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.9" @@ -6112,9 +6767,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -6190,7 +6845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ "heck 0.3.3", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -6202,12 +6857,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "rustversion", "syn 1.0.109", ] +[[package]] +name = "stun_codec" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feed9dafe0bda84f2b6ca3ce726b0a1f1ac2e8b63c6ecfb89b08b32313247b5b" +dependencies = [ + "bytecodec", + "byteorder", + "crc", + "hmac", + "md5", + "sha1", + "trackable 1.3.0", +] + +[[package]] +name = "stunclient" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c969a14b4a4c09c320416ebf880b3d5a81ad1612065741eb10521951c06c8991" +dependencies = [ + "bytecodec", + "rand 0.8.5", + "stun_codec", + "tokio", +] + [[package]] name = "subtle" version = "2.6.1" @@ -6231,27 +6913,30 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.68" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "unicode-ident", ] [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "sys-locale" @@ -6268,7 +6953,7 @@ version = "0.29.10" source = "git+https://github.com/rustdesk-org/sysinfo?branch=rlim_max#90b1705d909a4902dbbbdea37ee64db17841077d" dependencies = [ "cfg-if 1.0.0", - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.8.7", "libc", "ntapi", "once_cell", @@ -6293,7 +6978,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" dependencies = [ - "core-foundation-sys 0.8.6 (registry+https://github.com/rust-lang/crates.io-index)", + "core-foundation-sys 0.8.7", "libc", ] @@ -6307,7 +6992,7 @@ dependencies = [ "pkg-config", "strum 0.18.0", "strum_macros 0.18.0", - "thiserror", + "thiserror 1.0.61", "toml 0.5.11", "version-compare 0.0.10", ] @@ -6352,7 +7037,7 @@ dependencies = [ "gtk", "image 0.24.9", "instant", - "jni 0.21.1", + "jni", "lazy_static", "libc", "log", @@ -6369,7 +7054,7 @@ dependencies = [ "unicode-segmentation", "url", "windows 0.52.0", - "windows-implement", + "windows-implement 0.52.0", "windows-version", "x11-dl", "zbus", @@ -6380,7 +7065,7 @@ name = "tao-macros" version = "0.1.2" source = "git+https://github.com/rustdesk-org/tao?branch=dev#288c219cb0527e509590c2b2d8e7072aa9feb2d3" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] @@ -6403,8 +7088,8 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "013d134ae4a25ee744ad6129db589018558f620ddfa44043887cdd45fa08e75c" dependencies = [ - "phf", - "phf_codegen", + "phf 0.7.24", + "phf_codegen 0.7.24", "serde_json 0.9.10", ] @@ -6439,6 +7124,28 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminfo" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666cd3a6681775d22b200409aad3b089c5b99fb11ecdd8a204d9d62f8148498f" +dependencies = [ + "dirs 4.0.0", + "fnv", + "nom", + "phf 0.11.3", + "phf_codegen 0.11.3", +] + +[[package]] +name = "termios" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d9cf598a6d7ce700a4e6a9199da127e6819a61e64b68609683cc9a01b5683a" +dependencies = [ + "libc", +] + [[package]] name = "termios" version = "0.3.3" @@ -6475,7 +7182,16 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.61", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -6484,18 +7200,39 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] -name = "threadpool" -version = "1.8.1" +name = "thiserror-impl" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ - "num_cpus", + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", ] [[package]] @@ -6570,32 +7307,31 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.44.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" dependencies = [ "backtrace", "bytes", "libc", - "mio", - "num_cpus", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.7", + "socket2 0.5.10", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -6610,65 +7346,74 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.21.12", + "rustls", + "rustls-pki-types", "tokio", ] [[package]] -name = "tokio-rustls" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +name = "tokio-socks" +version = "0.5.2-3" +source = "git+https://github.com/rustdesk-org/tokio-socks#bdb9aa3de5bac41602d0742b8ef6bbc6bfebd127" dependencies = [ - "rustls 0.23.10", - "rustls-pki-types", + "bytes", + "either", + "futures-core", + "futures-sink", + "futures-util", + "pin-project", + "thiserror 2.0.11", "tokio", + "tokio-util", ] [[package]] name = "tokio-socks" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", "futures-util", - "thiserror", + "thiserror 1.0.61", "tokio", ] [[package]] -name = "tokio-socks" -version = "0.5.2-1" -source = "git+https://github.com/rustdesk-org/tokio-socks#94e97c6d7c93b0bcbfa54f2dc397c1da0a6e43d3" +name = "tokio-tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ - "bytes", - "either", - "futures-core", - "futures-sink", "futures-util", - "pin-project", - "thiserror", + "log", + "native-tls", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "tokio", - "tokio-util", + "tokio-native-tls", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.9", ] [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "futures-util", - "hashbrown 0.14.5", + "hashbrown 0.15.4", "pin-project-lite", "slab", "tokio", @@ -6758,17 +7503,38 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -6777,22 +7543,77 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ + "log", "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "nu-ansi-term 0.46.0", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trackable" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98abb9e7300b9ac902cc04920945a874c1973e08c310627cc4458c04b70dd32" +dependencies = [ + "trackable 1.3.0", + "trackable_derive", +] + +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote 1.0.36", + "syn 1.0.109", ] [[package]] @@ -6820,7 +7641,7 @@ dependencies = [ "objc2-foundation", "once_cell", "png", - "thiserror", + "thiserror 1.0.61", "windows-sys 0.52.0", ] @@ -6844,6 +7665,28 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.9.0", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "sha1", + "thiserror 2.0.11", + "utf-8", + "webpki-roots 0.26.9", +] + [[package]] name = "typenum" version = "1.17.0" @@ -6999,6 +7842,12 @@ dependencies = [ "log", ] +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf16string" version = "0.2.0" @@ -7022,13 +7871,39 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.9.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" dependencies = [ - "getrandom", + "getrandom 0.3.2", ] +[[package]] +name = "v4l" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fbfea44a46799d62c55323f3c55d06df722fbe577851d848d328a1041c3403" +dependencies = [ + "bitflags 1.3.2", + "libc", + "v4l2-sys-mit", +] + +[[package]] +name = "v4l2-sys-mit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6779878362b9bacadc7893eac76abe69612e8837ef746573c4a5239daf11990b" +dependencies = [ + "bindgen 0.65.1", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "vcpkg" version = "0.2.15" @@ -7091,7 +7966,7 @@ dependencies = [ "dirs 5.0.1", "enquote", "rust-ini", - "thiserror", + "thiserror 1.0.61", "winapi 0.3.9", "winreg 0.11.0", ] @@ -7117,6 +7992,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" @@ -7125,26 +8009,27 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if 1.0.0", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -7162,9 +8047,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote 1.0.36", "wasm-bindgen-macro-support", @@ -7172,22 +8057,25 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wayland-backend" @@ -7209,7 +8097,7 @@ version = "0.31.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e321577a0a165911bdcfb39cf029302479d7527b517ee58ab0f6ad09edf0943" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "rustix 0.38.34", "wayland-backend", "wayland-scanner", @@ -7221,7 +8109,7 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62989625a776e827cc0f15d41444a3cea5205b963c3a25be48ae1b52d6b4daaa" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7233,7 +8121,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd993de54a40a40fbe5601d9f1fbcaef0aebcc5fda447d7dc8f6dcbaae4f8953" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.9.1", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7246,7 +8134,7 @@ version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7b56f89937f1cf2ee1f1259cf2936a17a1f45d8f0aa1019fae6d470d304cfa6" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quick-xml 0.34.0", "quote 1.0.36", ] @@ -7272,6 +8160,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webm" version = "1.1.0" @@ -7288,17 +8186,29 @@ dependencies = [ "cc", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "29aad86cec885cafd03e8305fd727c418e970a521322c91688414d5b8efba16b" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "webpki-roots" -version = "0.26.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" dependencies = [ "rustls-pki-types", ] @@ -7323,11 +8233,11 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall 0.5.2", "wasite", "web-sys", ] @@ -7425,6 +8335,21 @@ dependencies = [ "windows_x86_64_msvc 0.34.0", ] +[[package]] +name = "windows" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows" version = "0.44.0" @@ -7460,8 +8385,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core 0.52.0", - "windows-implement", - "windows-interface", + "windows-implement 0.52.0", + "windows-interface 0.52.0", "windows-targets 0.52.5", ] @@ -7475,6 +8400,28 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" +dependencies = [ + "windows-collections", + "windows-core 0.61.0", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.0", +] + [[package]] name = "windows-core" version = "0.51.1" @@ -7499,19 +8446,53 @@ version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.5", ] +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement 0.60.0", + "windows-interface 0.59.1", + "windows-link", + "windows-result 0.3.2", + "windows-strings 0.4.0", +] + +[[package]] +name = "windows-future" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + [[package]] name = "windows-implement" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", ] [[package]] @@ -7520,9 +8501,47 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2 1.0.93", + "quote 1.0.36", + "syn 2.0.98", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.0", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +dependencies = [ + "windows-result 0.3.2", + "windows-strings 0.3.1", + "windows-targets 0.53.0", ] [[package]] @@ -7534,6 +8553,15 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-service" version = "0.6.0" @@ -7545,6 +8573,24 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "windows-strings" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -7611,13 +8657,29 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.5", "windows_aarch64_msvc 0.52.5", "windows_i686_gnu 0.52.5", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.5", "windows_i686_msvc 0.52.5", "windows_x86_64_gnu 0.52.5", "windows_x86_64_gnullvm 0.52.5", "windows_x86_64_msvc 0.52.5", ] +[[package]] +name = "windows-targets" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +dependencies = [ + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + [[package]] name = "windows-version" version = "0.1.1" @@ -7654,6 +8716,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -7684,6 +8752,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -7714,12 +8788,24 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -7750,6 +8836,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -7780,6 +8872,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -7798,6 +8896,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -7828,6 +8932,12 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + [[package]] name = "winnow" version = "0.5.40" @@ -7839,22 +8949,21 @@ dependencies = [ [[package]] name = "winreg" -version = "0.11.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ - "cfg-if 1.0.0", "winapi 0.3.9", ] [[package]] name = "winreg" -version = "0.50.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "76a1a57ff50e9b408431e8f97d5456f2807f8eb2a2cd79b06068fc87f8ecf189" dependencies = [ "cfg-if 1.0.0", - "windows-sys 0.48.0", + "winapi 0.3.9", ] [[package]] @@ -7866,6 +8975,15 @@ dependencies = [ "toml 0.5.11", ] +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + [[package]] name = "wl-clipboard-rs" version = "0.9.0" @@ -7877,7 +8995,7 @@ dependencies = [ "os_pipe", "rustix 0.38.34", "tempfile", - "thiserror", + "thiserror 1.0.61", "tree_magic_mini", "wayland-backend", "wayland-client", @@ -7987,6 +9105,17 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "xattr" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e105d177a3871454f754b33bb0ee637ecaaac997446375fd3e5d43a2ed00c909" +dependencies = [ + "libc", + "linux-raw-sys 0.4.14", + "rustix 0.38.34", +] + [[package]] name = "xdg-home" version = "1.2.0" @@ -8045,7 +9174,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7131497b0f887e8061b430c530240063d33bf9455fa34438f388a245da69e0a5" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "regex", "syn 1.0.109", @@ -8065,43 +9194,43 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.6.6" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854e949ac82d619ee9a14c66a1b674ac730422372ccb759ce0c39cabcf2bf8e6" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "byteorder", - "zerocopy-derive 0.6.6", + "zerocopy-derive 0.7.34", ] [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" dependencies = [ - "zerocopy-derive 0.7.34", + "zerocopy-derive 0.8.14", ] [[package]] name = "zerocopy-derive" -version = "0.6.6" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125139de3f6b9d625c39e2efdd73d41bdac468ccd556556440e322be0e1bbd91" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.98", ] [[package]] @@ -8207,7 +9336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c24dc0bed72f5f90d1f8bb5b07228cbf63b3c6e9f82d82559d4bae666e7ed9" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", "zvariant_utils", @@ -8219,7 +9348,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7234f0d811589db492d16893e3f21e8e2fd282e6d01b0cddee310322062cc200" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.93", "quote 1.0.36", "syn 1.0.109", ] diff --git a/Cargo.toml b/Cargo.toml index 5949b592585..d8403e14347 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk" -version = "1.3.5" +version = "1.4.1" authors = ["rustdesk "] edition = "2021" build= "build.rs" @@ -16,6 +16,10 @@ crate-type = ["cdylib", "staticlib", "rlib"] name = "naming" path = "src/naming.rs" +[[bin]] +name = "service" +path = "src/service.rs" + [features] inline = [] cli = [] @@ -42,7 +46,6 @@ screencapturekit = ["cpal/screencapturekit"] [dependencies] async-trait = "0.1" -whoami = "1.5.0" scrap = { path = "libs/scrap", features = ["wayland"] } hbb_common = { path = "libs/hbb_common" } serde_derive = "1.0" @@ -78,19 +81,24 @@ fon = "0.6" zip = "0.6" shutdown_hooks = "0.1" totp-rs = { version = "5.4", default-features = false, features = ["gen_secret", "otpauth"] } +stunclient = "0.4" +kcp-sys= { git = "https://github.com/rustdesk-org/kcp-sys"} +[target.'cfg(not(target_os = "linux"))'.dependencies] +# https://github.com/rustdesk/rustdesk/discussions/10197, not use cpal on linux cpal = { git = "https://github.com/rustdesk-org/cpal", branch = "osx-screencapturekit" } ringbuf = "0.3" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] mac_address = "1.1" -sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" } +sciter-rs = { git = "https://github.com/rustdesk-org/rust-sciter", branch = "dyn" } sys-locale = "0.3" enigo = { path = "libs/enigo", features = [ "with_serde" ] } clipboard = { path = "libs/clipboard" } ctrlc = "3.2" -# arboard = { version = "3.4.0", features = ["wayland-data-control"] } +# arboard = { version = "3.4", features = ["wayland-data-control"] } arboard = { git = "https://github.com/rustdesk-org/arboard", features = ["wayland-data-control"] } clipboard-master = { git = "https://github.com/rustdesk-org/clipboard-master" } +portable-pty = { git = "https://github.com/rustdesk-org/wezterm", branch = "rustdesk/pty_based_0.8.1", package = "portable-pty" } system_shutdown = "4.0" qrcode-generator = "4.1" @@ -109,13 +117,22 @@ winapi = { version = "0.3", features = [ "cguid", "cfgmgr32", "ioapiset", + "winspool", +] } +windows = { version = "0.61", features = [ + "Win32", + "Win32_System", + "Win32_System_Diagnostics", + "Win32_System_Threading", + "Win32_System_Diagnostics_ToolHelp", ] } winreg = "0.11" windows-service = "0.6" virtual_display = { path = "libs/virtual_display" } +remote_printer = { path = "libs/remote_printer" } impersonate_system = { git = "https://github.com/rustdesk-org/impersonate-system" } shared_memory = "0.12" -tauri-winrt-notification = "0.1.2" +tauri-winrt-notification = "0.1" runas = "1.2" [target.'cfg(target_os = "macos")'.dependencies] @@ -149,7 +166,7 @@ reqwest = { git = "https://github.com/rustdesk-org/reqwest", features = ["blocki [target.'cfg(target_os = "linux")'.dependencies] psimple = { package = "libpulse-simple-binding", version = "2.27" } pulse = { package = "libpulse-binding", version = "2.27" } -rust-pulsectl = { git = "https://github.com/open-trade/pulsectl" } +rust-pulsectl = { git = "https://github.com/rustdesk-org/pulsectl" } async-process = "1.7" evdev = { git="https://github.com/rustdesk-org/evdev" } dbus = "0.9" @@ -163,6 +180,7 @@ once_cell = {version = "1.18", optional = true} nix = { version = "0.29", features = ["term", "process"]} gtk = "0.18" termios = "0.3" +terminfo = "0.8" [target.'cfg(target_os = "android")'.dependencies] android_logger = "0.13" @@ -170,11 +188,11 @@ jni = "0.21" android-wakelock = { git = "https://github.com/rustdesk-org/android-wakelock" } [workspace] -members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable"] +members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/virtual_display/dylib", "libs/portable", "libs/remote_printer"] exclude = ["vdi/host", "examples/custom_plugin"] [package.metadata.winres] -LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved." +LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved." ProductName = "RustDesk" FileDescription = "RustDesk Remote Desktop" OriginalFilename = "rustdesk.exe" @@ -190,6 +208,7 @@ os-version = "0.2" [dev-dependencies] hound = "3.5" +docopt = "1.1" [package.metadata.bundle] name = "RustDesk" @@ -205,7 +224,3 @@ panic = 'abort' strip = true #opt-level = 'z' # only have smaller size after strip rpath = true - -[profile.dev] -split-debuginfo = '...' # Platform-specific. -#strip = "debuginfo" diff --git a/Dockerfile b/Dockerfile index 8544219c2b1..f0e4e4a4a62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM debian:bullseye-slim WORKDIR / ARG DEBIAN_FRONTEND=noninteractive +ENV VCPKG_FORCE_SYSTEM_BINARIES=1 RUN apt update -y && \ apt install --yes --no-install-recommends \ g++ \ @@ -21,7 +22,8 @@ RUN apt update -y && \ libpam0g-dev \ libpulse-dev \ make \ - cmake \ + wget \ + libssl-dev \ unzip \ zip \ sudo \ @@ -31,6 +33,13 @@ RUN apt update -y && \ ninja-build && \ rm -rf /var/lib/apt/lists/* +RUN wget https://github.com/Kitware/CMake/releases/download/v3.30.6/cmake-3.30.6.tar.gz --no-check-certificate && \ + tar xzf cmake-3.30.6.tar.gz && \ + cd cmake-3.30.6 && \ + ./configure --prefix=/usr/local && \ + make && \ + make install + RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg && \ /vcpkg/bootstrap-vcpkg.sh -disableMetrics && \ /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom diff --git a/README.md b/README.md index c193967d0b5..f29be76947a 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,23 @@

RustDesk - Your remote desktop
- Servers • Build • Docker • Structure • Snapshot
- [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
+ [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk]
We need your help to translate this README, RustDesk UI and RustDesk Doc to your native language

-Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +> [!Caution] +> **Misuse Disclaimer:**
+> The developers of RustDesk do not condone or support any unethical or illegal use of this software. Misuse, such as unauthorized access, control or invasion of privacy, is strictly against our guidelines. The authors are not responsible for any misuse of the application. + + +Chat with us: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Yet another remote desktop software, written in Rust. Works out of the box, no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). +Yet another remote desktop solution, written in Rust. Works out of the box with no configuration required. You have full control of your data, with no concerns about security. You can use our rendezvous/relay server, [set up your own](https://rustdesk.com/server), or [write your own rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) @@ -25,9 +29,12 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB [**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) -[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) ## Dependencies @@ -39,7 +46,7 @@ Please download Sciter dynamic library yourself. [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -## Raw steps to build +## Raw Steps to build - Prepare your Rust development env and C++ build env @@ -52,7 +59,7 @@ Please download Sciter dynamic library yourself. ## [Build](https://rustdesk.com/docs/en/dev/build/) -## How to build on Linux +## How to Build on Linux ### Ubuntu 18 (Debian 10) @@ -110,7 +117,7 @@ cd ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env -git clone https://github.com/rustdesk/rustdesk +git clone --recurse-submodules https://github.com/rustdesk/rustdesk cd rustdesk mkdir -p target/debug wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so @@ -125,6 +132,7 @@ Begin by cloning the repository and building the Docker container: ```sh git clone https://github.com/rustdesk/rustdesk cd rustdesk +git submodule update --init --recursive docker build -t "rustdesk-builder" . ``` @@ -146,7 +154,7 @@ Or, if you're running a release executable: target/release/rustdesk ``` -Please ensure that you are running these commands from the root of the RustDesk repository, otherwise the application might not be able to find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host. +Please ensure that you run these commands from the root of the RustDesk repository, or the application may not find the required resources. Also note that other cargo subcommands such as `install` or `run` are not currently supported via this method as they would install or run the program inside the container instead of the host. ## File Structure @@ -160,7 +168,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 desktop and 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/v1/js)**: JavaScript for Flutter web client ## Screenshots @@ -172,6 +180,3 @@ Please ensure that you are running these commands from the root of the RustDesk ![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) -## [Public Servers](#public-servers) - -RustDesk is supported by a free EU server, graciously provided by [Codext GmbH](https://codext.link/rustdesk?utm_source=github) diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml index 98ceca3bb5e..c7b8cfee1d1 100644 --- a/appimage/AppImageBuilder-aarch64.yml +++ b/appimage/AppImageBuilder-aarch64.yml @@ -18,8 +18,8 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.5 - exec: usr/lib/rustdesk/rustdesk + version: 1.4.1 + exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: arch: @@ -77,7 +77,7 @@ AppDir: env: GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/aarch64-linux-gnu/gio/modules:$APPDIR/usr/lib/aarch64-linux-gnu/gio/modules GDK_BACKEND: x11 - APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/aarch64 + APPDIR_LIBRARY_PATH: /lib64:/usr/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu:$APPDIR/lib/aarch64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/aarch64-linux-gnu:$APPDIR/usr/lib/aarch64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/aarch64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/aarch64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/aarch64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/aarch64-linux-gnu/pulseaudio:$APPDIR/usr/lib/aarch64-linux-gnu/sasl2:$APPDIR/usr/lib/aarch64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/aarch64 GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0 GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/aarch64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/aarch64-linux-gnu/gstreamer-1.0 test: @@ -99,3 +99,4 @@ AppDir: AppImage: arch: aarch64 update-information: guess + comp: gzip diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml index 9ce7cc717e3..4025f1669ef 100644 --- a/appimage/AppImageBuilder-x86_64.yml +++ b/appimage/AppImageBuilder-x86_64.yml @@ -18,8 +18,8 @@ AppDir: id: rustdesk name: rustdesk icon: rustdesk - version: 1.3.5 - exec: usr/lib/rustdesk/rustdesk + version: 1.4.1 + exec: usr/share/rustdesk/rustdesk exec_args: $@ apt: arch: @@ -80,7 +80,7 @@ AppDir: env: GIO_MODULE_DIR: /lib64/gio/modules:/usr/lib/x86_64-linux-gnu/gio/modules:$APPDIR/usr/lib/x86_64-linux-gnu/gio/modules GDK_BACKEND: x11 - APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/lib/rustdesk/lib:$APPDIR/lib/x86_64 + APPDIR_LIBRARY_PATH: /lib64:/usr/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu:$APPDIR/lib/x86_64-linux-gnu/security:$APPDIR/lib/systemd:$APPDIR/usr/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu/gdk-pixbuf-2.0/2.10.0/loaders:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/immodules:$APPDIR/usr/lib/x86_64-linux-gnu/gtk-3.0/3.0.0/printbackends:$APPDIR/usr/lib/x86_64-linux-gnu/krb5/plugins/preauth:$APPDIR/usr/lib/x86_64-linux-gnu/libcanberra-0.30:$APPDIR/usr/lib/x86_64-linux-gnu/pulseaudio:$APPDIR/usr/lib/x86_64-linux-gnu/sasl2:$APPDIR/usr/lib/x86_64-linux-gnu/vdpau:$APPDIR/usr/share/rustdesk/lib:$APPDIR/lib/x86_64 GST_PLUGIN_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0 GST_PLUGIN_SYSTEM_PATH: /lib64/gstreamer-1.0:/usr/lib/x86_64-linux-gnu/gstreamer-1.0:$APPDIR/usr/lib/x86_64-linux-gnu/gstreamer-1.0 test: @@ -102,3 +102,4 @@ AppDir: AppImage: arch: x86_64 update-information: guess + comp: gzip diff --git a/build.py b/build.py index 5d974092037..208b67359d0 100755 --- a/build.py +++ b/build.py @@ -9,6 +9,7 @@ import hashlib import argparse import sys +from pathlib import Path windows = platform.platform().startswith('Windows') osx = platform.platform().startswith( @@ -296,8 +297,8 @@ def generate_control_file(version): Priority: optional Version: %s Architecture: %s -Maintainer: rustdesk -Homepage: https://rustdesk.com +Maintainer: B1 Systems GmbH +Homepage: https://github.com/b1-systems/rustdesk Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s Recommends: libayatana-appindicator3-1 Description: A remote control software. @@ -321,7 +322,7 @@ def build_flutter_deb(version, features): os.chdir('flutter') system2('flutter build linux --release') system2('mkdir -p tmpdeb/usr/bin/') - system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mkdir -p tmpdeb/usr/share/rustdesk') system2('mkdir -p tmpdeb/etc/rustdesk/') system2('mkdir -p tmpdeb/etc/pam.d/') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') @@ -331,7 +332,7 @@ def build_flutter_deb(version, features): system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('rm tmpdeb/usr/bin/rustdesk || true') system2( - f'cp -r {flutter_build_dir}/* tmpdeb/usr/lib/rustdesk/') + f'cp -r {flutter_build_dir}/* tmpdeb/usr/share/rustdesk/') system2( 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') system2( @@ -354,19 +355,19 @@ def build_flutter_deb(version, features): system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file_folder("tmpdeb/") system2('dpkg-deb -b tmpdeb rustdesk.deb;') system2('/bin/rm -rf tmpdeb/') system2('/bin/rm -rf ../res/DEBIAN/control') - os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.rename('rustdesk.deb', '../rustdesk_%s-1_%s.deb' % (version, get_deb_arch())) os.chdir("..") def build_deb_from_folder(version, binary_folder): os.chdir('flutter') system2('mkdir -p tmpdeb/usr/bin/') - system2('mkdir -p tmpdeb/usr/lib/rustdesk') + system2('mkdir -p tmpdeb/usr/share/rustdesk') system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/') system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/') @@ -374,7 +375,7 @@ def build_deb_from_folder(version, binary_folder): system2('mkdir -p tmpdeb/usr/share/polkit-1/actions') system2('rm tmpdeb/usr/bin/rustdesk || true') system2( - f'cp -r ../{binary_folder}/* tmpdeb/usr/lib/rustdesk/') + f'cp -r ../{binary_folder}/* tmpdeb/usr/share/rustdesk/') system2( 'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') system2( @@ -391,12 +392,12 @@ def build_deb_from_folder(version, binary_folder): system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file_folder("tmpdeb/") system2('dpkg-deb -b tmpdeb rustdesk.deb;') system2('/bin/rm -rf tmpdeb/') system2('/bin/rm -rf ../res/DEBIAN/control') - os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.rename('rustdesk.deb', '../rustdesk_%s-1_%s.deb' % (version, get_deb_arch())) os.chdir("..") @@ -404,12 +405,13 @@ def build_flutter_dmg(version, features): if not skip_cargo: # set minimum osx build target, now is 10.14, which is the same as the flutter xcode project system2( - f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --lib --release') + f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features {features} --release') # copy dylib system2( "cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib") os.chdir('flutter') system2('flutter build macos --release') + system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/') ''' system2( "create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app") @@ -621,21 +623,24 @@ def main(): os.system('mkdir -p tmpdeb/etc/pam.d/') os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk') system2('strip tmpdeb/usr/bin/rustdesk') - system2('mkdir -p tmpdeb/usr/lib/rustdesk') - system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/lib/rustdesk/') - system2('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - md5_file('etc/rustdesk/startwm.sh') - md5_file('etc/X11/rustdesk/xorg.conf') - md5_file('etc/pam.d/rustdesk') - md5_file('usr/lib/rustdesk/libsciter-gtk.so') + system2('mkdir -p tmpdeb/usr/share/rustdesk') + system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/') + system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/') + md5_file_folder("tmpdeb/") system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) def md5_file(fn): md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest() - system2('echo "%s %s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) + system2('echo "%s /%s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn)) + +def md5_file_folder(base_dir): + base_path = Path(base_dir) + for file in base_path.rglob('*'): + if file.is_file() and 'DEBIAN' not in file.parts: + relative_path = file.relative_to(base_path) + md5_file(str(relative_path)) if __name__ == "__main__": diff --git a/docs/CODE_OF_CONDUCT-NO.md b/docs/CODE_OF_CONDUCT-NO.md new file mode 100644 index 00000000000..baefda0519a --- /dev/null +++ b/docs/CODE_OF_CONDUCT-NO.md @@ -0,0 +1,125 @@ + +# Atferdskodeks for bidragsyterpaktern + +## Hva Vi StÃ¥r For + +Vi som medlemer, bidragere, og ledere stÃ¥r for Ã¥ skape ett hat-fritt felleskap, +uansett alder, kroppstørrelse, synlig eller usynlige funksjonsnedsettninger, +etnesitet, kjønns karaktertrekk, kjønnsidentitet, kunnskapsnivÃ¥, utdanning, +sosial-økonomisk status, nasjonalitet, utsende, rase, religion, eller seksual +identitet og orientasjon. + +Vi stÃ¥r for Ã¥pen, velkommende, mangfold, inklusiv og sunn oppførsel i vÃ¥rt felleskap. + +## VÃ¥re Standarer + +Eksempler pÃ¥ oppførsel som hjelper ett positivt felleskap inkluderer: + +* Vise empati og vennlighet mot andre mennesker +* Være respektfull ovenfor ulike meninger, synspunkter og erfaringer +* Gi og ta konstruktiv kritikk i beste mening +* Akseptere ansvar og unskylde seg for de som er utsatt av vÃ¥re feil, + og lære av disse +* Fokusere pÃ¥ det som er best ikke bare for individer, men for felleskapet + +Eksempler pÃ¥ uakseptabel oppførsel inkluderer: + +* Bruk av seksualisert sprÃ¥k eller bilder, og seksual oppmerksomhet. +* Troll-ene, fornermende og nedsettende kommentarer, og personlig eller politiske angrep +* Offentlig eller privat trakassering +* Publisering av andres private informasjon, sÃ¥nn som bosteds- og epost-addresser, + uten deres godskjenning. +* Andre rettningslinjer som kan bli sett pÃ¥ som upassende i en profesjonell setting. + +## HÃ¥ndhevingsansvar + +Felleskapets ledere har ansvar for Ã¥ klarifisere og hÃ¥ndheve vÃ¥re standarer av +akseptert oppførsel og vill ta rimelige og rettferdige handliger som respons pÃ¥ +oppførsel de anser som upassende, truende, fornermende eller skadelig. + +Felleskapets ledere har retten og ansvaret til Ã¥ fjerne, redigere, eller avslÃ¥ +kommentarer, commits, kode, wiki endringer, issues, og andre birag som ikke +samsvarer med disse etiske rettningslinjene, og vill kommunisere grunner for +moderatorenes valg nÃ¥r passende. + +## Omfang + +Disse etiske rettningslinjene gjelder innenfor alle platformene til felleskapet, og +de gjelder ogsÃ¥ nÃ¥r ett individ representerer felleskapet pÃ¥ offentlige medier. +Eksempler pÃ¥ representasjon av vÃ¥rt felleskap inkluderer bruke av offisielle e-mail +addresser, publisering gjennom en offisiell sosial media bruker, eller oppførsel som en +utpekt representant pÃ¥ digitale og fysiske arrangsjemanger. + +## HÃ¥ndheving + +Hendelser av misbruk, trakasserende eller pÃ¥ noen mÃ¥te uakseptert oppførsel kann +bli raportert til felleskapets ledere med ansvar for hÃ¥ndheving pÃ¥ +[info@rustdesk.com](mailto:info@rustdesk.com). +All tilbakemelding vill bli sett gjennom og investigert rettferdig sÃ¥ fort som mulig. + +Alle felleskapets ledere er obligert til Ã¥ respektere privatlivet og sikkerhetet ovenfor +den som raporterer en hendelse. + +## HÃ¥ndhevings Guide + +Felleskapets ledere vill følge disse Rettningslinjene for sammfunspÃ¥virkning med +tanke pÃ¥ konsekvenser for en handling de anser i brudd med disse etiske rettningslinjene: + +### 1. Korreksjon + +**SammfunspÃ¥virkning**: Bruk av upassende sprÃ¥k eller annen oppførsel ansett som +uprofesjonelt eller uvelkommen i dette felleskapet. + +**Konsekvens**: En privat, skrevet advarsel fra en leder av felleskapet, som +klarifiserer grunnlaget til hvorfor denne oppførselen var upassende. En offentlig +unskyldning kan bli forespurt. + +### 2. Advarsel + +**SammfunspÃ¥virkning**: Ett brudd pÃ¥ en singulær hendelse eller en serie handlinger. + +**Konsekvens**: En advarsel med konsekvenser for kontinuerende oppførsel. Ingen +interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med +de som hÃ¥ndhever disse etiske rettningslinjene, er tillat for en spesifisert tidsperiode. +Dette inkluderer Ã¥ unngÃ¥ interaksjoner i felleskapets platformer, samt eksterne +kanaler, som f.eks sosial media. Brudd av disse vilkÃ¥rene kan føre til midlertidig +eller permanent bannlysning. + +### 3. Midlertidig Bannlysning + +**SammfunspÃ¥virkning**: Ett særiøst brudd pÃ¥ felleskapets standarer, inkludert +vedvarende upassende oppførsel. + +**Konsekvens**: En midlertidig bannlysning fra noen som helst interaksjon eller +offentlig kommunikasjon med felleskapet for en spesifisert tidsperiode. Ingen +interaksjon med individene involvert, inkluderer uoppfordret interaksjoner med +de som hÃ¥ndhever disse etiske rettningslinjene, er tillat for denne perioden. +Brudd pÃ¥ disse vilkÃ¥rene kan føre til permanent bannlysning. + +### 4. Permanent Bannlysning + +**SammfunspÃ¥virkning**: Demonstasjon av mønster i brudd pÃ¥ felleskapets standarer, +inklusivt vedvarende upassende oppførsel, trakassering av ett individ, eller +aggresjon mot eller nedsettelse av grupper individer. + +**Konsekvens**: En permanent bannlysning fra alle offentlige interaksjoner i +felleskapet + +## Attribusjon + +Disse etiske rettningslinjene er adaptert fra [Contributor Covenant][homepage], +versjon 2.0, tilgjengelig ved +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +SammfunspÃ¥virknings guid inspirert av +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For svar til vanlige spørsmÃ¥l angÃ¥ende disse etiske rettningslinjene, se FAQ pÃ¥ +[https://www.contributor-covenant.org/faq][FAQ]. Oversettelse tilgjengelig +ved [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/docs/CONTRIBUTING-DE.md b/docs/CONTRIBUTING-DE.md index 6258a9a7a11..b45c23d502e 100644 --- a/docs/CONTRIBUTING-DE.md +++ b/docs/CONTRIBUTING-DE.md @@ -1,42 +1,42 @@ -# Beiträge zu RustDesk +# Beiträge zu RustDesk -RustDesk begrüßt Beiträge von jedem. Hier sind die Richtlinien, wenn Sie uns -helfen möchten: +RustDesk begrüßt Beiträge von jedem. Hier sind die Richtlinien, wenn Sie uns +helfen möchten: -## Beiträge +## Beiträge -Beiträge zu RustDesk oder seinen Abhängigkeiten sollten in Form von Pull +Beiträge zu RustDesk oder seinen Abhängigkeiten sollten in Form von Pull Requests auf GitHub erfolgen. Jeder Pull Request wird von einem Hauptakteur -(jemand mit der Erlaubnis, Korrekturen einzubringen) geprüft und entweder in den -Hauptbaum eingefügt oder Feedback für notwendige Änderungen gegeben. Alle -Beiträge sollten diesem Format folgen, auch die von Hauptakteuren. +(jemand mit der Erlaubnis, Korrekturen einzubringen) geprüft und entweder in den +Hauptbaum eingefügt oder Feedback für notwendige Änderungen gegeben. Alle +Beiträge sollten diesem Format folgen, auch die von Hauptakteuren. -Wenn Sie an einem Problem arbeiten möchten, melden Sie es bitte zuerst an, indem -Sie auf GitHub erklären, dass Sie daran arbeiten möchten. Damit soll verhindert -werden, dass Beiträge zum gleichen Thema doppelt bearbeitet werden. +Wenn Sie an einem Problem arbeiten möchten, melden Sie es bitte zuerst an, indem +Sie auf GitHub erklären, dass Sie daran arbeiten möchten. Damit soll verhindert +werden, dass Beiträge zum gleichen Thema doppelt bearbeitet werden. -## Checkliste für Pull Requests +## Checkliste für Pull Requests -- Verzweigen Sie sich vom Master-Branch und, falls nötig, wechseln Sie zum +- Verzweigen Sie sich vom Master-Branch und, falls nötig, wechseln Sie zum aktuellen Master-Branch, bevor Sie Ihren Pull Request einreichen. Wenn das - Zusammenführen mit dem Master nicht reibungslos funktioniert, werden Sie - möglicherweise aufgefordert, Ihre Änderungen zu überarbeiten. + Zusammenführen mit dem Master nicht reibungslos funktioniert, werden Sie + möglicherweise aufgefordert, Ihre Änderungen zu überarbeiten. -- Commits sollten so klein wie möglich sein und gleichzeitig sicherstellen, dass - jeder Commit unabhängig voneinander korrekt ist (d. h., jeder Commit sollte - sich übersetzen lassen und Tests bestehen). +- Commits sollten so klein wie möglich sein und gleichzeitig sicherstellen, dass + jeder Commit unabhängig voneinander korrekt ist (d. h., jeder Commit sollte + sich übersetzen lassen und Tests bestehen). -- Commits sollten von einem "Herkunftszertifikat für Entwickler" +- Commits sollten von einem "Herkunftszertifikat für Entwickler" (https://developercertificate.org) begleitet werden, das besagt, dass Sie (und ggf. Ihr Arbeitgeber) mit den Bedingungen der [Projektlizenz](../LICENCE) - einverstanden sind. In Git ist dies die Option `-s` für `git commit`. + einverstanden sind. In Git ist dies die Option `-s` für `git commit`. - Wenn Ihr Patch nicht begutachtet wird oder Sie eine bestimmte Person zur - Begutachtung benötigen, können Sie einem Gutachter mit @ antworten und um eine - Begutachtung des Pull Requests oder einen Kommentar bitten. Sie können auch + Begutachtung benötigen, können Sie einem Gutachter mit @ antworten und um eine + Begutachtung des Pull Requests oder einen Kommentar bitten. Sie können auch per [E-Mail](mailto:info@rustdesk.com) um eine Begutachtung bitten. -- Fügen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue +- Fügen Sie Tests hinzu, die sich auf den behobenen Fehler oder die neue Funktion beziehen. Spezifische Git-Anweisungen finden Sie im [GitHub-Workflow](https://github.com/servo/servo/wiki/GitHub-workflow). @@ -47,4 +47,4 @@ https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md ## Kommunikation -RustDesk-Mitarbeiter arbeiten häufig im [Discord](https://discord.gg/nDceKgxnkV). +RustDesk-Mitarbeiter arbeiten häufig im [Discord](https://discord.gg/nDceKgxnkV). diff --git a/docs/CONTRIBUTING-KR.md b/docs/CONTRIBUTING-KR.md new file mode 100644 index 00000000000..5e432648eb2 --- /dev/null +++ b/docs/CONTRIBUTING-KR.md @@ -0,0 +1,46 @@ +# RustDesk 기여하기 + +RustDesk는 모든 ë¶„ë“¤ì˜ ì°¸ì—¬ë¥¼ 환ì˜í•©ë‹ˆë‹¤. ì €í¬ë¥¼ ë„와주실 ìƒê°ì´ 있으시다면 + ë‹¤ìŒ ì§€ì¹¨ì„ ë”°ë¥´ì„¸ìš”: + +## 기여 + +RustDesk ë˜ëŠ” ê·¸ 종ì†ì„±ì— 대한 기여는 GitHub í’€ 리퀘스트 형태로 +ì´ë£¨ì–´ì ¸ì•¼ 합니다. ê° í’€ 리퀘스트는 핵심 ê¸°ì—¬ìž (패치 ì ìš© ê¶Œí•œì´ +있는 사람)ê°€ 검토하여 ë©”ì¸ íŠ¸ë¦¬ì— ì¶”ê°€í•˜ê±°ë‚˜ 필요한 변경 ì‚¬í•­ì— +대한 í”¼ë“œë°±ì„ ì œê³µí•©ë‹ˆë‹¤. 핵심 기여ìžì˜ 기여를 í¬í•¨í•˜ì—¬ 모든 기여는 +ì´ í˜•ì‹ì„ ë”°ë¼ì•¼ 합니다. + +ì´ìŠˆì— ëŒ€í•´ 작업하고 싶으시면 먼저 해당 ì´ìŠˆì— ëŒ€í•´ 작업하고 싶다는 +ëŒ“ê¸€ì„ ë‹¬ì•„ 해당 ì´ìŠˆë¥¼ 요청하세요. ì´ëŠ” ë™ì¼í•œ ì´ìŠˆì— ëŒ€í•œ 기여ìžì˜ +ì¤‘ë³µëœ ë…¸ë ¥ì„ ë°©ì§€í•˜ê¸° 위한 것입니다. + +## í’€ 리퀘스트 ì²´í¬ë¦¬ìŠ¤íŠ¸ + +- Master 브랜치ì—서 브랜치를 만들고, 필요한 경우 í’€ 리퀘스트를 제출하기 + ì „ì— í˜„ìž¬ 마스터 브랜치로 리베ì´ìŠ¤í•˜ì„¸ìš”. 마스터 브랜치와 ê¹”ë”하게 + 병합ë˜ì§€ 않으면 변경 ì‚¬í•­ì„ ë¦¬ë² ì´ìŠ¤í•˜ë¼ëŠ” ìš”ì²­ì„ ë°›ì„ ìˆ˜ 있습니다. + +- ì»¤ë°‹ì€ ê°€ëŠ¥í•œ 한 작아야 하지만, ê° ì»¤ë°‹ì´ ë…립ì ìœ¼ë¡œ 올바른지 í™•ì¸ + 해야 합니다 (즉, ê° ì»¤ë°‹ì€ ì»´íŒŒì¼ë˜ì–´ 테스트를 통과해야 함). + +- 커밋ì—는 ê°œë°œìž ì¶œì²˜ ì¦ëª…서 (http://developercertificate.org) + ì„œëª…ì´ ì²¨ë¶€ë˜ì–´ì•¼ 하며, ì´ëŠ” 귀하 (ë° í•´ë‹¹ë˜ëŠ” 경우 고용주)ê°€ + [프로ì íЏ ë¼ì´ì„ ìФ](../LICENCE). ì¡°ê±´ì— êµ¬ì†ë˜ëŠ” ë° ë™ì˜í•œë‹¤ëŠ” ê²ƒì„ ë‚˜íƒ€ëƒ…ë‹ˆë‹¤. + gitì—서는 `git commit`ì— `-s` 옵션입니다 + +- 패치가 검토ë˜ì§€ 않거나 특정ì¸ì´ 검토해야 하는 경우, í’€ 리퀘스트나 + 댓글ì—서 검토ìžì—게 @-ë‹µê¸€ì„ ë³´ë‚´ 검토를 요청하거나 + [ì´ë©”ì¼](mailto:info@rustdesk.com)ì„ í†µí•´ 검토를 요청할 수 있습니다. + +- ìˆ˜ì •ëœ ë²„ê·¸ ë˜ëŠ” 새 기능과 ê´€ë ¨ëœ í…ŒìŠ¤íŠ¸ë¥¼ 추가합니다. + +구체ì ì¸ git 지침ì€, [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow)ì„ ì°¸ì¡°í•˜ì„¸ìš”. + +## í–‰ë™ ê°•ë ¹ + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## 커뮤니케ì´ì…˜ + +RustDesk 기여ìžë“¤ì€ [Discord](https://discord.gg/nDceKgxnkV)ì—서 활ë™í•˜ê³  있습니다. diff --git a/docs/CONTRIBUTING-NO.md b/docs/CONTRIBUTING-NO.md new file mode 100644 index 00000000000..89a57456327 --- /dev/null +++ b/docs/CONTRIBUTING-NO.md @@ -0,0 +1,46 @@ +# Bidrag til RustDesk + +RustDesk er Ã¥pene for bidrag fra alle. Her er reglene for de som har lyst til Ã¥ +hjelpe oss: + +## Bidrag + +Bidrag til RustDesk eller deres avhengigheter burde være i form av GitHub pull requests. +Hver pull request vill bli sett igjennom av en kjerne bidrager (noen med autoritet til +Ã¥ godkjenne endringene) og enten bli sendt til main treet eller respondert med +tilbakemelding pÃ¥ endringer som er nødvendig. Alle bidrag burde følge dette formate +ogsÃ¥ de fra kjerne bidragere. + +Om du ønsker Ã¥ jobbe pÃ¥ en issue mÃ¥ du huske Ã¥ gjøre krav pÃ¥ den først. Dette +kann gjøres ved Ã¥ kommentere pÃ¥ den GitHub issue-en du ønsker Ã¥ jobbe pÃ¥. +Dette er for Ã¥ hindre duplikat innsats pÃ¥ samme problem. + +## Pull Request Sjekkliste + +- Lag en gren fra master grenen og, hvis det er nødvendig, rebase den til den nÃ¥værende + master grenen før du sender inn din pull request. Hvis ikke dette gjøres pÃ¥ rent + vis vill du bli spurt om Ã¥ rebase dine endringer. + +- Commits burde være sÃ¥ smÃ¥ som mulig, samtidig som de mÃ¥ være korrekt uavhenging av hverandre + (hver commit burde kompilere og bestÃ¥ tester). + +- Commits burde være akkopaniert med en Developer Certificate of Origin + (http://developercertificate.org), som indikerer att du (og din arbeidsgiver + i det tilfellet) godkjenner Ã¥ bli knyttet til vilkÃ¥rene av [prosjekt lisensen](../LICENCE). + Ved bruk av git er dette `-s` opsjonen til `git commit`. + +- Hvis dine endringer ikke blir sett eller hvis du trenger en spesefik person til + Ã¥ se pÃ¥ dem kan du @-svare en med autoritet til Ã¥ godkjenne dine endringer. + Dette kann gjøres i en pull request, en kommentar eller via epost pÃ¥ [email](mailto:info@rustdesk.com). + +- Legg til tester relevant til en fikset bug eller en ny tilgjengelighet. + +For spesefike git instruksjoner, se [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow). + +## Oppførsel + +https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT.md + +## Kommunikasjon + +RustDesk bidragere burker [Discord](https://discord.gg/nDceKgxnkV). diff --git a/docs/CONTRIBUTING-RU.md b/docs/CONTRIBUTING-RU.md index acc233d003f..1cf9a472da7 100644 --- a/docs/CONTRIBUTING-RU.md +++ b/docs/CONTRIBUTING-RU.md @@ -5,18 +5,14 @@ RustDesk приветÑтвует вклад каждого. ## Вклад в развитие -Вклады в развитие RustDesk или его завиÑимоÑти должны быть -Ñделаны в виде `pull request` на GitHub. Каждый такой -`pull request` будет раÑÑмотрен оÑновным учаÑтником -(кем-то, у кого еÑть разрешение на влив иÑправлений) -и либо помещен в оÑновное дерево, либо Вам будет дан отзыв -о необходимых правках. Ð’Ñе материалы должны ÑоответÑтвовать -Ñтому формату, даже те, которые поÑтупают от оÑновных авторов. - -ЕÑли вы хотите поработать над какой-либо проблемой, то пожалуйÑта, -Ñначала напишите об Ñтом, Ñоздав тикет на GitHub, и опиÑав, -над чем вы хотите поработать. Это делаетÑÑ Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, чтобы -предотвратить дублирование уÑилий учаÑтников по одному и тому же вопроÑу. +Вклады в развитие RustDesk или его завиÑимоÑти должны быть Ñделаны в виде `pull request` на GitHub. +Каждый такой `pull request` будет раÑÑмотрен оÑновным учаÑтником (кем-то, у кого еÑть разрешение +на влив иÑправлений) и либо помещен в оÑновное дерево, либо Вам будет дан отзыв о необходимых правках. +Ð’Ñе материалы должны ÑоответÑтвовать Ñтому формату, даже те, которые поÑтупают от оÑновных авторов. + +ЕÑли вы хотите поработать над какой-либо проблемой, то пожалуйÑта, Ñначала напишите об Ñтом, +Ñоздав `issue` на GitHub, и опиÑав, над чем вы хотите поработать. Это делаетÑÑ Ð´Ð»Ñ Ñ‚Ð¾Ð³Ð¾, +чтобы предотвратить дублирование уÑилий учаÑтников по одному и тому же вопроÑу. ## Контрольный ÑпиÑок Ð´Ð»Ñ Ð’Ð°ÑˆÐ¸Ñ… `pull request` @@ -24,13 +20,13 @@ RustDesk приветÑтвует вклад каждого. ветку перед отправкой `pull request`. При наличии конфликтов ÑлиÑÐ½Ð¸Ñ Ð²Ð°Ð¼ будет предложено их уÑтранить, возможно при помощи того же `rebase`. -- Коммиты должны быть, по возможноÑти, небольшим, при Ñтом гарантируÑ, что каждаый +- Коммиты должны быть, по возможноÑти, небольшими, при Ñтом гарантируÑ, что каждый коммит ÑвлÑетÑÑ Ð½ÐµÐ·Ð°Ð²Ð¸Ñимо правильным (Ñ‚.е., каждый коммит должен компилироватьÑÑ Ð¸ проходить теÑты). -- Коммиты должны ÑопровождатьÑÑ `Developer Certificate of Origin` - (http://developercertificate.org) подпиÑью, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ ÑƒÐºÐ°Ð¶ÐµÑ‚ на то, что вы (и - ваш работодатель, еÑли Ñто применимо) ÑоглаÑны Ñоблюдать уÑÐ»Ð¾Ð²Ð¸Ñ - [лицензии проекта](../LICENCE). Ð’ `git` Ñто флаг `-s` при иÑпользовании `git commit` +- Коммиты должны ÑопровождатьÑÑ Ð¿Ð¾Ð´Ð¿Ð¸Ñью `Developer Certificate of Origin` + (http://developercertificate.org), ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ ÑƒÐºÐ°Ð¶ÐµÑ‚ на то, что вы (и ваш работодатель, + еÑли Ñто применимо) ÑоглаÑны Ñоблюдать уÑÐ»Ð¾Ð²Ð¸Ñ [лицензии проекта](../LICENCE). + Ð’ `git` Ñто флаг `-s` при иÑпользовании `git commit` - ЕÑли ваш патч не проходит рецензирование или вам нужно, чтобы его проверил конкретный человек, Ð’Ñ‹ можете ответить рецензенту через `@`, @@ -40,7 +36,7 @@ RustDesk приветÑтвует вклад каждого. Ð”Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ ÐºÐ¾Ð½ÐºÑ€ÐµÑ‚Ð½Ñ‹Ñ… инÑтрукций `git` Ñм. [GitHub workflow 101](https://github.com/servo/servo/wiki/Github-workflow). -## ÐšÐ¾Ð´ÐµÐºÑ Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ ÑƒÑ‡Ð°Ñтников и вкладчиков +## Правила Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ ÑƒÑ‡Ð°Ñтников и вкладчиков Ðормы Ð¿Ð¾Ð²ÐµÐ´ÐµÐ½Ð¸Ñ Ð²Ð½ÑƒÑ‚Ñ€Ð¸ ÑообщеÑтва подробно опиÑаны [здеÑÑŒ](CODE_OF_CONDUCT-RU.md). diff --git a/docs/DEVCONTAINER-DE.md b/docs/DEVCONTAINER-DE.md deleted file mode 100644 index 2a0d73f1797..00000000000 --- a/docs/DEVCONTAINER-DE.md +++ /dev/null @@ -1,14 +0,0 @@ - -Nach dem Start von Dev-Container im Docker-Container wird ein Linux-Binärprogramm im Debug-Modus erstellt. - -Derzeit bietet Dev-Container Linux- und Android-Builds sowohl im Debug- als auch im Release-Modus an. - -Nachfolgend finden Sie eine Tabelle mit Befehlen, die im Stammverzeichnis des Projekts ausgeführt werden müssen, um bestimmte Builds zu erstellen. - -Kommando|Build-Typ|Modus --|-|-| -`.devcontainer/build.sh --debug linux`|Linux|debug -`.devcontainer/build.sh --release linux`|Linux|release -`.devcontainer/build.sh --debug android`|android-arm64|debug -`.devcontainer/build.sh --release android`|android-arm64|release - diff --git a/docs/DEVCONTAINER-IT.md b/docs/DEVCONTAINER-IT.md deleted file mode 100644 index 713c6fc3768..00000000000 --- a/docs/DEVCONTAINER-IT.md +++ /dev/null @@ -1,14 +0,0 @@ - -Dopo l'avvio di devcontainer nel contenitore docker, viene creato un binario linux in modalità debug. - -Attualmente devcontainer consente creazione build Linux e Android sia in modalità debug che in modalità rilascio. - -Di seguito è riportata la tabella dei comandi da eseguire dalla root del progetto per la creazione di build specifiche. - -Comando|Tipo build|Modo --|-|-| -`.devcontainer/build.sh --debug linux`|Linux|debug -`.devcontainer/build.sh --release linux`|Linux|release -`.devcontainer/build.sh --debug android`|android-arm64|debug -`.devcontainer/build.sh --release android`|android-arm64|release - diff --git a/docs/DEVCONTAINER-JP.md b/docs/DEVCONTAINER-JP.md deleted file mode 100644 index d8a599bef8c..00000000000 --- a/docs/DEVCONTAINER-JP.md +++ /dev/null @@ -1,14 +0,0 @@ - -docker コンテナ㧠devcontainer ã‚’èµ·å‹•ã™ã‚‹ã¨ã€ãƒ‡ãƒãƒƒã‚°ãƒ¢ãƒ¼ãƒ‰ã® linux ãƒã‚¤ãƒŠãƒªãŒä½œæˆã•れã¾ã™ã€‚ - -ç¾åœ¨ devcontainer ã§ã¯ã€Linux 㨠android ã®ãƒ“ルドをデãƒãƒƒã‚°ãƒ¢ãƒ¼ãƒ‰ã¨ãƒªãƒªãƒ¼ã‚¹ãƒ¢ãƒ¼ãƒ‰ã®ä¸¡æ–¹ã§æä¾›ã—ã¦ã„ã¾ã™ã€‚ - -以下ã¯ã€ç‰¹å®šã®ãƒ“ルドを作æˆã™ã‚‹ãŸã‚ã«ãƒ—ロジェクトã®ãƒ«ãƒ¼ãƒˆã‹ã‚‰å®Ÿè¡Œã™ã‚‹ã‚³ãƒžãƒ³ãƒ‰ã®è¡¨ã«ãªã‚Šã¾ã™ã€‚ - -コマンド|ビルド タイプ|モード --|-|-| -`.devcontainer/build.sh --debug linux`|Linux|debug -`.devcontainer/build.sh --release linux`|Linux|release -`.devcontainer/build.sh --debug android`|android-arm64|debug -`.devcontainer/build.sh --release android`|android-arm64|release - diff --git a/docs/DEVCONTAINER-NL.md b/docs/DEVCONTAINER-NL.md deleted file mode 100644 index cd6ae456d89..00000000000 --- a/docs/DEVCONTAINER-NL.md +++ /dev/null @@ -1,15 +0,0 @@ - -Na de start van devcontainer in docker container wordt een linux binaire in foutmodus aangemaakt. - -Momenteel biedt devcontainer linux en android builds in zowel foutopsporing- als uitgave modus. - -Hieronder staat de tabel met commando's die vanuit de root van het project moeten worden -uitgevoerd om specifieke builds te maken. - -Commando|Build Type|Modus --|-|-| -`.devcontainer/build.sh --debug linux`|Linux|debug -`.devcontainer/build.sh --release linux`|Linux|release -`.devcontainer/build.sh --debug android`|android-arm64|debug -`.devcontainer/build.sh --release android`|android-arm64|debug - diff --git a/docs/DEVCONTAINER-PL.md b/docs/DEVCONTAINER-PL.md deleted file mode 100644 index 0aae2b975e3..00000000000 --- a/docs/DEVCONTAINER-PL.md +++ /dev/null @@ -1,14 +0,0 @@ - -Po uruchomieniu devcontainer w kontenerze docker, tworzony jest plik binarny linux w trybue debugowania. - -Obecnie devcontainer oferuje kompilowanie wersji dla linux i android w obu trybach - debugowania i wersji finalnej. - -Poniżej tabela poleceÅ„ do uruchomienia z głównego folderu do tworzenia wybranych kompilacji. - -Polecenie|Typ kompilacji|Tryb --|-|-| -`.devcontainer/build.sh --debug linux`|Linux|debug -`.devcontainer/build.sh --release linux`|Linux|release -`.devcontainer/build.sh --debug android`|android-arm64|debug -`.devcontainer/build.sh --release android`|android-arm64|debug - diff --git a/docs/DEVCONTAINER-TR.md b/docs/DEVCONTAINER-TR.md deleted file mode 100644 index 7fc14ce5ee9..00000000000 --- a/docs/DEVCONTAINER-TR.md +++ /dev/null @@ -1,12 +0,0 @@ -Docker konteynerinde devcontainer'ın baÅŸlatılmasından sonra, hata ayıklama modunda bir Linux ikili dosyası oluÅŸturulur. - -Åžu anda devcontainer, hata ayıklama ve sürüm modunda hem Linux hem de Android derlemeleri sunmaktadır. - -AÅŸağıda, belirli derlemeler oluÅŸturmak için projenin kökünden çalıştırılması gereken komutlar yer almaktadır. - -Komut | Derleme Türü | Mod --|-|- -`.devcontainer/build.sh --debug linux` | Linux | hata ayıklama -`.devcontainer/build.sh --release linux` | Linux | sürüm -`.devcontainer/build.sh --debug android` | Android-arm64 | hata ayıklama -`.devcontainer/build.sh --release android` | Android-arm64 | sürüm diff --git a/docs/DEVCONTAINER.md b/docs/DEVCONTAINER.md deleted file mode 100644 index 3d04fd3994e..00000000000 --- a/docs/DEVCONTAINER.md +++ /dev/null @@ -1,14 +0,0 @@ - -After the start of devcontainer in docker container, a linux binary in debug mode is created. - -Currently devcontainer offers linux and android builds in both debug and release mode. - -Below is the table on commands to run from root of the project for creating specific builds. - -Command|Build Type|Mode --|-|-| -`.devcontainer/build.sh --debug linux`|Linux|debug -`.devcontainer/build.sh --release linux`|Linux|release -`.devcontainer/build.sh --debug android`|android-arm64|debug -`.devcontainer/build.sh --release android`|android-arm64|release - diff --git a/docs/README-AR.md b/docs/README-AR.md index d6800dc6fc2..832ed5e8344 100644 --- a/docs/README-AR.md +++ b/docs/README-AR.md @@ -9,7 +9,7 @@ لغتك الأم, Doc Ùˆ RustDesk UI, README نحن بحاجة إلى مساعدتك لترجمة هذا

-[Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) :تواصل معنا عبر +[Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) :تواصل معنا عبر [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -27,6 +27,7 @@ [**BINARY تنزيل**](https://github.com/rustdesk/rustdesk/releases) + ## التبعيات لواجهة المستخدم الرسومية [sciter](https://sciter.com/) نسخة سطح المكتب تستخدم diff --git a/docs/README-CS.md b/docs/README-CS.md index 2a89997ce49..a00aa1a588f 100644 --- a/docs/README-CS.md +++ b/docs/README-CS.md @@ -9,7 +9,7 @@ Potřebujeme Vaši pomoc s překladem tohoto README, uživatelského rozhraní aplikace RustDesk a dokumentace k ní do vašeho jazyka

-Popovídejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Popovídejte si s námi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-DA.md b/docs/README-DA.md index 5ea61590963..2c6987053f3 100644 --- a/docs/README-DA.md +++ b/docs/README-DA.md @@ -9,13 +9,13 @@ Vi har brug for din hjælp til at oversætte denne README, RustDesk UI og Dokument til dit modersmål

-Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Chat med os: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) Endnu en fjernskrivebordssoftware, skrevet i Rust. Fungerer ud af æsken, ingen konfiguration påkrævet. Du har fuld kontrol over dine data uden bekymringer om sikkerhed. Du kan bruge vores rendezvous/relay-server, [opsætte din egen](https://rustdesk.com/server), eller [skrive din egen rendezvous/relay-server](https://github.com/rustdesk/rustdesk- server-demo). -RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) for at få hjælp til at komme i gang. +RustDesk hilser bidrag fra alle velkommen. Se [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) for at få hjælp til at komme i gang. [**PROGRAM DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) diff --git a/docs/README-DE.md b/docs/README-DE.md index 28e0bc19a1f..a4a5453c01d 100644 --- a/docs/README-DE.md +++ b/docs/README-DE.md @@ -9,7 +9,12 @@ Wir brauchen Ihre Hilfe, um dieses README, die RustDesk-Benutzeroberfläche und die Dokumentation in Ihre Muttersprache zu übersetzen.

-Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +> [!Vorsicht] +> **Haftungsausschluss bei Missbrauch::**
+> Die Entwickler von RustDesk billigen oder unterstützen keine unethische oder illegale Nutzung dieser Software. Missbrauch, wie unbefugter Zugriff, unbefugte Kontrolle oder Verletzung der Privatsphäre, verstößt strikt gegen unsere Richtlinien. Die Autoren sind nicht verantwortlich für jeglichen Missbrauch der Anwendung. + + +Reden Sie mit uns auf: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-EO.md b/docs/README-EO.md index de44d7a1600..e6bbd3ddec1 100644 --- a/docs/README-EO.md +++ b/docs/README-EO.md @@ -9,7 +9,7 @@ Ni bezonas helpon traduki tiun README kaj la interfacon al via denaska lingvo

-Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Babili kun ni: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-ES.md b/docs/README-ES.md index dea12c15253..607295bea02 100644 --- a/docs/README-ES.md +++ b/docs/README-ES.md @@ -9,12 +9,18 @@ Necesitamos tu ayuda para traducir este README a tu idioma

-Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +> [!Caution] +> **Descargo de responsabilidad por mal uso:**
+> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El mal uso, como el acceso no autorizado, el control o la invasión de la privacidad, va estrictamente en contra de nuestras directrices. Los autores no se hacen responsables de ningún uso indebido de la aplicación. + +Chatea con nosotros: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) Otro software de escritorio remoto, escrito en Rust. Funciona de forma inmediata, sin necesidad de configuración. Tienes el control total de tus datos, sin preocupaciones sobre la seguridad. Puedes utilizar nuestro servidor de rendezvous/relay, [instalar el tuyo](https://rustdesk.com/server), o [escribir tu propio servidor rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo). +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) para ayuda para empezar. [**¿Cómo funciona rustdesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) @@ -24,12 +30,15 @@ RustDesk agradece la contribución de todo el mundo. Lee [`docs/CONTRIBUTING.md` [Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) ## Dependencias -La versión Desktop usa [Sciter](https://sciter.com/) o Flutter para el GUI, este tutorial es solo para Sciter. +Las versiones de escritorio utilizan Flutter o Sciter (obsoleto) para GUI, este tutorial es sólo para Sciter, ya que es más fácil y más amigable para empezar. Echa un vistazo a nuestro [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) para la construcción de la versión Flutter. -Por favor descarga la librería dinámica de Sciter tu mismo. +Por favor descarga la librería dinámica de Sciter tú mismo. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | @@ -51,13 +60,21 @@ Por favor descarga la librería dinámica de Sciter tu mismo. ### Ubuntu 18 (Debian 10) ```sh -sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel ``` ### Fedora 28 (CentOS 8) ```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel ``` ### Arch (Manjaro) @@ -96,12 +113,12 @@ cd ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env -git clone https://github.com/rustdesk/rustdesk +git clone --recurse-submodules https://github.com/rustdesk/rustdesk cd rustdesk mkdir -p target/debug wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so mv libsciter-gtk.so target/debug -cargo run +VCPKG_ROOT=$HOME/vcpkg cargo run ``` ## Como compilar con Docker @@ -111,10 +128,11 @@ Empieza clonando el repositorio y compilando el contenedor de docker: ```sh git clone https://github.com/rustdesk/rustdesk cd rustdesk +git submodule update --init --recursive docker build -t "rustdesk-builder" . ``` -Entonces, cada vez que necesites compilar una modificación, ejecuta el siguiente comando: +Entonces, cada vez que necesites compilar la aplicación, ejecuta el siguiente comando: ```sh docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder @@ -147,12 +165,16 @@ Por favor, asegurate de que estás ejecutando estos comandos desde la raíz del - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter, código para moviles - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Javascript para el cliente web Flutter +> [!Precaución] +> **Descargo de responsabilidad por uso indebido:**
+> Los desarrolladores de RustDesk no aprueban ni apoyan ningún uso no ético o ilegal de este software. El uso indebido, como el acceso no autorizado, el control o la invasión de la privacidad, está estrictamente en contra de nuestras directrices. Los autores no son responsables de ningún uso indebido de la aplicación. + ## Capturas de pantalla -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) +![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) +![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) +![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) diff --git a/docs/README-FA.md b/docs/README-FA.md index ca060011b29..704775ffcaa 100644 --- a/docs/README-FA.md +++ b/docs/README-FA.md @@ -9,7 +9,7 @@

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

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

-با ما Ú¯ÙØªÚ¯Ùˆ کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) +با ما Ú¯ÙØªÚ¯Ùˆ کنید: [Reddit](https://www.reddit.com/r/rustdesk) | [Twitter](https://twitter.com/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-FI.md b/docs/README-FI.md index 2ba44394fcd..b0956c212ca 100644 --- a/docs/README-FI.md +++ b/docs/README-FI.md @@ -9,7 +9,7 @@ Tarvitsemme apua tämän README-tiedoston kääntämiseksi äidinkielellesi

-Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Juttele meidän kanssa: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-FR.md b/docs/README-FR.md index 1ad38ebc29f..39ff74f2004 100644 --- a/docs/README-FR.md +++ b/docs/README-FR.md @@ -9,7 +9,7 @@ Nous avons besoin de votre aide pour traduire ce README dans votre langue maternelle.

-Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Chattez avec nous : [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -137,6 +137,10 @@ Veuillez vous assurer que vous exécutez ces commandes à partir de la racine du - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)** : Communiquer avec [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attendre une connexion distante directe (TCP hole punching) ou relayée. - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)** : code spécifique à la plateforme +> [!Attention] +> **Avertissement contre l'utilisation abusive:**
+> Les développeurs de RustDesk ne cautionnent ni ne soutiennent aucune utilisation non éthique ou illégale de ce logiciel. Toute utilisation abusive, telle que l'accès non autorisé, le contrôle ou l'invasion de la vie privée, est strictement contraire à nos directives. Les auteurs ne sont pas responsables de toute utilisation abusive de l'application. + ## Images ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-GR.md b/docs/README-GR.md index 6fbb78fc84f..d834708ba6d 100644 --- a/docs/README-GR.md +++ b/docs/README-GR.md @@ -9,7 +9,7 @@ ΧÏειαζόμαστε τη βοήθειά σας για να μεταφÏάσουμε αυτό το αÏχείο README, το RustDesk UI και το Doc στη μητÏική σας γλώσσα

-Επικοινωνήστε μαζί μας μέσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Επικοινωνήστε μαζί μας μέσω: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -17,7 +17,7 @@ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -Το RustDesk ενθαÏÏÏνει τη συνεισφοÏά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε. +Το RustDesk ενθαÏÏÏνει τη συνεισφοÏά όλων. Διαβάστε το [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) για βοήθεια στο πως να ξεκινήσετε. [**Συχνές εÏωτήσεις**](https://github.com/rustdesk/rustdesk/wiki/FAQ) diff --git a/docs/README-HU.md b/docs/README-HU.md index 07c0fdc1d89..c0275f7f11d 100644 --- a/docs/README-HU.md +++ b/docs/README-HU.md @@ -9,7 +9,7 @@ Kell a segítséged, hogy lefordítsuk ezt a README-t, a RustDesk UI-t és a Dokumentációt az anyanyelvedre

-Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Beszélgess velünk: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-ID.md b/docs/README-ID.md index ae4bbfb75f1..b953b604ec7 100644 --- a/docs/README-ID.md +++ b/docs/README-ID.md @@ -9,7 +9,7 @@ Kami membutuhkan bantuanmu untuk menterjemahkan file README dan RustDesk UI ke Bahasa Indonesia

-Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-IT.md b/docs/README-IT.md index 4459f242352..7fac0e6e0ce 100644 --- a/docs/README-IT.md +++ b/docs/README-IT.md @@ -9,7 +9,7 @@ Abbiamo bisogno del tuo aiuto per tradurre questo file README e la UI RustDesk nella tua lingua nativa

-Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -164,6 +164,10 @@ Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altr - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: codice Flutter per desktop e mobile - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript per client web Flutter +> [!Attenzione] +> **Dichiarazione di non responsabilità per uso improprio:**
+> Gli sviluppatori di RustDesk non approvano né supportano alcun uso non etico o illegale di questo software. L'uso improprio, come l'accesso non autorizzato, il controllo o l'invasione della privacy, è strettamente contro le nostre linee guida. Gli autori non sono responsabili per qualsiasi uso improprio dell'applicazione. + ## Schermate ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-JP.md b/docs/README-JP.md index f1245fa05af..9beb259f2bb 100644 --- a/docs/README-JP.md +++ b/docs/README-JP.md @@ -5,11 +5,11 @@ Docker • Structure • Snapshot
- [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
+ [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe]
READMEã‚„RustDesk UI〠RustDesk Docã®ç¿»è¨³è€…を歓迎ã—ã¾ã™ï¼

-ç§ãŸã¡ã¨è©±ã™: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +ç§ãŸã¡ã¨è©±ã™: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -18,7 +18,7 @@ Rustã§æ›¸ã‹ã‚ŒãŸã€è¨­å®šä¸è¦ã§ã™ãã«ä½¿ãˆã‚‹ãƒªãƒ¢ãƒ¼ãƒˆãƒ‡ã‚¹ã‚¯ãƒˆ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) RustDeskã¯çš†ã•ã‚“ã®è²¢çŒ®ã‚’歓迎ã—ã¾ã™ã€‚ -è²¢çŒ®ã®æ–¹æ³•ã«ã¤ã„ã¦ã¯[CONTRIBUTING.md](docs/CONTRIBUTING.md)ã‚’ã”確èªãã ã•ã„。 +è²¢çŒ®ã®æ–¹æ³•ã«ã¤ã„ã¦ã¯[CONTRIBUTING.md](CONTRIBUTING.md)ã‚’ã”確èªãã ã•ã„。 [**よãã‚る質å•**](https://github.com/rustdesk/rustdesk/wiki/FAQ) @@ -168,6 +168,10 @@ target/release/rustdesk - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: デスクトップã¨ãƒ¢ãƒã‚¤ãƒ«å‘ã‘ã®Flutterコード - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutterウェブクライアントå‘ã‘ã®JavaScript +> [!注æ„] +> **:䏿­£ä½¿ç”¨ã«é–¢ã™ã‚‹å…責事項**
+> RustDeskã®é–‹ç™ºè€…ã¯ã€ã“ã®ã‚½ãƒ•トウェアã®éžå€«ç†çš„ã¾ãŸã¯é•法ãªä½¿ç”¨ã‚’容èªã¾ãŸã¯æ”¯æŒã—ã¾ã›ã‚“ã€‚ä¸æ­£ã‚¢ã‚¯ã‚»ã‚¹ã€ä¸æ­£ãªåˆ¶å¾¡ã€ã¾ãŸã¯ãƒ—ライãƒã‚·ãƒ¼ã®ä¾µå®³ãªã©ã®ä¸æ­£ä½¿ç”¨ã¯ã€å½“社ã®ã‚¬ã‚¤ãƒ‰ãƒ©ã‚¤ãƒ³ã«å޳坆ã«é•åã—ã¾ã™ã€‚開発者ã¯ã€ã‚¢ãƒ—リケーションã®ä¸æ­£ä½¿ç”¨ã«å¯¾ã—ã¦ä¸€åˆ‡ã®è²¬ä»»ã‚’è² ã„ã¾ã›ã‚“。 + ## スクリーンショット ![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) diff --git a/docs/README-KR.md b/docs/README-KR.md index b0a8b973e2a..d2123982225 100644 --- a/docs/README-KR.md +++ b/docs/README-KR.md @@ -1,64 +1,84 @@

RustDesk - Your remote desktop
- Servers • - Build • + 빌드 • Docker • - Structure • - Snapshot
- [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [العربي] | [Tiếng Việt] | [Ελληνικά]
- README를 모국어로 번역하기 위한 ë‹¹ì‹ ì˜ ë„ì›€ì˜ í•„ìš”í•©ë‹ˆë‹¤. + 구조 • + 스냇샷
+ [English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
+ ì´ README, RustDesk UI ë° RustDesk 문서를 ê·€í•˜ì˜ ëª¨êµ­ì–´ë¡œ 번역하는 ë° ë„ì›€ì´ í•„ìš”í•©ë‹ˆë‹¤

-채팅하기: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +> [!Caution] +> **오용 면책 조항:**
+> RustDeskì˜ ê°œë°œìžëŠ” ì´ ì†Œí”„íŠ¸ì›¨ì–´ì˜ ë¹„ìœ¤ë¦¬ì  ë˜ëŠ” 불법ì ì¸ ì‚¬ìš©ì„ ë¬µì¸í•˜ê±°ë‚˜ ì§€ì›í•˜ì§€ 않습니다. 무단 액세스, 제어 ë˜ëŠ” ê°œì¸ì •ë³´ 침해와 ê°™ì€ ì˜¤ìš©ì€ ì—„ê²©í•˜ê²Œ ë‹¹ì‚¬ì˜ ì§€ì¹¨ì— ìœ„ë°°ë©ë‹ˆë‹¤. 작성ìžëŠ” ì‘ìš© í”„ë¡œê·¸ëž¨ì˜ ì˜¤ìš©ì— ëŒ€í•´ ì±…ìž„ì„ ì§€ì§€ 않습니다. +우리와 채팅: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Rust로 작성ë˜ì—ˆê³ , ì„¤ì •ì—†ì´ ë°”ë¡œ 사용할 수 있는 ì›ê²© ë°ìŠ¤íŠ¸íƒ‘ 소프트웨어입니다. ìžì‹ ì˜ ë°ì´í„°ë¥¼ 완전히 컨트롤할 수 있고, ë³´ì•ˆì˜ ì—¼ë ¤ë„ ì—†ìŠµë‹ˆë‹¤. ìš°ë¦¬ì˜ rendezvous/relay 서버를 사용해ë„, [ì§ì ‘ 설정](https://rustdesk.com/server)하거나 [ì§ì ‘ rendezvous/relay 서버를 작성할 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤](https://github.com/rustdesk/rustdesk-server-demo). +Rust로 ìž‘ì„±ëœ ë˜ ë‹¤ë¥¸ ì›ê²© ë°ìФí¬í†± 소프트웨어입니다. 구성할 í•„ìš” ì—†ì´ ë°”ë¡œ 사용할 수 있습니다. ë³´ì•ˆì— ëŒ€í•œ 걱정 ì—†ì´ ë°ì´í„°ë¥¼ 완벽하게 제어할 수 있습니다. ì €í¬ì˜ rendezvous/relay server 서버를 사용하거나, [ì§ì ‘ 설정](https://rustdesk.com/server), ë˜ëŠ” [ì§ì ‘ rendezvous/relay 서버를 작성할 수 있습니다](https://github.com/rustdesk/rustdesk-server-demo). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk는 모든 기여를 환ì˜í•©ë‹ˆë‹¤. ê¸°ì—¬í•˜ê³ ìž í•œë‹¤ë©´ [`docs/CONTRIBUTING.md`](CONTRIBUTING.md)를 참조해주세요. +RustDesk는 모든 ë¶„ë“¤ì˜ ê¸°ì—¬ë¥¼ 환ì˜í•©ë‹ˆë‹¤. 시작하는 ë° ë„ì›€ì´ í•„ìš”í•˜ë©´ [CONTRIBUTING-KR.md](CONTRIBUTING-KR.md)를 참조하세요. + +[**ìžì£¼ 묻는 질문**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**ë°”ì´ë„ˆë¦¬ 다운로드**](https://github.com/rustdesk/rustdesk/releases) -[**RustDesk는 어떻게 ìž‘ë™í•˜ëŠ”ê°€?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[**ê°œë°œìž ë¹Œë“œ**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) -[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases) +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) -## ì˜ì¡´ê´€ê³„ +## 종ì†ì„± -ë°ìФí¬íƒ‘íŒì—는 GUIì— [sciter](https://sciter.com/)ê°€ 사용ë˜ì—ˆìŠµë‹ˆë‹¤. sciter dynamic library 를 다운로드해주세요. +ë°ìФí¬í†± ë²„ì „ì€ GUI로 Flutter ë˜ëŠ” Sciter (ë” ì´ìƒ ì§€ì›ë˜ì§€ 않ìŒ)를 사용하며, ì´ ìžìŠµì„œëŠ” 시작하기 ë” ì‰½ê³  친숙한 Sciter 전용입니다. Flutter 버전 빌드는 [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml)ì„ í™•ì¸í•˜ì„¸ìš”. + +Sciter ë™ì  ë¼ì´ë¸ŒëŸ¬ë¦¬ë¥¼ ì§ì ‘ 다운로드하세요. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | -[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) - -ëª¨ë°”ì¼ ë²„ì „ì€ Flutter를 사용합니다. ë°ìФí¬íƒ‘ ë˜í•œ Sciterì—서 Flutter로 마ì´ê·¸ë ˆì´ì…˜í•  예정입니다. +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -## 빌드 순서 +## 빌드를 위한 ì›ì‹œ 단계 -- Rust 개발환경, C++ 빌드 í™˜ê²½ì„ ì¤€ë¹„í•©ë‹ˆë‹¤. +- Rust 개발 환경과 C++ 빌드 í™˜ê²½ì„ ì¤€ë¹„í•©ë‹ˆë‹¤ -- [vcpkg](https://github.com/microsoft/vcpkg) 설치하고 `VCPKG_ROOT` 환경변수를 정확히 설정합니다. +- [vcpkg](https://github.com/microsoft/vcpkg)를 설치하고 `VCPKG_ROOT` 환경 변수를 올바르게 설정합니다 - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static - - Linux/MacOS: vcpkg install libvpx libyuv opus aom + - Linux/macOS: vcpkg install libvpx libyuv opus aom -- 실행 `cargo run` +- `cargo run` 실행 ## [빌드](https://rustdesk.com/docs/en/dev/build/) -## Linuxì—서 빌드 순서 +## Linuxì—서 빌드하는 방법 ### Ubuntu 18 (Debian 10) ```sh -sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel ``` ### Fedora 28 (CentOS 8) ```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel ``` ### Arch (Manjaro) @@ -79,7 +99,7 @@ export VCPKG_ROOT=$HOME/vcpkg vcpkg/vcpkg install libvpx libyuv opus aom ``` -### libvpx 수정 (For Fedoraìš©) +### libvpx 수정 (Fedoraìš©) ```sh cd vcpkg/buildtrees/libvpx/src @@ -97,7 +117,7 @@ cd ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env -git clone https://github.com/rustdesk/rustdesk +git clone --recurse-submodules https://github.com/rustdesk/rustdesk cd rustdesk mkdir -p target/debug wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so @@ -105,56 +125,58 @@ mv libsciter-gtk.so target/debug VCPKG_ROOT=$HOME/vcpkg cargo run ``` -## Dockerì— ë¹Œë“œí•˜ëŠ” 방법 +## Docker로 빌드하는 방법 -리í¬ì§€í† ë¦¬ë¥¼ í´ë¡ í•˜ê³ , Docker 컨테ì´ë„ˆ 구성하는 것으로 시작합니다. +먼저 리í¬ì§€í† ë¦¬ë¥¼ 복제하고 Docker 컨테ì´ë„ˆë¥¼ 빌드합니다: ```sh git clone https://github.com/rustdesk/rustdesk cd rustdesk +git submodule update --init --recursive 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 ``` -í˜¹ì€ ì¶œì‹œìš© 실행 파ì¼ì„ 실행할 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤. +ë˜ëŠ” 릴리스 실행 파ì¼ì„ 실행하는 경우: ```sh target/release/rustdesk ``` -ëª…ë ¹ì„ RustDesk 리í¬ì§€í† ë¦¬ 루트ì—서 실행한다는 ê²ƒì„ í™•ì¸í•´ì£¼ì„¸ìš”. 그렇게 하지 않으면 애플리케ì´ì…˜ì´ 필요한 리소스를 발견하지 못 í•  ê°€ëŠ¥ì„±ì´ ìžˆìŠµë‹ˆë‹¤. ë˜í•œ `install`, `run` ê°™ì€ cargo 하위 ëª…ë ¹ì€ í˜¸ìŠ¤íŠ¸ê°€ ì•„ë‹ˆë¼ ì»¨í…Œì´ë„ˆ í”„ë¡œê·¸ëž¨ì„ ì„¤ì¹˜, ì‹¤í–‰ì„ ìœ„í•¨ì´ë¯€ë¡œ 현재 ì´ ë°©ë²•ì€ ì§€ì›í•˜ì§€ 않다는 ì ì„ 유ë…해주시길 ë°”ëžë‹ˆë‹¤. +RustDesk 리í¬ì§€í† ë¦¬ì˜ 루트ì—서 ì´ëŸ¬í•œ ëª…ë ¹ì„ ì‹¤í–‰í•˜ê³  있는지 확ì¸í•˜ì„¸ìš”. 그렇지 않으면 ì‘ìš© í”„ë¡œê·¸ëž¨ì´ í•„ìš”í•œ 리소스를 찾지 못할 수 있습니다. ë˜í•œ `install` ë˜ëŠ” `run` ê³¼ ê°™ì€ ë‹¤ë¥¸ cargo 하위 ëª…ë ¹ì€ í˜¸ìŠ¤íŠ¸ê°€ 아닌 컨테ì´ë„ˆ ë‚´ë¶€ì— í”„ë¡œê·¸ëž¨ì„ ì„¤ì¹˜í•˜ê±°ë‚˜ 실행하므로 현재 ì´ ë°©ë²•ì„ í†µí•´ ì§€ì›ë˜ì§€ 않는다는 ì ì— 유ì˜í•˜ì„¸ìš”. ## íŒŒì¼ êµ¬ì¡° -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 ì½”ë±, 설정, tcp/udp ëž©í¼, protobuf, íŒŒì¼ ì „ì†¡ì„ ìœ„í•œ fs 함수, ê·¸ 외 유틸리티 함수 -- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 캡처 -- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: í”Œëž«í¼ ê³ ìœ  키보드/마우스 컨트롤 -- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오, í´ë¦½ë³´ë“œ, ìž…ë ¥, 비디오 서비스 그리고 ë„¤íŠ¸ì›Œí¬ ì—°ê²° -- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 ì ‘ì† ì‹œìž‘ -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신해서 리모트 다ì´ë ‰íЏ (TCP hole punching) í˜¹ì€ relayed ì ‘ì† -- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: í”Œëž«í¼ ê³ ìœ ì˜ ì½”ë“œ -- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: 모바ì¼ìš© Flutter 코드 -- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter 웹 í´ë¼ì´ì–¸íŠ¸ìš© ìžë°”스í¬ë¦½íЏ +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: 비디오 ì½”ë±, 구성, tcp/udp wrapper, protobuf, íŒŒì¼ ì „ì†¡ì„ ìœ„í•œ fs 함수 ë° ê¸°íƒ€ 유틸리티 함수 +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: 화면 ìº¡ì³ +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: 플랫í¼ë³„ 키보드/마우스 제어 +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: Windows, Linux, macOSìš© íŒŒì¼ ë³µì‚¬ ë° ë¶™ì—¬ë„£ê¸° 구현 +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: ë” ì´ìƒ 사용ë˜ì§€ 않는 Sciter UI (ì§€ì› ì¤‘ë‹¨) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: 오디오/í´ë¦½ë³´ë“œ/ìž…ë ¥/비디오 서비스 ë° ë„¤íŠ¸ì›Œí¬ ì—°ê²° +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: 피어 ì—°ê²° 시작 +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server)와 통신, ì›ê²© 다ì´ë ‰íЏ (TCP 홀 펀칭) ë˜ëŠ” ë¦´ë ˆì´ ì—°ê²° 대기 +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: 플랫í¼ë³„ 코드 +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: ë°ìФí¬í†± ë° ëª¨ë°”ì¼ìš© Flutter 코드 +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: Flutter 웹 í´ë¼ì´ì–¸íŠ¸ìš© JavaScript -## 스냅샷 +## 스í¬ë¦°ìƒ· -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) +![Connection Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) +![Connected to a Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) +![File Transfer](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) diff --git a/docs/README-ML.md b/docs/README-ML.md index 4e92bf73643..13b74ff0282 100644 --- a/docs/README-ML.md +++ b/docs/README-ML.md @@ -9,7 +9,7 @@ à´ˆ README നിങàµà´™à´³àµà´Ÿàµ† മാതൃഭാഷയിലേകàµà´•ൠവിവർതàµà´¤à´¨à´‚ ചെയàµà´¯à´¾àµ» à´žà´™àµà´™àµ¾à´•àµà´•ൠനിങàµà´™à´³àµà´Ÿàµ† സഹായം ആവശàµà´¯à´®à´¾à´£àµ

-à´žà´™àµà´™à´³àµà´®à´¾à´¯à´¿ ചാറàµà´±àµ ചെയàµà´¯àµà´•: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +à´žà´™àµà´™à´³àµà´®à´¾à´¯à´¿ ചാറàµà´±àµ ചെയàµà´¯àµà´•: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-NL.md b/docs/README-NL.md index 44fedd5afc4..cb5f5b3434a 100644 --- a/docs/README-NL.md +++ b/docs/README-NL.md @@ -9,7 +9,7 @@ Wij hebben uw hulp nodig om dit README bestand te vertalen, RustDesk UI en Doc naar uw moedertaal

-Chat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Chat met ons: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-NO.md b/docs/README-NO.md new file mode 100644 index 00000000000..e77dcf8539b --- /dev/null +++ b/docs/README-NO.md @@ -0,0 +1,177 @@ +

+ RustDesk - Your remote desktop
+ Servere • + Build • + Docker • + Struktur • + Snapshot
+ [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Dansk] | [Ελληνικά] | [Türkçe] | [Norsk
+ Vi trenger din hjelp til å oversette denne README-en, RustDesk UI og RustDesk Doc tid ditt morsmål +

+ +Snakk med oss: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) + +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) + +Enda en annen fjernstyrt desktop programvare, skrevet i Rust. Virker rett ut av pakken, ingen konfigurasjon nødvendig. Du har full kontroll over din data, uten beskymring for sikkerhet. Du kan bruke vår rendezvous_mediator/relay server, [sett opp din egen](https://rustdesk.com/server), eller [skriv din egen rendezvous_mediator/relay server](https://github.com/rustdesk/rustdesk-server-demo). + +![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) + +RustDesk er velkommen for bidrag fra alle. Se [CONTRIBUTING.md](CONTRIBUTING-NO.md) for hjelp med oppstart. + +[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ) + +[**BINARY NEDLASTING**](https://github.com/rustdesk/rustdesk/releases) + +[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) + +[Få det på F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Få det på Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) + +## Avhengigheter + +Desktop versjoner bruker Flutter eller Sciter (avviklet) for GUI, denne veiledningen er bare for Sciter, grunnet att det er letter og en mer venlig start. Skjekk ut vår [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) for bygging av Flutter versjonen. + +Venligst last ned Sciters dynamiske bibliotek selv. + +[Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | +[Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) + +## Rå steg for bygging + +- Klargjør ditt Rust development env og C++ build env + +- Installer [vcpkg](https://github.com/microsoft/vcpkg), og koriger `VCPKG_ROOT` env vaiabelen + + - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- Kjør `cargo run` + +## [Bygg](https://rustdesk.com/docs/en/dev/build/) + +## Hvordan Bygge til Linux + +### Ubuntu 18 (Debian 10) + +```sh +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel +``` + +### Fedora 28 (CentOS 8) + +```sh +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel +``` + +### Arch (Manjaro) + +```sh +sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire +``` + +### Installer vcpkg + +```sh +git clone https://github.com/microsoft/vcpkg +cd vcpkg +git checkout 2023.04.15 +cd .. +vcpkg/bootstrap-vcpkg.sh +export VCPKG_ROOT=$HOME/vcpkg +vcpkg/vcpkg install libvpx libyuv opus aom +``` + +### Fiks libvpx (For Fedora) + +```sh +cd vcpkg/buildtrees/libvpx/src +cd * +./configure +sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile +sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile +make +cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/ +cd +``` + +### Bygg + +```sh +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +source $HOME/.cargo/env +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +mkdir -p target/debug +wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so +mv libsciter-gtk.so target/debug +VCPKG_ROOT=$HOME/vcpkg cargo run +``` + +## Hvordan bygge med Docker + +Start med å klone repositoret og bygg Docker konteineren: + +```sh +git clone https://github.com/rustdesk/rustdesk +cd rustdesk +docker build -t "rustdesk-builder" . +``` + +Deretter, hver gang du trenger å bygge applikasjonen, kjør følgene kommando: + +```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 +``` + +Det kan ta lengere tid før avhengighetene blir bufret første gang du bygger, senere bygg er raskere. Hvis du trenger å spesifisere forkjellige argumenter til bygge kommandoen, kan du gjøre det på slutten av kommandoen ved `` feltet. For eksempel, hvis du ville bygge en optimalisert release versjon, ville du kjørt kommandoen over fulgt `--release`. Den kjørbare filen vill være tilgjengelig i mål direktive på ditt system, og kan bli kjørt med: + +```sh +target/debug/rustdesk +``` + +Eller, hvis du kjører ett release program: + +```sh +target/release/rustdesk +``` + +Venligst pass på att du kjører disse kommandoene fra roten av RustDesk repositoret, eller kan det hende att applikasjon ikke finner de riktige ressursene. Pass også på att andre cargo subkommandoer som for eksempel `install` eller `run` ikke støttes med denne metoden da de vill installere eller kjøre programmet i konteineren istedet for verten. + +## Fil Struktur + +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodek, configurasjon, tcp/udp innpakning, protobuf, fs funksjon for fil overføring, og noen andre verktøy funksjoner +- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: skjermfangst +- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platform spesefik keyboard/mus kontroll +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: fil kopi og innliming implementasjon for Windows, Linux, macOS. +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: foreldret Sciter UI (avviklet) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: lyd/utklippstavle/input/video tjenester, og internett tilkobling +- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: start en peer tilkobling +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Kommunikasjon med [rustdesk-server](https://github.com/rustdesk/rustdesk-server), vent på direkte fjernstyring (TCP hulling) eller vidresendt tilkobling +- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platform spesefik kode +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: Flutter kode for desktop og mobil +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: JavaScript for Flutter nettsted klient + +## Skjermbilder + +![Tilkoblings Manager](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) + +![Koble til Windows PC](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) + +![Fil Overføring](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) + +![TCP Tunneling](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) + diff --git a/docs/README-PL.md b/docs/README-PL.md index 295564457b8..a68d87dfc24 100644 --- a/docs/README-PL.md +++ b/docs/README-PL.md @@ -9,7 +9,7 @@ Potrzebujemy twojej pomocy w tłumaczeniu README na twój ojczysty język

-Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Porozmawiaj z nami na: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -165,6 +165,3 @@ Upewnij się, że uruchamiasz te polecenia z katalogu głównego repozytorium Ru ![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) -## [Serwery publiczne](#public-servers) - -RustDesk jest obsługiwany przez bezpłatne serwer w Unii Europejskiej, uprzejmie dostarczony przez [Codext GmbH](https://codext.link/rustdesk?utm_source=github) diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md index 64c5ae001de..ff1e8f7ef89 100644 --- a/docs/README-PTBR.md +++ b/docs/README-PTBR.md @@ -9,7 +9,7 @@ Precisamos de sua ajuda para traduzir este README e a UI do RustDesk para sua língua nativa

-Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Converse conosco: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -137,6 +137,10 @@ Por favor verifique que está executando estes comandos da raiz do repositório - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunicação com [rustdesk-server](https://github.com/rustdesk/rustdesk-server), aguardar pela conexão remota direta (TCP hole punching) ou conexão indireta (relayed) - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: código específico a cada plataforma +> [!Cuidadob] +> **Aviso de uso indevido:**
+> Os desenvolvedores do RustDesk não aprovam nem apoiam qualquer uso antiético ou ilegal deste software. O uso indevido, como acesso não autorizado, controle ou invasão de privacidade, é estritamente contra nossas diretrizes. Os autores não são responsáveis por qualquer uso indevido da aplicação. + ## Screenshots ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-RU.md b/docs/README-RU.md index 60972efd355..d6aaaf2cb79 100644 --- a/docs/README-RU.md +++ b/docs/README-RU.md @@ -1,42 +1,52 @@

RustDesk - Ваш удаленый рабочий Ñтол
- Servers • - Build • - Docker • - Structure • - Snapshot
+ Первичные шаги Ð´Ð»Ñ Ñборки • + Как Ñобрать Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ Docker • + Структура файлов • + Скриншоты
[English] | [УкраїнÑька] | [Äesky] | [中文] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]
- Ðам нужна ваша помощь Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ²Ð¾Ð´Ð° Ñтого README RustDesk UI - и документацию RustDesk на ваш родной Ñзык. RustDesk Doc + Ðам нужна ваша помощь в переводе Ñтого README, интерфейÑа RustDesk + и документации RustDesk на ваш родной Ñзык.

-Общение Ñ Ð½Ð°Ð¼Ð¸: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +> [!Caution] +> **Отказ от ответÑтвенноÑти за неправомерное иÑпользование**
+> Разработчики RustDesk не одобрÑÑŽÑ‚ и не поддерживают какое-либо неÑтичное или незаконное иÑпользование данного программного обеÑпечениÑ. Ðеправомерное иÑпользование (неÑанкционированный доÑтуп, контроль или вторжение в чаÑтную жизнь) Ñтрого противоречит нашим правилам. Ðвторы не неÑут ответÑтвенноÑти за любое неправомерное иÑпользование приложениÑ. + +Общение Ñ Ð½Ð°Ð¼Ð¸: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) -Еще одно программное обеÑпечение Ð´Ð»Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð½Ð¾Ð³Ð¾ рабочего Ñтола, напиÑанное на Rust. Работает из коробки, не требует наÑтройки. Ð’Ñ‹ полноÑтью контролируете Ñвои данные, не беÑпокоÑÑÑŒ о безопаÑноÑти. Ð’Ñ‹ можете иÑпользовать наш Ñервер ретранÑлÑции, [наÑтроить Ñвой ÑобÑтвенный](https://rustdesk.com/server), или [напиÑать Ñвой](https://github.com/rustdesk/rustdesk-server-demo). +Ещё одно программное обеÑпечение Ð´Ð»Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð½Ð¾Ð³Ð¾ рабочего Ñтола, напиÑанное на Rust. Работает из коробки, наÑтройки не требует. Ð’Ñ‹ полноÑтью контролируете Ñвои данные, не беÑпокоÑÑÑŒ о безопаÑноÑти. Ð’Ñ‹ можете иÑпользовать наш Ñервер ретранÑлÑции, [наÑтроить Ñвой ÑобÑтвенный](https://rustdesk.com/server), или [напиÑать Ñвой](https://github.com/rustdesk/rustdesk-server-demo). ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) RustDesk приветÑтвует вклад каждого. ОзнакомьтеÑÑŒ Ñ [`docs/CONTRIBUTING-RU.md`](CONTRIBUTING-RU.md) в начале работы Ð´Ð»Ñ Ð¿Ð¾Ð½Ð¸Ð¼Ð°Ð½Ð¸Ñ. -[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) +[**Как работает RustDesk?**](https://github.com/rustdesk/rustdesk/wiki/How-does-RustDesk-work%3F) (Ð”Ð¾ÐºÑƒÐ¼ÐµÐ½Ñ‚Ð°Ñ†Ð¸Ñ Ð½Ð° английÑком Ñзыке) + +[**ЧаÑто задаваемые вопроÑÑ‹**](https://github.com/rustdesk/rustdesk/wiki/FAQ) (Страница на английÑком Ñзыке) [**СКÐЧÐТЬ ПРИЛОЖЕÐИЕ**](https://github.com/rustdesk/rustdesk/releases) -[**ночные Ñборки (актуальные)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) +[**ÐОЧÐЫЕ СБОРКИ (Ðктуальные)**](https://github.com/rustdesk/rustdesk/releases/tag/nightly) -[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on F-Droid](https://f-droid.org/en/packages/com.carriez.flutter_hbb) +[Get it on Flathub](https://flathub.org/apps/com.rustdesk.RustDesk) ## ЗавиÑимоÑти -ÐаÑтольные верÑии иÑпользуют [sciter](https://sciter.com/) Ð´Ð»Ñ Ð³Ñ€Ð°Ñ„Ð¸Ñ‡ÐµÑкого интерфейÑа, загрузите динамичеÑкую библиотеку sciter ÑамоÑтоÑтельно. +Ð”Ð»Ñ ÐŸÐš-верÑии иÑпользуютÑÑ Ð±Ð¸Ð±Ð»Ð¸Ð¾Ñ‚ÐµÐºÐ¸ Flutter или Sciter (уÑтаревшее) Ð´Ð»Ñ Ð³Ñ€Ð°Ñ„Ð¸Ñ‡ÐµÑкого интерфейÑа. Данное руководÑтво подразумевает работу Ñ Sciter, так как он более проÑтой в иÑпользовании и Ñ Ð½Ð¸Ð¼ легче начать работу. Ð’Ñ‹ можете также поÑмотреть на механизм нашего [CI](https://github.com/rustdesk/rustdesk/blob/master/.github/workflows/flutter-build.yml) Ð´Ð»Ñ Ñборок на Flutter. + +Загрузите динамичеÑкую библиотеку Flutter ÑамоÑтоÑтельно. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | -[MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) - -Мобильные верÑии иÑпользуют Flutter. Ð’ будущем мы перенеÑем наÑтольную верÑию Ñо Sciter на Flutter. +[macOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) ## Первичные шаги Ð´Ð»Ñ Ñборки @@ -45,22 +55,32 @@ RustDesk приветÑтвует вклад каждого. Ознакомьт - УÑтановите [vcpkg](https://github.com/microsoft/vcpkg), и правильно уÑтановите переменную `VCPKG_ROOT` - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static - - Linux/MacOS: vcpkg install libvpx libyuv opus aom + - Linux/macOS: vcpkg install libvpx libyuv opus aom + +- Выполните команду `cargo run` -- ЗапуÑтите `cargo run` +## [Сборка](https://rustdesk.com/docs/ru/dev/build/) ## Как Ñобрать на Linux ### Ubuntu 18 (Debian 10) ```sh -sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake +sudo apt install -y zip g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev \ + libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake make \ + libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libpam0g-dev +``` + +### openSUSE Tumbleweed + +```sh +sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel pam-devel ``` ### Fedora 28 (CentOS 8) ```sh -sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel +sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel gstreamer1-devel gstreamer1-plugins-base-devel pam-devel ``` ### Arch (Manjaro) @@ -99,7 +119,7 @@ cd ```sh curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env -git clone https://github.com/rustdesk/rustdesk +git clone --recurse-submodules https://github.com/rustdesk/rustdesk cd rustdesk mkdir -p target/debug wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so @@ -114,16 +134,17 @@ VCPKG_ROOT=$HOME/vcpkg cargo run ```sh git clone https://github.com/rustdesk/rustdesk cd rustdesk +git submodule update --init --recursive 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 @@ -135,25 +156,28 @@ target/debug/rustdesk target/release/rustdesk ``` -ПожалуйÑта, убедитеÑÑŒ, что вы запуÑкаете Ñти команды из ÐºÐ¾Ñ€Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ RustDesk, иначе приложение не Ñможет найти необходимые реÑурÑÑ‹. Также обратите внимание, что другие cargo подкоманды, такие как `install` или `run`, в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½Ðµ поддерживаютÑÑ Ñтим методом, поÑкольку они будут уÑтанавливать или запуÑкать программу внутри контейнера, а не на хоÑте. +ПожалуйÑта, убедитеÑÑŒ, что вы запуÑкаете Ñти команды из ÐºÐ¾Ñ€Ð½Ñ Ñ€ÐµÐ¿Ð¾Ð·Ð¸Ñ‚Ð¾Ñ€Ð¸Ñ RustDesk, иначе приложение не Ñможет найти необходимые реÑурÑÑ‹. Также обратите внимание, что другие подкоманды Cargo, такие как `install` или `run`, в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½Ðµ поддерживаютÑÑ Ñтим методом, поÑкольку они будут уÑтанавливать или запуÑкать программу внутри контейнера, а не на хоÑте. ## Структура файлов -- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: видеокодек, конфиг, обертка tcp/udp, protobuf, функции fs Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‡Ð¸ файлов и некоторые другие Ñлужебные функции +- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: видеокодек, конфигурациÑ, враппер TCP/UDP, protobuf, функции файловой ÑиÑтемы Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ´Ð°Ñ‡Ð¸ файлов и некоторые другие Ñлужебные функции - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: захват Ñкрана - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: Ñпецифичное Ð´Ð»Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ñ‹ управление клавиатурой/мышью -- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графичеÑкий пользовательÑкий Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ -- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ÑервиÑÑ‹ аудио/буфера обмена/ввода/видео и Ñетевых подключений +- **[libs/clipboard](https://github.com/rustdesk/rustdesk/tree/master/libs/clipboard)**: функционал буфера обмена файлами Ð´Ð»Ñ Windows, Linux, и macOS +- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: графичеÑкий пользовательÑкий Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð½Ð° Sciter (уÑтаревшее) +- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ÑервиÑÑ‹ аудио, буфера обмена, ввода, видео и Ñетевых подключений - **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: одноранговое Ñоединение -- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: ÑвÑжитеÑÑŒ Ñ [rustdesk-server](https://github.com/rustdesk/rustdesk-server), дождитеÑÑŒ удаленного прÑмого (обход TCP NAT) или ретранÑлируемого ÑÐ¾ÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ +- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: ÑвÑзь Ñ [Ñервером Rustdesk](https://github.com/rustdesk/rustdesk-server), ожидает удаленного прÑмого (через TCP hole punching) или ретранÑлируемого ÑÐ¾ÐµÐ´Ð¸Ð½ÐµÐ½Ð¸Ñ - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: Ñпецифичный Ð´Ð»Ñ Ð¿Ð»Ð°Ñ‚Ñ„Ð¾Ñ€Ð¼Ñ‹ код +- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: код Flutter Ð´Ð»Ñ ÐŸÐš-верÑии и мобильных уÑтройÑтв +- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/v1/js)**: JavaScript Ð´Ð»Ñ Web-клиента Flutter ## Скриншоты -![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) +![Менеджер Ñоединений](https://github.com/rustdesk/rustdesk/assets/28412477/db82d4e7-c4bc-4823-8e6f-6af7eadf7651) -![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png) +![Подключение к удалённому рабочему Ñтолу на Windows](https://github.com/rustdesk/rustdesk/assets/28412477/9baa91e9-3362-4d06-aa1a-7518edcbd7ea) -![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png) +![Передача файлов](https://github.com/rustdesk/rustdesk/assets/28412477/39511ad3-aa9a-4f8c-8947-1cce286a46ad) -![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png) +![TCP-туннелирование](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) \ No newline at end of file diff --git a/docs/README-TR.md b/docs/README-TR.md index 3b4b34edd7c..d9481b2c745 100644 --- a/docs/README-TR.md +++ b/docs/README-TR.md @@ -10,7 +10,7 @@ README, RustDesk UI ve RustDesk Belge'sini ana dilinize çevirmemiz için yardımınıza ihtiyacımız var

-Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -18,7 +18,7 @@ Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kul ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın. +RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](CONTRIBUTING-TR.md) belgesine göz atın. [**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ) @@ -166,6 +166,10 @@ Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan e - **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu - **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript +> [!Dikkat] +> **Yanlış Kullanım Uyarısı:**
+> RustDesk geliÅŸtiricileri, bu yazılımın etik olmayan veya yasa dışı kullanımını onaylamaz veya desteklemez. Yetkisiz eriÅŸim, kontrol veya gizlilik ihlali gibi kötüye kullanımlar kesinlikle yönergelerimize aykırıdır. Yazarlar, uygulamanın herhangi bir yanlış kullanımından sorumlu deÄŸildir. + ## Ekran Görüntüleri ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png) diff --git a/docs/README-UA.md b/docs/README-UA.md index 8f226914d70..fb880749423 100644 --- a/docs/README-UA.md +++ b/docs/README-UA.md @@ -9,7 +9,7 @@ Ðам потрібна ваша допомога Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐºÐ»Ð°Ð´Ñƒ цього README, інтерфейÑу та документації RustDesk вашою рідною мовою

-Ð¡Ð¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð· нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Ð¡Ð¿Ñ–Ð»ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ð· нами: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) @@ -17,7 +17,7 @@ ![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png) -RustDesk вітає внеÑок кожного. ОзнайомтеÑÑ Ð· [CONTRIBUTING.md](docs/CONTRIBUTING.md), щоб отримати допомогу на початковому етапі. +RustDesk вітає внеÑок кожного. ОзнайомтеÑÑ Ð· [CONTRIBUTING.md](CONTRIBUTING.md), щоб отримати допомогу на початковому етапі. [**ЧаПи**](https://github.com/rustdesk/rustdesk/wiki/FAQ) @@ -172,6 +172,3 @@ target/release/rustdesk ![Ð¢ÑƒÐ½ÐµÐ»ÑŽÐ²Ð°Ð½Ð½Ñ TCP](https://github.com/rustdesk/rustdesk/assets/28412477/78e8708f-e87e-4570-8373-1360033ea6c5) -## [Публічні Ñервери](#публічні-Ñервери) - -RustDesk підтримуєтьÑÑ Ð±ÐµÐ·ÐºÐ¾ÑˆÑ‚Ð¾Ð²Ð½Ð¸Ð¼ європейÑьким Ñервером, любʼÑзно наданим [Codext GmbH](https://codext.link/rustdesk?utm_source=github) diff --git a/docs/README-VN.md b/docs/README-VN.md index 9c8ebcf237a..db83aff1cb1 100644 --- a/docs/README-VN.md +++ b/docs/README-VN.md @@ -11,7 +11,7 @@ Chúng tôi rất hoan nghênh sá»± há»— trợ cá»§a bạn trong việc dịch trang README, trang giao diện ngưá»i dùng cá»§a RustDesk - RustDesk UI và trang tài liệu cá»§a RustDesk - RustDesk Doc sang Tiếng Việt

-Hãy trao đổi vá»›i chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) +Hãy trao đổi vá»›i chúng tôi qua: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/README-ZH.md b/docs/README-ZH.md index 4920ade6d96..2899aa01bf7 100644 --- a/docs/README-ZH.md +++ b/docs/README-ZH.md @@ -8,7 +8,11 @@ [English] | [УкраїнÑька] | [Äesky] | [Magyar] | [Español] | [ÙØ§Ø±Ø³ÛŒ] | [Français] | [Deutsch] | [Polski] | [Indonesian] | [Suomi] | [മലയാളം] | [日本語] | [Nederlands] | [Italiano] | [РуÑÑкий] | [Português (Brasil)] | [Esperanto] | [한국어] | [العربي] | [Tiếng Việt] | [Ελληνικά]

-与我们交æµ: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) +> [!CAUTION] +> **å…责声明:**
+> RustDesk 的开å‘人员ä¸çºµå®¹æˆ–支æŒä»»ä½•ä¸é“å¾·æˆ–éžæ³•çš„è½¯ä»¶ä½¿ç”¨è¡Œä¸ºã€‚æ»¥ç”¨è¡Œä¸ºï¼Œä¾‹å¦‚æœªç»æŽˆæƒçš„è®¿é—®ã€æŽ§åˆ¶æˆ–ä¾µçŠ¯éšç§ï¼Œä¸¥æ ¼è¿å我们的准则。作者对应用程åºçš„任何滥用行为概ä¸è´Ÿè´£ã€‚ + +与我们交æµ: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https://discord.gg/nDceKgxnkV) | [Reddit](https://www.reddit.com/r/rustdesk) | [YouTube](https://www.youtube.com/@rustdesk) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09) diff --git a/docs/SECURITY-KR.md b/docs/SECURITY-KR.md new file mode 100644 index 00000000000..94ce8f2ba18 --- /dev/null +++ b/docs/SECURITY-KR.md @@ -0,0 +1,7 @@ +# 보안 ì •ì±… + +## ì·¨ì•½ì  ë³´ê³  + +ì €í¬ëŠ” 프로ì íŠ¸ì˜ ë³´ì•ˆì„ ë§¤ìš° 중요하게 ìƒê°í•©ë‹ˆë‹¤. 모든 사용ìžê°€ 발견한 취약ì ì„ ì €í¬ì—게 ë³´ê³ í•  ê²ƒì„ ê¶Œìž¥í•©ë‹ˆë‹¤. RustDesk 프로ì íЏì—서 보안 취약ì ì´ 발견ë˜ë©´ info@rustdesk.com으로 ì´ë©”ì¼ì„ ë³´ë‚´ ì±…ìž„ê° ìžˆê²Œ ë³´ê³ í•´ 주시기 ë°”ëžë‹ˆë‹¤. + +현재로서는 버그 현ìƒê¸ˆ í”„ë¡œê·¸ëž¨ì´ ì—†ìŠµë‹ˆë‹¤. ì €í¬ëŠ” í° ë¬¸ì œë¥¼ 해결하기 위해 노력하는 소규모 팀입니다. ì „ì²´ 커뮤니티를 위한 안전한 ì‘ìš© í”„ë¡œê·¸ëž¨ì„ ê³„ì† êµ¬ì¶•í•  수 있ë„ë¡ ì·¨ì•½ì ì„ ì±…ìž„ê° ìžˆê²Œ ì‹ ê³ í•´ 주시기 ë°”ëžë‹ˆë‹¤. diff --git a/docs/SECURITY-NO.md b/docs/SECURITY-NO.md new file mode 100644 index 00000000000..1f8dcb411bd --- /dev/null +++ b/docs/SECURITY-NO.md @@ -0,0 +1,9 @@ +# Sikkerhets Rettningslinjer + +## Reportering av en SÃ¥rbarhet + +Vi verdsetter pris pÃ¥ sikkerhet for prosjektet høyt. Og oppmunterer alle brukere til Ã¥ rapportere sÃ¥rbarheter de oppdager til oss. +Om du finner en sikkerhets sÃ¥rbarhet i RustDesk prosjektet, venligst raportere det ansvarsfult ved Ã¥ sende oss en email til info@rustdesk.com. + +PÃ¥ dette tidspunktet har vi ingen bug dusør program. Vi er ett lite team som prøver Ã¥ løse ett stort problem. Vi trenger att du raporterer alle sÃ¥rbarhetene +annsvarfult sÃ¥ vi kan fortsettte Ã¥ bygge ett en sikker applikasjon for hele felleskapet. diff --git a/examples/custom_plugin/Cargo.toml b/examples/custom_plugin/Cargo.toml deleted file mode 100644 index b9ee06ae734..00000000000 --- a/examples/custom_plugin/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "custom_plugin" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -name = "custom_plugin" -path = "src/lib.rs" -crate-type = ["cdylib"] - - -[features] -default = ["flutter"] -flutter = [] - -[dependencies] -lazy_static = "1.4.0" -rustdesk = { path = "../../", version = "1.2.0", features = ["flutter"]} - -[profile.release] -lto = true -codegen-units = 1 -panic = 'abort' -strip = true -#opt-level = 'z' # only have smaller size after strip -rpath = true \ No newline at end of file diff --git a/examples/custom_plugin/src/lib.rs b/examples/custom_plugin/src/lib.rs deleted file mode 100644 index 0b21f3fc854..00000000000 --- a/examples/custom_plugin/src/lib.rs +++ /dev/null @@ -1,30 +0,0 @@ -use librustdesk::api::RustDeskApiTable; -/// This file demonstrates how to write a custom plugin for RustDesk. -use std::ffi::{c_char, c_int, CString}; - -lazy_static::lazy_static! { - pub static ref PLUGIN_NAME: CString = CString::new("A Template Rust Plugin").unwrap(); - pub static ref PLUGIN_ID: CString = CString::new("TemplatePlugin").unwrap(); - // Do your own logic based on the API provided by RustDesk. - pub static ref API: RustDeskApiTable = RustDeskApiTable::default(); -} - -#[no_mangle] -fn plugin_name() -> *const c_char { - return PLUGIN_NAME.as_ptr(); -} - -#[no_mangle] -fn plugin_id() -> *const c_char { - return PLUGIN_ID.as_ptr(); -} - -#[no_mangle] -fn plugin_init() -> c_int { - return 0 as _; -} - -#[no_mangle] -fn plugin_dispose() -> c_int { - return 0 as _; -} diff --git a/examples/ipc.rs b/examples/ipc.rs new file mode 100644 index 00000000000..bca2321b124 --- /dev/null +++ b/examples/ipc.rs @@ -0,0 +1,90 @@ +use docopt::Docopt; +use hbb_common::{ + env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV}, + log, tokio, +}; +use librustdesk::{ipc::Data, *}; + +const USAGE: &'static str = " +IPC test program. + +Usage: + ipc (-s | --server | -c | --client) [-p | --postfix=] + ipc (-h | --help) + +Options: + -h --help Show this screen. + -s --server Run as IPC server. + -c --client Run as IPC client. + -p --postfix= IPC path postfix [default: ]. +"; + +#[derive(Debug, serde::Deserialize)] +struct Args { + flag_server: bool, + flag_client: bool, + flag_postfix: String, +} + +#[tokio::main] +async fn main() { + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); + + let args: Args = Docopt::new(USAGE) + .and_then(|d| d.deserialize()) + .unwrap_or_else(|e| e.exit()); + + if args.flag_server { + if args.flag_postfix.is_empty() { + log::info!("Starting IPC server..."); + } else { + log::info!( + "Starting IPC server with postfix: '{}'...", + args.flag_postfix + ); + } + ipc_server(&args.flag_postfix).await; + } else if args.flag_client { + if args.flag_postfix.is_empty() { + log::info!("Starting IPC client..."); + } else { + log::info!( + "Starting IPC client with postfix: '{}'...", + args.flag_postfix + ); + } + ipc_client(&args.flag_postfix).await; + } +} + +async fn ipc_server(postfix: &str) { + let postfix = postfix.to_string(); + let postfix2 = postfix.clone(); + std::thread::spawn(move || { + if let Err(err) = crate::ipc::start(&postfix) { + log::error!("Failed to start ipc: {}", err); + std::process::exit(-1); + } + }); + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + ipc_client(&postfix2).await; +} + +async fn ipc_client(postfix: &str) { + loop { + match crate::ipc::connect(1000, postfix).await { + Ok(mut conn) => match conn.send(&Data::Empty).await { + Ok(_) => { + log::info!("send message to ipc server success"); + } + Err(e) => { + log::error!("Failed to send message to ipc server: {}", e); + } + }, + Err(e) => { + log::error!("Failed to connect to ipc server: {}", e); + } + } + tokio::time::sleep(std::time::Duration::from_secs(6)).await; + } +} diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt index 357fb37ab68..91e796adaf6 100644 --- a/fastlane/metadata/android/en-US/short_description.txt +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -1 +1 @@ -An open-source remote desktop application, the open source TeamViewer alternative. +An open-source remote desktop application, the TeamViewer alternative diff --git a/flatpak/com.rustdesk.RustDesk.metainfo.xml b/flatpak/com.rustdesk.RustDesk.metainfo.xml new file mode 100644 index 00000000000..0d3b33bb8c3 --- /dev/null +++ b/flatpak/com.rustdesk.RustDesk.metainfo.xml @@ -0,0 +1,59 @@ + + + com.rustdesk.RustDesk + + RustDesk + + com.rustdesk.RustDesk.desktop + CC0-1.0 + AGPL-3.0-only + RustDesk + Secure remote desktop access + +

+ RustDesk is a full-featured open source remote control alternative for self-hosting and security with minimal configuration. +

+
    +
  • Works on Windows, macOS, Linux, iOS, Android, Web.
  • +
  • Supports VP8 / VP9 / AV1 software codecs, and H264 / H265 hardware codecs.
  • +
  • Own your data, easily set up self-hosting solution on your infrastructure.
  • +
  • P2P connection with end-to-end encryption based on NaCl.
  • +
  • No administrative privileges or installation needed for Windows, elevate priviledge locally or from remote on demand.
  • +
  • We like to keep things simple and will strive to make simpler where possible.
  • +
+

+ For self-hosting setup instructions please go to our home page. +

+
+ + Utility + + + + Remote desktop session + https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png + + + + #d9eaf8 + #0160ee + + https://rustdesk.com + https://github.com/rustdesk/rustdesk/issues + https://github.com/rustdesk/rustdesk/wiki/FAQ + https://rustdesk.com/docs + https://ko-fi.com/rustdesk + https://github.com/rustdesk/rustdesk + https://github.com/rustdesk/rustdesk/tree/master/src/lang + https://github.com/rustdesk/rustdesk/blob/master/docs/CONTRIBUTING.md + https://rustdesk.com/docs/en/technical-support + + 600 + always + + + keyboard + pointing + + +
\ No newline at end of file diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json index 6d7acb5b89c..af1bc5fe74a 100644 --- a/flatpak/rustdesk.json +++ b/flatpak/rustdesk.json @@ -1,19 +1,30 @@ { "id": "com.rustdesk.RustDesk", "runtime": "org.freedesktop.Platform", - "runtime-version": "23.08", + "runtime-version": "24.08", "sdk": "org.freedesktop.Sdk", "command": "rustdesk", - "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg", + "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"], + "rename-desktop-file": "rustdesk.desktop", + "rename-icon": "rustdesk", "modules": [ "shared-modules/libappindicator/libappindicator-gtk3-12.10.json", - "xdotool.json", { - "name": "pam", - "buildsystem": "simple", - "build-commands": [ - "./configure --disable-selinux --prefix=/app && make -j4 install" - ], + "name": "xdotool", + "no-autogen": true, + "make-install-args": ["PREFIX=${FLATPAK_DEST}"], + "sources": [ + { + "type": "archive", + "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", + "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" + } + ] + }, + { + "name": "pam", + "buildsystem": "autotools", + "config-opts": ["--disable-selinux"], "sources": [ { "type": "archive", @@ -26,32 +37,24 @@ "name": "rustdesk", "buildsystem": "simple", "build-commands": [ - "bsdtar -zxvf rustdesk.deb", - "tar -xvf ./data.tar.xz", - "cp -r ./usr/* /app/", - "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk", - "mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop", - "mv /app/share/applications/rustdesk-link.desktop /app/share/applications/com.rustdesk.RustDesk-link.desktop", - "sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/*.desktop", - "mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg", - "for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png scalable.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done" + "bsdtar -Oxf rustdesk.deb data.tar.xz | bsdtar -xf -", + "cp -r usr/* /app/", + "mkdir -p /app/bin && ln -s /app/share/rustdesk/rustdesk /app/bin/rustdesk" ], - "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"], "sources": [ { "type": "file", - "path": "./rustdesk.deb" + "path": "rustdesk.deb" }, { "type": "file", - "path": "../res/scalable.svg" + "path": "com.rustdesk.RustDesk.metainfo.xml" } ] } ], "finish-args": [ "--share=ipc", - "--socket=x11", "--socket=fallback-x11", "--socket=wayland", "--share=network", @@ -60,4 +63,4 @@ "--socket=pulseaudio", "--talk-name=org.freedesktop.Flatpak" ] -} +} \ No newline at end of file diff --git a/flatpak/xdotool.json b/flatpak/xdotool.json deleted file mode 100644 index d7f41bf0ec0..00000000000 --- a/flatpak/xdotool.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "xdotool", - "buildsystem": "simple", - "build-commands": [ - "make -j4 && PREFIX=./build make install", - "cp -r ./build/* /app/" - ], - "sources": [ - { - "type": "archive", - "url": "https://github.com/jordansissel/xdotool/releases/download/v3.20211022.1/xdotool-3.20211022.1.tar.gz", - "sha256": "96f0facfde6d78eacad35b91b0f46fecd0b35e474c03e00e30da3fdd345f9ada" - } - ] -} diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt index f53f95d6754..3ca83fbac73 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt @@ -19,6 +19,7 @@ import android.view.accessibility.AccessibilityEvent import android.view.ViewGroup.LayoutParams import android.view.accessibility.AccessibilityNodeInfo import android.view.KeyEvent as KeyEventAndroid +import android.view.ViewConfiguration import android.graphics.Rect import android.media.AudioManager import android.accessibilityservice.AccessibilityServiceInfo @@ -34,10 +35,15 @@ import hbb.MessageOuterClass.KeyEvent import hbb.MessageOuterClass.KeyboardMode import hbb.KeyEventConverter -const val LIFT_DOWN = 9 -const val LIFT_MOVE = 8 -const val LIFT_UP = 10 +// const val BUTTON_UP = 2 +// const val BUTTON_BACK = 0x08 + +const val LEFT_DOWN = 9 +const val LEFT_MOVE = 8 +const val LEFT_UP = 10 const val RIGHT_UP = 18 +// (BUTTON_BACK << 3) | BUTTON_UP +const val BACK_UP = 66 const val WHEEL_BUTTON_DOWN = 33 const val WHEEL_BUTTON_UP = 34 const val WHEEL_DOWN = 523331 @@ -65,11 +71,14 @@ class InputService : AccessibilityService() { private val logTag = "input service" private var leftIsDown = false private var touchPath = Path() + private var stroke: GestureDescription.StrokeDescription? = null private var lastTouchGestureStartTime = 0L private var mouseX = 0 private var mouseY = 0 private var timer = Timer() private var recentActionTask: TimerTask? = null + // 100(tap timeout) + 400(long press timeout) + private val longPressDuration = ViewConfiguration.getTapTimeout().toLong() + ViewConfiguration.getLongPressTimeout().toLong() private val wheelActionsQueue = LinkedList() private var isWheelActionsPolling = false @@ -77,6 +86,9 @@ class InputService : AccessibilityService() { private var fakeEditTextForTextStateCalculation: EditText? = null + private var lastX = 0 + private var lastY = 0 + private val volumeController: VolumeController by lazy { VolumeController(applicationContext.getSystemService(AUDIO_SERVICE) as AudioManager) } @RequiresApi(Build.VERSION_CODES.N) @@ -84,7 +96,7 @@ class InputService : AccessibilityService() { val x = max(0, _x) val y = max(0, _y) - if (mask == 0 || mask == LIFT_MOVE) { + if (mask == 0 || mask == LEFT_MOVE) { val oldX = mouseX val oldY = mouseY mouseX = x * SCREEN_INFO.scale @@ -98,31 +110,30 @@ class InputService : AccessibilityService() { } } - // left button down ,was up - if (mask == LIFT_DOWN) { + // left button down, was up + if (mask == LEFT_DOWN) { isWaitingLongPress = true timer.schedule(object : TimerTask() { override fun run() { if (isWaitingLongPress) { isWaitingLongPress = false - leftIsDown = false - endGesture(mouseX, mouseY) + continueGesture(mouseX, mouseY) } } - }, LONG_TAP_DELAY * 4) + }, longPressDuration) leftIsDown = true startGesture(mouseX, mouseY) return } - // left down ,was down + // left down, was down if (leftIsDown) { continueGesture(mouseX, mouseY) } - // left up ,was down - if (mask == LIFT_UP) { + // left up, was down + if (mask == LEFT_UP) { if (leftIsDown) { leftIsDown = false isWaitingLongPress = false @@ -132,6 +143,11 @@ class InputService : AccessibilityService() { } if (mask == RIGHT_UP) { + longPress(mouseX, mouseY) + return + } + + if (mask == BACK_UP) { performGlobalAction(GLOBAL_ACTION_BACK) return } @@ -241,18 +257,100 @@ class InputService : AccessibilityService() { } } + @RequiresApi(Build.VERSION_CODES.N) + private fun performClick(x: Int, y: Int, duration: Long) { + val path = Path() + path.moveTo(x.toFloat(), y.toFloat()) + try { + val longPressStroke = GestureDescription.StrokeDescription(path, 0, duration) + val builder = GestureDescription.Builder() + builder.addStroke(longPressStroke) + Log.d(logTag, "performClick x:$x y:$y time:$duration") + dispatchGesture(builder.build(), null, null) + } catch (e: Exception) { + Log.e(logTag, "performClick, error:$e") + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun longPress(x: Int, y: Int) { + performClick(x, y, longPressDuration) + } + private fun startGesture(x: Int, y: Int) { - touchPath = Path() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + touchPath.reset() + } else { + touchPath = Path() + } touchPath.moveTo(x.toFloat(), y.toFloat()) lastTouchGestureStartTime = System.currentTimeMillis() + lastX = x + lastY = y } - private fun continueGesture(x: Int, y: Int) { + @RequiresApi(Build.VERSION_CODES.N) + private fun doDispatchGesture(x: Int, y: Int, willContinue: Boolean) { touchPath.lineTo(x.toFloat(), y.toFloat()) + var duration = System.currentTimeMillis() - lastTouchGestureStartTime + if (duration <= 0) { + duration = 1 + } + try { + if (stroke == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration, + willContinue + ) + } else { + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration + ) + } + } else { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + stroke = stroke?.continueStroke(touchPath, 0, duration, willContinue) + } else { + stroke = null + stroke = GestureDescription.StrokeDescription( + touchPath, + 0, + duration + ) + } + } + stroke?.let { + val builder = GestureDescription.Builder() + builder.addStroke(it) + Log.d(logTag, "doDispatchGesture x:$x y:$y time:$duration") + dispatchGesture(builder.build(), null, null) + } + } catch (e: Exception) { + Log.e(logTag, "doDispatchGesture, willContinue:$willContinue, error:$e") + } } @RequiresApi(Build.VERSION_CODES.N) - private fun endGesture(x: Int, y: Int) { + private fun continueGesture(x: Int, y: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + doDispatchGesture(x, y, true) + touchPath.reset() + touchPath.moveTo(x.toFloat(), y.toFloat()) + lastTouchGestureStartTime = System.currentTimeMillis() + lastX = x + lastY = y + } else { + touchPath.lineTo(x.toFloat(), y.toFloat()) + } + } + + @RequiresApi(Build.VERSION_CODES.N) + private fun endGestureBelowO(x: Int, y: Int) { try { touchPath.lineTo(x.toFloat(), y.toFloat()) var duration = System.currentTimeMillis() - lastTouchGestureStartTime @@ -273,6 +371,17 @@ class InputService : AccessibilityService() { } } + @RequiresApi(Build.VERSION_CODES.N) + private fun endGesture(x: Int, y: Int) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + doDispatchGesture(x, y, false) + touchPath.reset() + stroke = null + } else { + endGestureBelowO(x, y) + } + } + @RequiresApi(Build.VERSION_CODES.N) fun onKeyEvent(data: ByteArray) { val keyEvent = KeyEvent.parseFrom(data) diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt index 5c54c18fb82..a19c2ae9d06 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainActivity.kt @@ -316,7 +316,7 @@ class MainActivity : FlutterActivity() { codecObject.put("mime_type", mime_type) val caps = codec.getCapabilitiesForType(mime_type) if (codec.isEncoder) { - // Encoder‘s max_height and max_width are interchangeable + // Encoder's max_height and max_width are interchangeable if (!caps.videoCapabilities.isSizeSupported(w,h) && !caps.videoCapabilities.isSizeSupported(h,w)) { return@forEach } diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt index e9ec0975d1b..7bb16a00ad6 100644 --- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt +++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt @@ -65,8 +65,8 @@ class MainService : Service() { @Keep @RequiresApi(Build.VERSION_CODES.N) fun rustPointerInput(kind: Int, mask: Int, x: Int, y: Int) { - // turn on screen with LIFT_DOWN when screen off - if (!powerManager.isInteractive && (kind == 0 || mask == LIFT_DOWN)) { + // turn on screen with LEFT_DOWN when screen off + if (!powerManager.isInteractive && (kind == 0 || mask == LEFT_DOWN)) { if (wakeLock.isHeld) { Log.d(logTag, "Turn on Screen, WakeLock release") wakeLock.release() @@ -122,9 +122,9 @@ class MainService : Service() { val authorized = jsonObject["authorized"] as Boolean val isFileTransfer = jsonObject["is_file_transfer"] as Boolean val type = if (isFileTransfer) { - translate("File Connection") + translate("Transfer file") } else { - translate("Screen Connection") + translate("Share screen") } if (authorized) { if (!isFileTransfer && !isStart) { diff --git a/flutter/android/gradle.properties b/flutter/android/gradle.properties index 94adc3a3f97..804b29b300a 100644 --- a/flutter/android/gradle.properties +++ b/flutter/android/gradle.properties @@ -1,3 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx1024M android.useAndroidX=true android.enableJetifier=true +org.gradle.daemon=false diff --git a/flutter/android/settings.gradle b/flutter/android/settings.gradle index c5fb685a161..ae32fa00e5d 100644 --- a/flutter/android/settings.gradle +++ b/flutter/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "1.9.10" apply false + id "com.android.application" version "7.3.1" apply false + id "org.jetbrains.kotlin.android" version "2.1.21" apply false } include ":app" diff --git a/flutter/assets/device_group.ttf b/flutter/assets/device_group.ttf new file mode 100644 index 00000000000..a6e42704f06 Binary files /dev/null and b/flutter/assets/device_group.ttf differ diff --git a/flutter/assets/more.ttf b/flutter/assets/more.ttf new file mode 100644 index 00000000000..3b01435df3a Binary files /dev/null and b/flutter/assets/more.ttf differ diff --git a/flutter/build_fdroid.sh b/flutter/build_fdroid.sh index 1821c529afb..ecfb444efea 100755 --- a/flutter/build_fdroid.sh +++ b/flutter/build_fdroid.sh @@ -150,6 +150,10 @@ prebuild) # Flutter used to compile Flutter<->Rust bridge files + CARGO_EXPAND_VERSION="$(yq -r \ + .env.CARGO_EXPAND_VERSION \ + .github/workflows/bridge.yml)" + FLUTTER_BRIDGE_VERSION="$(yq -r \ .env.FLUTTER_VERSION \ .github/workflows/bridge.yml)" @@ -237,7 +241,10 @@ prebuild) # Install rust bridge generator - cargo install cargo-expand + cargo install \ + cargo-expand \ + --version "${CARGO_EXPAND_VERSION}" \ + --locked cargo install flutter_rust_bridge_codegen \ --version "${FLUTTER_RUST_BRIDGE_VERSION}" \ --features "uuid" \ diff --git a/flutter/build_ios.sh b/flutter/build_ios.sh index 50f2f0056b4..cd12626263d 100755 --- a/flutter/build_ios.sh +++ b/flutter/build_ios.sh @@ -4,4 +4,5 @@ # no obfuscate, because no easy to check errors cd $(dirname $(dirname $(which flutter))) git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff +cd - flutter build ipa --release diff --git a/flutter/deploy.sh b/flutter/deploy.sh deleted file mode 100755 index f6826fd8720..00000000000 --- a/flutter/deploy.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -cd build/web/ -python3 -c 'x=open("./main.dart.js", "rt").read();import re;y=re.search("https://.*canvaskit-wasm@([\d\.]+)/bin/",x);dirname="canvaskit@"+y.groups()[0];z=x.replace(y.group(),"/"+dirname+"/");f=open("./main.dart.js", "wt");f.write(z);import os;os.system("ln -s canvaskit " + dirname);' -mv jds/dist/index.js ./ -mv jds/dist/vendor.js ./ -/bin/rm -rf js -python3 -c 'import hashlib;x=hashlib.sha1(open("./main.dart.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("main.dart.js", "main.dart.js?v="+x);open("index.html","wt").write(y)' -python3 -c 'import hashlib;x=hashlib.sha1(open("./index.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/index.js", "index.js?v="+x);open("index.html","wt").write(y)' -python3 -c 'import hashlib;x=hashlib.sha1(open("./vendor.js").read().encode()).hexdigest()[:10];y=open("index.html","rt").read().replace("js/dist/vendor.js", "vendor.js?v="+x);open("index.html","wt").write(y)' -tar czf x * -scp x sg:/tmp/ -ssh sg "sudo tar xzf /tmp/x -C /var/www/html/web.rustdesk.com/ && /bin/rm /tmp/x && sudo chown www-data:www-data /var/www/html/web.rustdesk.com/ -R" -/bin/rm x -cd - diff --git a/flutter/ios_arm64.sh b/flutter/ios_arm64.sh index 2d8410c7a4e..579baaa6dda 100755 --- a/flutter/ios_arm64.sh +++ b/flutter/ios_arm64.sh @@ -1,4 +1,2 @@ #!/usr/bin/env bash -cd $(dirname $(dirname $(which flutter))) -git apply ~/rustdesk/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff cargo build --features flutter,hwcodec --release --target aarch64-apple-ios --lib diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index 208897ed039..c1dd7cd4ec6 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -29,8 +29,11 @@ import '../consts.dart'; import 'common/widgets/overlay.dart'; import 'mobile/pages/file_manager_page.dart'; import 'mobile/pages/remote_page.dart'; +import 'mobile/pages/view_camera_page.dart'; +import 'mobile/pages/terminal_page.dart'; import 'desktop/pages/remote_page.dart' as desktop_remote; import 'desktop/pages/file_manager_page.dart' as desktop_file_manager; +import 'desktop/pages/view_camera_page.dart' as desktop_view_camera; import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; import 'models/model.dart'; import 'models/platform_model.dart'; @@ -96,6 +99,8 @@ enum DesktopType { main, remote, fileTransfer, + viewCamera, + terminal, cm, portForward, } @@ -103,6 +108,10 @@ enum DesktopType { class IconFont { static const _family1 = 'Tabbar'; static const _family2 = 'PeerSearchbar'; + static const _family3 = 'AddressBook'; + static const _family4 = 'DeviceGroup'; + static const _family5 = 'More'; + IconFont._(); static const IconData max = IconData(0xe606, fontFamily: _family1); @@ -113,8 +122,12 @@ class IconFont { static const IconData menu = IconData(0xe628, fontFamily: _family1); static const IconData search = IconData(0xe6a4, fontFamily: _family2); static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2); - static const IconData addressBook = - IconData(0xe602, fontFamily: "AddressBook"); + static const IconData addressBook = IconData(0xe602, fontFamily: _family3); + static const IconData deviceGroupOutline = + IconData(0xe623, fontFamily: _family4); + static const IconData deviceGroupFill = + IconData(0xe748, fontFamily: _family4); + static const IconData more = IconData(0xe609, fontFamily: _family5); } class ColorThemeExtension extends ThemeExtension { @@ -817,7 +830,11 @@ class OverlayDialogManager { close([res]) { _dialogs.remove(dialogTag); - dialog.complete(res); + try { + dialog.complete(res); + } catch (e) { + debugPrint("Dialog complete catch error: $e"); + } BackButtonInterceptor.removeByName(dialogTag); } @@ -1137,15 +1154,23 @@ Widget createDialogContent(String text) { void msgBox(SessionID sessionId, String type, String title, String text, String link, OverlayDialogManager dialogManager, - {bool? hasCancel, ReconnectHandle? reconnect, int? reconnectTimeout}) { + {bool? hasCancel, + ReconnectHandle? reconnect, + int? reconnectTimeout, + VoidCallback? onSubmit, + int? submitTimeout}) { dialogManager.dismissAll(); List buttons = []; bool hasOk = false; submit() { dialogManager.dismissAll(); - // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 - if (!type.contains("custom") && desktopType != DesktopType.portForward) { - closeConnection(); + if (onSubmit != null) { + onSubmit.call(); + } else { + // https://github.com/rustdesk/rustdesk/blob/5e9a31340b899822090a3731769ae79c6bf5f3e5/src/ui/common.tis#L263 + if (!type.contains("custom") && desktopType != DesktopType.portForward) { + closeConnection(); + } } } @@ -1161,7 +1186,18 @@ void msgBox(SessionID sessionId, String type, String title, String text, if (type != "connecting" && type != "success" && !type.contains("nook")) { hasOk = true; - buttons.insert(0, dialogButton('OK', onPressed: submit)); + late final Widget btn; + if (submitTimeout != null) { + btn = _CountDownButton( + text: 'OK', + second: submitTimeout, + onPressed: submit, + submitOnTimeout: true, + ); + } else { + btn = dialogButton('OK', onPressed: submit); + } + buttons.insert(0, btn); } hasCancel ??= !type.contains("error") && !type.contains("nocancel") && @@ -1182,7 +1218,8 @@ void msgBox(SessionID sessionId, String type, String title, String text, reconnectTimeout != null) { // `enabled` is used to disable the dialog button once the button is clicked. final enabled = true.obs; - final button = Obx(() => _ReconnectCountDownButton( + final button = Obx(() => _CountDownButton( + text: 'Reconnect', second: reconnectTimeout, onPressed: enabled.isTrue ? () { @@ -1536,7 +1573,9 @@ bool option2bool(String option, String value) { String bool2option(String option, bool b) { String res; - if (option.startsWith('enable-')) { + if (option.startsWith('enable-') && + option != kOptionEnableUdpPunch && + option != kOptionEnableIpv6Punch) { res = b ? defaultOptionYes : 'N'; } else if (option.startsWith('allow-') || option == kOptionStopService || @@ -1544,7 +1583,9 @@ String bool2option(String option, bool b) { option == kOptionForceAlwaysRelay) { res = b ? 'Y' : defaultOptionNo; } else { - assert(false); + if (option != kOptionEnableUdpPunch && option != kOptionEnableIpv6Punch) { + assert(false); + } res = b ? 'Y' : 'N'; } return res; @@ -1741,7 +1782,8 @@ Future saveWindowPosition(WindowType type, {int? windowId}) async { await bind.setLocalFlutterOption( k: windowFramePrefix + type.name, v: pos.toString()); - if (type == WindowType.RemoteDesktop && windowId != null) { + if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) && + windowId != null) { await _saveSessionWindowPosition( type, windowId, isMaximized, isFullscreen, pos); } @@ -1892,7 +1934,9 @@ Future restoreWindowPosition(WindowType type, String? pos; // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs) // Though "open in tabs" is true and the new window restore peer position, it's ok. - if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) { + if ((type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) && + windowId != null && + peerId != null) { final peerPos = bind.mainGetPeerFlutterOptionSync( id: peerId, k: windowFramePrefix + type.name); if (peerPos.isNotEmpty) { @@ -1907,7 +1951,7 @@ Future restoreWindowPosition(WindowType type, debugPrint("no window position saved, ignoring position restoration"); return false; } - if (type == WindowType.RemoteDesktop) { + if (type == WindowType.RemoteDesktop || type == WindowType.ViewCamera) { if (!isRemotePeerPos && windowId != null) { if (lpos.offsetWidth != null) { lpos.offsetWidth = lpos.offsetWidth! + windowId * kNewWindowOffset; @@ -2076,8 +2120,14 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) { enum UriLinkType { remoteDesktop, fileTransfer, + viewCamera, portForward, rdp, + terminal, +} + +setEnvTerminalAdmin() { + bind.mainSetEnv(key: 'IS_TERMINAL_ADMIN', value: 'Y'); } // uri link handler @@ -2127,6 +2177,11 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { id = args[i + 1]; i++; break; + case '--view-camera': + type = UriLinkType.viewCamera; + id = args[i + 1]; + i++; + break; case '--port-forward': type = UriLinkType.portForward; id = args[i + 1]; @@ -2137,6 +2192,17 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { id = args[i + 1]; i++; break; + case '--terminal': + type = UriLinkType.terminal; + id = args[i + 1]; + i++; + break; + case '--terminal-admin': + setEnvTerminalAdmin(); + type = UriLinkType.terminal; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; @@ -2168,6 +2234,12 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { password: password, forceRelay: forceRelay); }); break; + case UriLinkType.viewCamera: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newViewCamera(id!, + password: password, forceRelay: forceRelay); + }); + break; case UriLinkType.portForward: Future.delayed(Duration.zero, () { rustDeskWinManager.newPortForward(id!, false, @@ -2180,6 +2252,12 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { password: password, forceRelay: forceRelay); }); break; + case UriLinkType.terminal: + Future.delayed(Duration.zero, () { + rustDeskWinManager.newTerminal(id!, + password: password, forceRelay: forceRelay); + }); + break; } return true; @@ -2191,7 +2269,16 @@ bool handleUriLink({List? cmdArgs, Uri? uri, String? uriString}) { List? urlLinkToCmdArgs(Uri uri) { String? command; String? id; - final options = ["connect", "play", "file-transfer", "port-forward", "rdp"]; + final options = [ + "connect", + "play", + "file-transfer", + "view-camera", + "port-forward", + "rdp", + "terminal", + "terminal-admin", + ]; if (uri.authority.isEmpty && uri.path.split('').every((char) => char == '/')) { return []; @@ -2219,19 +2306,10 @@ List? urlLinkToCmdArgs(Uri uri) { } } } else if (options.contains(uri.authority)) { - final optionIndex = options.indexOf(uri.authority); command = '--${uri.authority}'; if (uri.path.length > 1) { id = uri.path.substring(1); } - if (isMobile && id != null) { - if (optionIndex == 0 || optionIndex == 1) { - connect(Get.context!, id); - } else if (optionIndex == 2) { - connect(Get.context!, id, isFileTransfer: true); - } - return null; - } } else if (uri.authority.length > 2 && (uri.path.length <= 1 || (uri.path == '/r' || uri.path.startsWith('/r@')))) { @@ -2255,12 +2333,29 @@ List? urlLinkToCmdArgs(Uri uri) { } } - if (isMobile) { - if (id != null) { - final forceRelay = queryParameters["relay"] != null; - connect(Get.context!, id, forceRelay: forceRelay); - return null; + if (isMobile && id != null) { + final forceRelay = queryParameters["relay"] != null; + final password = queryParameters["password"]; + + // Determine connection type based on command + if (command == '--file-transfer') { + connect(Get.context!, id, + isFileTransfer: true, forceRelay: forceRelay, password: password); + } else if (command == '--view-camera') { + connect(Get.context!, id, + isViewCamera: true, forceRelay: forceRelay, password: password); + } else if (command == '--terminal') { + connect(Get.context!, id, + isTerminal: true, forceRelay: forceRelay, password: password); + } else if (command == 'terminal-admin') { + setEnvTerminalAdmin(); + connect(Get.context!, id, + isTerminal: true, forceRelay: forceRelay, password: password); + } else { + // Default to remote desktop for '--connect', '--play', or direct connection + connect(Get.context!, id, forceRelay: forceRelay, password: password); } + return null; } List args = List.empty(growable: true); @@ -2281,6 +2376,8 @@ List? urlLinkToCmdArgs(Uri uri) { connectMainDesktop(String id, {required bool isFileTransfer, + required bool isViewCamera, + required bool isTerminal, required bool isTcpTunneling, required bool isRDP, bool? forceRelay, @@ -2293,12 +2390,24 @@ connectMainDesktop(String id, isSharedPassword: isSharedPassword, connToken: connToken, forceRelay: forceRelay); + } else if (isViewCamera) { + await rustDeskWinManager.newViewCamera(id, + password: password, + isSharedPassword: isSharedPassword, + connToken: connToken, + forceRelay: forceRelay); } else if (isTcpTunneling || isRDP) { await rustDeskWinManager.newPortForward(id, isRDP, password: password, isSharedPassword: isSharedPassword, connToken: connToken, forceRelay: forceRelay); + } else if (isTerminal) { + await rustDeskWinManager.newTerminal(id, + password: password, + isSharedPassword: isSharedPassword, + connToken: connToken, + forceRelay: forceRelay); } else { await rustDeskWinManager.newRemoteDesktop(id, password: password, @@ -2309,10 +2418,13 @@ connectMainDesktop(String id, /// Connect to a peer with [id]. /// If [isFileTransfer], starts a session only for file transfer. +/// If [isViewCamera], starts a session only for view camera. /// If [isTcpTunneling], starts a session only for tcp tunneling. /// If [isRDP], starts a session only for rdp. connect(BuildContext context, String id, {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTerminal = false, bool isTcpTunneling = false, bool isRDP = false, bool forceRelay = false, @@ -2335,7 +2447,7 @@ connect(BuildContext context, String id, id = id.replaceAll(' ', ''); final oldId = id; id = await bind.mainHandleRelayId(id: id); - final forceRelay2 = id != oldId || forceRelay; + forceRelay = id != oldId || forceRelay; assert(!(isFileTransfer && isTcpTunneling && isRDP), "more than one connect type"); @@ -2344,16 +2456,20 @@ connect(BuildContext context, String id, await connectMainDesktop( id, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTerminal: isTerminal, isTcpTunneling: isTcpTunneling, isRDP: isRDP, password: password, isSharedPassword: isSharedPassword, - forceRelay: forceRelay2, + forceRelay: forceRelay, ); } else { await rustDeskWinManager.call(WindowType.Main, kWindowConnect, { 'id': id, 'isFileTransfer': isFileTransfer, + 'isViewCamera': isViewCamera, + 'isTerminal': isTerminal, 'isTcpTunneling': isTcpTunneling, 'isRDP': isRDP, 'password': password, @@ -2387,10 +2503,52 @@ connect(BuildContext context, String id, context, MaterialPageRoute( builder: (BuildContext context) => FileManagerPage( - id: id, password: password, isSharedPassword: isSharedPassword), + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay), ), ); } + } else if (isViewCamera) { + if (isWeb) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => + desktop_view_camera.ViewCameraPage( + key: ValueKey(id), + id: id, + toolbarState: ToolbarState(), + password: password, + isSharedPassword: isSharedPassword, + ), + ), + ); + } else { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ViewCameraPage( + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay), + ), + ); + } + } else if (isTerminal) { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => TerminalPage( + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay, + ), + ), + ); } else { if (isWeb) { Navigator.push( @@ -2401,7 +2559,6 @@ connect(BuildContext context, String id, id: id, toolbarState: ToolbarState(), password: password, - forceRelay: forceRelay, isSharedPassword: isSharedPassword, ), ), @@ -2411,7 +2568,10 @@ connect(BuildContext context, String id, context, MaterialPageRoute( builder: (BuildContext context) => RemotePage( - id: id, password: password, isSharedPassword: isSharedPassword), + id: id, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay), ), ); } @@ -2566,6 +2726,8 @@ bool get kUseCompatibleUiMode => isWindows && const [WindowsTarget.w7].contains(windowsBuildNumber.windowsVersion); +bool get isWin10 => windowsBuildNumber.windowsVersion == WindowsTarget.w10; + class ServerConfig { late String idServer; late String relayServer; @@ -2675,6 +2837,8 @@ String getWindowName({WindowType? overrideType}) { return name; case WindowType.FileTransfer: return "File Transfer - $name"; + case WindowType.ViewCamera: + return "View Camera - $name"; case WindowType.PortForward: return "Port Forward - $name"; case WindowType.RemoteDesktop: @@ -2779,6 +2943,7 @@ Future canBeBlocked() async { return access_mode == 'view' || (access_mode.isEmpty && !option); } +// to-do: web not implemented Future shouldBeBlocked(RxBool block, WhetherUseRemoteBlock? use) async { if (use != null && !await use()) { block.value = false; @@ -3040,6 +3205,7 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi, 'peer_id': peerId, 'display': i, 'display_count': pi.displays.length, + 'window_type': (kWindowType ?? WindowType.RemoteDesktop).index, }; if (screenRect != null) { args['screen_rect'] = { @@ -3054,12 +3220,12 @@ openMonitorInNewTabOrWindow(int i, String peerId, PeerInfo pi, } setNewConnectWindowFrame(int windowId, String peerId, int preSessionCount, - int? display, Rect? screenRect) async { + WindowType windowType, int? display, Rect? screenRect) async { if (screenRect == null) { // Do not restore window position to new connection if there's a pre-session. // https://github.com/rustdesk/rustdesk/discussions/8825 if (preSessionCount == 0) { - await restoreWindowPosition(WindowType.RemoteDesktop, + await restoreWindowPosition(windowType, windowId: windowId, display: display, peerId: peerId); } } else { @@ -3103,21 +3269,24 @@ parseParamScreenRect(Map params) { get isInputSourceFlutter => stateGlobal.getInputSource() == "Input source 2"; -class _ReconnectCountDownButton extends StatefulWidget { - _ReconnectCountDownButton({ +class _CountDownButton extends StatefulWidget { + _CountDownButton({ Key? key, + required this.text, required this.second, required this.onPressed, + this.submitOnTimeout = false, }) : super(key: key); + final String text; final VoidCallback? onPressed; final int second; + final bool submitOnTimeout; @override - State<_ReconnectCountDownButton> createState() => - _ReconnectCountDownButtonState(); + State<_CountDownButton> createState() => _CountDownButtonState(); } -class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { +class _CountDownButtonState extends State<_CountDownButton> { late int _countdownSeconds = widget.second; Timer? _timer; @@ -3138,6 +3307,9 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { _timer = Timer.periodic(Duration(seconds: 1), (timer) { if (_countdownSeconds <= 0) { timer.cancel(); + if (widget.submitOnTimeout) { + widget.onPressed?.call(); + } } else { setState(() { _countdownSeconds--; @@ -3149,7 +3321,7 @@ class _ReconnectCountDownButtonState extends State<_ReconnectCountDownButton> { @override Widget build(BuildContext context) { return dialogButton( - '${translate('Reconnect')} (${_countdownSeconds}s)', + '${translate(widget.text)} (${_countdownSeconds}s)', onPressed: widget.onPressed, isOutline: true, ); @@ -3339,6 +3511,9 @@ Color? disabledTextColor(BuildContext context, bool enabled) { } Widget loadPowered(BuildContext context) { + if (bind.mainGetBuildinOption(key: "hide-powered-by-me") == 'Y') { + return SizedBox.shrink(); + } return MouseRegion( cursor: SystemMouseCursors.click, child: GestureDetector( @@ -3610,7 +3785,7 @@ void earlyAssert() { } void checkUpdate() { - if (isDesktop || isAndroid) { + if (!isWeb) { if (!bind.isCustomClient()) { platformFFI.registerEventHandler( kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish, @@ -3625,3 +3800,134 @@ void checkUpdate() { } } } + +// https://github.com/flutter/flutter/issues/153560#issuecomment-2497160535 +// For TextField, TextFormField +extension WorkaroundFreezeLinuxMint on Widget { + Widget workaroundFreezeLinuxMint() { + // No need to check if is Linux Mint, because this workaround is harmless on other platforms. + if (isLinux) { + return ExcludeSemantics(child: this); + } else { + return this; + } + } +} + +// Don't use `extension` here, the border looks weird if using `extension` in my test. +Widget workaroundWindowBorder(BuildContext context, Widget child) { + if (!isWin10) { + return child; + } + + final isLight = Theme.of(context).brightness == Brightness.light; + final borderColor = isLight ? Colors.black87 : Colors.grey; + final width = isLight ? 0.5 : 0.1; + + getBorderWidget(Widget child) { + return Obx(() => + (stateGlobal.isMaximized.isTrue || stateGlobal.fullscreen.isTrue) + ? Offstage() + : child); + } + + final List borders = [ + getBorderWidget(Container( + color: borderColor, + height: width + 0.1, + )) + ]; + if (kWindowType == WindowType.Main && !isLight) { + borders.addAll([ + getBorderWidget(Align( + alignment: Alignment.topLeft, + child: Container( + color: borderColor, + width: width, + ), + )), + getBorderWidget(Align( + alignment: Alignment.topRight, + child: Container( + color: borderColor, + width: width, + ), + )), + getBorderWidget(Align( + alignment: Alignment.bottomCenter, + child: Container( + color: borderColor, + height: width, + ), + )), + ]); + } + return Stack( + children: [ + child, + ...borders, + ], + ); +} + +void updateTextAndPreserveSelection( + TextEditingController controller, String text) { + // Only care about select all for now. + final isSelected = controller.selection.isValid && + controller.selection.end > controller.selection.start; + + // Set text will make the selection invalid. + controller.text = text; + + if (isSelected) { + controller.selection = TextSelection( + baseOffset: 0, extentOffset: controller.value.text.length); + } +} + +List getPrinterNames() { + final printerNamesJson = bind.mainGetPrinterNames(); + if (printerNamesJson.isEmpty) { + return []; + } + try { + final List printerNamesList = jsonDecode(printerNamesJson); + final appPrinterName = '$appName Printer'; + return printerNamesList + .map((e) => e.toString()) + .where((name) => name != appPrinterName) + .toList(); + } catch (e) { + debugPrint('failed to parse printer names, err: $e'); + return []; + } +} + +String _appName = ''; +String get appName { + if (_appName.isEmpty) { + _appName = bind.mainGetAppNameSync(); + } + return _appName; +} + +String getConnectionText(bool secure, bool direct, String streamType) { + String connectionText; + if (secure && direct) { + connectionText = translate("Direct and encrypted connection"); + } else if (secure && !direct) { + connectionText = translate("Relayed and encrypted connection"); + } else if (!secure && direct) { + connectionText = translate("Direct and unencrypted connection"); + } else { + connectionText = translate("Relayed and unencrypted connection"); + } + if (streamType == 'Relay') { + streamType = 'TCP'; + } + if (streamType.isEmpty) { + return connectionText; + } else { + return '$connectionText ($streamType)'; + } +} diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart index e189cc7b23f..97baf546a64 100644 --- a/flutter/lib/common/hbbs/hbbs.dart +++ b/flutter/lib/common/hbbs/hbbs.dart @@ -27,6 +27,7 @@ class UserPayload { String name = ''; String email = ''; String note = ''; + String? verifier; UserStatus status; bool isAdmin = false; @@ -34,6 +35,7 @@ class UserPayload { : name = json['name'] ?? '', email = json['email'] ?? '', note = json['note'] ?? '', + verifier = json['verifier'], status = json['status'] == 0 ? UserStatus.kDisabled : json['status'] == -1 @@ -67,6 +69,7 @@ class PeerPayload { int? status; String user = ''; String user_name = ''; + String? device_group_name; String note = ''; PeerPayload.fromJson(Map json) @@ -75,6 +78,7 @@ class PeerPayload { status = json['status'], user = json['user'] ?? '', user_name = json['user_name'] ?? '', + device_group_name = json['device_group_name'] ?? '', note = json['note'] ?? ''; static Peer toPeer(PeerPayload p) { @@ -84,6 +88,7 @@ class PeerPayload { "username": p.info['username'] ?? '', "platform": _platform(p.info['os']), "hostname": p.info['device_name'], + "device_group_name": p.device_group_name, }); } @@ -265,3 +270,19 @@ class AbTag { : name = json['name'] ?? '', color = json['color'] ?? ''; } + +class DeviceGroupPayload { + String name; + + DeviceGroupPayload(this.name); + + DeviceGroupPayload.fromJson(Map json) + : name = json['name'] ?? ''; + + Map toGroupCacheJson() { + final Map map = { + 'name': name, + }; + return map; + } +} diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart index 908c98a70e3..4f9373ccd4e 100644 --- a/flutter/lib/common/shared_state.dart +++ b/flutter/lib/common/shared_state.dart @@ -77,9 +77,11 @@ class CurrentDisplayState { class ConnectionType { final Rx _secure = kInvalidValueStr.obs; final Rx _direct = kInvalidValueStr.obs; + final Rx _stream_type = kInvalidValueStr.obs; Rx get secure => _secure; Rx get direct => _direct; + Rx get stream_type => _stream_type; static String get strSecure => 'secure'; static String get strInsecure => 'insecure'; @@ -94,9 +96,14 @@ class ConnectionType { _direct.value = v ? strDirect : strIndirect; } + void setStreamType(String v) { + _stream_type.value = v; + } + bool isValid() { return _secure.value != kInvalidValueStr && - _direct.value != kInvalidValueStr; + _direct.value != kInvalidValueStr && + _stream_type.value != kInvalidValueStr; } } diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart index ae07c1498cf..6a3cec8ade8 100644 --- a/flutter/lib/common/widgets/address_book.dart +++ b/flutter/lib/common/widgets/address_book.dart @@ -286,7 +286,7 @@ class _AddressBookState extends State { borderRadius: BorderRadius.circular(8), ), ), - ), + ).workaroundFreezeLinuxMint(), ), searchMatchFn: (item, searchValue) { return item.value @@ -509,13 +509,13 @@ class _AddressBookState extends State { double marginBottom = 4; - row({required Widget lable, required Widget input}) { + row({required Widget label, required Widget input}) { makeChild(bool isPortrait) => Row( children: [ !isPortrait ? ConstrainedBox( constraints: const BoxConstraints(minWidth: 100), - child: lable.marginOnly(right: 10)) + child: label.marginOnly(right: 10)) : SizedBox.shrink(), Expanded( child: ConstrainedBox( @@ -535,7 +535,7 @@ class _AddressBookState extends State { Column( children: [ row( - lable: Row( + label: Row( children: [ Text( '*', @@ -556,9 +556,9 @@ class _AddressBookState extends State { : translate('ID'), errorText: errorMsg, errorMaxLines: 5), - ))), + ).workaroundFreezeLinuxMint())), row( - lable: Text( + label: Text( translate('Alias'), style: style, ), @@ -569,11 +569,11 @@ class _AddressBookState extends State { ? null : translate('Alias'), ), - )), + ).workaroundFreezeLinuxMint()), ), if (isCurrentAbShared) row( - lable: Text( + label: Text( translate('Password'), style: style, ), @@ -598,7 +598,7 @@ class _AddressBookState extends State { }, ), ), - ), + ).workaroundFreezeLinuxMint(), )), if (gFFI.abModel.currentAbTags.isNotEmpty) Align( @@ -704,7 +704,7 @@ class _AddressBookState extends State { ), controller: controller, autofocus: true, - ), + ).workaroundFreezeLinuxMint(), ), ], ), diff --git a/flutter/lib/common/widgets/autocomplete.dart b/flutter/lib/common/widgets/autocomplete.dart index 978d053df4b..ec64cca1892 100644 --- a/flutter/lib/common/widgets/autocomplete.dart +++ b/flutter/lib/common/widgets/autocomplete.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/formatter/id_formatter.dart'; import '../../../models/platform_model.dart'; @@ -6,56 +5,104 @@ import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/common/widgets/peer_card.dart'; -Future> getAllPeers() async { - Map recentPeers = jsonDecode(bind.mainLoadRecentPeersSync()); - Map lanPeers = jsonDecode(bind.mainLoadLanPeersSync()); - Map combinedPeers = {}; - - void mergePeers(Map peers) { - if (peers.containsKey("peers")) { - dynamic peerData = peers["peers"]; - - if (peerData is String) { - try { - peerData = jsonDecode(peerData); - } catch (e) { - print("Error decoding peers: $e"); - return; - } - } +class AllPeersLoader { + List peers = []; - if (peerData is List) { - for (var peer in peerData) { - if (peer is Map && peer.containsKey("id")) { - String id = peer["id"]; - if (!combinedPeers.containsKey(id)) { - combinedPeers[id] = peer; - } - } - } - } - } + bool _isPeersLoading = false; + bool _isPeersLoaded = false; + + final String _listenerKey = 'AllPeersLoader'; + + late void Function(VoidCallback) setState; + + bool get needLoad => !_isPeersLoaded && !_isPeersLoading; + bool get isPeersLoaded => _isPeersLoaded; + + AllPeersLoader(); + + void init(void Function(VoidCallback) setState) { + this.setState = setState; + gFFI.recentPeersModel.addListener(_mergeAllPeers); + gFFI.lanPeersModel.addListener(_mergeAllPeers); + gFFI.abModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); + gFFI.groupModel.addPeerUpdateListener(_listenerKey, _mergeAllPeers); } - mergePeers(recentPeers); - mergePeers(lanPeers); - for (var p in gFFI.abModel.allPeers()) { - if (!combinedPeers.containsKey(p.id)) { - combinedPeers[p.id] = p.toJson(); - } + void clear() { + gFFI.recentPeersModel.removeListener(_mergeAllPeers); + gFFI.lanPeersModel.removeListener(_mergeAllPeers); + gFFI.abModel.removePeerUpdateListener(_listenerKey); + gFFI.groupModel.removePeerUpdateListener(_listenerKey); } - for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) { - if (!combinedPeers.containsKey(p.id)) { - combinedPeers[p.id] = p.toJson(); + + Future getAllPeers() async { + if (!needLoad) { + return; + } + _isPeersLoading = true; + + if (gFFI.recentPeersModel.peers.isEmpty) { + bind.mainLoadRecentPeers(); + } + if (gFFI.lanPeersModel.peers.isEmpty) { + bind.mainLoadLanPeers(); + } + // No need to care about peers from abModel, and group model. + // Because they will pull data in `refreshCurrentUser()` on startup. + + final startTime = DateTime.now(); + _mergeAllPeers(); + final diffTime = DateTime.now().difference(startTime).inMilliseconds; + if (diffTime < 100) { + await Future.delayed(Duration(milliseconds: diffTime)); } } - List parsedPeers = []; + void _mergeAllPeers() { + Map combinedPeers = {}; + for (var p in gFFI.abModel.allPeers()) { + if (!combinedPeers.containsKey(p.id)) { + combinedPeers[p.id] = p.toJson(); + } + } + for (var p in gFFI.groupModel.peers.map((e) => Peer.copy(e)).toList()) { + if (!combinedPeers.containsKey(p.id)) { + combinedPeers[p.id] = p.toJson(); + } + } + + List parsedPeers = []; + for (var peer in combinedPeers.values) { + parsedPeers.add(Peer.fromJson(peer)); + } + + Set peerIds = combinedPeers.keys.toSet(); + for (final peer in gFFI.lanPeersModel.peers) { + if (!peerIds.contains(peer.id)) { + parsedPeers.add(peer); + peerIds.add(peer.id); + } + } + + for (final peer in gFFI.recentPeersModel.peers) { + if (!peerIds.contains(peer.id)) { + parsedPeers.add(peer); + peerIds.add(peer.id); + } + } + for (final id in gFFI.recentPeersModel.restPeerIds) { + if (!peerIds.contains(id)) { + parsedPeers.add(Peer.fromJson({'id': id})); + peerIds.add(id); + } + } - for (var peer in combinedPeers.values) { - parsedPeers.add(Peer.fromJson(peer)); + peers = parsedPeers; + setState(() { + _isPeersLoading = false; + _isPeersLoaded = true; + }); } - return parsedPeers; } class AutocompletePeerTile extends StatefulWidget { diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart index b6611d3ede3..4b0954d40b1 100644 --- a/flutter/lib/common/widgets/chat_page.dart +++ b/flutter/lib/common/widgets/chat_page.dart @@ -167,7 +167,7 @@ class ChatPage extends StatelessWidget implements PageShape { ); }, ), - ); + ).workaroundFreezeLinuxMint(); return SelectionArea(child: chat); }), ], diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart index cc3e0613105..fe0b799ac49 100644 --- a/flutter/lib/common/widgets/dialog.dart +++ b/flutter/lib/common/widgets/dialog.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_hbb/common/shared_state.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/consts.dart'; @@ -71,7 +70,7 @@ void changeIdDialog() { final rules = [ RegexValidationRule('starts with a letter', RegExp(r'^[a-zA-Z]')), LengthRangeValidationRule(6, 16), - RegexValidationRule('allowed characters', RegExp(r'^\w*$')) + RegexValidationRule('allowed characters', RegExp(r'^[\w-]*$')) ]; gFFI.dialogManager.show((setState, close, context) { @@ -140,7 +139,7 @@ void changeIdDialog() { msg = ''; }); }, - ), + ).workaroundFreezeLinuxMint(), const SizedBox( height: 8.0, ), @@ -201,13 +200,14 @@ void changeWhiteList({Function()? callback}) async { children: [ Expanded( child: TextField( - maxLines: null, - decoration: InputDecoration( - errorText: msg.isEmpty ? null : translate(msg), - ), - controller: controller, - enabled: !isOptFixed, - autofocus: true), + maxLines: null, + decoration: InputDecoration( + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: controller, + enabled: !isOptFixed, + autofocus: true) + .workaroundFreezeLinuxMint(), ), ], ), @@ -287,22 +287,23 @@ Future changeDirectAccessPort( children: [ Expanded( child: TextField( - maxLines: null, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: '21118', - isCollapsed: true, - prefix: Text('$currentIP : '), - suffix: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.clear, size: 16), - onPressed: () => controller.clear())), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp( - r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), - ], - controller: controller, - autofocus: true), + maxLines: null, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: '21118', + isCollapsed: true, + prefix: Text('$currentIP : '), + suffix: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.clear, size: 16), + onPressed: () => controller.clear())), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), + ], + controller: controller, + autofocus: true) + .workaroundFreezeLinuxMint(), ), ], ), @@ -335,21 +336,22 @@ Future changeAutoDisconnectTimeout(String old) async { children: [ Expanded( child: TextField( - maxLines: null, - keyboardType: TextInputType.number, - decoration: InputDecoration( - hintText: '10', - isCollapsed: true, - suffix: IconButton( - padding: EdgeInsets.zero, - icon: const Icon(Icons.clear, size: 16), - onPressed: () => controller.clear())), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp( - r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), - ], - controller: controller, - autofocus: true), + maxLines: null, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: '10', + isCollapsed: true, + suffix: IconButton( + padding: EdgeInsets.zero, + icon: const Icon(Icons.clear, size: 16), + onPressed: () => controller.clear())), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + r'^([0-9]|[1-9]\d|[1-9]\d{2}|[1-9]\d{3}|[1-5]\d{4}|6[0-4]\d{3}|65[0-4]\d{2}|655[0-2]\d|6553[0-5])$')), + ], + controller: controller, + autofocus: true) + .workaroundFreezeLinuxMint(), ), ], ), @@ -409,25 +411,39 @@ class DialogTextField extends StatelessWidget { return Row( children: [ Expanded( - child: TextField( - decoration: InputDecoration( - labelText: title, - hintText: hintText, - prefixIcon: prefixIcon, - suffixIcon: suffixIcon, - helperText: helperText, - helperMaxLines: 8, - errorText: errorText, - errorMaxLines: 8, - ), - controller: controller, - focusNode: focusNode, - autofocus: true, - obscureText: obscureText, - keyboardType: keyboardType, - inputFormatters: inputFormatters, - maxLength: maxLength, - ), + child: Column( + children: [ + TextField( + decoration: InputDecoration( + labelText: title, + hintText: hintText, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + helperText: helperText, + helperMaxLines: 8, + ), + controller: controller, + focusNode: focusNode, + autofocus: true, + obscureText: obscureText, + keyboardType: keyboardType, + inputFormatters: inputFormatters, + maxLength: maxLength, + ), + if (errorText != null) + Align( + alignment: Alignment.centerLeft, + child: SelectableText( + errorText!, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + textAlign: TextAlign.left, + ).paddingOnly(top: 8, left: 12), + ), + ], + ).workaroundFreezeLinuxMint(), ), ], ).paddingSymmetric(vertical: 4.0); @@ -803,23 +819,33 @@ void enterPasswordDialog( } void enterUserLoginDialog( - SessionID sessionId, OverlayDialogManager dialogManager) async { + SessionID sessionId, + OverlayDialogManager dialogManager, + String osAccountDescTip, + bool canRememberAccount) async { await _connectDialog( sessionId, dialogManager, osUsernameController: TextEditingController(), osPasswordController: TextEditingController(), + osAccountDescTip: osAccountDescTip, + canRememberAccount: canRememberAccount, ); } void enterUserLoginAndPasswordDialog( - SessionID sessionId, OverlayDialogManager dialogManager) async { + SessionID sessionId, + OverlayDialogManager dialogManager, + String osAccountDescTip, + bool canRememberAccount) async { await _connectDialog( sessionId, dialogManager, osUsernameController: TextEditingController(), osPasswordController: TextEditingController(), passwordController: TextEditingController(), + osAccountDescTip: osAccountDescTip, + canRememberAccount: canRememberAccount, ); } @@ -829,17 +855,28 @@ _connectDialog( TextEditingController? osUsernameController, TextEditingController? osPasswordController, TextEditingController? passwordController, + String? osAccountDescTip, + bool canRememberAccount = true, }) async { + final errUsername = ''.obs; var rememberPassword = false; if (passwordController != null) { rememberPassword = await bind.sessionGetRemember(sessionId: sessionId) ?? false; } var rememberAccount = false; - if (osUsernameController != null) { + if (canRememberAccount && osUsernameController != null) { rememberAccount = await bind.sessionGetRemember(sessionId: sessionId) ?? false; } + if (osUsernameController != null) { + osUsernameController.addListener(() { + if (errUsername.value.isNotEmpty) { + errUsername.value = ''; + } + }); + } + dialogManager.dismissAll(); dialogManager.show((setState, close, context) { cancel() { @@ -848,6 +885,13 @@ _connectDialog( } submit() { + if (osUsernameController != null) { + if (osUsernameController.text.trim().isEmpty) { + errUsername.value = translate('Empty Username'); + setState(() {}); + return; + } + } final osUsername = osUsernameController?.text.trim() ?? ''; final osPassword = osPasswordController?.text.trim() ?? ''; final password = passwordController?.text.trim() ?? ''; @@ -911,26 +955,39 @@ _connectDialog( } return Column( children: [ - descWidget(translate('login_linux_tip')), + if (osAccountDescTip != null) descWidget(translate(osAccountDescTip)), DialogTextField( title: translate(DialogTextField.kUsernameTitle), controller: osUsernameController, prefixIcon: DialogTextField.kUsernameIcon, errorText: null, ), + if (errUsername.value.isNotEmpty) + Align( + alignment: Alignment.centerLeft, + child: SelectableText( + errUsername.value, + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + textAlign: TextAlign.left, + ).paddingOnly(left: 12, bottom: 2), + ), PasswordWidget( controller: osPasswordController, autoFocus: false, ), - rememberWidget( - translate('remember_account_tip'), - rememberAccount, - (v) { - if (v != null) { - setState(() => rememberAccount = v); - } - }, - ), + if (canRememberAccount) + rememberWidget( + translate('remember_account_tip'), + rememberAccount, + (v) { + if (v != null) { + setState(() => rememberAccount = v); + } + }, + ), ], ); } @@ -1120,7 +1177,7 @@ void showRequestElevationDialog( DialogTextField( controller: userController, title: translate('Username'), - hintText: translate('eg: admin'), + hintText: translate('elevation_username_tip'), prefixIcon: DialogTextField.kUsernameIcon, errorText: errUser.isEmpty ? null : errUser.value, ), @@ -1501,7 +1558,7 @@ showAuditDialog(FFI ffi) async { maxLength: 256, controller: controller, focusNode: focusNode, - )), + ).workaroundFreezeLinuxMint()), actions: [ dialogButton('Cancel', onPressed: close, isOutline: true), dialogButton('OK', onPressed: submit) @@ -1607,6 +1664,28 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async { msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]); } +trackpadSpeedDialog(SessionID sessionId, FFI ffi) async { + int initSpeed = ffi.inputModel.trackpadSpeed; + final curSpeed = SimpleWrapper(initSpeed); + final btnClose = dialogButton('Close', onPressed: () async { + if (curSpeed.value <= kMaxTrackpadSpeed && + curSpeed.value >= kMinTrackpadSpeed && + curSpeed.value != initSpeed) { + await bind.sessionSetTrackpadSpeed( + sessionId: sessionId, value: curSpeed.value); + await ffi.inputModel.updateTrackpadSpeed(); + } + ffi.dialogManager.dismissAll(); + }); + msgBoxCommon( + ffi.dialogManager, + 'Trackpad speed', + TrackpadSpeedWidget( + value: curSpeed, + ), + [btnClose]); +} + void deleteConfirmDialog(Function onSubmit, String title) async { gFFI.dialogManager.show( (setState, close, context) { @@ -1748,7 +1827,7 @@ void renameDialog( autofocus: true, decoration: InputDecoration(labelText: translate('Name')), validator: validator, - ), + ).workaroundFreezeLinuxMint(), ), ), // NOT use Offstage to wrap LinearProgressIndicator @@ -1808,7 +1887,7 @@ void changeBot({Function()? callback}) async { decoration: InputDecoration( hintText: translate('Token'), ), - ); + ).workaroundFreezeLinuxMint(); return CustomAlertDialog( title: Text(translate("Telegram bot")), @@ -2178,7 +2257,7 @@ void setSharedAbPasswordDialog(String abName, Peer peer) { }, ), ), - ), + ).workaroundFreezeLinuxMint(), if (!gFFI.abModel.current.isPersonal()) Row(children: [ Icon(Icons.info, color: Colors.amber).marginOnly(right: 4), diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart index 71f3dacc3b2..c1f18f0a82d 100644 --- a/flutter/lib/common/widgets/login.dart +++ b/flutter/lib/common/widgets/login.dart @@ -166,10 +166,13 @@ class _WidgetOPState extends State { final String stateMsg = resultMap['state_msg']; String failedMsg = resultMap['failed_msg']; final String? url = resultMap['url']; + final bool urlLaunched = (resultMap['url_launched'] as bool?) ?? false; final authBody = resultMap['auth_body']; if (_stateMsg != stateMsg || _failedMsg != failedMsg) { if (_url.isEmpty && url != null && url.isNotEmpty) { - launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + if (!urlLaunched) { + launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication); + } _url = url; } if (authBody != null) { @@ -455,10 +458,14 @@ Future loginDialog() async { resp.user, resp.secret, isEmailVerification); } else { setState(() => isInProgress = false); + // Workaround for web, close the dialog first, then show the verification code dialog. + // Otherwise, the text field will keep selecting the text and we can't input the code. + // Not sure why this happens. + if (isWeb && close != null) close(null); final res = await verificationCodeDialog( resp.user, resp.secret, isEmailVerification); if (res == true) { - if (close != null) close(false); + if (!isWeb && close != null) close(false); return; } } @@ -678,7 +685,7 @@ Future verificationCodeDialog( labelText: "Email", prefixIcon: Icon(Icons.email)), readOnly: true, controller: TextEditingController(text: user?.email), - )), + ).workaroundFreezeLinuxMint()), isEmailVerification ? const SizedBox(height: 8) : const Offstage(), codeField, /* diff --git a/flutter/lib/common/widgets/my_group.dart b/flutter/lib/common/widgets/my_group.dart index 867d71dff2d..6207a736321 100644 --- a/flutter/lib/common/widgets/my_group.dart +++ b/flutter/lib/common/widgets/my_group.dart @@ -20,8 +20,11 @@ class MyGroup extends StatefulWidget { } class _MyGroupState extends State { - RxString get selectedUser => gFFI.groupModel.selectedUser; - RxString get searchUserText => gFFI.groupModel.searchUserText; + RxBool get isSelectedDeviceGroup => gFFI.groupModel.isSelectedDeviceGroup; + RxString get selectedAccessibleItemName => + gFFI.groupModel.selectedAccessibleItemName; + RxString get searchAccessibleItemNameText => + gFFI.groupModel.searchAccessibleItemNameText; static TextEditingController searchUserController = TextEditingController(); @override @@ -72,7 +75,7 @@ class _MyGroupState extends State { child: Container( width: double.infinity, height: double.infinity, - child: _buildUserContacts(), + child: _buildLeftList(), ), ) ], @@ -105,7 +108,7 @@ class _MyGroupState extends State { _buildLeftHeader(), Container( width: double.infinity, - child: _buildUserContacts(), + child: _buildLeftList(), ) ], ), @@ -130,7 +133,8 @@ class _MyGroupState extends State { child: TextField( controller: searchUserController, onChanged: (value) { - searchUserText.value = value; + searchAccessibleItemNameText.value = value; + selectedAccessibleItemName.value = ''; }, textAlignVertical: TextAlignVertical.center, style: TextStyle(fontSize: fontSize), @@ -145,25 +149,35 @@ class _MyGroupState extends State { border: InputBorder.none, isDense: true, ), - )), + ).workaroundFreezeLinuxMint()), ], ); } - Widget _buildUserContacts() { + Widget _buildLeftList() { return Obx(() { - final items = gFFI.groupModel.users.where((p0) { - if (searchUserText.isNotEmpty) { + final userItems = gFFI.groupModel.users.where((p0) { + if (searchAccessibleItemNameText.isNotEmpty) { return p0.name .toLowerCase() - .contains(searchUserText.value.toLowerCase()); + .contains(searchAccessibleItemNameText.value.toLowerCase()); + } + return true; + }).toList(); + final deviceGroupItems = gFFI.groupModel.deviceGroups.where((p0) { + if (searchAccessibleItemNameText.isNotEmpty) { + return p0.name + .toLowerCase() + .contains(searchAccessibleItemNameText.value.toLowerCase()); } return true; }).toList(); listView(bool isPortrait) => ListView.builder( shrinkWrap: isPortrait, - itemCount: items.length, - itemBuilder: (context, index) => _buildUserItem(items[index])); + itemCount: deviceGroupItems.length + userItems.length, + itemBuilder: (context, index) => index < deviceGroupItems.length + ? _buildDeviceGroupItem(deviceGroupItems[index]) + : _buildUserItem(userItems[index - deviceGroupItems.length])); var maxHeight = max(MediaQuery.of(context).size.height / 6, 100.0); return Obx(() => stateGlobal.isPortrait.isFalse ? listView(false) @@ -174,14 +188,16 @@ class _MyGroupState extends State { Widget _buildUserItem(UserPayload user) { final username = user.name; return InkWell(onTap: () { - if (selectedUser.value != username) { - selectedUser.value = username; + isSelectedDeviceGroup.value = false; + if (selectedAccessibleItemName.value != username) { + selectedAccessibleItemName.value = username; } else { - selectedUser.value = ''; + selectedAccessibleItemName.value = ''; } }, child: Obx( () { - bool selected = selectedUser.value == username; + bool selected = !isSelectedDeviceGroup.value && + selectedAccessibleItemName.value == username; final isMe = username == gFFI.userModel.userName.value; final colorMe = MyTheme.color(context).me!; return Container( @@ -238,4 +254,43 @@ class _MyGroupState extends State { }, )).marginSymmetric(horizontal: 12).marginOnly(bottom: 6); } + + Widget _buildDeviceGroupItem(DeviceGroupPayload deviceGroup) { + final name = deviceGroup.name; + return InkWell(onTap: () { + isSelectedDeviceGroup.value = true; + if (selectedAccessibleItemName.value != name) { + selectedAccessibleItemName.value = name; + } else { + selectedAccessibleItemName.value = ''; + } + }, child: Obx( + () { + bool selected = isSelectedDeviceGroup.value && + selectedAccessibleItemName.value == name; + 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: [ + Container( + width: 20, + height: 20, + child: Icon(IconFont.deviceGroupOutline, + color: MyTheme.accent, size: 19), + ).marginOnly(right: 4), + Expanded(child: Text(name)), + ], + ).paddingSymmetric(vertical: 4), + ), + ); + }, + )).marginSymmetric(horizontal: 12).marginOnly(bottom: 6); + } } diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart index 0a15eb45b88..db9f7af008f 100644 --- a/flutter/lib/common/widgets/peer_card.dart +++ b/flutter/lib/common/widgets/peer_card.dart @@ -488,8 +488,11 @@ abstract class BasePeerCard extends StatelessWidget { BuildContext context, String title, { bool isFileTransfer = false, + bool isViewCamera = false, bool isTcpTunneling = false, bool isRDP = false, + bool isTerminal = false, + bool isTerminalRunAsAdmin = false, }) { return MenuEntryButton( childBuilder: (TextStyle? style) => Text( @@ -497,13 +500,18 @@ abstract class BasePeerCard extends StatelessWidget { style: style, ), proc: () { + if (isTerminalRunAsAdmin) { + setEnvTerminalAdmin(); + } connectInPeerTab( context, peer, tab, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP, + isTerminal: isTerminal || isTerminalRunAsAdmin, ); }, padding: menuPadding, @@ -530,6 +538,33 @@ abstract class BasePeerCard extends StatelessWidget { ); } + @protected + MenuEntryBase _viewCameraAction(BuildContext context) { + return _connectCommonAction( + context, + translate('View camera'), + isViewCamera: true, + ); + } + + @protected + MenuEntryBase _terminalAction(BuildContext context) { + return _connectCommonAction( + context, + '${translate('Terminal')} (beta)', + isTerminal: true, + ); + } + + @protected + MenuEntryBase _terminalRunAsAdminAction(BuildContext context) { + return _connectCommonAction( + context, + '${translate('Terminal (Run as administrator)')} (beta)', + isTerminalRunAsAdmin: true, + ); + } + @protected MenuEntryBase _tcpTunnelingAction(BuildContext context) { return _connectCommonAction( @@ -716,18 +751,18 @@ abstract class BasePeerCard extends StatelessWidget { switch (tab) { case PeerTabIndex.recent: await bind.mainRemovePeer(id: id); - await bind.mainLoadRecentPeers(); + bind.mainLoadRecentPeers(); break; case PeerTabIndex.fav: final favs = (await bind.mainGetFav()).toList(); if (favs.remove(id)) { await bind.mainStoreFav(favs: favs); - await bind.mainLoadFavPeers(); + bind.mainLoadFavPeers(); } break; case PeerTabIndex.lan: await bind.mainRemoveDiscovered(id: id); - await bind.mainLoadLanPeers(); + bind.mainLoadLanPeers(); break; case PeerTabIndex.ab: await gFFI.abModel.deletePeers([id]); @@ -880,8 +915,14 @@ class RecentPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), ]; + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + final List favs = (await bind.mainGetFav()).toList(); if (isDesktop && peer.platform != kPeerPlatformAndroid) { @@ -939,7 +980,14 @@ class FavoritePeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); } @@ -992,8 +1040,14 @@ class DiscoveredPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), ]; + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + final List favs = (await bind.mainGetFav()).toList(); if (isDesktop && peer.platform != kPeerPlatformAndroid) { @@ -1045,12 +1099,21 @@ class AddressBookPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); } // menuItems.add(await _openNewConnInOptAction(peer.id)); - // menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (!isWeb) { + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + } if (isWindows && peer.platform == kPeerPlatformWindows) { menuItems.add(_rdpAction(context, peer.id)); } @@ -1177,12 +1240,21 @@ class MyGroupPeerCard extends BasePeerCard { final List> menuItems = [ _connectAction(context), _transferFileAction(context), + _viewCameraAction(context), + _terminalAction(context), ]; + + if (peer.platform == kPeerPlatformWindows) { + menuItems.add(_terminalRunAsAdminAction(context)); + } + if (isDesktop && peer.platform != kPeerPlatformAndroid) { menuItems.add(_tcpTunnelingAction(context)); } // menuItems.add(await _openNewConnInOptAction(peer.id)); - // menuItems.add(await _forceAlwaysRelayAction(peer.id)); + if (!isWeb) { + menuItems.add(await _forceAlwaysRelayAction(peer.id)); + } if (isWindows && peer.platform == kPeerPlatformWindows) { menuItems.add(_rdpAction(context, peer.id)); } @@ -1257,7 +1329,7 @@ void _rdpDialog(String id) async { hintText: '3389'), controller: portController, autofocus: true, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: isDesktop ? 8 : 0), @@ -1277,7 +1349,7 @@ void _rdpDialog(String id) async { labelText: isDesktop ? null : translate('Username')), controller: userController, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: stateGlobal.isPortrait.isFalse ? 8 : 0)), @@ -1305,7 +1377,7 @@ void _rdpDialog(String id) async { ? Icons.visibility_off : Icons.visibility))), controller: passwordController, - )), + ).workaroundFreezeLinuxMint()), ), ], )) @@ -1398,8 +1470,10 @@ class TagPainter extends CustomPainter { void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, {bool isFileTransfer = false, + bool isViewCamera = false, bool isTcpTunneling = false, - bool isRDP = false}) async { + bool isRDP = false, + bool isTerminal = false}) async { var password = ''; bool isSharedPassword = false; if (tab == PeerTabIndex.ab) { @@ -1423,6 +1497,8 @@ void connectInPeerTab(BuildContext context, Peer peer, PeerTabIndex tab, password: password, isSharedPassword: isSharedPassword, isFileTransfer: isFileTransfer, + isTerminal: isTerminal, + isViewCamera: isViewCamera, isTcpTunneling: isTcpTunneling, isRDP: isRDP); } diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart index 35975078805..4849f278327 100644 --- a/flutter/lib/common/widgets/peer_tab_page.dart +++ b/flutter/lib/common/widgets/peer_tab_page.dart @@ -33,8 +33,8 @@ class PeerTabPage extends StatefulWidget { class _TabEntry { final Widget widget; - final Function({dynamic hint}) load; - _TabEntry(this.widget, this.load); + final Function({dynamic hint})? load; + _TabEntry(this.widget, [this.load]); } EdgeInsets? _menuPadding() { @@ -44,21 +44,15 @@ EdgeInsets? _menuPadding() { class _PeerTabPageState extends State with SingleTickerProviderStateMixin { final List<_TabEntry> entries = [ - _TabEntry( - RecentPeersView( - menuPadding: _menuPadding(), - ), - bind.mainLoadRecentPeers), - _TabEntry( - FavoritePeersView( - menuPadding: _menuPadding(), - ), - bind.mainLoadFavPeers), - _TabEntry( - DiscoveredPeersView( - menuPadding: _menuPadding(), - ), - bind.mainDiscover), + _TabEntry(RecentPeersView( + menuPadding: _menuPadding(), + )), + _TabEntry(FavoritePeersView( + menuPadding: _menuPadding(), + )), + _TabEntry(DiscoveredPeersView( + menuPadding: _menuPadding(), + )), _TabEntry( AddressBook( menuPadding: _menuPadding(), @@ -100,7 +94,7 @@ class _PeerTabPageState extends State gFFI.peerTabModel.setCurrentTabCachedPeers([]); } gFFI.peerTabModel.setCurrentTab(tabIndex); - entries[tabIndex].load(hint: false); + entries[tabIndex].load?.call(hint: false); } } @@ -225,7 +219,7 @@ class _PeerTabPageState extends State child: RefreshWidget( onPressed: () { if (gFFI.peerTabModel.currentTab < entries.length) { - entries[gFFI.peerTabModel.currentTab].load(); + entries[gFFI.peerTabModel.currentTab].load?.call(); } }, spinning: loading, @@ -404,7 +398,7 @@ class _PeerTabPageState extends State for (var p in peers) { await bind.mainRemovePeer(id: p.id); } - await bind.mainLoadRecentPeers(); + bind.mainLoadRecentPeers(); break; case 1: final favs = (await bind.mainGetFav()).toList(); @@ -412,13 +406,13 @@ class _PeerTabPageState extends State favs.remove(p.id); }).toList(); await bind.mainStoreFav(favs: favs); - await bind.mainLoadFavPeers(); + bind.mainLoadFavPeers(); break; case 2: for (var p in peers) { await bind.mainRemoveDiscovered(id: p.id); } - await bind.mainLoadLanPeers(); + bind.mainLoadLanPeers(); break; case 3: await gFFI.abModel.deletePeers(peers.map((p) => p.id).toList()); @@ -743,7 +737,7 @@ class _PeerSearchBarState extends State { border: InputBorder.none, isDense: true, ), - ), + ).workaroundFreezeLinuxMint(), ), // Icon(Icons.close), IconButton( diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart index 3e34f882d1d..94f4af035a6 100644 --- a/flutter/lib/common/widgets/peers_view.dart +++ b/flutter/lib/common/widgets/peers_view.dart @@ -25,13 +25,13 @@ class PeerSortType { static const String remoteId = 'Remote ID'; static const String remoteHost = 'Remote Host'; static const String username = 'Username'; - // static const String status = 'Status'; + static const String status = 'Status'; static List values = [ PeerSortType.remoteId, PeerSortType.remoteHost, PeerSortType.username, - // PeerSortType.status + PeerSortType.status ]; } @@ -384,9 +384,9 @@ class _PeersViewState extends State<_PeersView> peers.sort((p1, p2) => p1.username.toLowerCase().compareTo(p2.username.toLowerCase())); break; - // case PeerSortType.status: - // peers.sort((p1, p2) => p1.online ? -1 : 1); - // break; + case PeerSortType.status: + peers.sort((p1, p2) => p1.online ? -1 : 1); + break; } } @@ -501,6 +501,7 @@ class DiscoveredPeersView extends BasePeersView { Widget build(BuildContext context) { final widget = super.build(context); bind.mainLoadLanPeers(); + bind.mainDiscover(); return widget; } } @@ -562,14 +563,26 @@ class MyGroupPeerView extends BasePeersView { ); static bool filter(Peer peer) { - if (gFFI.groupModel.searchUserText.isNotEmpty) { - if (!peer.loginName.contains(gFFI.groupModel.searchUserText)) { + final model = gFFI.groupModel; + if (model.searchAccessibleItemNameText.isNotEmpty) { + final text = model.searchAccessibleItemNameText.value; + final searchPeersOfUser = peer.loginName.contains(text) && + model.users.any((user) => user.name == peer.loginName); + final searchPeersOfDeviceGroup = peer.device_group_name.contains(text) && + model.deviceGroups.any((g) => g.name == peer.device_group_name); + if (!searchPeersOfUser && !searchPeersOfDeviceGroup) { return false; } } - if (gFFI.groupModel.selectedUser.isNotEmpty) { - if (gFFI.groupModel.selectedUser.value != peer.loginName) { - return false; + if (model.selectedAccessibleItemName.isNotEmpty) { + if (model.isSelectedDeviceGroup.value) { + if (model.selectedAccessibleItemName.value != peer.device_group_name) { + return false; + } + } else { + if (model.selectedAccessibleItemName.value != peer.loginName) { + return false; + } } } return true; diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart index c31350b047c..8eb0ecbc355 100644 --- a/flutter/lib/common/widgets/remote_input.dart +++ b/flutter/lib/common/widgets/remote_input.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -53,13 +54,14 @@ class RawKeyFocusScope extends StatelessWidget { class RawTouchGestureDetectorRegion extends StatefulWidget { final Widget child; final FFI ffi; - + final bool isCamera; late final InputModel inputModel = ffi.inputModel; late final FfiModel ffiModel = ffi.ffiModel; RawTouchGestureDetectorRegion({ required this.child, required this.ffi, + this.isCamera = false, }); @override @@ -109,9 +111,13 @@ class _RawTouchGestureDetectorRegionState ); } + bool isNotTouchBasedDevice() { + return !kTouchBasedDeviceKinds.contains(lastDeviceKind); + } + onTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -124,7 +130,7 @@ class _RawTouchGestureDetectorRegionState onTapUp(TapUpDetails d) async { final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; _lastTapDownDetails = null; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -140,7 +146,7 @@ class _RawTouchGestureDetectorRegionState } onTap() async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (!handleTouch) { @@ -151,7 +157,7 @@ class _RawTouchGestureDetectorRegionState onDoubleTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -161,7 +167,7 @@ class _RawTouchGestureDetectorRegionState } onDoubleTap() async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (ffiModel.touchMode && ffi.cursorModel.lastIsBlocked) { @@ -177,7 +183,7 @@ class _RawTouchGestureDetectorRegionState onLongPressDown(LongPressDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -187,11 +193,16 @@ class _RawTouchGestureDetectorRegionState return; } _cacheLongPressPositionTs = DateTime.now().millisecondsSinceEpoch; + if (ffiModel.isPeerMobile) { + await ffi.cursorModel + .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + await inputModel.tapDown(MouseButtons.left); + } } } onLongPressUp() async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -201,24 +212,40 @@ class _RawTouchGestureDetectorRegionState // for mobiles onLongPress() async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { + return; + } + if (!ffi.ffiModel.isPeerMobile) { + if (handleTouch) { + final isMoved = await ffi.cursorModel + .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); + if (!isMoved) { + return; + } + } + await inputModel.tap(MouseButtons.right); + } else { + // It's better to send a message to tell the controlled device that the long press event is triggered. + // We're now using a `TimerTask` in `InputService.kt` to decide whether to trigger the long press event. + // It's not accurate and it's better to use the same detection logic in the controlling side. + } + } + + onLongPressMoveUpdate(LongPressMoveUpdateDetails d) async { + if (!ffiModel.isPeerMobile || isNotTouchBasedDevice()) { return; } if (handleTouch) { - final isMoved = await ffi.cursorModel - .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); - if (!isMoved) { + if (!ffi.cursorModel.isInRemoteRect(d.localPosition)) { return; } - } - if (!ffi.ffiModel.isPeerMobile) { - await inputModel.tap(MouseButtons.right); + await ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } } onDoubleFinerTapDown(TapDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } _doubleFinerTapPosition = d.localPosition; @@ -227,7 +254,7 @@ class _RawTouchGestureDetectorRegionState onDoubleFinerTap(TapDownDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } @@ -243,7 +270,7 @@ class _RawTouchGestureDetectorRegionState onHoldDragStart(DragStartDetails d) async { lastDeviceKind = d.kind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (!handleTouch) { @@ -252,7 +279,7 @@ class _RawTouchGestureDetectorRegionState } onHoldDragUpdate(DragUpdateDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (!handleTouch) { @@ -261,7 +288,7 @@ class _RawTouchGestureDetectorRegionState } onHoldDragEnd(DragEndDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (!handleTouch) { @@ -273,7 +300,7 @@ class _RawTouchGestureDetectorRegionState final TapDownDetails? lastTapDownDetails = _lastTapDownDetails; _lastTapDownDetails = null; lastDeviceKind = d.kind ?? lastDeviceKind; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (handleTouch) { @@ -319,7 +346,7 @@ class _RawTouchGestureDetectorRegionState } onOneFingerPanUpdate(DragUpdateDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (ffi.cursorModel.shouldBlock(d.localPosition.dx, d.localPosition.dy)) { @@ -333,25 +360,27 @@ class _RawTouchGestureDetectorRegionState onOneFingerPanEnd(DragEndDetails d) async { _touchModePanStarted = false; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if (isDesktop || isWebDesktop) { ffi.cursorModel.clearRemoteWindowCoords(); } - await inputModel.sendMouse('up', MouseButtons.left); + if (handleTouch) { + await inputModel.sendMouse('up', MouseButtons.left); + } } // scale + pan event onTwoFingerScaleStart(ScaleStartDetails d) { _lastTapDownDetails = null; - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } } onTwoFingerScaleUpdate(ScaleUpdateDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if ((isDesktop || isWebDesktop)) { @@ -359,6 +388,7 @@ class _RawTouchGestureDetectorRegionState _scale = d.scale; if (scale != 0) { + if (widget.isCamera) return; await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( @@ -375,10 +405,11 @@ class _RawTouchGestureDetectorRegionState } onTwoFingerScaleEnd(ScaleEndDetails d) async { - if (lastDeviceKind != PointerDeviceKind.touch) { + if (isNotTouchBasedDevice()) { return; } if ((isDesktop || isWebDesktop)) { + if (widget.isCamera) return; await bind.sessionSendPointer( sessionId: sessionId, msg: json.encode( @@ -430,7 +461,8 @@ class _RawTouchGestureDetectorRegionState instance ..onLongPressDown = onLongPressDown ..onLongPressUp = onLongPressUp - ..onLongPress = onLongPress; + ..onLongPress = onLongPress + ..onLongPressMoveUpdate = onLongPressMoveUpdate; }), // Customized HoldTapMoveGestureRecognizer: @@ -512,3 +544,46 @@ class RawPointerMouseRegion extends StatelessWidget { ); } } + +class CameraRawPointerMouseRegion extends StatelessWidget { + final InputModel inputModel; + final Widget child; + final PointerEnterEventListener? onEnter; + final PointerExitEventListener? onExit; + final PointerDownEventListener? onPointerDown; + final PointerUpEventListener? onPointerUp; + + CameraRawPointerMouseRegion({ + this.onEnter, + this.onExit, + this.onPointerDown, + this.onPointerUp, + required this.inputModel, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerHover: (evt) { + final offset = evt.position; + double x = offset.dx; + double y = max(0.0, offset.dy); + inputModel.handlePointerDevicePos( + kPointerEventKindMouse, x, y, true, kMouseEventTypeDefault); + }, + onPointerDown: (evt) { + onPointerDown?.call(evt); + }, + onPointerUp: (evt) { + onPointerUp?.call(evt); + }, + child: MouseRegion( + cursor: MouseCursor.defer, + onEnter: onEnter, + onExit: onExit, + child: child, + ), + ); + } +} diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart index 5bcb73a4c5e..b57657274a2 100644 --- a/flutter/lib/common/widgets/setting_widgets.dart +++ b/flutter/lib/common/widgets/setting_widgets.dart @@ -243,8 +243,99 @@ List<(String, String)> otherDefaultSettings() { ( 'Use all my displays for the remote session', kKeyUseAllMyDisplaysForTheRemoteSession - ) + ), + ('Keep terminal sessions on disconnect', kOptionTerminalPersistent), ]; return v; } + +class TrackpadSpeedWidget extends StatefulWidget { + final SimpleWrapper value; + // If null, no debouncer will be applied. + final Function(int)? onDebouncer; + + TrackpadSpeedWidget({Key? key, required this.value, this.onDebouncer}); + + @override + TrackpadSpeedWidgetState createState() => TrackpadSpeedWidgetState(); +} + +class TrackpadSpeedWidgetState extends State { + final TextEditingController _controller = TextEditingController(); + late final Debouncer debouncerSpeed; + + set value(int v) => widget.value.value = v; + int get value => widget.value.value; + + void updateValue(int newValue) { + setState(() { + value = newValue.clamp(kMinTrackpadSpeed, kMaxTrackpadSpeed); + // Scale the trackpad speed value to a percentage for display purposes. + _controller.text = value.toString(); + if (widget.onDebouncer != null) { + debouncerSpeed.setValue(value); + } + }); + } + + @override + void initState() { + super.initState(); + debouncerSpeed = Debouncer( + Duration(milliseconds: 1000), + onChanged: widget.onDebouncer, + initialValue: widget.value.value, + ); + } + + @override + Widget build(BuildContext context) { + if (_controller.text.isEmpty) { + _controller.text = value.toString(); + } + return Row( + children: [ + Expanded( + flex: 3, + child: Slider( + value: value.toDouble(), + min: kMinTrackpadSpeed.toDouble(), + max: kMaxTrackpadSpeed.toDouble(), + divisions: ((kMaxTrackpadSpeed - kMinTrackpadSpeed) / 10).round(), + onChanged: (double v) => updateValue(v.round()), + ), + ), + Expanded( + flex: 1, + child: Row( + children: [ + SizedBox( + width: 56, + child: TextField( + controller: _controller, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + onSubmitted: (text) { + int? v = int.tryParse(text); + if (v != null) { + updateValue(v); + } + }, + style: const TextStyle(fontSize: 13), + decoration: InputDecoration( + contentPadding: + EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0), + ), + ), + ).marginOnly(right: 8.0), + Text( + '%', + style: const TextStyle(fontSize: 15), + ) + ], + )), + ], + ); + } +} diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart index 153121057e5..cf5ed5c97bb 100644 --- a/flutter/lib/common/widgets/toolbar.dart +++ b/flutter/lib/common/widgets/toolbar.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; @@ -15,7 +16,7 @@ bool isEditOsPassword = false; class TTextMenu { final Widget child; - final VoidCallback onPressed; + final VoidCallback? onPressed; Widget? trailingIcon; bool divider; TTextMenu( @@ -89,10 +90,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { final pi = ffiModel.pi; final perms = ffiModel.permissions; final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; List v = []; // elevation - if (perms['keyboard'] != false && ffi.elevationModel.showRequestMenu) { + if (isDefaultConn && + perms['keyboard'] != false && + ffi.elevationModel.showRequestMenu) { v.add( TTextMenu( child: Text(translate('Request Elevation')), @@ -101,7 +105,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // osAccount / osPassword - if (perms['keyboard'] != false) { + if (isDefaultConn && perms['keyboard'] != false) { v.add( TTextMenu( child: Row(children: [ @@ -130,7 +134,9 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // paste - if (pi.platform != kPeerPlatformAndroid && perms['keyboard'] != false) { + if (isDefaultConn && + pi.platform != kPeerPlatformAndroid && + perms['keyboard'] != false) { v.add(TTextMenu( child: Text(translate('Send clipboard keystrokes')), onPressed: () async { @@ -142,43 +148,55 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { })); } // reset canvas - if (isMobile) { + if (isDefaultConn && isMobile) { v.add(TTextMenu( child: Text(translate('Reset canvas')), onPressed: () => ffi.cursorModel.reset())); } + // https://github.com/rustdesk/rustdesk/pull/9731 + // Does not work for connection established by "accept". connectWithToken( - {required bool isFileTransfer, required bool isTcpTunneling}) { + {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTcpTunneling = false, + bool isTerminal = false}) { final connToken = bind.sessionGetConnToken(sessionId: ffi.sessionId); connect(context, id, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTerminal: isTerminal, isTcpTunneling: isTcpTunneling, connToken: connToken); } - // transferFile - if (isDesktop) { + if (isDefaultConn && isDesktop) { v.add( TTextMenu( child: Text(translate('Transfer file')), - onPressed: () => - connectWithToken(isFileTransfer: true, isTcpTunneling: false)), + onPressed: () => connectWithToken(isFileTransfer: true)), + ); + v.add( + TTextMenu( + child: Text(translate('View camera')), + onPressed: () => connectWithToken(isViewCamera: true)), + ); + v.add( + TTextMenu( + child: Text('${translate('Terminal')} (beta)'), + onPressed: () => connectWithToken(isTerminal: true)), ); - } - // tcpTunneling - if (isDesktop) { v.add( TTextMenu( child: Text(translate('TCP tunneling')), - onPressed: () => - connectWithToken(isFileTransfer: false, isTcpTunneling: true)), + onPressed: () => connectWithToken(isTcpTunneling: true)), ); } // note - if (bind - .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn") - .isNotEmpty) { + if (isDefaultConn && + bind + .sessionGetAuditServerSync(sessionId: sessionId, typ: "conn") + .isNotEmpty) { v.add( TTextMenu( child: Text(translate('Note')), @@ -186,11 +204,12 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // divider - if (isDesktop || isWebDesktop) { + if (isDefaultConn && (isDesktop || isWebDesktop)) { v.add(TTextMenu(child: Offstage(), onPressed: () {}, divider: true)); } // ctrlAltDel - if (!ffiModel.viewOnly && + if (isDefaultConn && + !ffiModel.viewOnly && ffiModel.keyboard && (pi.platform == kPeerPlatformLinux || pi.sasEnabled)) { v.add( @@ -200,7 +219,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // restart - if (perms['restart'] != false && + if (isDefaultConn && + perms['restart'] != false && (pi.platform == kPeerPlatformLinux || pi.platform == kPeerPlatformWindows || pi.platform == kPeerPlatformMacOS)) { @@ -212,7 +232,7 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // insertLock - if (!ffiModel.viewOnly && ffi.ffiModel.keyboard) { + if (isDefaultConn && !ffiModel.viewOnly && ffi.ffiModel.keyboard) { v.add( TTextMenu( child: Text(translate('Insert Lock')), @@ -220,7 +240,8 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ); } // blockUserInput - if (ffi.ffiModel.keyboard && + if (isDefaultConn && + ffi.ffiModel.keyboard && ffi.ffiModel.permissions['block_input'] != false && pi.platform == kPeerPlatformWindows) // privacy-mode != true ?? { @@ -236,12 +257,13 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { })); } // switchSides - if (isDesktop && + if (isDefaultConn && + isDesktop && ffiModel.keyboard && pi.platform != kPeerPlatformAndroid && pi.platform != kPeerPlatformMacOS && versionCmp(pi.version, '1.2.0') >= 0 && - bind.peerGetDefaultSessionsCount(id: id) == 1) { + bind.peerGetSessionsCount(id: id, connType: ffi.connType.index) == 1) { v.add(TTextMenu( child: Text(translate('Switch Sides')), onPressed: () => @@ -275,6 +297,41 @@ List toolbarControls(BuildContext context, String id, FFI ffi) { ), onPressed: () => ffi.recordingModel.toggle())); } + + // to-do: + // 1. Web desktop + // 2. Mobile, copy the image to the clipboard + if (isDesktop) { + final isScreenshotSupported = bind.sessionGetCommonSync( + sessionId: sessionId, key: 'is_screenshot_supported', param: ''); + if ('true' == isScreenshotSupported) { + v.add(TTextMenu( + child: Text(ffi.ffiModel.timerScreenshot != null + ? '${translate('Taking screenshot')} ...' + : translate('Take screenshot')), + onPressed: ffi.ffiModel.timerScreenshot != null + ? null + : () { + if (pi.currentDisplay == kAllDisplayValue) { + msgBox( + sessionId, + 'custom-nook-nocancel-hasclose-info', + 'Take screenshot', + 'screenshot-merged-screen-not-supported-tip', + '', + ffi.dialogManager); + } else { + bind.sessionTakeScreenshot( + sessionId: sessionId, display: pi.currentDisplay); + ffi.ffiModel.timerScreenshot = + Timer(Duration(seconds: 30), () { + ffi.ffiModel.timerScreenshot = null; + }); + } + }, + )); + } + } // fingerprint if (!(isDesktop || isWebDesktop)) { v.add(TTextMenu( @@ -523,6 +580,7 @@ Future> toolbarDisplayToggle( final pi = ffiModel.pi; final perms = ffiModel.permissions; final sessionId = ffi.sessionId; + final isDefaultConn = ffi.connType == ConnType.defaultConn; // show quality monitor final option = 'show-quality-monitor'; @@ -535,7 +593,7 @@ Future> toolbarDisplayToggle( }, child: Text(translate('Show quality monitor')))); // mute - if (perms['audio'] != false) { + if (isDefaultConn && perms['audio'] != false) { final option = 'disable-audio'; final value = bind.sessionGetToggleOptionSync(sessionId: sessionId, arg: option); @@ -556,7 +614,8 @@ Future> toolbarDisplayToggle( final isSupportIfPeer_1_2_4 = versionCmp(pi.version, '1.2.4') >= 0 && bind.mainHasFileClipboard() && pi.platformAdditions.containsKey(kPlatformAdditionsHasFileClipboard); - if (ffiModel.keyboard && + if (isDefaultConn && + ffiModel.keyboard && perms['file'] != false && (isSupportIfPeer_1_2_3 || isSupportIfPeer_1_2_4)) { final enabled = !ffiModel.viewOnly; @@ -574,7 +633,7 @@ Future> toolbarDisplayToggle( child: Text(translate('Enable file copy and paste')))); } // disable clipboard - if (ffiModel.keyboard && perms['clipboard'] != false) { + if (isDefaultConn && ffiModel.keyboard && perms['clipboard'] != false) { final enabled = !ffiModel.viewOnly; final option = 'disable-clipboard'; var value = @@ -591,7 +650,7 @@ Future> toolbarDisplayToggle( child: Text(translate('Disable clipboard')))); } // lock after session end - if (ffiModel.keyboard && !ffiModel.isPeerAndroid) { + if (isDefaultConn && ffiModel.keyboard && !ffiModel.isPeerAndroid) { final enabled = !ffiModel.viewOnly; final option = 'lock-after-session-end'; final value = @@ -656,12 +715,12 @@ Future> toolbarDisplayToggle( child: Text(translate('True color (4:4:4)')))); } - if (isMobile) { + if (isDefaultConn && isMobile) { v.addAll(toolbarKeyboardToggles(ffi)); } // view mode (mobile only, desktop is in keyboard menu) - if (isMobile && versionCmp(pi.version, '1.2.0') >= 0) { + if (isDefaultConn && isMobile && versionCmp(pi.version, '1.2.0') >= 0) { v.add(TToggleMenu( value: ffiModel.viewOnly, onChanged: (value) async { diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart index 95b207826a7..eda0e11cff4 100644 --- a/flutter/lib/consts.dart +++ b/flutter/lib/consts.dart @@ -1,3 +1,4 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -44,7 +45,9 @@ const String kAppTypeConnectionManager = "cm"; const String kAppTypeDesktopRemote = "remote"; const String kAppTypeDesktopFileTransfer = "file transfer"; +const String kAppTypeDesktopViewCamera = "view camera"; const String kAppTypeDesktopPortForward = "port forward"; +const String kAppTypeDesktopTerminal = "terminal"; const String kWindowMainWindowOnTop = "main_window_on_top"; const String kWindowGetWindowInfo = "get_window_info"; @@ -58,7 +61,10 @@ const String kWindowConnect = "connect"; const String kWindowEventNewRemoteDesktop = "new_remote_desktop"; const String kWindowEventNewFileTransfer = "new_file_transfer"; +const String kWindowEventNewViewCamera = "new_view_camera"; const String kWindowEventNewPortForward = "new_port_forward"; +const String kWindowEventNewTerminal = "new_terminal"; +const String kWindowEventRestoreTerminalSessions = "restore_terminal_sessions"; const String kWindowEventActiveSession = "active_session"; const String kWindowEventActiveDisplaySession = "active_display_session"; const String kWindowEventGetRemoteList = "get_remote_list"; @@ -75,6 +81,7 @@ const String kOptionScrollStyle = "scroll_style"; const String kOptionImageQuality = "image_quality"; const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs"; const String kOptionTextureRender = "use-texture-render"; +const String kOptionD3DRender = "allow-d3d-render"; const String kOptionOpenInTabs = "allow-open-in-tabs"; const String kOptionOpenInWindows = "allow-open-in-windows"; const String kOptionForceAlwaysRelay = "force-always-relay"; @@ -94,9 +101,13 @@ const String kOptionVideoSaveDirectory = "video-save-directory"; const String kOptionAccessMode = "access-mode"; const String kOptionEnableKeyboard = "enable-keyboard"; // "Settings -> Security -> Permissions" +const String kOptionEnableRemotePrinter = "enable-remote-printer"; const String kOptionEnableClipboard = "enable-clipboard"; const String kOptionEnableFileTransfer = "enable-file-transfer"; const String kOptionEnableAudio = "enable-audio"; +const String kOptionEnableCamera = "enable-camera"; +const String kOptionEnableTerminal = "enable-terminal"; +const String kOptionTerminalPersistent = "terminal-persistent"; const String kOptionEnableTunnel = "enable-tunnel"; const String kOptionEnableRemoteRestart = "enable-remote-restart"; const String kOptionEnableBlockInput = "enable-block-input"; @@ -104,6 +115,8 @@ const String kOptionAllowRemoteConfigModification = "allow-remote-config-modification"; const String kOptionVerificationMethod = "verification-method"; const String kOptionApproveMode = "approve-mode"; +const String kOptionAllowNumericOneTimePassword = + "allow-numeric-one-time-password"; const String kOptionCollapseToolbar = "collapse_toolbar"; const String kOptionShowRemoteCursor = "show_remote_cursor"; const String kOptionFollowRemoteCursor = "follow_remote_cursor"; @@ -133,16 +146,24 @@ const String kOptionCurrentAbName = "current-ab-name"; const String kOptionEnableConfirmClosingTabs = "enable-confirm-closing-tabs"; const String kOptionAllowAlwaysSoftwareRender = "allow-always-software-render"; const String kOptionEnableCheckUpdate = "enable-check-update"; +const String kOptionAllowAutoUpdate = "allow-auto-update"; const String kOptionAllowLinuxHeadless = "allow-linux-headless"; const String kOptionAllowRemoveWallpaper = "allow-remove-wallpaper"; const String kOptionStopService = "stop-service"; const String kOptionDirectxCapture = "enable-directx-capture"; const String kOptionAllowRemoteCmModification = "allow-remote-cm-modification"; +const String kOptionEnableUdpPunch = "enable-udp-punch"; +const String kOptionEnableIpv6Punch = "enable-ipv6-punch"; const String kOptionEnableTrustedDevices = "enable-trusted-devices"; +// network options +const String kOptionAllowWebSocket = "allow-websocket"; + // buildin opitons const String kOptionHideServerSetting = "hide-server-settings"; const String kOptionHideProxySetting = "hide-proxy-settings"; +const String kOptionHideWebSocketSetting = "hide-websocket-settings"; +const String kOptionHideRemotePrinterSetting = "hide-remote-printer-settings"; const String kOptionHideSecuritySetting = "hide-security-settings"; const String kOptionHideNetworkSetting = "hide-network-settings"; const String kOptionRemovePresetPasswordWarning = @@ -214,6 +235,21 @@ const double kDefaultQuality = 50; const double kMaxQuality = 100; const double kMaxMoreQuality = 2000; +// trackpad speed +const String kKeyTrackpadSpeed = 'trackpad-speed'; +const int kMinTrackpadSpeed = 10; +const int kDefaultTrackpadSpeed = 100; +const int kMaxTrackpadSpeed = 1000; + +// incomming (should be incoming) is kept, because change it will break the previous setting. +const String kKeyPrinterIncomingJobAction = 'printer-incomming-job-action'; +const String kValuePrinterIncomingJobDismiss = 'dismiss'; +const String kValuePrinterIncomingJobDefault = ''; +const String kValuePrinterIncomingJobSelected = 'selected'; +const String kKeyPrinterSelected = 'printer-selected-name'; +const String kKeyPrinterSave = 'allow-printer-dialog-save'; +const String kKeyPrinterAllowAutoPrint = 'allow-printer-auto-print'; + double kNewWindowOffset = isWindows ? 56.0 : isLinux @@ -248,7 +284,7 @@ const kFullScreenEdgeSize = 0.0; const kMaximizeEdgeSize = 0.0; // Do not use kWindowResizeEdgeSize directly. Use `windowResizeEdgeSize` in `common.dart` instead. const kWindowResizeEdgeSize = 5.0; -const kWindowBorderWidth = 1.0; +final kWindowBorderWidth = isWindows ? 0.0 : 1.0; const kDesktopMenuPadding = EdgeInsets.only(left: 12.0, right: 3.0); const kFrameBorderRadius = 12.0; const kFrameClipRRectBorderRadius = 12.0; @@ -302,6 +338,12 @@ const kRemoteImageQualityCustom = 'custom'; const kIgnoreDpi = true; +const Set kTouchBasedDeviceKinds = { + PointerDeviceKind.touch, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, +}; + // ================================ mobile ================================ // Magic numbers, maybe need to avoid it or use a better way to get them. diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart index f2c7121016e..2f244011ee5 100644 --- a/flutter/lib/desktop/pages/connection_page.dart +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/connection_page_title.dart'; import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -17,7 +19,7 @@ import '../../common/formatter/id_formatter.dart'; import '../../common/widgets/peer_tab_page.dart'; import '../../common/widgets/autocomplete.dart'; import '../../models/platform_model.dart'; -import '../widgets/button.dart'; +import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu; class OnlineStatusWidget extends StatefulWidget { const OnlineStatusWidget({Key? key, this.onSvcStatusChanged}) @@ -39,7 +41,7 @@ class _OnlineStatusWidgetState extends State { double? get height => bind.isIncomingOnly() ? null : em * 3; void onUsePublicServerGuide() { - const url = "https://rustdesk.com/pricing.html"; + const url = "https://rustdesk.com/pricing"; canLaunchUrlString(url).then((can) { if (can) { launchUrlString(url); @@ -200,18 +202,25 @@ class _ConnectionPageState extends State final _idController = IDTextEditingController(); final RxBool _idInputFocused = false.obs; + final FocusNode _idFocusNode = FocusNode(); + final TextEditingController _idEditingController = TextEditingController(); + + String selectedConnectionType = 'Connect'; bool isWindowMinimized = false; - List peers = []; - bool isPeersLoading = false; - bool isPeersLoaded = false; + final AllPeersLoader _allPeersLoader = AllPeersLoader(); + // https://github.com/flutter/flutter/issues/157244 Iterable _autocompleteOpts = []; + final _menuOpen = false.obs; + @override void initState() { super.initState(); + _allPeersLoader.init(setState); + _idFocusNode.addListener(onFocusChanged); if (_idController.text.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) async { final lastRemoteId = await bind.mainGetLastRemoteId(); @@ -222,6 +231,7 @@ class _ConnectionPageState extends State } }); } + Get.put(_idEditingController); Get.put(_idController); windowManager.addListener(this); } @@ -230,6 +240,10 @@ class _ConnectionPageState extends State void dispose() { _idController.dispose(); windowManager.removeListener(this); + _allPeersLoader.clear(); + _idFocusNode.removeListener(onFocusChanged); + _idFocusNode.dispose(); + _idEditingController.dispose(); if (Get.isRegistered()) { Get.delete(); } @@ -273,6 +287,20 @@ class _ConnectionPageState extends State bind.mainOnMainWindowClose(); } + void onFocusChanged() { + _idInputFocused.value = _idFocusNode.hasFocus; + if (_idFocusNode.hasFocus) { + if (_allPeersLoader.needLoad) { + _allPeersLoader.getAllPeers(); + } + + final textLength = _idEditingController.value.text.length; + // Select all to facilitate removing text, just following the behavior of address input of chrome. + _idEditingController.selection = + TextSelection(baseOffset: 0, extentOffset: textLength); + } + } + @override Widget build(BuildContext context) { final isOutgoingOnly = bind.isOutgoingOnly(); @@ -281,12 +309,6 @@ class _ConnectionPageState extends State Expanded( child: Column( children: [ - Row( - children: [ - Flexible(child: _buildRemoteIDTextField(context)), - ], - ).marginOnly(top: 22), - SizedBox(height: 12), Divider().paddingOnly(right: 12), Expanded(child: PeerTabPage()), ], @@ -299,21 +321,15 @@ class _ConnectionPageState extends State /// Callback for the connect button. /// Connects to the selected peer. - void onConnect({bool isFileTransfer = false}) { + void onConnect( + {bool isFileTransfer = false, + bool isViewCamera = false, + bool isTerminal = false}) { var id = _idController.id; - connect(context, id, isFileTransfer: isFileTransfer); - } - - Future _fetchPeers() async { - setState(() { - isPeersLoading = true; - }); - await Future.delayed(Duration(milliseconds: 100)); - peers = await getAllPeers(); - setState(() { - isPeersLoading = false; - isPeersLoaded = true; - }); + connect(context, id, + isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTerminal: isTerminal); } /// UI for the remote ID TextField. @@ -332,11 +348,12 @@ class _ConnectionPageState extends State Row( children: [ Expanded( - child: Autocomplete( + child: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { _autocompleteOpts = const Iterable.empty(); - } else if (peers.isEmpty && !isPeersLoaded) { + } else if (_allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded) { Peer emptyPeer = Peer( id: '', username: '', @@ -350,6 +367,7 @@ class _ConnectionPageState extends State rdpPort: '', rdpUsername: '', loginName: '', + device_group_name: '', ); _autocompleteOpts = [emptyPeer]; } else { @@ -362,7 +380,7 @@ class _ConnectionPageState extends State ); } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = peers + _autocompleteOpts = _allPeersLoader.peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -376,25 +394,16 @@ class _ConnectionPageState extends State } return _autocompleteOpts; }, + focusNode: _idFocusNode, + textEditingController: _idEditingController, fieldViewBuilder: ( BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted, ) { - fieldTextEditingController.text = _idController.text; - Get.put(fieldTextEditingController); - fieldFocusNode.addListener(() async { - _idInputFocused.value = fieldFocusNode.hasFocus; - if (fieldFocusNode.hasFocus && !isPeersLoading) { - _fetchPeers(); - } - }); - final textLength = - fieldTextEditingController.value.text.length; - // select all to facilitate removing text, just following the behavior of address input of chrome - fieldTextEditingController.selection = - TextSelection(baseOffset: 0, extentOffset: textLength); + updateTextAndPreserveSelection( + fieldTextEditingController, _idController.text); return Obx(() => TextField( autocorrect: false, enableSuggestions: false, @@ -424,7 +433,7 @@ class _ConnectionPageState extends State onSubmitted: (_) { onConnect(); }, - )); + ).workaroundFreezeLinuxMint()); }, onSelected: (option) { setState(() { @@ -467,7 +476,8 @@ class _ConnectionPageState extends State maxHeight: maxHeight, maxWidth: 319, ), - child: peers.isEmpty && isPeersLoading + child: _allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded ? Container( height: 80, child: Center( @@ -497,21 +507,97 @@ class _ConnectionPageState extends State ), Padding( padding: const EdgeInsets.only(top: 13.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Button( - isOutline: true, - onTap: () => onConnect(isFileTransfer: true), - text: "Transfer file", + child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + SizedBox( + height: 28.0, + child: ElevatedButton( + onPressed: () { + onConnect(); + }, + child: Text(translate("Connect")), ), - const SizedBox( - width: 17, + ), + const SizedBox(width: 8), + Container( + height: 28.0, + width: 28.0, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(8), ), - Button(onTap: onConnect, text: "Connect"), - ], - ), - ) + child: Center( + child: StatefulBuilder( + builder: (context, setState) { + var offset = Offset(0, 0); + return Obx(() => InkWell( + child: _menuOpen.value + ? Transform.rotate( + angle: pi, + child: Icon(IconFont.more, size: 14), + ) + : Icon(IconFont.more, size: 14), + onTapDown: (e) { + offset = e.globalPosition; + }, + onTap: () async { + _menuOpen.value = true; + final x = offset.dx; + final y = offset.dy; + await mod_menu + .showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: [ + ( + 'Transfer file', + () => onConnect(isFileTransfer: true) + ), + ( + 'View camera', + () => onConnect(isViewCamera: true) + ), + ( + '${translate('Terminal')} (beta)', + () => onConnect(isTerminal: true) + ), + ] + .map((e) => MenuEntryButton( + childBuilder: (TextStyle? style) => + Text( + translate(e.$1), + style: style, + ), + proc: () => e.$2(), + padding: EdgeInsets.symmetric( + horizontal: + kDesktopMenuPadding.left), + dismissOnClicked: true, + )) + .map((e) => e.build( + context, + const MenuConfig( + commonColor: CustomPopupMenuTheme + .commonColor, + height: + CustomPopupMenuTheme.height, + dividerHeight: + CustomPopupMenuTheme + .dividerHeight))) + .expand((i) => i) + .toList(), + elevation: 8, + ) + .then((_) { + _menuOpen.value = false; + }); + }, + )); + }, + ), + ), + ), + ]), + ), ], ), ), diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart index 10f5cc4fdba..23769115919 100644 --- a/flutter/lib/desktop/pages/desktop_home_page.dart +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -12,6 +12,7 @@ import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/connection_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/desktop/widgets/update_progress.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -22,7 +23,6 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; import 'package:window_size/window_size.dart' as window_size; - import '../widgets/button.dart'; class DesktopHomePage extends StatefulWidget { @@ -134,12 +134,17 @@ class _DesktopHomePageState extends State color: Theme.of(context).colorScheme.background, child: Stack( children: [ - SingleChildScrollView( - controller: _leftPaneScrollController, - child: Column( - key: _childKey, - children: children, - ), + Column( + children: [ + SingleChildScrollView( + controller: _leftPaneScrollController, + child: Column( + key: _childKey, + children: children, + ), + ), + Expanded(child: Container()) + ], ), if (isOutgoingOnly) Positioned( @@ -237,7 +242,7 @@ class _DesktopHomePageState extends State style: TextStyle( fontSize: 22, ), - ), + ).workaroundFreezeLinuxMint(), ), ) ], @@ -333,7 +338,7 @@ class _DesktopHomePageState extends State EdgeInsets.only(top: 14, bottom: 10), ), style: TextStyle(fontSize: 15), - ), + ).workaroundFreezeLinuxMint(), ), ), if (showOneTime) @@ -428,13 +433,23 @@ class _DesktopHomePageState extends State updateUrl.isNotEmpty && !isCardClosed && bind.mainUriPrefixSync().contains('rustdesk')) { + final isToUpdate = (isWindows || isMacOS) && bind.mainIsInstalled(); + String btnText = isToUpdate ? 'Update' : 'Download'; + GestureTapCallback onPressed = () async { + final Uri url = Uri.parse('https://rustdesk.com/download'); + await launchUrl(url); + }; + if (isToUpdate) { + onPressed = () { + handleUpdate(updateUrl); + }; + } return buildInstallCard( "Status", "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", - "Click to download", () async { - final Uri url = Uri.parse('https://rustdesk.com/download'); - await launchUrl(url); - }, closeButton: true); + btnText, + onPressed, + closeButton: true); } if (systemError.isNotEmpty) { return buildInstallCard("", systemError, "", () {}); @@ -770,6 +785,8 @@ class _DesktopHomePageState extends State await connectMainDesktop( call.arguments['id'], isFileTransfer: call.arguments['isFileTransfer'], + isViewCamera: call.arguments['isViewCamera'], + isTerminal: call.arguments['isTerminal'], isTcpTunneling: call.arguments['isTcpTunneling'], isRDP: call.arguments['isRDP'], password: call.arguments['password'], @@ -784,9 +801,15 @@ class _DesktopHomePageState extends State } catch (e) { debugPrint("Failed to parse window id '${call.arguments}': $e"); } - if (windowId != null) { + WindowType? windowType; + try { + windowType = WindowType.values.byName(args[3]); + } catch (e) { + debugPrint("Failed to parse window type '${call.arguments}': $e"); + } + if (windowId != null && windowType != null) { await rustDeskWinManager.moveTabToNewWindow( - windowId, args[1], args[2]); + windowId, args[1], args[2], windowType); } } else if (call.method == kWindowEventOpenMonitorSession) { final args = jsonDecode(call.arguments); @@ -794,9 +817,10 @@ class _DesktopHomePageState extends State final peerId = args['peer_id'] as String; final display = args['display'] as int; final displayCount = args['display_count'] as int; + final windowType = args['window_type'] as int; final screenRect = parseParamScreenRect(args); await rustDeskWinManager.openMonitorSession( - windowId, peerId, display, displayCount, screenRect); + windowId, peerId, display, displayCount, screenRect, windowType); } else if (call.method == kWindowEventRemoteWindowCoords) { final windowId = int.tryParse(call.arguments); if (windowId != null) { @@ -834,10 +858,6 @@ class _DesktopHomePageState extends State _uniLinksSubscription?.cancel(); Get.delete(tag: 'stop-service'); _updateTimer?.cancel(); - if (!bind.isCustomClient()) { - platformFFI.unregisterEventHandler( - kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); - } WidgetsBinding.instance.removeObserver(this); super.dispose(); } @@ -940,7 +960,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { }); }, maxLength: maxLength, - ), + ).workaroundFreezeLinuxMint(), ), ], ), @@ -967,7 +987,7 @@ void setPasswordDialog({VoidCallback? notEmptyCallback}) async { }); }, maxLength: maxLength, - ), + ).workaroundFreezeLinuxMint(), ), ], ), diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart index 56a99446c38..cc1f3f27194 100644 --- a/flutter/lib/desktop/pages/desktop_setting_page.dart +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -13,6 +13,7 @@ import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/plugin/manager.dart'; @@ -55,6 +56,7 @@ enum SettingsTabKey { display, plugin, account, + printer, about, } @@ -74,6 +76,9 @@ class DesktopSettingPage extends StatefulWidget { if (!isWeb && !bind.isIncomingOnly() && bind.pluginFeatureIsEnabled()) SettingsTabKey.plugin, if (!bind.isDisableAccount()) SettingsTabKey.account, + if (isWindows && + bind.mainGetBuildinOption(key: kOptionHideRemotePrinterSetting) != 'Y') + SettingsTabKey.printer, SettingsTabKey.about, ]; @@ -198,6 +203,10 @@ class _DesktopSettingPageState extends State settingTabs.add( _TabInfo(tab, 'Account', Icons.person_outline, Icons.person)); break; + case SettingsTabKey.printer: + settingTabs + .add(_TabInfo(tab, 'Printer', Icons.print_outlined, Icons.print)); + break; case SettingsTabKey.about: settingTabs .add(_TabInfo(tab, 'About', Icons.info_outline, Icons.info)); @@ -229,6 +238,9 @@ class _DesktopSettingPageState extends State case SettingsTabKey.account: children.add(const _Account()); break; + case SettingsTabKey.printer: + children.add(const _Printer()); + break; case SettingsTabKey.about: children.add(const _About()); break; @@ -460,6 +472,8 @@ class _GeneralState extends State<_General> { } Widget other() { + final showAutoUpdate = + isWindows && bind.mainIsInstalled() && !bind.isCustomClient(); final children = [ if (!isWeb && !bind.isIncomingOnly()) _OptionCheckBox(context, 'Confirm before closing multiple tabs', @@ -496,6 +510,16 @@ class _GeneralState extends State<_General> { await bind.mainSetLocalOption(key: k, value: v ? 'Y' : 'N'), ), ), + if (isWindows) + Tooltip( + message: translate('d3d_render_tip'), + child: _OptionCheckBox( + context, + "Use D3D rendering", + kOptionD3DRender, + isServer: false, + ), + ), if (!isWeb && !bind.isCustomClient()) _OptionCheckBox( context, @@ -503,12 +527,33 @@ class _GeneralState extends State<_General> { kOptionEnableCheckUpdate, isServer: false, ), + if (showAutoUpdate) + _OptionCheckBox( + context, + 'Auto update', + kOptionAllowAutoUpdate, + isServer: true, + ), if (isWindows && !bind.isOutgoingOnly()) _OptionCheckBox( context, 'Capture screen using DirectX', kOptionDirectxCapture, - ) + ), + if (!bind.isIncomingOnly()) ...[ + _OptionCheckBox( + context, + 'Enable UDP hole punching', + kOptionEnableUdpPunch, + isServer: false, + ), + _OptionCheckBox( + context, + 'Enable IPv6 P2P connection', + kOptionEnableIpv6Punch, + isServer: false, + ), + ], ], ]; if (!isWeb && bind.mainShowOption(key: kOptionAllowLinuxHeadless)) { @@ -607,7 +652,6 @@ class _GeneralState extends State<_General> { bool user_dir_exists = await Directory(user_dir).exists(); bool root_dir_exists = showRootDir ? await Directory(root_dir).exists() : false; - // canLaunchUrl blocked on windows portable, user SYSTEM return { 'user_dir': user_dir, 'root_dir': root_dir, @@ -954,6 +998,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { _OptionCheckBox( context, 'Enable keyboard/mouse', kOptionEnableKeyboard, enabled: enabled, fakeValue: fakeValue), + if (isWindows) + _OptionCheckBox( + context, 'Enable remote printer', kOptionEnableRemotePrinter, + enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable clipboard', kOptionEnableClipboard, enabled: enabled, fakeValue: fakeValue), _OptionCheckBox( @@ -961,6 +1009,10 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { enabled: enabled, fakeValue: fakeValue), _OptionCheckBox(context, 'Enable audio', kOptionEnableAudio, enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable camera', kOptionEnableCamera, + enabled: enabled, fakeValue: fakeValue), + _OptionCheckBox(context, 'Enable terminal', kOptionEnableTerminal, + enabled: enabled, fakeValue: fakeValue), _OptionCheckBox( context, 'Enable TCP tunneling', kOptionEnableTunnel, enabled: enabled, fakeValue: fakeValue), @@ -1061,6 +1113,34 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { )) .toList(); + final isOptFixedNumOTP = + isOptionFixed(kOptionAllowNumericOneTimePassword); + final isNumOPTChangable = !isOptFixedNumOTP && tmpEnabled && !locked; + final numericOneTimePassword = GestureDetector( + child: InkWell( + child: Row( + children: [ + Checkbox( + value: model.allowNumericOneTimePassword, + onChanged: isNumOPTChangable + ? (bool? v) { + model.switchAllowNumericOneTimePassword(); + } + : null) + .marginOnly(right: 5), + Expanded( + child: Text( + translate('Numeric one-time password'), + style: TextStyle( + color: disabledTextColor(context, isNumOPTChangable)), + )) + ], + )), + onTap: isNumOPTChangable + ? () => model.switchAllowNumericOneTimePassword() + : null, + ).marginOnly(left: _kContentHSubMargin - 5); + final modeKeys = [ 'password', 'click', @@ -1097,6 +1177,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { ], ), enabled: tmpEnabled && !locked), + numericOneTimePassword, if (usePassword) radios[1], if (usePassword) _SubButton('Set permanent password', setPasswordDialog, @@ -1189,7 +1270,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 12), ), - ).marginOnly(right: 15), + ).workaroundFreezeLinuxMint().marginOnly(right: 15), ), Obx(() => ElevatedButton( onPressed: applyEnabled.value && @@ -1346,7 +1427,7 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { contentPadding: EdgeInsets.symmetric(vertical: 12, horizontal: 12), ), - ).marginOnly(right: 15), + ).workaroundFreezeLinuxMint().marginOnly(right: 15), ), Obx(() => ElevatedButton( onPressed: @@ -1441,11 +1522,69 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { bind.mainGetBuildinOption(key: kOptionHideServerSetting) == 'Y'; final hideProxy = isWeb || bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; + final hideWebSocket = isWeb || + bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y'; - if (hideServer && hideProxy) { + if (hideServer && hideProxy && hideWebSocket) { return Offstage(); } + // Helper function to create network setting ListTiles + Widget listTile({ + required IconData icon, + required String title, + VoidCallback? onTap, + Widget? trailing, + bool showTooltip = false, + String tooltipMessage = '', + }) { + final titleWidget = showTooltip + ? Row( + children: [ + Tooltip( + waitDuration: Duration(milliseconds: 1000), + message: translate(tooltipMessage), + child: Row( + children: [ + Text( + translate(title), + style: TextStyle(fontSize: _kContentFontSize), + ), + SizedBox(width: 5), + Icon( + Icons.help_outline, + size: 14, + color: Theme.of(context) + .textTheme + .titleLarge + ?.color + ?.withOpacity(0.7), + ), + ], + ), + ), + ], + ) + : Text( + translate(title), + style: TextStyle(fontSize: _kContentFontSize), + ); + + return ListTile( + leading: Icon(icon, color: _accentColor), + title: titleWidget, + enabled: !locked, + onTap: onTap, + trailing: trailing, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + contentPadding: EdgeInsets.symmetric(horizontal: 16), + minLeadingWidth: 0, + horizontalTitleGap: 10, + ); + } + return _Card( title: 'Network', children: [ @@ -1454,39 +1593,36 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!hideServer) - ListTile( - leading: Icon(Icons.dns_outlined, color: _accentColor), - title: Text( - translate('ID/Relay Server'), - style: TextStyle(fontSize: _kContentFontSize), - ), - enabled: !locked, + listTile( + icon: Icons.dns_outlined, + title: 'ID/Relay Server', onTap: () => showServerSettings(gFFI.dialogManager), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - contentPadding: EdgeInsets.symmetric(horizontal: 16), - minLeadingWidth: 0, - horizontalTitleGap: 10, ), - if (!hideServer && !hideProxy) + if (!hideServer && (!hideProxy || !hideWebSocket)) Divider(height: 1, indent: 16, endIndent: 16), if (!hideProxy) - ListTile( - leading: - Icon(Icons.network_ping_outlined, color: _accentColor), - title: Text( - translate('Socks5/Http(s) Proxy'), - style: TextStyle(fontSize: _kContentFontSize), - ), - enabled: !locked, + listTile( + icon: Icons.network_ping_outlined, + title: 'Socks5/Http(s) Proxy', onTap: changeSocks5Proxy, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), + ), + if (!hideProxy && !hideWebSocket) + Divider(height: 1, indent: 16, endIndent: 16), + if (!hideWebSocket) + listTile( + icon: Icons.web_asset_outlined, + title: 'Use WebSocket', + showTooltip: true, + tooltipMessage: 'websocket_tip', + trailing: Switch( + value: mainGetBoolOptionSync(kOptionAllowWebSocket), + onChanged: locked + ? null + : (value) { + mainSetBoolOption(kOptionAllowWebSocket, value); + setState(() {}); + }, ), - contentPadding: EdgeInsets.symmetric(horizontal: 16), - minLeadingWidth: 0, - horizontalTitleGap: 10, ), ], ), @@ -1512,6 +1648,7 @@ class _DisplayState extends State<_Display> { scrollStyle(context), imageQuality(context), codec(context), + if (isDesktop) trackpadSpeed(context), if (!isWeb) privacyModeImpl(context), other(context), ]).marginOnly(bottom: _kListViewBottomMargin); @@ -1599,6 +1736,26 @@ class _DisplayState extends State<_Display> { ]); } + Widget trackpadSpeed(BuildContext context) { + final initSpeed = (int.tryParse( + bind.mainGetUserDefaultOption(key: kKeyTrackpadSpeed)) ?? + kDefaultTrackpadSpeed); + final curSpeed = SimpleWrapper(initSpeed); + void onDebouncer(int v) { + bind.mainSetUserDefaultOption( + key: kKeyTrackpadSpeed, value: v.toString()); + // It's better to notify all sessions that the default speed is changed. + // But it may also be ok to take effect in the next connection. + } + + return _Card(title: 'Default trackpad speed', children: [ + TrackpadSpeedWidget( + value: curSpeed, + onDebouncer: onDebouncer, + ), + ]); + } + Widget codec(BuildContext context) { onChanged(String value) async { await bind.mainSetUserDefaultOption( @@ -1870,6 +2027,153 @@ class _PluginState extends State<_Plugin> { } } +class _Printer extends StatefulWidget { + const _Printer({super.key}); + + @override + State<_Printer> createState() => __PrinterState(); +} + +class __PrinterState extends State<_Printer> { + @override + Widget build(BuildContext context) { + final scrollController = ScrollController(); + return ListView(controller: scrollController, children: [ + outgoing(context), + incoming(context), + ]).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget outgoing(BuildContext context) { + final isSupportPrinterDriver = + bind.mainGetCommonSync(key: 'is-support-printer-driver') == 'true'; + + Widget tipOsNotSupported() { + return Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-os-requirement-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + Widget tipClientNotInstalled() { + return Align( + alignment: Alignment.topLeft, + child: + Text(translate('printer-requires-installed-{$appName}-client-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + Widget tipPrinterNotInstalled() { + final failedMsg = ''.obs; + platformFFI.registerEventHandler( + 'install-printer-res', 'install-printer-res', (evt) async { + if (evt['success'] as bool) { + setState(() {}); + } else { + failedMsg.value = evt['msg'] as String; + } + }, replace: true); + return Column(children: [ + Obx( + () => failedMsg.value.isNotEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-{$appName}-not-installed-tip')) + .marginOnly(bottom: 10.0), + ), + ), + Obx( + () => failedMsg.value.isEmpty + ? Offstage() + : Align( + alignment: Alignment.topLeft, + child: Text(failedMsg.value, + style: DefaultTextStyle.of(context) + .style + .copyWith(color: Colors.red)) + .marginOnly(bottom: 10.0)), + ), + _Button('Install {$appName} Printer', () { + failedMsg.value = ''; + bind.mainSetCommon(key: 'install-printer', value: ''); + }) + ]).marginOnly(left: _kCardLeftMargin, bottom: 2.0); + } + + Widget tipReady() { + return Align( + alignment: Alignment.topLeft, + child: Text(translate('printer-{$appName}-ready-tip')), + ).marginOnly(left: _kCardLeftMargin); + } + + final installed = bind.mainIsInstalled(); + // `is-printer-installed` may fail, but it's rare case. + // Add additional error message here if it's really needed. + final isPrinterInstalled = + bind.mainGetCommonSync(key: 'is-printer-installed') == 'true'; + + final List children = []; + if (!isSupportPrinterDriver) { + children.add(tipOsNotSupported()); + } else { + children.addAll([ + if (!installed) tipClientNotInstalled(), + if (installed && !isPrinterInstalled) tipPrinterNotInstalled(), + if (installed && isPrinterInstalled) tipReady() + ]); + } + return _Card(title: 'Outgoing Print Jobs', children: children); + } + + Widget incoming(BuildContext context) { + onRadioChanged(String value) async { + await bind.mainSetLocalOption( + key: kKeyPrinterIncomingJobAction, value: value); + setState(() {}); + } + + PrinterOptions printerOptions = PrinterOptions.load(); + return _Card(title: 'Incoming Print Jobs', children: [ + _Radio(context, + value: kValuePrinterIncomingJobDismiss, + groupValue: printerOptions.action, + label: 'Dismiss', + onChanged: onRadioChanged), + _Radio(context, + value: kValuePrinterIncomingJobDefault, + groupValue: printerOptions.action, + label: 'use-the-default-printer-tip', + onChanged: onRadioChanged), + _Radio(context, + value: kValuePrinterIncomingJobSelected, + groupValue: printerOptions.action, + label: 'use-the-selected-printer-tip', + onChanged: onRadioChanged), + if (printerOptions.printerNames.isNotEmpty) + ComboBox( + initialKey: printerOptions.printerName, + keys: printerOptions.printerNames, + values: printerOptions.printerNames, + enabled: printerOptions.action == kValuePrinterIncomingJobSelected, + onChanged: (value) async { + await bind.mainSetLocalOption( + key: kKeyPrinterSelected, value: value); + setState(() {}); + }, + ).marginOnly(left: 10), + _OptionCheckBox( + context, + 'auto-print-tip', + kKeyPrinterAllowAutoPrint, + isServer: false, + enabled: printerOptions.action != kValuePrinterIncomingJobDismiss, + ) + ]); + } +} + class _About extends StatefulWidget { const _About({Key? key}) : super(key: key); @@ -2312,7 +2616,7 @@ _LabeledTextField( style: TextStyle( color: disabledTextColor(context, enabled), ), - ), + ).workaroundFreezeLinuxMint(), ], ), ], @@ -2491,7 +2795,7 @@ void changeSocks5Proxy() async { controller: proxyController, autofocus: true, enabled: !isOptFixed, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: 8), @@ -2511,7 +2815,7 @@ void changeSocks5Proxy() async { labelText: isMobile ? translate('Username') : null, ), enabled: !isOptFixed, - ), + ).workaroundFreezeLinuxMint(), ), ], ).marginOnly(bottom: 8), @@ -2537,7 +2841,7 @@ void changeSocks5Proxy() async { controller: pwdController, enabled: !isOptFixed, maxLength: bind.mainMaxEncryptLen(), - )), + ).workaroundFreezeLinuxMint()), ), ], ), diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart index 90b8d7dcbf3..3f555dcaa66 100644 --- a/flutter/lib/desktop/pages/file_manager_page.dart +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -768,7 +768,7 @@ class _FileManagerViewState extends State { ), controller: name, autofocus: true, - ), + ).workaroundFreezeLinuxMint(), ], ), actions: [ @@ -1657,7 +1657,7 @@ class _FileManagerViewState extends State { onChanged: _locationStatus.value == LocationStatus.fileSearchBar ? (searchText) => onSearchText(searchText, isLocal) : null, - ), + ).workaroundFreezeLinuxMint(), ) ], ); diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart index cc77cdd9581..5251498895d 100644 --- a/flutter/lib/desktop/pages/file_manager_tab_page.dart +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -103,11 +103,13 @@ class _FileManagerTabPageState extends State { )); final tabWidget = isLinux ? buildVirtualWindowFrame(context, child) - : Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: child, - ); + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + )); return isMacOS || kUseCompatibleUiMode ? tabWidget : SubWindowDragToResizeArea( diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart index 0ff04240b55..5bf6bafeea0 100644 --- a/flutter/lib/desktop/pages/install_page.dart +++ b/flutter/lib/desktop/pages/install_page.dart @@ -65,6 +65,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> late final TextEditingController controller; final RxBool startmenu = true.obs; final RxBool desktopicon = true.obs; + final RxBool printer = true.obs; final RxBool showProgress = false.obs; final RxBool btnEnabled = true.obs; @@ -79,6 +80,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> final installOptions = jsonDecode(bind.installInstallOptions()); startmenu.value = installOptions['STARTMENUSHORTCUTS'] != '0'; desktopicon.value = installOptions['DESKTOPSHORTCUTS'] != '0'; + printer.value = installOptions['PRINTER'] != '0'; } @override @@ -147,7 +149,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> decoration: InputDecoration( contentPadding: EdgeInsets.all(0.75 * em), ), - ).marginOnly(right: 10), + ).workaroundFreezeLinuxMint().marginOnly(right: 10), ), Obx( () => OutlinedButton.icon( @@ -161,7 +163,9 @@ class _InstallPageBodyState extends State<_InstallPageBody> ).marginSymmetric(vertical: 2 * em), Option(startmenu, label: 'Create start menu shortcuts') .marginOnly(bottom: 7), - Option(desktopicon, label: 'Create desktop icon'), + Option(desktopicon, label: 'Create desktop icon') + .marginOnly(bottom: 7), + Option(printer, label: 'Install {$appName} Printer'), Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( @@ -253,6 +257,7 @@ class _InstallPageBodyState extends State<_InstallPageBody> String args = ''; if (startmenu.value) args += ' startmenu'; if (desktopicon.value) args += ' desktopicon'; + if (printer.value) args += ' printer'; bind.installInstallMe(options: args, path: controller.text); } diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart index d6d243c5026..6671d041bbf 100644 --- a/flutter/lib/desktop/pages/port_forward_page.dart +++ b/flutter/lib/desktop/pages/port_forward_page.dart @@ -238,7 +238,7 @@ class _PortForwardPageState extends State inputFormatters: inputFormatters, decoration: InputDecoration( hintText: hint, - ))), + )).workaroundFreezeLinuxMint()), ); } diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart index f399f7cab68..9d366bcb0cf 100644 --- a/flutter/lib/desktop/pages/port_forward_tab_page.dart +++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart @@ -118,11 +118,13 @@ class _PortForwardTabPageState extends State { backgroundColor: Theme.of(context).colorScheme.background, body: child), ) - : Container( - decoration: BoxDecoration( - border: Border.all(color: MyTheme.color(context).border!)), - child: child, - ); + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + )); return isMacOS || kUseCompatibleUiMode ? tabWidget : Obx( diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart index efd437e1ff7..ba698bd56b7 100644 --- a/flutter/lib/desktop/pages/remote_tab_page.dart +++ b/flutter/lib/desktop/pages/remote_tab_page.dart @@ -146,16 +146,8 @@ class _ConnectionTabPageState extends State { connectionType.secure.value == ConnectionType.strSecure; bool direct = connectionType.direct.value == ConnectionType.strDirect; - String msgConn; - if (secure && direct) { - msgConn = translate("Direct and encrypted connection"); - } else if (secure && !direct) { - msgConn = translate("Relayed and encrypted connection"); - } else if (!secure && direct) { - msgConn = translate("Direct and unencrypted connection"); - } else { - msgConn = translate("Relayed and unencrypted connection"); - } + String msgConn = getConnectionText( + secure, direct, connectionType.stream_type.value); var msgFingerprint = '${translate('Fingerprint')}:\n'; var fingerprint = FingerprintState.find(key).value; if (fingerprint.isEmpty) { @@ -212,14 +204,16 @@ class _ConnectionTabPageState extends State { ); final tabWidget = isLinux ? buildVirtualWindowFrame(context, child) - : Obx(() => Container( - decoration: BoxDecoration( - border: Border.all( - color: MyTheme.color(context).border!, - width: stateGlobal.windowBorderWidth.value), - ), - child: child, - )); + : workaroundWindowBorder( + context, + Obx(() => Container( + decoration: BoxDecoration( + border: Border.all( + color: MyTheme.color(context).border!, + width: stateGlobal.windowBorderWidth.value), + ), + child: child, + ))); return isMacOS || kUseCompatibleUiMode ? tabWidget : Obx(() => SubWindowDragToResizeArea( @@ -267,8 +261,10 @@ class _ConnectionTabPageState extends State { style: style, ), proc: () async { - await DesktopMultiWindow.invokeMethod(kMainWindowId, - kWindowEventMoveTabToNewWindow, '${windowId()},$key,$sessionId'); + await DesktopMultiWindow.invokeMethod( + kMainWindowId, + kWindowEventMoveTabToNewWindow, + '${windowId()},$key,$sessionId,RemoteDesktop'); cancelFunc(); }, padding: padding, @@ -415,8 +411,8 @@ class _ConnectionTabPageState extends State { await WindowController.fromWindowId(windowId()).setFullscreen(false); stateGlobal.setFullscreen(false, procWnd: false); } - await setNewConnectWindowFrame( - windowId(), id!, prePeerCount, display, screenRect); + await setNewConnectWindowFrame(windowId(), id!, prePeerCount, + WindowType.RemoteDesktop, display, screenRect); Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { await windowOnTop(windowId()); }); diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart index 95d9f2c7c7d..4ee29756fc2 100644 --- a/flutter/lib/desktop/pages/server_page.dart +++ b/flutter/lib/desktop/pages/server_page.dart @@ -88,12 +88,14 @@ class _DesktopServerPageState extends State ); return isLinux ? buildVirtualWindowFrame(context, body) - : Container( - decoration: BoxDecoration( - border: - Border.all(color: MyTheme.color(context).border!)), - child: body, - ); + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: + Border.all(color: MyTheme.color(context).border!)), + child: body, + )); }, ), ); @@ -351,7 +353,10 @@ Widget buildConnectionCard(Client client) { key: ValueKey(client.id), children: [ _CmHeader(client: client), - client.type_() != ClientType.remote || client.disconnected + client.type_() == ClientType.file || + client.type_() == ClientType.portForward || + client.type_() == ClientType.terminal || + client.disconnected ? Offstage() : _PrivilegeBoard(client: client), Expanded( @@ -495,7 +500,36 @@ class _CmHeaderState extends State<_CmHeader> "(${client.peerId})", style: TextStyle(color: Colors.white, fontSize: 14), ), - ).marginOnly(bottom: 10.0), + ), + if (client.type_() == ClientType.terminal) + FittedBox( + child: Text( + translate("Terminal"), + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (client.type_() == ClientType.file) + FittedBox( + child: Text( + translate("File Transfer"), + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (client.type_() == ClientType.camera) + FittedBox( + child: Text( + translate("View Camera"), + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + if (client.portForward.isNotEmpty) + FittedBox( + child: Text( + "Port Forward: ${client.portForward}", + style: TextStyle(color: Colors.white70, fontSize: 12), + ), + ), + SizedBox(height: 10.0), FittedBox( child: Row( children: [ @@ -524,7 +558,8 @@ class _CmHeaderState extends State<_CmHeader> Offstage( offstage: !client.authorized || (client.type_() != ClientType.remote && - client.type_() != ClientType.file), + client.type_() != ClientType.file && + client.type_() != ClientType.camera), child: IconButton( onPressed: () => checkClickTime(client.id, () { if (client.type_() == ClientType.file) { @@ -625,96 +660,139 @@ class _PrivilegeBoardState extends State<_PrivilegeBoard> { padding: EdgeInsets.symmetric(horizontal: spacing), mainAxisSpacing: spacing, crossAxisSpacing: spacing, - children: [ - buildPermissionIcon( - client.keyboard, - Icons.keyboard, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "keyboard", enabled: enabled); - setState(() { - client.keyboard = enabled; - }); - }, - translate('Enable keyboard/mouse'), - ), - buildPermissionIcon( - client.clipboard, - Icons.assignment_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "clipboard", enabled: enabled); - setState(() { - client.clipboard = enabled; - }); - }, - translate('Enable clipboard'), - ), - buildPermissionIcon( - client.audio, - Icons.volume_up_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "audio", enabled: enabled); - setState(() { - client.audio = enabled; - }); - }, - translate('Enable audio'), - ), - buildPermissionIcon( - client.file, - Icons.upload_file_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "file", enabled: enabled); - setState(() { - client.file = enabled; - }); - }, - translate('Enable file copy and paste'), - ), - buildPermissionIcon( - client.restart, - Icons.restart_alt_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "restart", enabled: enabled); - setState(() { - client.restart = enabled; - }); - }, - translate('Enable remote restart'), - ), - buildPermissionIcon( - client.recording, - Icons.videocam_rounded, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, name: "recording", enabled: enabled); - setState(() { - client.recording = enabled; - }); - }, - translate('Enable recording session'), - ), - // only windows support block input - if (isWindows) - buildPermissionIcon( - client.blockInput, - Icons.block, - (enabled) { - bind.cmSwitchPermission( - connId: client.id, - name: "block_input", - enabled: enabled); - setState(() { - client.blockInput = enabled; - }); - }, - translate('Enable blocking user input'), - ) - ], + children: client.type_() == ClientType.camera + ? [ + buildPermissionIcon( + client.audio, + Icons.volume_up_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "audio", + enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, + translate('Enable audio'), + ), + buildPermissionIcon( + client.recording, + Icons.videocam_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "recording", + enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, + translate('Enable recording session'), + ), + ] + : [ + buildPermissionIcon( + client.keyboard, + Icons.keyboard, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "keyboard", + enabled: enabled); + setState(() { + client.keyboard = enabled; + }); + }, + translate('Enable keyboard/mouse'), + ), + buildPermissionIcon( + client.clipboard, + Icons.assignment_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "clipboard", + enabled: enabled); + setState(() { + client.clipboard = enabled; + }); + }, + translate('Enable clipboard'), + ), + buildPermissionIcon( + client.audio, + Icons.volume_up_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "audio", + enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, + translate('Enable audio'), + ), + buildPermissionIcon( + client.file, + Icons.upload_file_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "file", + enabled: enabled); + setState(() { + client.file = enabled; + }); + }, + translate('Enable file copy and paste'), + ), + buildPermissionIcon( + client.restart, + Icons.restart_alt_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "restart", + enabled: enabled); + setState(() { + client.restart = enabled; + }); + }, + translate('Enable remote restart'), + ), + buildPermissionIcon( + client.recording, + Icons.videocam_rounded, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "recording", + enabled: enabled); + setState(() { + client.recording = enabled; + }); + }, + translate('Enable recording session'), + ), + // only windows support block input + if (isWindows) + buildPermissionIcon( + client.blockInput, + Icons.block, + (enabled) { + bind.cmSwitchPermission( + connId: client.id, + name: "block_input", + enabled: enabled); + setState(() { + client.blockInput = enabled; + }); + }, + translate('Enable blocking user input'), + ) + ], ), ), ], diff --git a/flutter/lib/desktop/pages/terminal_connection_manager.dart b/flutter/lib/desktop/pages/terminal_connection_manager.dart new file mode 100644 index 00000000000..91b8baa9751 --- /dev/null +++ b/flutter/lib/desktop/pages/terminal_connection_manager.dart @@ -0,0 +1,98 @@ +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import '../../models/model.dart'; + +/// Manages terminal connections to ensure one FFI instance per peer +class TerminalConnectionManager { + static final Map _connections = {}; + static final Map _connectionRefCount = {}; + + // Track service IDs per peer + static final Map _serviceIds = {}; + + /// Get or create an FFI instance for a peer + static FFI getConnection({ + required String peerId, + required String? password, + required bool? isSharedPassword, + required bool? forceRelay, + required String? connToken, + }) { + final existingFfi = _connections[peerId]; + if (existingFfi != null && !existingFfi.closed) { + // Increment reference count + _connectionRefCount[peerId] = (_connectionRefCount[peerId] ?? 0) + 1; + debugPrint('[TerminalConnectionManager] Reusing existing connection for peer $peerId. Reference count: ${_connectionRefCount[peerId]}'); + return existingFfi; + } + + // Create new FFI instance for first terminal + debugPrint('[TerminalConnectionManager] Creating new terminal connection for peer $peerId'); + final ffi = FFI(null); + ffi.start( + peerId, + password: password, + isSharedPassword: isSharedPassword, + forceRelay: forceRelay, + connToken: connToken, + isTerminal: true, + ); + + _connections[peerId] = ffi; + _connectionRefCount[peerId] = 1; + + // Register the FFI instance with Get for dependency injection + Get.put(ffi, tag: 'terminal_$peerId'); + + debugPrint('[TerminalConnectionManager] New connection created. Total connections: ${_connections.length}'); + return ffi; + } + + /// Release a connection reference + static void releaseConnection(String peerId) { + final refCount = _connectionRefCount[peerId] ?? 0; + debugPrint('[TerminalConnectionManager] Releasing connection for peer $peerId. Current ref count: $refCount'); + + if (refCount <= 1) { + // Last reference, close the connection + final ffi = _connections[peerId]; + if (ffi != null) { + debugPrint('[TerminalConnectionManager] Closing connection for peer $peerId (last reference)'); + ffi.close(); + _connections.remove(peerId); + _connectionRefCount.remove(peerId); + Get.delete(tag: 'terminal_$peerId'); + } + } else { + // Decrement reference count + _connectionRefCount[peerId] = refCount - 1; + debugPrint('[TerminalConnectionManager] Connection still in use. New ref count: ${_connectionRefCount[peerId]}'); + } + } + + /// Check if a connection exists for a peer + static bool hasConnection(String peerId) { + final ffi = _connections[peerId]; + return ffi != null && !ffi.closed; + } + + /// Get existing connection without creating new one + static FFI? getExistingConnection(String peerId) { + return _connections[peerId]; + } + + /// Get connection count for debugging + static int getConnectionCount() => _connections.length; + + /// Get terminal count for a peer + static int getTerminalCount(String peerId) => _connectionRefCount[peerId] ?? 0; + + /// Get service ID for a peer + static String? getServiceId(String peerId) => _serviceIds[peerId]; + + /// Set service ID for a peer + static void setServiceId(String peerId, String serviceId) { + _serviceIds[peerId] = serviceId; + debugPrint('[TerminalConnectionManager] Service ID for $peerId: $serviceId'); + } +} \ No newline at end of file diff --git a/flutter/lib/desktop/pages/terminal_page.dart b/flutter/lib/desktop/pages/terminal_page.dart new file mode 100644 index 00000000000..f28545415ee --- /dev/null +++ b/flutter/lib/desktop/pages/terminal_page.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/terminal_model.dart'; +import 'package:xterm/xterm.dart'; +import 'terminal_connection_manager.dart'; + +class TerminalPage extends StatefulWidget { + const TerminalPage({ + Key? key, + required this.id, + required this.password, + required this.tabController, + required this.isSharedPassword, + required this.terminalId, + this.forceRelay, + this.connToken, + }) : super(key: key); + final String id; + final String? password; + final DesktopTabController tabController; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final int terminalId; + + @override + State createState() => _TerminalPageState(); +} + +class _TerminalPageState extends State + with AutomaticKeepAliveClientMixin { + late FFI _ffi; + late TerminalModel _terminalModel; + + @override + void initState() { + super.initState(); + + // Use shared FFI instance from connection manager + _ffi = TerminalConnectionManager.getConnection( + peerId: widget.id, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + connToken: widget.connToken, + ); + + // Create terminal model with specific terminal ID + _terminalModel = TerminalModel(_ffi, widget.terminalId); + debugPrint( + '[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); + + // Register this terminal model with FFI for event routing + _ffi.registerTerminalModel(widget.terminalId, _terminalModel); + + // Initialize terminal connection + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController.onSelected?.call(widget.id); + + // Check if this is a new connection or additional terminal + // Note: When a connection exists, the ref count will be > 1 after this terminal is added + final isExistingConnection = TerminalConnectionManager.hasConnection(widget.id) && + TerminalConnectionManager.getTerminalCount(widget.id) > 1; + + if (!isExistingConnection) { + // First terminal - show loading dialog, wait for onReady + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + } else { + // Additional terminal - connection already established + // Open the terminal directly + _terminalModel.openTerminal(); + } + }); + } + + @override + void dispose() { + // Unregister terminal model from FFI + _ffi.unregisterTerminalModel(widget.terminalId); + _terminalModel.dispose(); + // Release connection reference instead of closing directly + TerminalConnectionManager.releaseConnection(widget.id); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: TerminalView( + _terminalModel.terminal, + controller: _terminalModel.terminalController, + autofocus: true, + backgroundOpacity: 0.7, + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), + onSecondaryTapDown: (details, offset) async { + final selection = _terminalModel.terminalController.selection; + if (selection != null) { + final text = _terminalModel.terminal.buffer.getText(selection); + _terminalModel.terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + _terminalModel.terminal.paste(text); + } + } + }, + ), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/flutter/lib/desktop/pages/terminal_tab_page.dart b/flutter/lib/desktop/pages/terminal_tab_page.dart new file mode 100644 index 00000000000..754b309aec1 --- /dev/null +++ b/flutter/lib/desktop/pages/terminal_tab_page.dart @@ -0,0 +1,427 @@ +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:get/get.dart'; + +import '../../models/platform_model.dart'; +import 'terminal_page.dart'; +import 'terminal_connection_manager.dart'; +import '../widgets/material_mod_popup_menu.dart' as mod_menu; +import '../widgets/popup_menu.dart'; +import 'package:bot_toast/bot_toast.dart'; + +class TerminalTabPage extends StatefulWidget { + final Map params; + + const TerminalTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _TerminalTabPageState(params); +} + +class _TerminalTabPageState extends State { + DesktopTabController get tabController => Get.find(); + + static const IconData selectedIcon = Icons.terminal; + static const IconData unselectedIcon = Icons.terminal_outlined; + int _nextTerminalId = 1; + + _TerminalTabPageState(Map params) { + Get.put(DesktopTabController(tabType: DesktopTabType.terminal)); + tabController.onSelected = (id) { + WindowController.fromWindowId(windowId()) + .setTitle(getWindowNameWithId(id)); + }; + tabController.onRemoved = (_, id) => onRemoveId(id); + final terminalId = params['terminalId'] ?? _nextTerminalId++; + tabController.add(_createTerminalTab( + peerId: params['id'], + terminalId: terminalId, + password: params['password'], + isSharedPassword: params['isSharedPassword'], + forceRelay: params['forceRelay'], + connToken: params['connToken'], + )); + } + + TabInfo _createTerminalTab({ + required String peerId, + required int terminalId, + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) { + final tabKey = '${peerId}_$terminalId'; + return TabInfo( + key: tabKey, + label: '$peerId #$terminalId', + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () async { + // Close the terminal session first + final ffi = TerminalConnectionManager.getExistingConnection(peerId); + if (ffi != null) { + final terminalModel = ffi.terminalModels[terminalId]; + if (terminalModel != null) { + await terminalModel.closeTerminal(); + } + } + // Then close the tab + tabController.closeBy(tabKey); + }, + page: TerminalPage( + key: ValueKey(tabKey), + id: peerId, + terminalId: terminalId, + password: password, + isSharedPassword: isSharedPassword, + tabController: tabController, + forceRelay: forceRelay, + connToken: connToken, + ), + ); + } + + Widget _tabMenuBuilder(String peerId, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + + // New tab menu item + menu.add(MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('New tab'), + style: style, + ), + proc: () { + _addNewTerminal(peerId); + cancelFunc(); + // Also try to close any BotToast overlays + BotToast.cleanAll(); + }, + padding: padding, + )); + + menu.add(MenuEntryDivider()); + + menu.add(MenuEntrySwitch( + switchType: SwitchType.scheckbox, + text: translate('Keep terminal sessions on disconnect'), + getter: () async { + final ffi = Get.find(tag: 'terminal_$peerId'); + return bind.sessionGetToggleOptionSync( + sessionId: ffi.sessionId, + arg: kOptionTerminalPersistent, + ); + }, + setter: (bool v) async { + final ffi = Get.find(tag: 'terminal_$peerId'); + await bind.sessionToggleOption( + sessionId: ffi.sessionId, + value: kOptionTerminalPersistent, + ); + }, + padding: padding, + )); + + return mod_menu.PopupMenu( + items: menu + .map((e) => e.build( + context, + const MenuConfig( + commonColor: CustomPopupMenuTheme.commonColor, + height: CustomPopupMenuTheme.height, + dividerHeight: CustomPopupMenuTheme.dividerHeight, + ), + )) + .expand((i) => i) + .toList(), + ); + } + + @override + void initState() { + super.initState(); + + // Add keyboard shortcut handler + HardwareKeyboard.instance.addHandler(_handleKeyEvent); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "[Remote Terminal] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + if (call.method == kWindowEventNewTerminal) { + final args = jsonDecode(call.arguments); + final id = args['id']; + windowOnTop(windowId()); + // Allow multiple terminals for the same connection + final terminalId = args['terminalId'] ?? _nextTerminalId++; + tabController.add(_createTerminalTab( + peerId: id, + terminalId: terminalId, + password: args['password'], + isSharedPassword: args['isSharedPassword'], + forceRelay: args['forceRelay'], + connToken: args['connToken'], + )); + } else if (call.method == kWindowEventRestoreTerminalSessions) { + _restoreSessions(call.arguments); + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventActiveSession) { + if (tabController.state.value.tabs.isEmpty) { + return false; + } + final currentTab = tabController.state.value.selectedTabInfo; + assert(call.arguments is String, + "Expected String arguments for kWindowEventActiveSession, got ${call.arguments.runtimeType}"); + if (currentTab.key.startsWith(call.arguments)) { + windowOnTop(windowId()); + return true; + } + return false; + } + }); + Future.delayed(Duration.zero, () { + restoreWindowPosition(WindowType.Terminal, windowId: windowId()); + }); + } + + @override + void dispose() { + HardwareKeyboard.instance.removeHandler(_handleKeyEvent); + super.dispose(); + } + + Future _restoreSessions(String arguments) async { + Map? args; + try { + args = jsonDecode(arguments) as Map; + } catch (e) { + debugPrint("Error parsing JSON arguments in _restoreSessions: $e"); + return; + } + final persistentSessions = + args['persistent_sessions'] as List? ?? []; + final sortedSessions = persistentSessions.whereType().toList()..sort(); + for (final terminalId in sortedSessions) { + _addNewTerminalForCurrentPeer(terminalId: terminalId); + // A delay is required to ensure the UI has sufficient time to update + // before adding the next terminal. Without this delay, `_TerminalPageState::dispose()` + // may be called prematurely while the tab widget is still in the tab controller. + // This behavior is likely due to a race condition between the UI rendering lifecycle + // and the addition of new tabs. Attempts to use `_TerminalPageState::addPostFrameCallback()` + // to wait for the previous page to be ready were unsuccessful, as the observed call sequence is: + // `initState() 2 -> dispose() 2 -> postFrameCallback() 2`, followed by `initState() 3`. + // The `Future.delayed` approach mitigates this issue by introducing a buffer period, + // allowing the UI to stabilize before proceeding. + await Future.delayed(const Duration(milliseconds: 300)); + } + } + + bool _handleKeyEvent(KeyEvent event) { + if (event is KeyDownEvent) { + // Use Cmd+T on macOS, Ctrl+Shift+T on other platforms + if (event.logicalKey == LogicalKeyboardKey.keyT) { + if (isMacOS && + HardwareKeyboard.instance.isMetaPressed && + !HardwareKeyboard.instance.isShiftPressed) { + // macOS: Cmd+T (standard for new tab) + _addNewTerminalForCurrentPeer(); + return true; + } else if (!isMacOS && + HardwareKeyboard.instance.isControlPressed && + HardwareKeyboard.instance.isShiftPressed) { + // Other platforms: Ctrl+Shift+T (to avoid conflict with Ctrl+T in terminal) + _addNewTerminalForCurrentPeer(); + return true; + } + } + + // Use Cmd+W on macOS, Ctrl+Shift+W on other platforms + if (event.logicalKey == LogicalKeyboardKey.keyW) { + if (isMacOS && + HardwareKeyboard.instance.isMetaPressed && + !HardwareKeyboard.instance.isShiftPressed) { + // macOS: Cmd+W (standard for close tab) + final currentTab = tabController.state.value.selectedTabInfo; + if (tabController.state.value.tabs.length > 1) { + tabController.closeBy(currentTab.key); + return true; + } + } else if (!isMacOS && + HardwareKeyboard.instance.isControlPressed && + HardwareKeyboard.instance.isShiftPressed) { + // Other platforms: Ctrl+Shift+W (to avoid conflict with Ctrl+W word delete) + final currentTab = tabController.state.value.selectedTabInfo; + if (tabController.state.value.tabs.length > 1) { + tabController.closeBy(currentTab.key); + return true; + } + } + } + + // Use Alt+Left/Right for tab navigation (avoids conflicts) + if (HardwareKeyboard.instance.isAltPressed) { + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + // Previous tab + final currentIndex = tabController.state.value.selected; + if (currentIndex > 0) { + tabController.jumpTo(currentIndex - 1); + } + return true; + } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + // Next tab + final currentIndex = tabController.state.value.selected; + if (currentIndex < tabController.length - 1) { + tabController.jumpTo(currentIndex + 1); + } + return true; + } + } + + // Check for Cmd/Ctrl + Number (switch to specific tab) + final numberKeys = [ + LogicalKeyboardKey.digit1, + LogicalKeyboardKey.digit2, + LogicalKeyboardKey.digit3, + LogicalKeyboardKey.digit4, + LogicalKeyboardKey.digit5, + LogicalKeyboardKey.digit6, + LogicalKeyboardKey.digit7, + LogicalKeyboardKey.digit8, + LogicalKeyboardKey.digit9, + ]; + + for (int i = 0; i < numberKeys.length; i++) { + if (event.logicalKey == numberKeys[i] && + ((isMacOS && HardwareKeyboard.instance.isMetaPressed) || + (!isMacOS && HardwareKeyboard.instance.isControlPressed))) { + if (i < tabController.length) { + tabController.jumpTo(i); + return true; + } + } + } + } + return false; + } + + void _addNewTerminal(String peerId, {int? terminalId}) { + // Find first tab for this peer to get connection parameters + final firstTab = tabController.state.value.tabs.firstWhere( + (tab) => tab.key.startsWith('$peerId\_'), + ); + if (firstTab.page is TerminalPage) { + final page = firstTab.page as TerminalPage; + final newTerminalId = terminalId ?? _nextTerminalId++; + if (terminalId != null && terminalId >= _nextTerminalId) { + _nextTerminalId = terminalId + 1; + } + tabController.add(_createTerminalTab( + peerId: peerId, + terminalId: newTerminalId, + password: page.password, + isSharedPassword: page.isSharedPassword, + forceRelay: page.forceRelay, + connToken: page.connToken, + )); + } + } + + void _addNewTerminalForCurrentPeer({int? terminalId}) { + final currentTab = tabController.state.value.selectedTabInfo; + final parts = currentTab.key.split('_'); + if (parts.isNotEmpty) { + final peerId = parts[0]; + _addNewTerminal(peerId, terminalId: terminalId); + } + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).cardColor, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: _buildAddButton(), + selectedBorderColor: MyTheme.accent, + labelGetter: DesktopTab.tablabelGetter, + tabMenuBuilder: (key) { + // Extract peerId from tab key (format: "peerId_terminalId") + final parts = key.split('_'); + if (parts.isEmpty) return Container(); + final peerId = parts[0]; + return _tabMenuBuilder(peerId, () {}); + }, + )); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : workaroundWindowBorder( + context, + Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: child, + )); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : SubWindowDragToResizeArea( + child: tabWidget, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + ); + } + + void onRemoveId(String id) { + if (tabController.state.value.tabs.isEmpty) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } + + Widget _buildAddButton() { + return ActionIcon( + message: 'New tab', + icon: IconFont.add, + onTap: () { + _addNewTerminalForCurrentPeer(); + }, + isClose: false, + ); + } + + Future handleWindowCloseButton() async { + final connLength = tabController.state.value.tabs.length; + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } +} diff --git a/flutter/lib/desktop/pages/view_camera_page.dart b/flutter/lib/desktop/pages/view_camera_page.dart new file mode 100644 index 00000000000..a1cc5c8a074 --- /dev/null +++ b/flutter/lib/desktop/pages/view_camera_page.dart @@ -0,0 +1,728 @@ +import 'dart:async'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/widgets/remote_input.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:flutter_hbb/models/state_model.dart'; + +import '../../consts.dart'; +import '../../common/widgets/overlay.dart'; +import '../../common.dart'; +import '../../common/widgets/dialog.dart'; +import '../../common/widgets/toolbar.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../../common/shared_state.dart'; +import '../../utils/image.dart'; +import '../widgets/remote_toolbar.dart'; +import '../widgets/kb_layout_type_chooser.dart'; +import '../widgets/tabbar_widget.dart'; + +import 'package:flutter_hbb/native/custom_cursor.dart' + if (dart.library.html) 'package:flutter_hbb/web/custom_cursor.dart'; + +final SimpleWrapper _firstEnterImage = SimpleWrapper(false); + +// Used to skip session close if "move to new window" is clicked. +final Map closeSessionOnDispose = {}; + +class ViewCameraPage extends StatefulWidget { + ViewCameraPage({ + Key? key, + required this.id, + required this.toolbarState, + this.sessionId, + this.tabWindowId, + this.password, + this.display, + this.displays, + this.tabController, + this.connToken, + this.forceRelay, + this.isSharedPassword, + }) : super(key: key) { + initSharedStates(id); + } + + final String id; + final SessionID? sessionId; + final int? tabWindowId; + final int? display; + final List? displays; + final String? password; + final ToolbarState toolbarState; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final SimpleWrapper?> _lastState = SimpleWrapper(null); + final DesktopTabController? tabController; + + FFI get ffi => (_lastState.value! as _ViewCameraPageState)._ffi; + + @override + State createState() { + final state = _ViewCameraPageState(id); + _lastState.value = state; + return state; + } +} + +class _ViewCameraPageState extends State + with AutomaticKeepAliveClientMixin, MultiWindowListener { + Timer? _timer; + String keyboardMode = "legacy"; + bool _isWindowBlur = false; + final _cursorOverImage = false.obs; + + var _blockableOverlayState = BlockableOverlayState(); + + final FocusNode _rawKeyFocusNode = FocusNode(debugLabel: "rawkeyFocusNode"); + + // We need `_instanceIdOnEnterOrLeaveImage4Toolbar` together with `_onEnterOrLeaveImage4Toolbar` + // to identify the toolbar instance and its callback function. + int? _instanceIdOnEnterOrLeaveImage4Toolbar; + Function(bool)? _onEnterOrLeaveImage4Toolbar; + + late FFI _ffi; + + SessionID get sessionId => _ffi.sessionId; + + _ViewCameraPageState(String id) { + _initStates(id); + } + + void _initStates(String id) {} + + @override + void initState() { + super.initState(); + _ffi = FFI(widget.sessionId); + Get.put(_ffi, tag: widget.id); + _ffi.imageModel.addCallbackOnFirstImage((String peerId) { + showKBLayoutTypeChooserIfNeeded( + _ffi.ffiModel.pi.platform, _ffi.dialogManager); + _ffi.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: _ffi.sessionId)); + }); + _ffi.start( + widget.id, + isViewCamera: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + tabWindowId: widget.tabWindowId, + display: widget.display, + displays: widget.displays, + connToken: widget.connToken, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + if (!isLinux) { + WakelockPlus.enable(); + } + + _ffi.ffiModel.updateEventListener(sessionId, widget.id); + if (!isWeb) bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote); + _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId); + _ffi.dialogManager.loadMobileActionsOverlayVisible(); + 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; + // } + + _blockableOverlayState.applyFfi(_ffi); + // Call onSelected in post frame callback, since we cannot guarantee that the callback will not call setState. + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.tabController?.onSelected?.call(widget.id); + }); + } + + @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 (isWindows) { + _isWindowBlur = true; + // unfocus the primary-focus when the whole window is lost focus, + // and let OS to handle events instead. + _rawKeyFocusNode.unfocus(); + } + stateGlobal.isFocused.value = false; + } + + @override + void onWindowFocus() { + super.onWindowFocus(); + // See [onWindowBlur]. + if (isWindows) { + _isWindowBlur = false; + } + stateGlobal.isFocused.value = true; + } + + @override + void onWindowRestore() { + super.onWindowRestore(); + // On windows, we use `onWindowRestore` way to handle window restore from + // a minimized state. + if (isWindows) { + _isWindowBlur = false; + } + if (!isLinux) { + WakelockPlus.enable(); + } + } + + // When the window is unminimized, onWindowMaximize or onWindowRestore can be called when the old state was maximized or not. + @override + void onWindowMaximize() { + super.onWindowMaximize(); + if (!isLinux) { + WakelockPlus.enable(); + } + } + + @override + void onWindowMinimize() { + super.onWindowMinimize(); + if (!isLinux) { + WakelockPlus.disable(); + } + } + + @override + void onWindowEnterFullScreen() { + super.onWindowEnterFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(true); + } + } + + @override + void onWindowLeaveFullScreen() { + super.onWindowLeaveFullScreen(); + if (isMacOS) { + stateGlobal.setFullscreen(false); + } + } + + @override + Future dispose() async { + final closeSession = closeSessionOnDispose.remove(widget.id) ?? true; + + // https://github.com/flutter/flutter/issues/64935 + super.dispose(); + debugPrint("VIEW CAMERA PAGE dispose session $sessionId ${widget.id}"); + _ffi.textureModel.onViewCameraPageDispose(closeSession); + if (closeSession) { + // ensure we leave this session, this is a double check + _ffi.inputModel.enterOrLeave(false); + } + DesktopMultiWindow.removeListener(this); + _ffi.dialogManager.hideMobileActionsOverlay(); + _ffi.imageModel.disposeImage(); + _ffi.cursorModel.disposeImages(); + _rawKeyFocusNode.dispose(); + await _ffi.close(closeSession: closeSession); + _timer?.cancel(); + _ffi.dialogManager.dismissAll(); + if (closeSession) { + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + } + if (!isLinux) { + await WakelockPlus.disable(); + } + await Get.delete(tag: widget.id); + removeSharedStates(widget.id); + } + + Widget emptyOverlay() => BlockableOverlay( + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + state: _blockableOverlayState, + underlying: Container( + color: Colors.transparent, + ), + ); + + Widget buildBody(BuildContext context) { + remoteToolbar(BuildContext context) => RemoteToolbar( + id: widget.id, + ffi: _ffi, + state: widget.toolbarState, + onEnterOrLeaveImageSetter: (id, func) { + _instanceIdOnEnterOrLeaveImage4Toolbar = id; + _onEnterOrLeaveImage4Toolbar = func; + }, + onEnterOrLeaveImageCleaner: (id) { + // If _instanceIdOnEnterOrLeaveImage4Toolbar != id + // it means `_onEnterOrLeaveImage4Toolbar` is not set or it has been changed to another toolbar. + if (_instanceIdOnEnterOrLeaveImage4Toolbar == id) { + _instanceIdOnEnterOrLeaveImage4Toolbar = null; + _onEnterOrLeaveImage4Toolbar = null; + } + }, + setRemoteState: setState, + ); + + bodyWidget() { + return Stack( + children: [ + Container( + color: kColorCanvas, + child: getBodyForDesktop(context), + ), + Stack( + children: [ + _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isTrue + ? emptyOverlay() + : () { + if (!_ffi.ffiModel.isPeerAndroid) { + return Offstage(); + } else { + return Obx(() => Offstage( + offstage: _ffi.dialogManager + .mobileActionsOverlayVisible.isFalse, + child: Overlay(initialEntries: [ + makeMobileActionsOverlayEntry( + () => _ffi.dialogManager + .setMobileActionsOverlayVisible(false), + ffi: _ffi, + ) + ]), + )); + } + }(), + // Use Overlay to enable rebuild every time on menu button click. + _ffi.ffiModel.pi.isSet.isTrue + ? Overlay( + initialEntries: [OverlayEntry(builder: remoteToolbar)]) + : remoteToolbar(context), + _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(), + ], + ), + ], + ); + } + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: Obx(() { + final imageReady = _ffi.ffiModel.pi.isSet.isTrue && + _ffi.ffiModel.waitForFirstImage.isFalse; + if (imageReady) { + // If the privacy mode(disable physical displays) is switched, + // we should not dismiss the dialog immediately. + if (DateTime.now().difference(togglePrivacyModeTime) > + const Duration(milliseconds: 3000)) { + // `dismissAll()` is to ensure that the state is clean. + // It's ok to call dismissAll() here. + _ffi.dialogManager.dismissAll(); + // Recreate the block state to refresh the state. + _blockableOverlayState = BlockableOverlayState(); + _blockableOverlayState.applyFfi(_ffi); + } + // Block the whole `bodyWidget()` when dialog shows. + return BlockableOverlay( + underlying: bodyWidget(), + state: _blockableOverlayState, + ); + } else { + // `_blockableOverlayState` is not recreated here. + // The toolbar's block state won't work properly when reconnecting, but that's okay. + return bodyWidget(); + } + }), + ); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return WillPopScope( + onWillPop: () async { + clientClose(sessionId, _ffi.dialogManager); + return false; + }, + child: MultiProvider(providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ChangeNotifierProvider.value(value: _ffi.recordingModel), + ], child: buildBody(context))); + } + + void enterView(PointerEnterEvent evt) { + _cursorOverImage.value = true; + _firstEnterImage.value = true; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(true); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!isWindows) { + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + _ffi.inputModel.enterOrLeave(true); + } + } + + void leaveView(PointerExitEvent evt) { + if (_ffi.ffiModel.keyboard) { + _ffi.inputModel.tryMoveEdgeOnExit(evt.position); + } + + _cursorOverImage.value = false; + _firstEnterImage.value = false; + if (_onEnterOrLeaveImage4Toolbar != null) { + try { + _onEnterOrLeaveImage4Toolbar!(false); + } catch (e) { + // + } + } + // See [onWindowBlur]. + if (!isWindows) { + _ffi.inputModel.enterOrLeave(false); + } + } + + Widget _buildRawTouchAndPointerRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return RawTouchGestureDetectorRegion( + child: _buildRawPointerMouseRegion(child, onEnter, onExit), + ffi: _ffi, + isCamera: true, + ); + } + + Widget _buildRawPointerMouseRegion( + Widget child, + PointerEnterEventListener? onEnter, + PointerExitEventListener? onExit, + ) { + return CameraRawPointerMouseRegion( + onEnter: onEnter, + onExit: onExit, + onPointerDown: (event) { + // A double check for blur status. + // Note: If there's an `onPointerDown` event is triggered, `_isWindowBlur` is expected being false. + // Sometimes the system does not send the necessary focus event to flutter. We should manually + // handle this inconsistent status by setting `_isWindowBlur` to false. So we can + // ensure the grab-key thread is running when our users are clicking the remote canvas. + if (_isWindowBlur) { + debugPrint( + "Unexpected status: onPointerDown is triggered while the remote window is in blur status"); + _isWindowBlur = false; + } + if (!_rawKeyFocusNode.hasFocus) { + _rawKeyFocusNode.requestFocus(); + } + }, + inputModel: _ffi.inputModel, + child: child, + ); + } + + Widget getBodyForDesktop(BuildContext context) { + var paints = [ + MouseRegion(onEnter: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: false); + }, onExit: (evt) { + if (!isWeb) bind.hostStopSystemKeyPropagate(stopped: true); + }, child: LayoutBuilder(builder: (context, constraints) { + final c = Provider.of(context, listen: false); + Future.delayed(Duration.zero, () => c.updateViewStyle()); + final peerDisplay = CurrentDisplayState.find(widget.id); + return Obx( + () => _ffi.ffiModel.pi.isSet.isFalse + ? Container(color: Colors.transparent) + : Obx(() { + widget.toolbarState.initShow(sessionId); + _ffi.textureModel.updateCurrentDisplay(peerDisplay.value); + return ImagePaint( + id: widget.id, + cursorOverImage: _cursorOverImage, + listenerBuilder: (child) => _buildRawTouchAndPointerRegion( + child, enterView, leaveView), + ffi: _ffi, + ); + }), + ); + })) + ]; + + paints.add( + Positioned( + top: 10, + right: 10, + child: _buildRawTouchAndPointerRegion( + QualityMonitor(_ffi.qualityMonitorModel), null, null), + ), + ); + return Stack( + children: paints, + ); + } + + @override + bool get wantKeepAlive => true; +} + +class ImagePaint extends StatefulWidget { + final FFI ffi; + final String id; + final RxBool cursorOverImage; + final Widget Function(Widget)? listenerBuilder; + + ImagePaint( + {Key? key, + required this.ffi, + required this.id, + required this.cursorOverImage, + this.listenerBuilder}) + : super(key: key); + + @override + State createState() => _ImagePaintState(); +} + +class _ImagePaintState extends State { + String get id => widget.id; + RxBool get cursorOverImage => widget.cursorOverImage; + Widget Function(Widget)? get listenerBuilder => widget.listenerBuilder; + + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + var c = Provider.of(context); + final s = c.scale; + + bool isViewOriginal() => c.viewStyle.style == kRemoteViewStyleOriginal; + + if (c.imageOverflow.isTrue && c.scrollStyle == ScrollStyle.scrollbar) { + final paintWidth = c.getDisplayWidth() * s; + final paintHeight = c.getDisplayHeight() * s; + final paintSize = Size(paintWidth, paintHeight); + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, s, Offset.zero, paintSize, isViewOriginal()) + : _buildScrollbarNonTextureRender(m, paintSize, s); + return NotificationListener( + onNotification: (notification) { + c.updateScrollPercent(); + return false; + }, + child: Container( + child: _buildCrossScrollbarFromLayout( + context, + _buildListener(paintWidget), + c.size, + paintSize, + c.scrollHorizontal, + c.scrollVertical, + )), + ); + } else { + if (c.size.width > 0 && c.size.height > 0) { + final paintWidget = + m.useTextureRender || widget.ffi.ffiModel.pi.forceTextureRender + ? _BuildPaintTextureRender( + c, + s, + Offset( + isLinux ? c.x.toInt().toDouble() : c.x, + isLinux ? c.y.toInt().toDouble() : c.y, + ), + c.size, + isViewOriginal()) + : _buildScrollAutoNonTextureRender(m, c, s); + return Container(child: _buildListener(paintWidget)); + } else { + return Container(); + } + } + } + + Widget _buildScrollbarNonTextureRender( + ImageModel m, Size imageSize, double s) { + return CustomPaint( + size: imageSize, + painter: ImagePainter(image: m.image, x: 0, y: 0, scale: s), + ); + } + + Widget _buildScrollAutoNonTextureRender( + ImageModel m, CanvasModel c, double s) { + return CustomPaint( + size: Size(c.size.width, c.size.height), + painter: ImagePainter(image: m.image, x: c.x / s, y: c.y / s, scale: s), + ); + } + + Widget _BuildPaintTextureRender( + CanvasModel c, double s, Offset offset, Size size, bool isViewOriginal) { + final ffiModel = c.parent.target!.ffiModel; + final displays = ffiModel.pi.getCurDisplays(); + final children = []; + final rect = ffiModel.rect; + if (rect == null) { + return Container(); + } + final curDisplay = ffiModel.pi.currentDisplay; + for (var i = 0; i < displays.length; i++) { + final textureId = widget.ffi.textureModel + .getTextureId(curDisplay == kAllDisplayValue ? i : curDisplay); + if (true) { + // both "textureId.value != -1" and "true" seems ok + children.add(Positioned( + left: (displays[i].x - rect.left) * s + offset.dx, + top: (displays[i].y - rect.top) * s + offset.dy, + width: displays[i].width * s, + height: displays[i].height * s, + child: Obx(() => Texture( + textureId: textureId.value, + filterQuality: + isViewOriginal ? FilterQuality.none : FilterQuality.low, + )), + )); + } + } + return SizedBox( + width: size.width, + height: size.height, + child: Stack(children: children), + ); + } + + MouseCursor _buildCustomCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = cursor.cache ?? preDefaultCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + MouseCursor _buildDisabledCursor(BuildContext context, double scale) { + final cursor = Provider.of(context); + final cache = preForbiddenCursor.cache; + return buildCursorOfCache(cursor, scale, cache); + } + + Widget _buildCrossScrollbarFromLayout( + BuildContext context, + Widget child, + Size layoutSize, + Size size, + ScrollController horizontal, + ScrollController vertical, + ) { + var widget = child; + if (layoutSize.width < size.width) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: horizontal, + scrollDirection: Axis.horizontal, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Row( + children: [ + Container( + width: ((layoutSize.width - size.width) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.height < size.height) { + widget = ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: SingleChildScrollView( + controller: vertical, + physics: cursorOverImage.isTrue + ? const NeverScrollableScrollPhysics() + : null, + child: widget, + ), + ); + } else { + widget = Column( + children: [ + Container( + height: ((layoutSize.height - size.height) ~/ 2).toDouble(), + ), + widget, + ], + ); + } + if (layoutSize.width < size.width) { + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: horizontal, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: layoutSize.height < size.height + ? (notification) => notification.depth == 1 + : defaultScrollNotificationPredicate, + child: widget, + ); + } + if (layoutSize.height < size.height) { + widget = RawScrollbar( + thickness: kScrollbarThickness, + thumbColor: Colors.grey, + controller: vertical, + thumbVisibility: false, + trackVisibility: false, + child: widget, + ); + } + + return Container( + child: widget, + width: layoutSize.width, + height: layoutSize.height, + ); + } + + Widget _buildListener(Widget child) { + if (listenerBuilder != null) { + return listenerBuilder!(child); + } else { + return child; + } + } +} diff --git a/flutter/lib/desktop/pages/view_camera_tab_page.dart b/flutter/lib/desktop/pages/view_camera_tab_page.dart new file mode 100644 index 00000000000..a31ba0fffc0 --- /dev/null +++ b/flutter/lib/desktop/pages/view_camera_tab_page.dart @@ -0,0 +1,491 @@ +import 'dart:convert'; +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/input_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_page.dart'; +import 'package:flutter_hbb/desktop/widgets/remote_toolbar.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/material_mod_popup_menu.dart' + as mod_menu; +import 'package:flutter_hbb/desktop/widgets/popup_menu.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:bot_toast/bot_toast.dart'; + +import '../../models/platform_model.dart'; + +class _MenuTheme { + static const Color blueColor = MyTheme.button; + // kMinInteractiveDimension + static const double height = 20.0; + static const double dividerHeight = 12.0; +} + +class ViewCameraTabPage extends StatefulWidget { + final Map params; + + const ViewCameraTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _ViewCameraTabPageState(params); +} + +class _ViewCameraTabPageState extends State { + final tabController = + Get.put(DesktopTabController(tabType: DesktopTabType.viewCamera)); + final contentKey = UniqueKey(); + static const IconData selectedIcon = Icons.desktop_windows_sharp; + static const IconData unselectedIcon = Icons.desktop_windows_outlined; + + String? peerId; + bool _isScreenRectSet = false; + int? _display; + + var connectionMap = RxList.empty(growable: true); + + _ViewCameraTabPageState(Map params) { + RemoteCountState.init(); + peerId = params['id']; + final sessionId = params['session_id']; + final tabWindowId = params['tab_window_id']; + final display = params['display']; + final displays = params['displays']; + final screenRect = parseParamScreenRect(params); + _isScreenRectSet = screenRect != null; + _display = display as int?; + tryMoveToScreenAndSetFullscreen(screenRect); + if (peerId != null) { + ConnectionTypeState.init(peerId!); + tabController.onSelected = (id) { + final viewCameraPage = tabController.widget(id); + if (viewCameraPage is ViewCameraPage) { + final ffi = viewCameraPage.ffi; + bind.setCurSessionId(sessionId: ffi.sessionId); + } + WindowController.fromWindowId(params['windowId']) + .setTitle(getWindowNameWithId(id)); + UnreadChatCountState.find(id).value = 0; + }; + tabController.add(TabInfo( + key: peerId!, + label: peerId!, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(peerId), + page: ViewCameraPage( + key: ValueKey(peerId), + id: peerId!, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: params['password'], + toolbarState: ToolbarState(), + tabController: tabController, + connToken: params['connToken'], + forceRelay: params['forceRelay'], + isSharedPassword: params['isSharedPassword'], + ), + )); + _update_remote_count(); + } + tabController.onRemoved = (_, id) => onRemoveId(id); + rustDeskWinManager.setMethodHandler(_remoteMethodHandler); + } + + @override + void initState() { + super.initState(); + + if (!_isScreenRectSet) { + Future.delayed(Duration.zero, () { + restoreWindowPosition( + WindowType.ViewCamera, + windowId: windowId(), + peerId: tabController.state.value.tabs.isEmpty + ? null + : tabController.state.value.tabs[0].key, + display: _display, + ); + }); + } + } + + @override + Widget build(BuildContext context) { + final child = Scaffold( + backgroundColor: Theme.of(context).colorScheme.background, + body: DesktopTab( + controller: tabController, + onWindowCloseButton: handleWindowCloseButton, + tail: const AddButton(), + selectedBorderColor: MyTheme.accent, + pageViewBuilder: (pageView) => pageView, + labelGetter: DesktopTab.tablabelGetter, + tabBuilder: (key, icon, label, themeConf) => Obx(() { + final connectionType = ConnectionTypeState.find(key); + if (!connectionType.isValid()) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + label, + ], + ); + } else { + bool secure = + connectionType.secure.value == ConnectionType.strSecure; + bool direct = + connectionType.direct.value == ConnectionType.strDirect; + String msgConn = getConnectionText( + secure, direct, connectionType.stream_type.value); + var msgFingerprint = '${translate('Fingerprint')}:\n'; + var fingerprint = FingerprintState.find(key).value; + if (fingerprint.isEmpty) { + fingerprint = 'N/A'; + } + if (fingerprint.length > 5 * 8) { + var first = fingerprint.substring(0, 39); + var second = fingerprint.substring(40); + msgFingerprint += '$first\n$second'; + } else { + msgFingerprint += fingerprint; + } + + final tab = Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + icon, + Tooltip( + message: '$msgConn\n$msgFingerprint', + child: SvgPicture.asset( + 'assets/${connectionType.secure.value}${connectionType.direct.value}.svg', + width: themeConf.iconSize, + height: themeConf.iconSize, + ).paddingOnly(right: 5), + ), + label, + unreadMessageCountBuilder(UnreadChatCountState.find(key)) + .marginOnly(left: 4), + ], + ); + + return Listener( + onPointerDown: (e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + return; + } + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as ViewCameraPage; + if (viewCameraPage.ffi.ffiModel.pi.isSet.isTrue && + e.buttons == 2) { + showRightMenu( + (CancelFunc cancelFunc) { + return _tabMenuBuilder(key, cancelFunc); + }, + target: e.position, + ); + } + }, + child: tab, + ); + } + }), + ), + ); + final tabWidget = isLinux + ? buildVirtualWindowFrame(context, child) + : workaroundWindowBorder( + context, + Obx(() => Container( + decoration: BoxDecoration( + border: Border.all( + color: MyTheme.color(context).border!, + width: stateGlobal.windowBorderWidth.value), + ), + child: child, + ))); + return isMacOS || kUseCompatibleUiMode + ? tabWidget + : Obx(() => SubWindowDragToResizeArea( + key: contentKey, + child: tabWidget, + // Specially configured for a better resize area and remote control. + childPadding: kDragToResizeAreaPadding, + resizeEdgeSize: stateGlobal.resizeEdgeSize.value, + enableResizeEdges: subWindowManagerEnableResizeEdges, + windowId: stateGlobal.windowId, + )); + } + + // Note: Some dup code to ../widgets/remote_toolbar + Widget _tabMenuBuilder(String key, CancelFunc cancelFunc) { + final List> menu = []; + const EdgeInsets padding = EdgeInsets.only(left: 8.0, right: 5.0); + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == key) + .page as ViewCameraPage; + final ffi = viewCameraPage.ffi; + final sessionId = ffi.sessionId; + final toolbarState = viewCameraPage.toolbarState; + menu.addAll([ + MenuEntryButton( + childBuilder: (TextStyle? style) => Obx(() => Text( + translate( + toolbarState.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'), + style: style, + )), + proc: () { + toolbarState.switchShow(sessionId); + cancelFunc(); + }, + padding: padding, + ), + ]); + + if (tabController.state.value.tabs.length > 1) { + final splitAction = MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Move tab to new window'), + style: style, + ), + proc: () async { + await DesktopMultiWindow.invokeMethod( + kMainWindowId, + kWindowEventMoveTabToNewWindow, + '${windowId()},$key,$sessionId,ViewCamera'); + cancelFunc(); + }, + padding: padding, + ); + menu.insert(1, splitAction); + } + + menu.addAll([ + MenuEntryDivider(), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Copy Fingerprint'), + style: style, + ), + proc: () => onCopyFingerprint(FingerprintState.find(key).value), + padding: padding, + dismissOnClicked: true, + dismissCallback: cancelFunc, + ), + MenuEntryButton( + childBuilder: (TextStyle? style) => Text( + translate('Close'), + style: style, + ), + proc: () { + tabController.closeBy(key); + cancelFunc(); + }, + padding: padding, + ) + ]); + + return mod_menu.PopupMenu( + items: menu + .map((entry) => entry.build( + context, + const MenuConfig( + commonColor: _MenuTheme.blueColor, + height: _MenuTheme.height, + dividerHeight: _MenuTheme.dividerHeight, + ))) + .expand((i) => i) + .toList(), + ); + } + + void onRemoveId(String id) async { + if (tabController.state.value.tabs.isEmpty) { + // Keep calling until the window status is hidden. + // + // Workaround for Windows: + // If you click other buttons and close in msgbox within a very short period of time, the close may fail. + // `await WindowController.fromWindowId(windowId()).close();`. + Future loopCloseWindow() async { + int c = 0; + final windowController = WindowController.fromWindowId(windowId()); + while (c < 20 && + tabController.state.value.tabs.isEmpty && + (!await windowController.isHidden())) { + await windowController.close(); + await Future.delayed(Duration(milliseconds: 100)); + c++; + } + } + + loopCloseWindow(); + } + ConnectionTypeState.delete(id); + _update_remote_count(); + } + + int windowId() { + return widget.params["windowId"]; + } + + Future handleWindowCloseButton() async { + final connLength = tabController.length; + if (connLength <= 1) { + tabController.clear(); + return true; + } else { + final bool res; + if (!option2bool(kOptionEnableConfirmClosingTabs, + bind.mainGetLocalOption(key: kOptionEnableConfirmClosingTabs))) { + res = true; + } else { + res = await closeConfirmDialog(); + } + if (res) { + tabController.clear(); + } + return res; + } + } + + _update_remote_count() => + RemoteCountState.find().value = tabController.length; + + Future _remoteMethodHandler(call, fromWindowId) async { + debugPrint( + "[View Camera Page] call ${call.method} with args ${call.arguments} from window $fromWindowId"); + + dynamic returnValue; + // for simplify, just replace connectionId + if (call.method == kWindowEventNewViewCamera) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final sessionId = args['session_id']; + final tabWindowId = args['tab_window_id']; + final display = args['display']; + final displays = args['displays']; + final screenRect = parseParamScreenRect(args); + final prePeerCount = tabController.length; + Future.delayed(Duration.zero, () async { + if (stateGlobal.fullscreen.isTrue) { + await WindowController.fromWindowId(windowId()).setFullscreen(false); + stateGlobal.setFullscreen(false, procWnd: false); + } + await setNewConnectWindowFrame(windowId(), id!, prePeerCount, + WindowType.ViewCamera, display, screenRect); + Future.delayed(Duration(milliseconds: isWindows ? 100 : 0), () async { + await windowOnTop(windowId()); + }); + }); + ConnectionTypeState.init(id); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + onTabCloseButton: () => tabController.closeBy(id), + page: ViewCameraPage( + key: ValueKey(id), + id: id, + sessionId: sessionId == null ? null : SessionID(sessionId), + tabWindowId: tabWindowId, + display: display, + displays: displays?.cast(), + password: args['password'], + toolbarState: ToolbarState(), + tabController: tabController, + connToken: args['connToken'], + forceRelay: args['forceRelay'], + isSharedPassword: args['isSharedPassword'], + ), + )); + } else if (call.method == kWindowDisableGrabKeyboard) { + // ??? + } else if (call.method == "onDestroy") { + tabController.clear(); + } else if (call.method == kWindowActionRebuild) { + reloadCurrentWindow(); + } else if (call.method == kWindowEventActiveSession) { + final jumpOk = tabController.jumpToByKey(call.arguments); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventActiveDisplaySession) { + final args = jsonDecode(call.arguments); + final id = args['id']; + final display = args['display']; + final jumpOk = + tabController.jumpToByKeyAndDisplay(id, display, isCamera: true); + if (jumpOk) { + windowOnTop(windowId()); + } + return jumpOk; + } else if (call.method == kWindowEventGetRemoteList) { + return tabController.state.value.tabs + .map((e) => e.key) + .toList() + .join(','); + } else if (call.method == kWindowEventGetSessionIdList) { + return tabController.state.value.tabs + .map((e) => '${e.key},${(e.page as ViewCameraPage).ffi.sessionId}') + .toList() + .join(';'); + } else if (call.method == kWindowEventGetCachedSessionData) { + // Ready to show new window and close old tab. + final args = jsonDecode(call.arguments); + final id = args['id']; + final close = args['close']; + try { + final viewCameraPage = tabController.state.value.tabs + .firstWhere((tab) => tab.key == id) + .page as ViewCameraPage; + returnValue = viewCameraPage.ffi.ffiModel.cachedPeerData.toString(); + } catch (e) { + debugPrint('Failed to get cached session data: $e'); + } + if (close && returnValue != null) { + closeSessionOnDispose[id] = false; + tabController.closeBy(id); + } + } else if (call.method == kWindowEventRemoteWindowCoords) { + final viewCameraPage = + tabController.state.value.selectedTabInfo.page as ViewCameraPage; + final ffi = viewCameraPage.ffi; + final displayRect = ffi.ffiModel.displaysRect(); + if (displayRect != null) { + final wc = WindowController.fromWindowId(windowId()); + Rect? frame; + try { + frame = await wc.getFrame(); + } catch (e) { + debugPrint( + "Failed to get frame of window $windowId, it may be hidden"); + } + if (frame != null) { + ffi.cursorModel.moveLocal(0, 0); + final coords = RemoteWindowCoords( + frame, + CanvasCoords.fromCanvasModel(ffi.canvasModel), + CursorCoords.fromCursorModel(ffi.cursorModel), + displayRect); + returnValue = jsonEncode(coords.toJson()); + } + } + } else if (call.method == kWindowEventSetFullscreen) { + stateGlobal.setFullscreen(call.arguments == 'true'); + } + _update_remote_count(); + return returnValue; + } +} diff --git a/flutter/lib/desktop/screen/desktop_terminal_screen.dart b/flutter/lib/desktop/screen/desktop_terminal_screen.dart new file mode 100644 index 00000000000..301489c8650 --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_terminal_screen.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:provider/provider.dart'; + +import 'package:flutter_hbb/desktop/pages/terminal_tab_page.dart'; + +class DesktopTerminalScreen extends StatelessWidget { + final Map params; + + const DesktopTerminalScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ], + child: Scaffold( + backgroundColor: isLinux ? Colors.transparent : null, + body: TerminalTabPage( + params: params, + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/screen/desktop_view_camera_screen.dart b/flutter/lib/desktop/screen/desktop_view_camera_screen.dart new file mode 100644 index 00000000000..a845b89d01c --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_view_camera_screen.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_tab_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/state_model.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab desktop remote screen +class DesktopViewCameraScreen extends StatelessWidget { + final Map params; + + DesktopViewCameraScreen({Key? key, required this.params}) : super(key: key) { + bind.mainInitInputSource(); + stateGlobal.getInputSource(force: true); + } + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ], + child: Scaffold( + // Set transparent background for padding the resize area out of the flutter view. + // This allows the wallpaper goes through our resize area. (Linux only now). + backgroundColor: isLinux ? Colors.transparent : null, + body: ViewCameraTabPage( + params: params, + ), + )); + } +} diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart index 839ea1a81db..f29908d5198 100644 --- a/flutter/lib/desktop/widgets/remote_toolbar.dart +++ b/flutter/lib/desktop/widgets/remote_toolbar.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hbb/common/widgets/audio_input.dart'; +import 'package:flutter_hbb/common/widgets/dialog.dart'; import 'package:flutter_hbb/common/widgets/toolbar.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -478,7 +479,10 @@ class _RemoteToolbarState extends State { state: widget.state, setFullscreen: _setFullscreen, )); - toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + // Do not show keyboard for camera connection type. + if (widget.ffi.connType == ConnType.defaultConn) { + toolbarItems.add(_KeyboardMenu(id: widget.id, ffi: widget.ffi)); + } toolbarItems.add(_ChatMenu(id: widget.id, ffi: widget.ffi)); if (!isWeb) { toolbarItems.add(_VoiceCallMenu(id: widget.id, ffi: widget.ffi)); @@ -1043,23 +1047,26 @@ class _DisplayMenuState extends State<_DisplayMenu> { scrollStyle(), imageQuality(), codec(), - _ResolutionsMenu( - id: widget.id, - ffi: widget.ffi, - screenAdjustor: _screenAdjustor, - ), - if (showVirtualDisplayMenu(ffi)) + if (ffi.connType == ConnType.defaultConn) + _ResolutionsMenu( + id: widget.id, + ffi: widget.ffi, + screenAdjustor: _screenAdjustor, + ), + if (showVirtualDisplayMenu(ffi) && ffi.connType == ConnType.defaultConn) _SubmenuButton( ffi: widget.ffi, menuChildren: getVirtualDisplayMenuChildren(ffi, id, null), child: Text(translate("Virtual display")), ), - cursorToggles(), + if (ffi.connType == ConnType.defaultConn) cursorToggles(), Divider(), toggles(), ]; // privacy mode - if (ffiModel.keyboard && pi.features.privacyMode) { + if (ffi.connType == ConnType.defaultConn && + ffiModel.keyboard && + pi.features.privacyMode) { final privacyModeState = PrivacyModeState.find(id); final privacyModeList = toolbarPrivacyMode(privacyModeState, context, id, ffi); @@ -1085,7 +1092,9 @@ class _DisplayMenuState extends State<_DisplayMenu> { ]); } } - menuChildren.add(widget.pluginItem); + if (ffi.connType == ConnType.defaultConn) { + menuChildren.add(widget.pluginItem); + } return menuChildren; } @@ -1495,7 +1504,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { ); } - TextField _resolutionInput(TextEditingController controller) { + Widget _resolutionInput(TextEditingController controller) { return TextField( decoration: InputDecoration( border: InputBorder.none, @@ -1509,7 +1518,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> { FilteringTextInputFormatter.allow(RegExp(r'[0-9]')), ], controller: controller, - ); + ).workaroundFreezeLinuxMint(); } List _supportedResolutionMenuButtons() => resolutions @@ -1586,10 +1595,28 @@ class _KeyboardMenu extends StatelessWidget { viewMode(), Divider(), ...toolbarToggles(), + ...mouseSpeed(), ...mobileActions(), ]); } + mouseSpeed() { + final speedWidgets = []; + final sessionId = ffi.sessionId; + if (isDesktop) { + if (ffi.ffiModel.keyboard) { + final enabled = !ffi.ffiModel.viewOnly; + final trackpad = MenuButton( + child: Text(translate('Trackpad speed')).paddingOnly(left: 26.0), + onPressed: enabled ? () => trackpadSpeedDialog(sessionId, ffi) : null, + ffi: ffi, + ); + speedWidgets.add(trackpad); + } + } + return speedWidgets; + } + keyboardMode() { return futureBuilder(future: () async { return await bind.sessionGetKeyboardMode(sessionId: ffi.sessionId) ?? diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart index 96ada22c907..c1cc433ad10 100644 --- a/flutter/lib/desktop/widgets/tabbar_widget.dart +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart' hide TabBarTheme; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/desktop/pages/remote_page.dart'; +import 'package:flutter_hbb/desktop/pages/view_camera_page.dart'; import 'package:flutter_hbb/main.dart'; import 'package:flutter_hbb/models/platform_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; @@ -51,7 +52,9 @@ enum DesktopTabType { cm, remoteScreen, fileTransfer, + viewCamera, portForward, + terminal, install, } @@ -179,11 +182,13 @@ class DesktopTabController { jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key), callOnSelected: callOnSelected); - bool jumpToByKeyAndDisplay(String key, int display) { + bool jumpToByKeyAndDisplay(String key, int display, {bool isCamera = false}) { for (int i = 0; i < state.value.tabs.length; i++) { final tab = state.value.tabs[i]; if (tab.key == key) { - final ffi = (tab.page as RemotePage).ffi; + final ffi = isCamera + ? (tab.page as ViewCameraPage).ffi + : (tab.page as RemotePage).ffi; if (ffi.ffiModel.pi.currentDisplay == display) { return jumpTo(i, callOnSelected: true); } @@ -647,7 +652,9 @@ class _DesktopTabState extends State controller.state.value.scrollController; if (!sc.canScroll) return; _scrollDebounce.call(() { - sc.animateTo(sc.offset + e.scrollDelta.dy, + double adjust = 2.5; + sc.animateTo( + sc.offset + e.scrollDelta.dy * adjust, duration: Duration(milliseconds: 200), curve: Curves.ease); }); @@ -725,6 +732,7 @@ class WindowActionPanelState extends State { return widget.tabController.state.value.tabs.length > 1 && (widget.tabController.tabType == DesktopTabType.remoteScreen || widget.tabController.tabType == DesktopTabType.fileTransfer || + widget.tabController.tabType == DesktopTabType.viewCamera || widget.tabController.tabType == DesktopTabType.portForward || widget.tabController.tabType == DesktopTabType.cm); } diff --git a/flutter/lib/desktop/widgets/update_progress.dart b/flutter/lib/desktop/widgets/update_progress.dart new file mode 100644 index 00000000000..93f661b7b7d --- /dev/null +++ b/flutter/lib/desktop/widgets/update_progress.dart @@ -0,0 +1,267 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:url_launcher/url_launcher.dart'; + +final _isExtracting = false.obs; + +void handleUpdate(String releasePageUrl) { + _isExtracting.value = false; + String downloadUrl = releasePageUrl.replaceAll('tag', 'download'); + String version = downloadUrl.substring(downloadUrl.lastIndexOf('/') + 1); + final String downloadFile = + bind.mainGetCommonSync(key: 'download-file-$version'); + if (downloadFile.startsWith('error:')) { + final error = downloadFile.replaceFirst('error:', ''); + msgBox(gFFI.sessionId, 'custom-nocancel-nook-hasclose', 'Error', error, + releasePageUrl, gFFI.dialogManager); + return; + } + downloadUrl = '$downloadUrl/$downloadFile'; + + SimpleWrapper downloadId = SimpleWrapper(''); + SimpleWrapper onCanceled = SimpleWrapper(() {}); + gFFI.dialogManager.dismissAll(); + gFFI.dialogManager.show((setState, close, context) { + return CustomAlertDialog( + title: Obx(() => Text(translate(_isExtracting.isTrue + ? 'Preparing for installation ...' + : 'Downloading {$appName}'))), + content: + UpdateProgress(releasePageUrl, downloadUrl, downloadId, onCanceled) + .marginSymmetric(horizontal: 8) + .paddingOnly(top: 12), + actions: [ + if (_isExtracting.isFalse) dialogButton(translate('Cancel'), onPressed: () async { + onCanceled.value(); + await bind.mainSetCommon( + key: 'cancel-downloader', value: downloadId.value); + // Wait for the downloader to be removed. + for (int i = 0; i < 10; i++) { + await Future.delayed(const Duration(milliseconds: 300)); + final isCanceled = 'error:Downloader not found' == + await bind.mainGetCommon( + key: 'download-data-${downloadId.value}'); + if (isCanceled) { + break; + } + } + close(); + }, isOutline: true), + ]); + }); +} + +class UpdateProgress extends StatefulWidget { + final String releasePageUrl; + final String downloadUrl; + final SimpleWrapper downloadId; + final SimpleWrapper onCanceled; + UpdateProgress( + this.releasePageUrl, this.downloadUrl, this.downloadId, this.onCanceled, + {Key? key}) + : super(key: key); + + @override + State createState() => UpdateProgressState(); +} + +class UpdateProgressState extends State { + Timer? _timer; + int? _totalSize; + int _downloadedSize = 0; + int _getDataFailedCount = 0; + final String _eventKeyDownloadNewVersion = 'download-new-version'; + final String _eventKeyExtractUpdateDmg = 'extract-update-dmg'; + + @override + void initState() { + super.initState(); + widget.onCanceled.value = () { + cancelQueryTimer(); + }; + platformFFI.registerEventHandler(_eventKeyDownloadNewVersion, + _eventKeyDownloadNewVersion, handleDownloadNewVersion, + replace: true); + bind.mainSetCommon(key: 'download-new-version', value: widget.downloadUrl); + if (isMacOS) { + platformFFI.registerEventHandler(_eventKeyExtractUpdateDmg, + _eventKeyExtractUpdateDmg, handleExtractUpdateDmg, + replace: true); + } + } + + @override + void dispose() { + cancelQueryTimer(); + platformFFI.unregisterEventHandler( + _eventKeyDownloadNewVersion, _eventKeyDownloadNewVersion); + if (isMacOS) { + platformFFI.unregisterEventHandler( + _eventKeyExtractUpdateDmg, _eventKeyExtractUpdateDmg); + } + super.dispose(); + } + + void cancelQueryTimer() { + _timer?.cancel(); + _timer = null; + } + + Future handleDownloadNewVersion(Map evt) async { + if (evt.containsKey('id')) { + widget.downloadId.value = evt['id'] as String; + _timer = Timer.periodic(const Duration(milliseconds: 300), (timer) { + _updateDownloadData(); + }); + } else { + if (evt.containsKey('error')) { + _onError(evt['error'] as String); + } else { + // unreachable + _onError('$evt'); + } + } + } + + // `isExtractDmg` is true when handling extract-update-dmg event. + // It's a rare case that the dmg file is corrupted and cannot be extracted. + void _onError(String error, {bool isExtractDmg = false}) { + cancelQueryTimer(); + + debugPrint( + '${isExtractDmg ? "Extract" : "Download"} new version error: $error'); + final msgBoxType = 'custom-nocancel-nook-hasclose'; + final msgBoxTitle = 'Error'; + final msgBoxText = 'download-new-version-failed-tip'; + final dialogManager = gFFI.dialogManager; + + close() { + dialogManager.dismissAll(); + } + + jumplink() { + launchUrl(Uri.parse(widget.releasePageUrl)); + dialogManager.dismissAll(); + } + + retry() { + dialogManager.dismissAll(); + handleUpdate(widget.releasePageUrl); + } + + final List buttons = [ + dialogButton('Download', onPressed: jumplink), + if (!isExtractDmg) dialogButton('Retry', onPressed: retry), + dialogButton('Close', onPressed: close), + ]; + dialogManager.dismissAll(); + dialogManager.show( + (setState, close, context) => CustomAlertDialog( + title: null, + content: SelectionArea( + child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)), + actions: buttons, + ), + tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle', + ); + } + + void _updateDownloadData() { + String err = ''; + String downloadData = + bind.mainGetCommonSync(key: 'download-data-${widget.downloadId.value}'); + if (downloadData.startsWith('error:')) { + err = downloadData.substring('error:'.length); + } else { + try { + jsonDecode(downloadData).forEach((key, value) { + if (key == 'total_size') { + if (value != null && value is int) { + _totalSize = value; + } + } else if (key == 'downloaded_size') { + _downloadedSize = value as int; + } else if (key == 'error') { + if (value != null) { + err = value.toString(); + } + } + }); + } catch (e) { + _getDataFailedCount += 1; + debugPrint( + 'Failed to get download data ${widget.downloadUrl}, error $e'); + if (_getDataFailedCount > 3) { + err = e.toString(); + } + } + } + if (err != '') { + _onError(err); + } else { + if (_totalSize != null && _downloadedSize >= _totalSize!) { + cancelQueryTimer(); + bind.mainSetCommon( + key: 'remove-downloader', value: widget.downloadId.value); + if (_totalSize == 0) { + _onError('The download file size is 0.'); + } else { + setState(() {}); + if (isMacOS) { + bind.mainSetCommon( + key: 'extract-update-dmg', value: widget.downloadUrl); + _isExtracting.value = true; + } else { + updateMsgBox(); + } + } + } else { + setState(() {}); + } + } + } + + void updateMsgBox() { + msgBox( + gFFI.sessionId, + 'custom-nocancel', + '{$appName} Update', + '{$appName}-to-update-tip', + '', + gFFI.dialogManager, + onSubmit: () { + debugPrint('Downloaded, update to new version now'); + bind.mainSetCommon(key: 'update-me', value: widget.downloadUrl); + }, + submitTimeout: 5, + ); + } + + Future handleExtractUpdateDmg(Map evt) async { + _isExtracting.value = false; + if (evt.containsKey('err') && (evt['err'] as String).isNotEmpty) { + _onError(evt['err'] as String, isExtractDmg: true); + } else { + updateMsgBox(); + } + } + + @override + Widget build(BuildContext context) { + getValue() => _totalSize == null + ? 0.0 + : (_totalSize == 0 ? 1.0 : _downloadedSize / _totalSize!); + return LinearProgressIndicator( + value: _isExtracting.isTrue ? null : getValue(), + minHeight: 20, + borderRadius: BorderRadius.circular(5), + backgroundColor: Colors.grey[300], + valueColor: const AlwaysStoppedAnimation(Colors.blue), + ); + } +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 3032a2321f0..80a3bff894a 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -11,8 +11,10 @@ import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; import 'package:flutter_hbb/desktop/pages/install_page.dart'; import 'package:flutter_hbb/desktop/pages/server_page.dart'; import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_view_camera_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_port_forward_screen.dart'; import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_terminal_screen.dart'; import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/utils/multi_window_manager.dart'; @@ -76,6 +78,13 @@ Future main(List args) async { kAppTypeDesktopFileTransfer, ); break; + case WindowType.ViewCamera: + desktopType = DesktopType.viewCamera; + runMultiWindow( + argument, + kAppTypeDesktopViewCamera, + ); + break; case WindowType.PortForward: desktopType = DesktopType.portForward; runMultiWindow( @@ -83,6 +92,12 @@ Future main(List args) async { kAppTypeDesktopPortForward, ); break; + case WindowType.Terminal: + desktopType = DesktopType.terminal; + runMultiWindow( + argument, + kAppTypeDesktopTerminal, + ); default: break; } @@ -133,7 +148,8 @@ void runMainApp(bool startService) async { runApp(App()); // Set window option. - WindowOptions windowOptions = getHiddenTitleBarWindowOptions(); + WindowOptions windowOptions = + getHiddenTitleBarWindowOptions(isMainWindow: true); windowManager.waitUntilReadyToShow(windowOptions, () async { // Restore the location of the main window before window hide or show. await restoreWindowPosition(WindowType.Main); @@ -191,11 +207,22 @@ void runMultiWindow( params: argument, ); break; + case kAppTypeDesktopViewCamera: + draggablePositions.load(); + widget = DesktopViewCameraScreen( + params: argument, + ); + break; case kAppTypeDesktopPortForward: widget = DesktopPortForwardScreen( params: argument, ); break; + case kAppTypeDesktopTerminal: + widget = DesktopTerminalScreen( + params: argument, + ); + break; default: // no such appType exit(0); @@ -226,9 +253,25 @@ void runMultiWindow( await restoreWindowPosition(WindowType.FileTransfer, windowId: kWindowId!); break; + case kAppTypeDesktopViewCamera: + // If screen rect is set, the window will be moved to the target screen and then set fullscreen. + if (argument['screen_rect'] == null) { + // display can be used to control the offset of the window. + await restoreWindowPosition( + WindowType.ViewCamera, + windowId: kWindowId!, + peerId: argument['id'] as String?, + // FIXME: fix display index. + display: argument['display'] as int?, + ); + } + break; case kAppTypeDesktopPortForward: await restoreWindowPosition(WindowType.PortForward, windowId: kWindowId!); break; + case kAppTypeDesktopTerminal: + await restoreWindowPosition(WindowType.Terminal, windowId: kWindowId!); + break; default: // no such appType exit(0); @@ -354,7 +397,10 @@ void runInstallPage() async { } WindowOptions getHiddenTitleBarWindowOptions( - {Size? size, bool center = false, bool? alwaysOnTop}) { + {bool isMainWindow = false, + Size? size, + bool center = false, + bool? alwaysOnTop}) { var defaultTitleBarStyle = TitleBarStyle.hidden; // we do not hide titlebar on win7 because of the frame overflow. if (kUseCompatibleUiMode) { @@ -363,7 +409,7 @@ WindowOptions getHiddenTitleBarWindowOptions( return WindowOptions( size: size, center: center, - backgroundColor: Colors.transparent, + backgroundColor: (isMacOS && isMainWindow) ? null : Colors.transparent, skipTaskbar: false, titleBarStyle: defaultTitleBarStyle, alwaysOnTop: alwaysOnTop, @@ -485,9 +531,10 @@ class _AppState extends State with WidgetsBindingObserver { child = keyListenerBuilder(context, child); } if (isLinux) { - child = buildVirtualWindowFrame(context, child); + return buildVirtualWindowFrame(context, child); + } else { + return workaroundWindowBorder(context, child); } - return child; }, ), ); diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart index 49e3b2c9107..07aaaef8cdf 100644 --- a/flutter/lib/mobile/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -41,10 +41,11 @@ class _ConnectionPageState extends State { final _idController = IDTextEditingController(); final RxBool _idEmpty = true.obs; - List peers = []; + final FocusNode _idFocusNode = FocusNode(); + final TextEditingController _idEditingController = TextEditingController(); + + final AllPeersLoader _allPeersLoader = AllPeersLoader(); - bool isPeersLoading = false; - bool isPeersLoaded = false; StreamSubscription? _uniLinksSubscription; // https://github.com/flutter/flutter/issues/157244 @@ -61,6 +62,8 @@ class _ConnectionPageState extends State { @override void initState() { super.initState(); + _allPeersLoader.init(setState); + _idFocusNode.addListener(onFocusChanged); if (_idController.text.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((_) async { final lastRemoteId = await bind.mainGetLastRemoteId(); @@ -71,6 +74,7 @@ class _ConnectionPageState extends State { } }); } + Get.put(_idEditingController); } @override @@ -80,7 +84,7 @@ class _ConnectionPageState extends State { slivers: [ SliverList( delegate: SliverChildListDelegate([ - if (!bind.isCustomClient()) + if (!bind.isCustomClient() && !isIOS) Obx(() => _buildUpdateUI(stateGlobal.updateUrl.value)), _buildRemoteIDTextField(), ])), @@ -99,6 +103,20 @@ class _ConnectionPageState extends State { connect(context, id); } + void onFocusChanged() { + _idEmpty.value = _idEditingController.text.isEmpty; + if (_idFocusNode.hasFocus) { + if (_allPeersLoader.needLoad) { + _allPeersLoader.getAllPeers(); + } + + final textLength = _idEditingController.value.text.length; + // Select all to facilitate removing text, just following the behavior of address input of chrome. + _idEditingController.selection = + TextSelection(baseOffset: 0, extentOffset: textLength); + } + } + /// UI for software update. /// If _updateUrl] is not empty, shows a button to update the software. Widget _buildUpdateUI(String updateUrl) { @@ -107,7 +125,7 @@ class _ConnectionPageState extends State { : InkWell( onTap: () async { final url = 'https://rustdesk.com/download'; - // https://pub.dev/packages/url_launcher#configuration + // https://pub.dev/packages/url_launcher#configuration // https://developer.android.com/training/package-visibility/use-cases#open-urls-custom-tabs // // `await launchUrl(Uri.parse(url))` can also run if skip @@ -115,9 +133,7 @@ class _ConnectionPageState extends State { // 2. `` in AndroidManifest.xml // // But it is better to add the check. - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } + await launchUrl(Uri.parse(url)); }, child: Container( alignment: AlignmentDirectional.center, @@ -129,18 +145,6 @@ class _ConnectionPageState extends State { color: Colors.white, fontWeight: FontWeight.bold)))); } - Future _fetchPeers() async { - setState(() { - isPeersLoading = true; - }); - await Future.delayed(Duration(milliseconds: 100)); - peers = await getAllPeers(); - setState(() { - isPeersLoading = false; - isPeersLoaded = true; - }); - } - /// UI for the remote ID TextField. /// Search for a peer and connect to it if the id exists. Widget _buildRemoteIDTextField() { @@ -158,11 +162,12 @@ class _ConnectionPageState extends State { Expanded( child: Container( padding: const EdgeInsets.only(left: 16, right: 16), - child: Autocomplete( + child: RawAutocomplete( optionsBuilder: (TextEditingValue textEditingValue) { if (textEditingValue.text == '') { _autocompleteOpts = const Iterable.empty(); - } else if (peers.isEmpty && !isPeersLoaded) { + } else if (_allPeersLoader.peers.isEmpty && + !_allPeersLoader.isPeersLoaded) { Peer emptyPeer = Peer( id: '', username: '', @@ -176,6 +181,7 @@ class _ConnectionPageState extends State { rdpPort: '', rdpUsername: '', loginName: '', + device_group_name: '', ); _autocompleteOpts = [emptyPeer]; } else { @@ -189,7 +195,7 @@ class _ConnectionPageState extends State { } String textToFind = textEditingValue.text.toLowerCase(); - _autocompleteOpts = peers + _autocompleteOpts = _allPeersLoader.peers .where((peer) => peer.id.toLowerCase().contains(textToFind) || peer.username @@ -203,25 +209,14 @@ class _ConnectionPageState extends State { } return _autocompleteOpts; }, + focusNode: _idFocusNode, + textEditingController: _idEditingController, fieldViewBuilder: (BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) { - fieldTextEditingController.text = _idController.text; - Get.put( - fieldTextEditingController); - fieldFocusNode.addListener(() async { - _idEmpty.value = - fieldTextEditingController.text.isEmpty; - if (fieldFocusNode.hasFocus && !isPeersLoading) { - _fetchPeers(); - } - }); - final textLength = - fieldTextEditingController.value.text.length; - // select all to facilitate removing text, just following the behavior of address input of chrome - fieldTextEditingController.selection = TextSelection( - baseOffset: 0, extentOffset: textLength); + updateTextAndPreserveSelection( + fieldTextEditingController, _idController.text); return AutoSizeTextField( controller: fieldTextEditingController, focusNode: fieldFocusNode, @@ -301,7 +296,9 @@ class _ConnectionPageState extends State { maxHeight: maxHeight, maxWidth: 320, ), - child: peers.isEmpty && isPeersLoading + child: _allPeersLoader + .peers.isEmpty && + !_allPeersLoader.isPeersLoaded ? Container( height: 80, child: Center( @@ -364,16 +361,16 @@ class _ConnectionPageState extends State { void dispose() { _uniLinksSubscription?.cancel(); _idController.dispose(); + _idFocusNode.removeListener(onFocusChanged); + _allPeersLoader.clear(); + _idFocusNode.dispose(); + _idEditingController.dispose(); if (Get.isRegistered()) { Get.delete(); } if (Get.isRegistered()) { Get.delete(); } - if (!bind.isCustomClient()) { - platformFFI.unregisterEventHandler( - kCheckSoftwareUpdateFinish, kCheckSoftwareUpdateFinish); - } super.dispose(); } } diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart index e017b5b6fae..3faf8f8b08e 100644 --- a/flutter/lib/mobile/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -12,11 +12,12 @@ import '../../common/widgets/dialog.dart'; class FileManagerPage extends StatefulWidget { FileManagerPage( - {Key? key, required this.id, this.password, this.isSharedPassword}) + {Key? key, required this.id, this.password, this.isSharedPassword, this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; + final bool? forceRelay; @override State createState() => _FileManagerPageState(); @@ -74,7 +75,8 @@ class _FileManagerPageState extends State { gFFI.start(widget.id, isFileTransfer: true, password: widget.password, - isSharedPassword: widget.isSharedPassword); + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay); WidgetsBinding.instance.addPostFrameCallback((_) { gFFI.dialogManager .showLoading(translate('Connecting...'), onCancel: closeConnection); @@ -225,7 +227,7 @@ class _FileManagerPageState extends State { errorText: errorText, ), controller: name, - ), + ).workaroundFreezeLinuxMint(), ], ), actions: [ diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart index efccc5de65e..651ec4f1727 100644 --- a/flutter/lib/mobile/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -204,13 +204,14 @@ class WebHomePage extends StatelessWidget { return; } bool isFileTransfer = false; + bool isViewCamera = false; + bool isTerminal = false; String? id; String? password; for (int i = 0; i < args.length; i++) { switch (args[i]) { case '--connect': case '--play': - isFileTransfer = false; id = args[i + 1]; i++; break; @@ -219,6 +220,22 @@ class WebHomePage extends StatelessWidget { id = args[i + 1]; i++; break; + case '--view-camera': + isViewCamera = true; + id = args[i + 1]; + i++; + break; + case '--terminal': + isTerminal = true; + id = args[i + 1]; + i++; + break; + case '--terminal-admin': + setEnvTerminalAdmin(); + isTerminal = true; + id = args[i + 1]; + i++; + break; case '--password': password = args[i + 1]; i++; @@ -228,7 +245,11 @@ class WebHomePage extends StatelessWidget { } } if (id != null) { - connect(context, id, isFileTransfer: isFileTransfer, password: password); + connect(context, id, + isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, + isTerminal: isTerminal, + password: password); } } } diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart index 003640e05e1..4c8081465b2 100644 --- a/flutter/lib/mobile/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -40,12 +40,18 @@ void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { } class RemotePage extends StatefulWidget { - RemotePage({Key? key, required this.id, this.password, this.isSharedPassword}) + RemotePage( + {Key? key, + required this.id, + this.password, + this.isSharedPassword, + this.forceRelay}) : super(key: key); final String id; final String? password; final bool? isSharedPassword; + final bool? forceRelay; @override State createState() => _RemotePageState(id); @@ -89,6 +95,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { widget.id, password: widget.password, isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, ); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); @@ -604,7 +611,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { // ko/zh/ja input method: the button will trigger `onKeyEvent` // and the event will not popup if `KeyEventResult.handled` is returned. onChanged: handleSoftKeyboardInput, - ), + ).workaroundFreezeLinuxMint(), ), ]; if (showCursorPaint) { @@ -695,9 +702,9 @@ class _RemotePageState extends State with WidgetsBindingObserver { ); if (index != null) { if (index < mobileActionMenus.length) { - mobileActionMenus[index].onPressed.call(); + mobileActionMenus[index].onPressed?.call(); } else if (index < mobileActionMenus.length + more.length) { - menus[index - mobileActionMenus.length].onPressed.call(); + menus[index - mobileActionMenus.length].onPressed?.call(); } } }(); @@ -770,7 +777,7 @@ class _RemotePageState extends State with WidgetsBindingObserver { elevation: 8, ); if (index != null && index < menus.length) { - menus[index].onPressed.call(); + menus[index].onPressed?.call(); } }); } @@ -1103,7 +1110,7 @@ void showOptions( BuildContext context, String id, OverlayDialogManager dialogManager) async { var displays = []; final pi = gFFI.ffiModel.pi; - final image = gFFI.ffiModel.getConnectionImage(); + final image = gFFI.ffiModel.getConnectionImageText(); if (image != null) { displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); } @@ -1267,7 +1274,7 @@ void showOptions( title: resolution.child, onTap: () { close(); - resolution.onPressed(); + resolution.onPressed?.call(); }, )); } @@ -1279,7 +1286,7 @@ void showOptions( title: virtualDisplayMenu.child, onTap: () { close(); - virtualDisplayMenu.onPressed(); + virtualDisplayMenu.onPressed?.call(); }, )); } diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart index db91e998b6e..ed4fe4d98d1 100644 --- a/flutter/lib/mobile/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -17,7 +17,7 @@ import 'home_page.dart'; class ServerPage extends StatefulWidget implements PageShape { @override - final title = translate("Share Screen"); + final title = translate("Share screen"); @override final icon = const Icon(Icons.mobile_screen_share); @@ -56,6 +56,10 @@ class _DropDownAction extends StatelessWidget { final verificationMethod = gFFI.serverModel.verificationMethod; final showPasswordOption = approveMode != 'click'; final isApproveModeFixed = isOptionFixed(kOptionApproveMode); + final isNumericOneTimePasswordFixed = + isOptionFixed(kOptionAllowNumericOneTimePassword); + final isAllowNumericOneTimePassword = + gFFI.serverModel.allowNumericOneTimePassword; return [ PopupMenuItem( enabled: gFFI.serverModel.connectStatus > 0, @@ -94,6 +98,14 @@ class _DropDownAction extends StatelessWidget { value: "setTemporaryPasswordLength", child: Text(translate("One-time password length")), ), + if (showPasswordOption && + verificationMethod != kUsePermanentPassword) + PopupMenuItem( + value: "allowNumericOneTimePassword", + child: listTile(translate("Numeric one-time password"), + isAllowNumericOneTimePassword), + enabled: !isNumericOneTimePasswordFixed, + ), if (showPasswordOption) const PopupMenuDivider(), if (showPasswordOption) PopupMenuItem( @@ -124,6 +136,9 @@ class _DropDownAction extends StatelessWidget { setPasswordDialog(); } else if (value == "setTemporaryPasswordLength") { setTemporaryPasswordLengthDialog(gFFI.dialogManager); + } else if (value == "allowNumericOneTimePassword") { + gFFI.serverModel.switchAllowNumericOneTimePassword(); + gFFI.serverModel.updatePasswordModel(); } else if (value == kUsePermanentPassword || value == kUseTemporaryPassword || value == kUseBothPasswords) { @@ -634,8 +649,8 @@ class ConnectionManager extends StatelessWidget { children: serverModel.clients .map((client) => PaddingCard( title: translate(client.isFileTransfer - ? "File Connection" - : "Screen Connection"), + ? "Transfer file" + : "Share screen"), titleIcon: client.isFileTransfer ? Icon(Icons.folder_outlined) : Icon(Icons.mobile_screen_share), diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart index ede66d78ae7..5c9d28383e2 100644 --- a/flutter/lib/mobile/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -5,7 +5,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common/widgets/setting_widgets.dart'; import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; -import 'package:flutter_hbb/models/state_model.dart'; import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:settings_ui/settings_ui.dart'; @@ -80,6 +79,7 @@ class _SettingsState extends State with WidgetsBindingObserver { var _enableDirectIPAccess = false; var _enableRecordSession = false; var _enableHardwareCodec = false; + var _allowWebSocket = false; var _autoRecordIncomingSession = false; var _autoRecordOutgoingSession = false; var _allowAutoDisconnect = false; @@ -91,7 +91,10 @@ class _SettingsState extends State with WidgetsBindingObserver { var _hideServer = false; var _hideProxy = false; var _hideNetwork = false; + var _hideWebSocket = false; var _enableTrustedDevices = false; + var _enableUdpPunch = false; + var _enableIpv6Punch = false; _SettingsState() { _enableAbr = option2bool( @@ -105,6 +108,7 @@ class _SettingsState extends State with WidgetsBindingObserver { bind.mainGetOptionSync(key: kOptionEnableRecordSession)); _enableHardwareCodec = option2bool(kOptionEnableHwcodec, bind.mainGetOptionSync(key: kOptionEnableHwcodec)); + _allowWebSocket = mainGetBoolOptionSync(kOptionAllowWebSocket); _autoRecordIncomingSession = option2bool(kOptionAllowAutoRecordIncoming, bind.mainGetOptionSync(key: kOptionAllowAutoRecordIncoming)); _autoRecordOutgoingSession = option2bool(kOptionAllowAutoRecordOutgoing, @@ -120,7 +124,12 @@ class _SettingsState extends State with WidgetsBindingObserver { _hideProxy = bind.mainGetBuildinOption(key: kOptionHideProxySetting) == 'Y'; _hideNetwork = bind.mainGetBuildinOption(key: kOptionHideNetworkSetting) == 'Y'; + _hideWebSocket = + bind.mainGetBuildinOption(key: kOptionHideWebSocketSetting) == 'Y' || + isWeb; _enableTrustedDevices = mainGetBoolOptionSync(kOptionEnableTrustedDevices); + _enableUdpPunch = mainGetLocalBoolOptionSync(kOptionEnableUdpPunch); + _enableIpv6Punch = mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch); } @override @@ -243,7 +252,7 @@ class _SettingsState extends State with WidgetsBindingObserver { Widget build(BuildContext context) { Provider.of(context); final outgoingOnly = bind.isOutgoingOnly(); - final incommingOnly = bind.isIncomingOnly(); + final incomingOnly = bind.isIncomingOnly(); final customClientSection = CustomSettingsSection( child: Column( children: [ @@ -369,7 +378,7 @@ class _SettingsState extends State with WidgetsBindingObserver { }, ), SettingsTile.switchTile( - title: Text('${translate('Adaptive bitrate')} (beta)'), + title: Text(translate('Adaptive bitrate')), initialValue: _enableAbr, onToggle: isOptionFixed(kOptionEnableAbr) ? null @@ -531,7 +540,7 @@ class _SettingsState extends State with WidgetsBindingObserver { enhancementsTiles.add(SettingsTile.switchTile( initialValue: _enableStartOnBoot, title: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text("${translate('Start on boot')} (beta)"), + Text(translate('Start on boot')), Text( '* ${translate('Start the screen sharing service on boot, requires special permissions')}', style: Theme.of(context).textTheme.bodySmall), @@ -667,6 +676,47 @@ class _SettingsState extends State with WidgetsBindingObserver { onPressed: (context) { changeSocks5Proxy(); }), + if (!disabledSettings && !_hideNetwork && !_hideWebSocket) + SettingsTile.switchTile( + title: Text(translate('Use WebSocket')), + initialValue: _allowWebSocket, + onToggle: isOptionFixed(kOptionAllowWebSocket) + ? null + : (v) async { + await mainSetBoolOption(kOptionAllowWebSocket, v); + final newValue = + await mainGetBoolOption(kOptionAllowWebSocket); + setState(() { + _allowWebSocket = newValue; + }); + }, + ), + if (!incomingOnly) + SettingsTile.switchTile( + title: Text(translate('Enable UDP hole punching')), + initialValue: _enableUdpPunch, + onToggle: (v) async { + await mainSetLocalBoolOption(kOptionEnableUdpPunch, v); + final newValue = + mainGetLocalBoolOptionSync(kOptionEnableUdpPunch); + setState(() { + _enableUdpPunch = newValue; + }); + }, + ), + if (!incomingOnly) + SettingsTile.switchTile( + title: Text(translate('Enable IPv6 P2P connection')), + initialValue: _enableIpv6Punch, + onToggle: (v) async { + await mainSetLocalBoolOption(kOptionEnableIpv6Punch, v); + final newValue = + mainGetLocalBoolOptionSync(kOptionEnableIpv6Punch); + setState(() { + _enableIpv6Punch = newValue; + }); + }, + ), SettingsTile( title: Text(translate('Language')), leading: Icon(Icons.translate), @@ -728,7 +778,7 @@ class _SettingsState extends State with WidgetsBindingObserver { }); }, ), - if (!incommingOnly) + if (!incomingOnly) SettingsTile.switchTile( title: Text(translate('Automatically record outgoing sessions')), @@ -765,7 +815,7 @@ class _SettingsState extends State with WidgetsBindingObserver { !outgoingOnly && !hideSecuritySettings) SettingsSection( - title: Text(translate("Share Screen")), + title: Text(translate("Share screen")), tiles: shareScreenTiles, ), if (!bind.isIncomingOnly()) defaultDisplaySection(), @@ -782,9 +832,7 @@ class _SettingsState extends State with WidgetsBindingObserver { tiles: [ SettingsTile( onPressed: (context) async { - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } + await launchUrl(Uri.parse(url)); }, title: Text(translate("Version: ") + version), value: Padding( @@ -928,9 +976,7 @@ void showAbout(OverlayDialogManager dialogManager) { InkWell( onTap: () async { const url = 'https://rustdesk.com/'; - if (await canLaunchUrl(Uri.parse(url))) { - await launchUrl(Uri.parse(url)); - } + await launchUrl(Uri.parse(url)); }, child: Padding( padding: EdgeInsets.symmetric(vertical: 8), diff --git a/flutter/lib/mobile/pages/terminal_page.dart b/flutter/lib/mobile/pages/terminal_page.dart new file mode 100644 index 00000000000..e1e06c26c7f --- /dev/null +++ b/flutter/lib/mobile/pages/terminal_page.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/terminal_model.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:xterm/xterm.dart'; +import '../../desktop/pages/terminal_connection_manager.dart'; + +class TerminalPage extends StatefulWidget { + const TerminalPage({ + Key? key, + required this.id, + required this.password, + required this.isSharedPassword, + this.forceRelay, + this.connToken, + }) : super(key: key); + final String id; + final String? password; + final bool? forceRelay; + final bool? isSharedPassword; + final String? connToken; + final terminalId = 0; + + @override + State createState() => _TerminalPageState(); +} + +class _TerminalPageState extends State + with AutomaticKeepAliveClientMixin { + late FFI _ffi; + late TerminalModel _terminalModel; + + // For web only. + // 'monospace' does not work on web, use Google Fonts, `??` is only for null safety. + final String _robotoMonoFontFamily = isWeb + ? (GoogleFonts.robotoMono().fontFamily ?? 'monospace') + : 'monospace'; + + @override + void initState() { + super.initState(); + + debugPrint( + '[TerminalPage] Initializing terminal ${widget.terminalId} for peer ${widget.id}'); + + // Use shared FFI instance from connection manager + _ffi = TerminalConnectionManager.getConnection( + peerId: widget.id, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + connToken: widget.connToken, + ); + + // Create terminal model with specific terminal ID + _terminalModel = TerminalModel(_ffi, widget.terminalId); + debugPrint( + '[TerminalPage] Terminal model created for terminal ${widget.terminalId}'); + + // Register this terminal model with FFI for event routing + _ffi.registerTerminalModel(widget.terminalId, _terminalModel); + + // Initialize terminal connection + WidgetsBinding.instance.addPostFrameCallback((_) { + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + _ffi.ffiModel.updateEventListener(_ffi.sessionId, widget.id); + } + + @override + void dispose() { + // Unregister terminal model from FFI + _ffi.unregisterTerminalModel(widget.terminalId); + _terminalModel.dispose(); + super.dispose(); + TerminalConnectionManager.releaseConnection(widget.id); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + body: TerminalView( + _terminalModel.terminal, + controller: _terminalModel.terminalController, + autofocus: true, + textStyle: _getTerminalStyle(), + backgroundOpacity: 0.7, + padding: const EdgeInsets.symmetric(horizontal: 5.0, vertical: 2.0), + onSecondaryTapDown: (details, offset) async { + final selection = _terminalModel.terminalController.selection; + if (selection != null) { + final text = _terminalModel.terminal.buffer.getText(selection); + _terminalModel.terminalController.clearSelection(); + await Clipboard.setData(ClipboardData(text: text)); + } else { + final data = await Clipboard.getData('text/plain'); + final text = data?.text; + if (text != null) { + _terminalModel.terminal.paste(text); + } + } + }, + ), + ); + } + + // https://github.com/TerminalStudio/xterm.dart/issues/42#issuecomment-877495472 + // https://github.com/TerminalStudio/xterm.dart/issues/198#issuecomment-2526548458 + TerminalStyle _getTerminalStyle() { + return isWeb + ? TerminalStyle( + fontFamily: _robotoMonoFontFamily, + fontSize: 14, + ) + : const TerminalStyle(); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/flutter/lib/mobile/pages/view_camera_page.dart b/flutter/lib/mobile/pages/view_camera_page.dart new file mode 100644 index 00000000000..53af56267da --- /dev/null +++ b/flutter/lib/mobile/pages/view_camera_page.dart @@ -0,0 +1,727 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common/shared_state.dart'; +import 'package:flutter_hbb/common/widgets/toolbar.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; + +import '../../common.dart'; +import '../../common/widgets/overlay.dart'; +import '../../common/widgets/dialog.dart'; +import '../../common/widgets/remote_input.dart'; +import '../../models/input_model.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; +import '../../utils/image.dart'; + +final initText = '1' * 1024; + +// Workaround for Android (default input method, Microsoft SwiftKey keyboard) when using physical keyboard. +// When connecting a physical keyboard, `KeyEvent.physicalKey.usbHidUsage` are wrong is using Microsoft SwiftKey keyboard. +// https://github.com/flutter/flutter/issues/159384 +// https://github.com/flutter/flutter/issues/159383 +void _disableAndroidSoftKeyboard({bool? isKeyboardVisible}) { + if (isAndroid) { + if (isKeyboardVisible != true) { + // `enable_soft_keyboard` will be set to `true` when clicking the keyboard icon, in `openKeyboard()`. + gFFI.invokeMethod("enable_soft_keyboard", false); + } + } +} + +class ViewCameraPage extends StatefulWidget { + ViewCameraPage( + {Key? key, + required this.id, + this.password, + this.isSharedPassword, + this.forceRelay}) + : super(key: key); + + final String id; + final String? password; + final bool? isSharedPassword; + final bool? forceRelay; + + @override + State createState() => _ViewCameraPageState(id); +} + +class _ViewCameraPageState extends State + with WidgetsBindingObserver { + Timer? _timer; + bool _showBar = !isWebDesktop; + bool _showGestureHelp = false; + Orientation? _currentOrientation; + double _viewInsetsBottom = 0; + + Timer? _timerDidChangeMetrics; + + final _blockableOverlayState = BlockableOverlayState(); + + final keyboardVisibilityController = KeyboardVisibilityController(); + final FocusNode _mobileFocusNode = FocusNode(); + final FocusNode _physicalFocusNode = FocusNode(); + var _showEdit = false; // use soft keyboard + + InputModel get inputModel => gFFI.inputModel; + SessionID get sessionId => gFFI.sessionId; + + final TextEditingController _textController = + TextEditingController(text: initText); + + _ViewCameraPageState(String id) { + initSharedStates(id); + gFFI.chatModel.voiceCallStatus.value = VoiceCallStatus.notStarted; + gFFI.dialogManager.loadMobileActionsOverlayVisible(); + } + + @override + void initState() { + super.initState(); + gFFI.ffiModel.updateEventListener(sessionId, widget.id); + gFFI.start( + widget.id, + isViewCamera: true, + password: widget.password, + isSharedPassword: widget.isSharedPassword, + forceRelay: widget.forceRelay, + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + gFFI.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + if (!isWeb) { + WakelockPlus.enable(); + } + _physicalFocusNode.requestFocus(); + gFFI.inputModel.listenToMouse(true); + gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId); + gFFI.chatModel + .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID)); + _blockableOverlayState.applyFfi(gFFI); + gFFI.imageModel.addCallbackOnFirstImage((String peerId) { + gFFI.recordingModel + .updateStatus(bind.sessionGetIsRecording(sessionId: gFFI.sessionId)); + if (gFFI.recordingModel.start) { + showToast(translate('Automatically record outgoing sessions')); + } + _disableAndroidSoftKeyboard( + isKeyboardVisible: keyboardVisibilityController.isVisible); + }); + WidgetsBinding.instance.addObserver(this); + } + + @override + Future dispose() async { + WidgetsBinding.instance.removeObserver(this); + // https://github.com/flutter/flutter/issues/64935 + super.dispose(); + gFFI.dialogManager.hideMobileActionsOverlay(store: false); + gFFI.inputModel.listenToMouse(false); + gFFI.imageModel.disposeImage(); + gFFI.cursorModel.disposeImages(); + await gFFI.invokeMethod("enable_soft_keyboard", true); + _mobileFocusNode.dispose(); + _physicalFocusNode.dispose(); + await gFFI.close(); + _timer?.cancel(); + _timerDidChangeMetrics?.cancel(); + gFFI.dialogManager.dismissAll(); + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + if (!isWeb) { + await WakelockPlus.disable(); + } + removeSharedStates(widget.id); + // `on_voice_call_closed` should be called when the connection is ended. + // The inner logic of `on_voice_call_closed` will check if the voice call is active. + // Only one client is considered here for now. + gFFI.chatModel.onVoiceCallClosed("End connetion"); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) {} + + @override + void didChangeMetrics() { + // If the soft keyboard is visible and the canvas has been changed(panned or scaled) + // Don't try reset the view style and focus the cursor. + if (gFFI.cursorModel.lastKeyboardIsVisible && + gFFI.canvasModel.isMobileCanvasChanged) { + return; + } + + final newBottom = MediaQueryData.fromView(ui.window).viewInsets.bottom; + _timerDidChangeMetrics?.cancel(); + _timerDidChangeMetrics = Timer(Duration(milliseconds: 100), () async { + // We need this comparation because poping up the floating action will also trigger `didChangeMetrics()`. + if (newBottom != _viewInsetsBottom) { + gFFI.canvasModel.mobileFocusCanvasCursor(); + _viewInsetsBottom = newBottom; + } + }); + } + + // to-do: It should be better to use transparent color instead of the bgColor. + // But for now, the transparent color will cause the canvas to be white. + // I'm sure that the white color is caused by the Overlay widget in BlockableOverlay. + // But I don't know why and how to fix it. + Widget emptyOverlay(Color bgColor) => BlockableOverlay( + /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay + /// see override build() in [BlockableOverlay] + state: _blockableOverlayState, + underlying: Container( + color: bgColor, + ), + ); + + Widget _bottomWidget() => (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty + ? getBottomAppBar() + : Offstage()); + + @override + Widget build(BuildContext context) { + final keyboardIsVisible = + keyboardVisibilityController.isVisible && _showEdit; + final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp; + + return WillPopScope( + onWillPop: () async { + clientClose(sessionId, gFFI.dialogManager); + return false; + }, + child: Scaffold( + // workaround for https://github.com/rustdesk/rustdesk/issues/3131 + floatingActionButtonLocation: keyboardIsVisible + ? FABLocation(FloatingActionButtonLocation.endFloat, 0, -35) + : null, + floatingActionButton: !showActionButton + ? null + : FloatingActionButton( + mini: !keyboardIsVisible, + child: Icon( + (keyboardIsVisible || _showGestureHelp) + ? Icons.expand_more + : Icons.expand_less, + color: Colors.white, + ), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + if (keyboardIsVisible) { + _showEdit = false; + gFFI.invokeMethod("enable_soft_keyboard", false); + _mobileFocusNode.unfocus(); + _physicalFocusNode.requestFocus(); + } else if (_showGestureHelp) { + _showGestureHelp = false; + } else { + _showBar = !_showBar; + } + }); + }), + bottomNavigationBar: Obx(() => Stack( + alignment: Alignment.bottomCenter, + children: [ + gFFI.ffiModel.pi.isSet.isTrue && + gFFI.ffiModel.waitForFirstImage.isTrue + ? emptyOverlay(MyTheme.canvasColor) + : () { + gFFI.ffiModel.tryShowAndroidActionsOverlay(); + return Offstage(); + }(), + _bottomWidget(), + gFFI.ffiModel.pi.isSet.isFalse + ? emptyOverlay(MyTheme.canvasColor) + : Offstage(), + ], + )), + body: Obx( + () => getRawPointerAndKeyBody(Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + return Container( + color: kColorCanvas, + child: SafeArea( + child: OrientationBuilder(builder: (ctx, orientation) { + if (_currentOrientation != orientation) { + Timer(const Duration(milliseconds: 200), () { + gFFI.dialogManager + .resetMobileActionsOverlay(ffi: gFFI); + _currentOrientation = orientation; + gFFI.canvasModel.updateViewStyle(); + }); + } + return Container( + color: MyTheme.canvasColor, + child: inputModel.isPhysicalMouse.value + ? getBodyForMobile() + : RawTouchGestureDetectorRegion( + child: getBodyForMobile(), + ffi: gFFI, + isCamera: true, + ), + ); + }), + ), + ); + }) + ], + )), + )), + ); + } + + Widget getRawPointerAndKeyBody(Widget child) { + return CameraRawPointerMouseRegion( + inputModel: inputModel, + // Disable RawKeyFocusScope before the connecting is established. + // The "Delete" key on the soft keyboard may be grabbed when inputting the password dialog. + child: gFFI.ffiModel.pi.isSet.isTrue + ? RawKeyFocusScope( + focusNode: _physicalFocusNode, + inputModel: inputModel, + child: child) + : child, + ); + } + + Widget getBottomAppBar() { + return BottomAppBar( + elevation: 10, + color: MyTheme.accent, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + color: Colors.white, + icon: Icon(Icons.clear), + onPressed: () { + clientClose(sessionId, gFFI.dialogManager); + }, + ), + IconButton( + color: Colors.white, + icon: Icon(Icons.tv), + onPressed: () { + setState(() => _showEdit = false); + showOptions(context, widget.id, gFFI.dialogManager); + }, + ) + ] + + (isWeb + ? [] + : [ + futureBuilder( + future: gFFI.invokeMethod( + "get_value", "KEY_IS_SUPPORT_VOICE_CALL"), + hasData: (isSupportVoiceCall) => IconButton( + color: Colors.white, + icon: isAndroid && isSupportVoiceCall + ? SvgPicture.asset('assets/chat.svg', + colorFilter: ColorFilter.mode( + Colors.white, BlendMode.srcIn)) + : Icon(Icons.message), + onPressed: () => + isAndroid && isSupportVoiceCall + ? showChatOptions(widget.id) + : onPressedTextChat(widget.id), + )) + ]) + + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.more_vert), + onPressed: () { + setState(() => _showEdit = false); + showActions(widget.id); + }, + ), + ]), + Obx(() => IconButton( + color: Colors.white, + icon: Icon(Icons.expand_more), + onPressed: gFFI.ffiModel.waitForFirstImage.isTrue + ? null + : () { + setState(() => _showBar = !_showBar); + }, + )), + ], + ), + ); + } + + Widget getBodyForMobile() { + return Container( + color: MyTheme.canvasColor, + child: Stack(children: () { + final paints = [ + ImagePaint(), + Positioned( + top: 10, + right: 10, + child: QualityMonitor(gFFI.qualityMonitorModel), + ), + SizedBox( + width: 0, + height: 0, + child: !_showEdit + ? Container() + : TextFormField( + textInputAction: TextInputAction.newline, + autocorrect: false, + // Flutter 3.16.9 Android. + // `enableSuggestions` causes secure keyboard to be shown. + // https://github.com/flutter/flutter/issues/139143 + // https://github.com/flutter/flutter/issues/146540 + // enableSuggestions: false, + autofocus: true, + focusNode: _mobileFocusNode, + maxLines: null, + controller: _textController, + // trick way to make backspace work always + keyboardType: TextInputType.multiline, + // `onChanged` may be called depending on the input method if this widget is wrapped in + // `Focus(onKeyEvent: ..., child: ...)` + // For `Backspace` button in the soft keyboard: + // en/fr input method: + // 1. The button will not trigger `onKeyEvent` if the text field is not empty. + // 2. The button will trigger `onKeyEvent` if the text field is empty. + // ko/zh/ja input method: the button will trigger `onKeyEvent` + // and the event will not popup if `KeyEventResult.handled` is returned. + onChanged: null, + ).workaroundFreezeLinuxMint(), + ), + ]; + return paints; + }())); + } + + Widget getBodyForDesktopWithListener() { + var paints = [ImagePaint()]; + return Container( + color: MyTheme.canvasColor, child: Stack(children: paints)); + } + + List _getMobileActionMenus() { + if (gFFI.ffiModel.pi.platform != kPeerPlatformAndroid || + !gFFI.ffiModel.keyboard) { + return []; + } + final enabled = versionCmp(gFFI.ffiModel.pi.version, '1.2.7') >= 0; + if (!enabled) return []; + return [ + TTextMenu( + child: Text(translate('Back')), + onPressed: () => gFFI.inputModel.onMobileBack(), + ), + TTextMenu( + child: Text(translate('Home')), + onPressed: () => gFFI.inputModel.onMobileHome(), + ), + TTextMenu( + child: Text(translate('Apps')), + onPressed: () => gFFI.inputModel.onMobileApps(), + ), + TTextMenu( + child: Text(translate('Volume up')), + onPressed: () => gFFI.inputModel.onMobileVolumeUp(), + ), + TTextMenu( + child: Text(translate('Volume down')), + onPressed: () => gFFI.inputModel.onMobileVolumeDown(), + ), + TTextMenu( + child: Text(translate('Power')), + onPressed: () => gFFI.inputModel.onMobilePower(), + ), + ]; + } + + void showActions(String id) async { + final size = MediaQuery.of(context).size; + final x = 120.0; + final y = size.height; + final mobileActionMenus = _getMobileActionMenus(); + final menus = toolbarControls(context, id, gFFI); + + final List> more = [ + ...mobileActionMenus + .asMap() + .entries + .map((e) => + PopupMenuItem(child: e.value.getChild(), value: e.key)) + .toList(), + if (mobileActionMenus.isNotEmpty) PopupMenuDivider(), + ...menus + .asMap() + .entries + .map((e) => PopupMenuItem( + child: e.value.getChild(), + value: e.key + mobileActionMenus.length)) + .toList(), + ]; + () async { + var index = await showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: more, + elevation: 8, + ); + if (index != null) { + if (index < mobileActionMenus.length) { + mobileActionMenus[index].onPressed?.call(); + } else if (index < mobileActionMenus.length + more.length) { + menus[index - mobileActionMenus.length].onPressed?.call(); + } + } + }(); + } + + onPressedTextChat(String id) { + gFFI.chatModel.changeCurrentKey(MessageKey(id, ChatModel.clientModeID)); + gFFI.chatModel.toggleChatOverlay(); + } + + showChatOptions(String id) async { + onPressVoiceCall() => bind.sessionRequestVoiceCall(sessionId: sessionId); + onPressEndVoiceCall() => bind.sessionCloseVoiceCall(sessionId: sessionId); + + makeTextMenu(String label, Widget icon, VoidCallback onPressed, + {TextStyle? labelStyle}) => + TTextMenu( + child: Text(translate(label), style: labelStyle), + trailingIcon: Transform.scale( + scale: (isDesktop || isWebDesktop) ? 0.8 : 1, + child: IgnorePointer( + child: IconButton( + onPressed: null, + icon: icon, + ), + ), + ), + onPressed: onPressed, + ); + + final isInVoice = [ + VoiceCallStatus.waitingForResponse, + VoiceCallStatus.connected + ].contains(gFFI.chatModel.voiceCallStatus.value); + final menus = [ + makeTextMenu('Text chat', Icon(Icons.message, color: MyTheme.accent), + () => onPressedTextChat(widget.id)), + isInVoice + ? makeTextMenu( + 'End voice call', + SvgPicture.asset( + 'assets/call_wait.svg', + colorFilter: + ColorFilter.mode(Colors.redAccent, BlendMode.srcIn), + ), + onPressEndVoiceCall, + labelStyle: TextStyle(color: Colors.redAccent)) + : makeTextMenu( + 'Voice call', + SvgPicture.asset( + 'assets/call_wait.svg', + colorFilter: ColorFilter.mode(MyTheme.accent, BlendMode.srcIn), + ), + onPressVoiceCall), + ]; + + final menuItems = menus + .asMap() + .entries + .map((e) => PopupMenuItem(child: e.value.getChild(), value: e.key)) + .toList(); + Future.delayed(Duration.zero, () async { + final size = MediaQuery.of(context).size; + final x = 120.0; + final y = size.height; + var index = await showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: menuItems, + elevation: 8, + ); + if (index != null && index < menus.length) { + menus[index].onPressed?.call(); + } + }); + } +} + +class ImagePaint extends StatelessWidget { + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + final c = Provider.of(context); + var s = c.scale; + final adjust = c.getAdjustY(); + return CustomPaint( + painter: ImagePainter( + image: m.image, x: c.x / s, y: (c.y + adjust) / s, scale: s), + ); + } +} + +void showOptions( + BuildContext context, String id, OverlayDialogManager dialogManager) async { + var displays = []; + final pi = gFFI.ffiModel.pi; + final image = gFFI.ffiModel.getConnectionImageText(); + if (image != null) { + displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); + } + if (pi.displays.length > 1 && pi.currentDisplay != kAllDisplayValue) { + final cur = pi.currentDisplay; + final children = []; + for (var i = 0; i < pi.displays.length; ++i) { + children.add(InkWell( + onTap: () { + if (i == cur) return; + openMonitorInTheSameTab(i, gFFI, pi); + gFFI.dialogManager.dismissAll(); + }, + child: Ink( + width: 40, + height: 40, + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).hintColor), + borderRadius: BorderRadius.circular(2), + color: i == cur + ? Theme.of(context).primaryColor.withOpacity(0.6) + : null), + child: Center( + child: Text((i + 1).toString(), + style: TextStyle( + color: i == cur ? Colors.white : Colors.black87, + fontWeight: FontWeight.bold)))))); + } + displays.add(Padding( + padding: const EdgeInsets.only(top: 8), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8, + children: children, + ))); + } + if (displays.isNotEmpty) { + displays.add(const Divider(color: MyTheme.border)); + } + + List> viewStyleRadios = + await toolbarViewStyle(context, id, gFFI); + List> imageQualityRadios = + await toolbarImageQuality(context, id, gFFI); + List> codecRadios = await toolbarCodec(context, id, gFFI); + List displayToggles = + await toolbarDisplayToggle(context, id, gFFI); + + dialogManager.show((setState, close, context) { + var viewStyle = + (viewStyleRadios.isNotEmpty ? viewStyleRadios[0].groupValue : '').obs; + var imageQuality = + (imageQualityRadios.isNotEmpty ? imageQualityRadios[0].groupValue : '') + .obs; + var codec = (codecRadios.isNotEmpty ? codecRadios[0].groupValue : '').obs; + final radios = [ + for (var e in viewStyleRadios) + Obx(() => getRadio( + e.child, + e.value, + viewStyle.value, + e.onChanged != null + ? (v) { + e.onChanged?.call(v); + if (v != null) viewStyle.value = v; + } + : null)), + const Divider(color: MyTheme.border), + for (var e in imageQualityRadios) + Obx(() => getRadio( + e.child, + e.value, + imageQuality.value, + e.onChanged != null + ? (v) { + e.onChanged?.call(v); + if (v != null) imageQuality.value = v; + } + : null)), + const Divider(color: MyTheme.border), + for (var e in codecRadios) + Obx(() => getRadio( + e.child, + e.value, + codec.value, + e.onChanged != null + ? (v) { + e.onChanged?.call(v); + if (v != null) codec.value = v; + } + : null)), + if (codecRadios.isNotEmpty) const Divider(color: MyTheme.border), + ]; + + final rxToggleValues = displayToggles.map((e) => e.value.obs).toList(); + final displayTogglesList = displayToggles + .asMap() + .entries + .map((e) => Obx(() => CheckboxListTile( + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + value: rxToggleValues[e.key].value, + onChanged: e.value.onChanged != null + ? (v) { + e.value.onChanged?.call(v); + if (v != null) rxToggleValues[e.key].value = v; + } + : null, + title: e.value.child))) + .toList(); + final toggles = [ + ...displayTogglesList, + ]; + + var popupDialogMenus = List.empty(growable: true); + if (popupDialogMenus.isNotEmpty) { + popupDialogMenus.add(const Divider(color: MyTheme.border)); + } + + return CustomAlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: displays + radios + popupDialogMenus + toggles), + ); + }, clickMaskDismiss: true, backDismiss: true).then((value) { + _disableAndroidSoftKeyboard(); + }); +} + +class FABLocation extends FloatingActionButtonLocation { + FloatingActionButtonLocation location; + double offsetX; + double offsetY; + FABLocation(this.location, this.offsetX, this.offsetY); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + final offset = location.getOffset(scaffoldGeometry); + return Offset(offset.dx + offsetX, offset.dy + offsetY); + } +} diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart index 1c8b4dd3d7b..ebedd79d44f 100644 --- a/flutter/lib/mobile/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -66,7 +66,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { ? null : translate('Too short, at least 6 characters.'); }, - ), + ).workaroundFreezeLinuxMint(), TextFormField( obscureText: true, keyboardType: TextInputType.visiblePassword, @@ -85,7 +85,7 @@ void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { ? null : translate('The confirmation is not identical.'); }, - ), + ).workaroundFreezeLinuxMint(), ])), onCancel: close, onSubmit: (validateLength && validateSame) ? submit : null, @@ -216,7 +216,7 @@ void showServerSettingsWithValue( ), validator: validator, autofocus: autofocus, - ), + ).workaroundFreezeLinuxMint(), ), ], ); @@ -229,7 +229,7 @@ void showServerSettingsWithValue( errorText: errorMsg.isEmpty ? null : errorMsg, ), validator: validator, - ); + ).workaroundFreezeLinuxMint(); } return CustomAlertDialog( diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart index 3aa722a5abf..790bc62fe67 100644 --- a/flutter/lib/models/ab_model.dart +++ b/flutter/lib/models/ab_model.dart @@ -58,6 +58,9 @@ class AbModel { String? _personalAbGuid; RxBool legacyMode = false.obs; + // Only handles peers add/remove + final Map _peerIdUpdateListeners = {}; + final sortTags = shouldSortTags().obs; final filterByIntersection = filterAbTagByIntersection().obs; @@ -188,6 +191,7 @@ class AbModel { debugPrint("pull current Ab error: $e"); } } + _callbackPeerUpdate(); if (listInitialized && current.initialized) { _saveCache(); } @@ -343,6 +347,9 @@ class AbModel { if (ab == null) { return 'no such addressbook: $name'; } + for (var p in ps) { + ab.removeNonExistentTags(p); + } String? errMsg = await ab.addPeers(ps); await pullNonLegacyAfterChange(name: name); if (name == _currentName.value) { @@ -419,6 +426,7 @@ class AbModel { } }); } + _callbackPeerUpdate(); return ret; } @@ -620,6 +628,9 @@ class AbModel { } } } + if (abEntries.isNotEmpty) { + _callbackPeerUpdate(); + } } } @@ -742,6 +753,20 @@ class AbModel { } } + void _callbackPeerUpdate() { + for (var listener in _peerIdUpdateListeners.values) { + listener(); + } + } + + void addPeerUpdateListener(String key, VoidCallback listener) { + _peerIdUpdateListeners[key] = listener; + } + + void removePeerUpdateListener(String key) { + _peerIdUpdateListeners.remove(key); + } + // #endregion } @@ -753,7 +778,10 @@ abstract class BaseAb { final pullError = "".obs; final pushError = "".obs; - final abLoading = false.obs; + final abLoading = false + .obs; // Indicates whether the UI should show a loading state for the address book. + var abPulling = + false; // Tracks whether a pull operation is currently in progress to prevent concurrent pulls. Unlike abLoading, this is not tied to UI updates. bool initialized = false; String name(); @@ -768,17 +796,22 @@ abstract class BaseAb { } Future pullAb({quiet = false}) async { - debugPrint("pull ab \"${name()}\""); - if (abLoading.value) return; + if (abPulling) return; + abPulling = true; if (!quiet) { abLoading.value = true; pullError.value = ""; } initialized = false; + debugPrint("pull ab \"${name()}\""); try { initialized = await pullAbImpl(quiet: quiet); - } catch (_) {} - abLoading.value = false; + } catch (e) { + debugPrint("Error occurred while pulling address book: $e"); + } finally { + abLoading.value = false; + abPulling = false; + } } Future pullAbImpl({quiet = false}); @@ -792,6 +825,18 @@ abstract class BaseAb { p.remove('password'); } + removeNonExistentTags(Map p) { + try { + final oldTags = p.remove('tags'); + if (oldTags is List) { + final newTags = oldTags.where((e) => tagContainBy(e)).toList(); + p['tags'] = newTags; + } + } catch (e) { + print("removeNonExistentTags: $e"); + } + } + Future changeTagForPeers(List ids, List tags); Future changeAlias({required String id, required String alias}); diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart index c6cf55256de..a960491346f 100644 --- a/flutter/lib/models/desktop_render_texture.dart +++ b/flutter/lib/models/desktop_render_texture.dart @@ -235,6 +235,17 @@ class TextureModel { } } + onViewCameraPageDispose(bool closeSession) async { + final ffi = parent.target; + if (ffi == null) return; + for (final texture in _pixelbufferRenderTextures.values) { + await texture.destroy(closeSession, ffi); + } + for (final texture in _gpuRenderTextures.values) { + await texture.destroy(closeSession, ffi); + } + } + ensureControl(int display) { var ctl = _control[display]; if (ctl == null) { diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 4a00b803e34..db9b13e45ff 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -30,6 +30,15 @@ enum SortBy { class JobID { int _count = 0; int next() { + try { + if (!isWeb) { + String v = bind.mainGetCommonSync(key: 'transfer-job-id'); + return int.parse(v); + } + } catch (e) { + debugPrint("Failed to get transfer job id: $e"); + } + // Finally increase the count if on the web or if failed to get the id. _count++; return _count; } diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart index b14ccd46b0e..534e897e943 100644 --- a/flutter/lib/models/group_model.dart +++ b/flutter/lib/models/group_model.dart @@ -12,16 +12,20 @@ import '../utils/http_service.dart' as http; class GroupModel { final RxBool groupLoading = false.obs; final RxString groupLoadError = "".obs; + final RxList deviceGroups = RxList.empty(growable: true); final RxList users = RxList.empty(growable: true); final RxList peers = RxList.empty(growable: true); - final RxString selectedUser = ''.obs; - final RxString searchUserText = ''.obs; + final RxBool isSelectedDeviceGroup = false.obs; + final RxString selectedAccessibleItemName = ''.obs; + final RxString searchAccessibleItemNameText = ''.obs; WeakReference parent; var initialized = false; var _cacheLoadOnceFlag = false; var _statusCode = 200; - bool get emtpy => users.isEmpty && peers.isEmpty; + final Map _peerIdUpdateListeners = {}; + + bool get emtpy => deviceGroups.isEmpty && users.isEmpty && peers.isEmpty; late final Peers peersModel; @@ -43,7 +47,10 @@ class GroupModel { } try { await _pull(); - } catch (_) {} + _tryHandlePullError(); + } catch (e) { + print("pull accessibles error: $e"); + } groupLoading.value = false; initialized = true; platformFFI.tryHandle({'name': LoadEvent.group}); @@ -55,6 +62,12 @@ class GroupModel { } Future _pull() async { + List tmpDeviceGroups = List.empty(growable: true); + if (!await _getDeviceGroups(tmpDeviceGroups)) { + // old hbbs doesn't support this api + // return; + } + tmpDeviceGroups.sort((a, b) => a.name.compareTo(b.name)); List tmpUsers = List.empty(growable: true); if (!await _getUsers(tmpUsers)) { return; @@ -63,6 +76,7 @@ class GroupModel { if (!await _getPeers(tmpPeers)) { return; } + deviceGroups.value = tmpDeviceGroups; // me first var index = tmpUsers .indexWhere((user) => user.name == gFFI.userModel.userName.value); @@ -71,8 +85,9 @@ class GroupModel { tmpUsers.insert(0, user); } users.value = tmpUsers; - if (!users.any((u) => u.name == selectedUser.value)) { - selectedUser.value = ''; + if (!users.any((u) => u.name == selectedAccessibleItemName.value) && + !deviceGroups.any((d) => d.name == selectedAccessibleItemName.value)) { + selectedAccessibleItemName.value = ''; } // recover online final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList(); @@ -82,6 +97,64 @@ class GroupModel { .map((e) => e.online = true) .toList(); groupLoadError.value = ''; + _callbackPeerUpdate(); + } + + Future _getDeviceGroups( + List tmpDeviceGroups) async { + final api = "${await bind.mainGetApiServer()}/api/device-group/accessible"; + try { + var uri0 = Uri.parse(api); + final pageSize = 100; + 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(), + }); + final resp = await http.get(uri, headers: getHttpHeaders()); + _statusCode = resp.statusCode; + Map json = + _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode); + if (json.containsKey('error')) { + throw json['error']; + } + if (resp.statusCode != 200) { + throw 'HTTP ${resp.statusCode}'; + } + if (json.containsKey('total')) { + if (total == 0) total = json['total']; + if (json.containsKey('data')) { + final data = json['data']; + if (data is List) { + for (final user in data) { + final u = DeviceGroupPayload.fromJson(user); + int index = tmpDeviceGroups.indexWhere((e) => e.name == u.name); + if (index < 0) { + tmpDeviceGroups.add(u); + } else { + tmpDeviceGroups[index] = u; + } + } + } + } + } + } while (current * pageSize < total); + return true; + } catch (err) { + debugPrint('get accessible device groups: $err'); + // old hbbs doesn't support this api + // groupLoadError.value = + // '${translate('pull_group_failed_tip')}: ${translate(err.toString())}'; + } + return false; } Future _getUsers(List tmpUsers) async { @@ -225,6 +298,7 @@ class GroupModel { try { final map = ({ "access_token": bind.mainGetLocalOption(key: 'access_token'), + "device_groups": deviceGroups.map((e) => e.toGroupCacheJson()).toList(), "users": users.map((e) => e.toGroupCacheJson()).toList(), 'peers': peers.map((e) => e.toGroupCacheJson()).toList() }); @@ -244,8 +318,14 @@ class GroupModel { if (groupLoading.value) return; final data = jsonDecode(cache); if (data == null || data['access_token'] != access_token) return; + deviceGroups.clear(); users.clear(); peers.clear(); + if (data['device_groups'] is List) { + for (var u in data['device_groups']) { + deviceGroups.add(DeviceGroupPayload.fromJson(u)); + } + } if (data['users'] is List) { for (var u in data['users']) { users.add(UserPayload.fromJson(u)); @@ -255,6 +335,7 @@ class GroupModel { for (final peer in data['peers']) { peers.add(Peer.fromJson(peer)); } + _callbackPeerUpdate(); } } catch (e) { debugPrint("load group cache: $e"); @@ -263,9 +344,34 @@ class GroupModel { reset() async { groupLoadError.value = ''; + deviceGroups.clear(); users.clear(); peers.clear(); - selectedUser.value = ''; + selectedAccessibleItemName.value = ''; await bind.mainClearGroup(); } + + void _callbackPeerUpdate() { + for (var listener in _peerIdUpdateListeners.values) { + listener(); + } + } + + void addPeerUpdateListener(String key, VoidCallback listener) { + _peerIdUpdateListeners[key] = listener; + } + + void removePeerUpdateListener(String key) { + _peerIdUpdateListeners.remove(key); + } + + void _tryHandlePullError() { + String errorMessage = groupLoadError.value; + // The error message is "Retrieving accessible devices is disabled." + if (errorMessage.toLowerCase().contains('disabled')) { + users.clear(); + peers.clear(); + deviceGroups.clear(); + } + } } diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart index a30bb79fdbd..dcccf8f7c9a 100644 --- a/flutter/lib/models/input_model.dart +++ b/flutter/lib/models/input_model.dart @@ -18,7 +18,7 @@ import '../common.dart'; import '../consts.dart'; /// Mouse button enum. -enum MouseButtons { left, right, wheel } +enum MouseButtons { left, right, wheel, back } const _kMouseEventDown = 'mousedown'; const _kMouseEventUp = 'mouseup'; @@ -155,6 +155,8 @@ extension ToString on MouseButtons { return 'right'; case MouseButtons.wheel: return 'wheel'; + case MouseButtons.back: + return 'back'; } } } @@ -343,8 +345,11 @@ class InputModel { var _fling = false; Timer? _flingTimer; final _flingBaseDelay = 30; - // trackpad, peer linux - final _trackpadSpeed = 0.06; + final _trackpadAdjustPeerLinux = 0.06; + // This is an experience value. + final _trackpadAdjustMacToWin = 2.50; + int _trackpadSpeed = kDefaultTrackpadSpeed; + double _trackpadSpeedInner = kDefaultTrackpadSpeed / 100.0; var _trackpadScrollUnsent = Offset.zero; var _lastScale = 1.0; @@ -367,6 +372,8 @@ class InputModel { String? get peerPlatform => parent.target?.ffiModel.pi.platform; bool get isViewOnly => parent.target!.ffiModel.viewOnly; double get devicePixelRatio => parent.target!.canvasModel.devicePixelRatio; + bool get isViewCamera => parent.target!.connType == ConnType.viewCamera; + int get trackpadSpeed => _trackpadSpeed; InputModel(this.parent) { sessionId = parent.target!.sessionId; @@ -382,6 +389,28 @@ class InputModel { } } + /// Updates the trackpad speed based on the session value. + /// + /// The expected format of the retrieved value is a string that can be parsed into a double. + /// If parsing fails or the value is out of bounds (less than `kMinTrackpadSpeed` or greater + /// than `kMaxTrackpadSpeed`), the trackpad speed is reset to the default + /// value (`kDefaultTrackpadSpeed`). + /// + /// Bounds: + /// - Minimum: `kMinTrackpadSpeed` + /// - Maximum: `kMaxTrackpadSpeed` + /// - Default: `kDefaultTrackpadSpeed` + Future updateTrackpadSpeed() async { + _trackpadSpeed = + (await bind.sessionGetTrackpadSpeed(sessionId: sessionId) ?? + kDefaultTrackpadSpeed); + if (_trackpadSpeed < kMinTrackpadSpeed || + _trackpadSpeed > kMaxTrackpadSpeed) { + _trackpadSpeed = kDefaultTrackpadSpeed; + } + _trackpadSpeedInner = _trackpadSpeed / 100.0; + } + void handleKeyDownEventModifiers(KeyEvent e) { KeyUpEvent upEvent(e) => KeyUpEvent( physicalKey: e.physicalKey, @@ -469,6 +498,7 @@ class InputModel { KeyEventResult handleRawKeyEvent(RawKeyEvent e) { if (isViewOnly) return KeyEventResult.handled; + if (isViewCamera) return KeyEventResult.handled; if (!isInputSourceFlutter) { if (isDesktop) { return KeyEventResult.handled; @@ -523,6 +553,7 @@ class InputModel { KeyEventResult handleKeyEvent(KeyEvent e) { if (isViewOnly) return KeyEventResult.handled; + if (isViewCamera) return KeyEventResult.handled; if (!isInputSourceFlutter) { if (isDesktop) { return KeyEventResult.handled; @@ -722,6 +753,7 @@ class InputModel { /// [press] indicates a click event(down and up). void inputKey(String name, {bool? down, bool? press}) { if (!keyboardPerm) return; + if (isViewCamera) return; bind.sessionInputKey( sessionId: sessionId, name: name, @@ -783,6 +815,7 @@ class InputModel { /// Send scroll event with scroll distance [y]. Future scroll(int y) async { + if (isViewCamera) return; await bind.sessionSendMouse( sessionId: sessionId, msg: json @@ -806,6 +839,7 @@ class InputModel { /// Send mouse press event. Future sendMouse(String type, MouseButtons button) async { if (!keyboardPerm) return; + if (isViewCamera) return; await bind.sessionSendMouse( sessionId: sessionId, msg: json.encode(modify({'type': type, 'buttons': button.value}))); @@ -832,6 +866,7 @@ class InputModel { /// Send mouse movement event with distance in [x] and [y]. Future moveMouse(double x, double y) async { if (!keyboardPerm) return; + if (isViewCamera) return; var x2 = x.toInt(); var y2 = y.toInt(); await bind.sessionSendMouse( @@ -855,6 +890,7 @@ class InputModel { _lastScale = 1.0; _stopFling = true; if (isViewOnly) return; + if (isViewCamera) return; if (peerPlatform == kPeerPlatformAndroid) { handlePointerEvent('touch', kMouseEventTypePanStart, e.position); } @@ -863,6 +899,7 @@ class InputModel { // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) { if (isViewOnly) return; + if (isViewCamera) return; if (peerPlatform != kPeerPlatformAndroid) { final scale = ((e.scale - _lastScale) * 1000).toInt(); _lastScale = e.scale; @@ -877,13 +914,16 @@ class InputModel { } } - final delta = e.panDelta; + var delta = e.panDelta * _trackpadSpeedInner; + if (isMacOS && peerPlatform == kPeerPlatformWindows) { + delta *= _trackpadAdjustMacToWin; + } _trackpadLastDelta = delta; var x = delta.dx.toInt(); var y = delta.dy.toInt(); if (peerPlatform == kPeerPlatformLinux) { - _trackpadScrollUnsent += (delta * _trackpadSpeed); + _trackpadScrollUnsent += (delta * _trackpadAdjustPeerLinux); x = _trackpadScrollUnsent.dx.truncate(); y = _trackpadScrollUnsent.dy.truncate(); _trackpadScrollUnsent -= Offset(x.toDouble(), y.toDouble()); @@ -902,6 +942,7 @@ class InputModel { handlePointerEvent('touch', kMouseEventTypePanUpdate, Offset(x.toDouble(), y.toDouble())); } else { + if (isViewCamera) return; bind.sessionSendMouse( sessionId: sessionId, msg: '{"type": "trackpad", "x": "$x", "y": "$y"}'); @@ -910,6 +951,7 @@ class InputModel { } void _scheduleFling(double x, double y, int delay) { + if (isViewCamera) return; if ((x == 0 && y == 0) || _stopFling) { _fling = false; return; @@ -929,8 +971,8 @@ class InputModel { var dx = x.toInt(); var dy = y.toInt(); if (parent.target?.ffiModel.pi.platform == kPeerPlatformLinux) { - dx = (x * _trackpadSpeed).toInt(); - dy = (y * _trackpadSpeed).toInt(); + dx = (x * _trackpadAdjustPeerLinux).toInt(); + dy = (y * _trackpadAdjustPeerLinux).toInt(); } var delay = _flingBaseDelay; @@ -961,6 +1003,7 @@ class InputModel { } void onPointerPanZoomEnd(PointerPanZoomEndEvent e) { + if (isViewCamera) return; if (peerPlatform == kPeerPlatformAndroid) { handlePointerEvent('touch', kMouseEventTypePanEnd, e.position); return; @@ -975,7 +1018,10 @@ class InputModel { _stopFling = false; // 2.0 is an experience value - double minFlingValue = 2.0; + double minFlingValue = 2.0 * _trackpadSpeedInner; + if (isMacOS && peerPlatform == kPeerPlatformWindows) { + minFlingValue *= _trackpadAdjustMacToWin; + } if (_trackpadLastDelta.dx.abs() > minFlingValue || _trackpadLastDelta.dy.abs() > minFlingValue) { _fling = true; @@ -992,6 +1038,7 @@ class InputModel { _remoteWindowCoords = []; _windowRect = null; if (isViewOnly) return; + if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) { if (isPhysicalMouse.value) { isPhysicalMouse.value = false; @@ -1005,6 +1052,7 @@ class InputModel { void onPointUpImage(PointerUpEvent e) { if (isDesktop) _queryOtherWindowCoords = false; if (isViewOnly) return; + if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) return; if (isPhysicalMouse.value) { handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position); @@ -1013,6 +1061,7 @@ class InputModel { void onPointMoveImage(PointerMoveEvent e) { if (isViewOnly) return; + if (isViewCamera) return; if (e.kind != ui.PointerDeviceKind.mouse) return; if (_queryOtherWindowCoords) { Future.delayed(Duration.zero, () async { @@ -1047,6 +1096,7 @@ class InputModel { void onPointerSignalImage(PointerSignalEvent e) { if (isViewOnly) return; + if (isViewCamera) return; if (e is PointerScrollEvent) { var dx = e.scrollDelta.dx.toInt(); var dy = e.scrollDelta.dy.toInt(); @@ -1144,6 +1194,7 @@ class InputModel { } final evt = PointerEventToRust(kind, type, evtValue).toJson(); + if (isViewCamera) return; bind.sessionSendPointer( sessionId: sessionId, msg: json.encode(modify(evt))); } @@ -1175,6 +1226,7 @@ class InputModel { Offset offset, { bool onExit = false, }) { + if (isViewCamera) return; double x = offset.dx; double y = max(0.0, offset.dy); if (_checkPeerControlProtected(x, y)) { @@ -1426,7 +1478,18 @@ class InputModel { } } - void onMobileBack() => tap(MouseButtons.right); + void onMobileBack() { + final minBackButtonVersion = "1.3.8"; + final peerVersion = + parent.target?.ffiModel.pi.version ?? minBackButtonVersion; + var btn = MouseButtons.back; + // For compatibility with old versions + if (versionCmp(peerVersion, minBackButtonVersion) < 0) { + btn = MouseButtons.right; + } + tap(btn); + } + void onMobileHome() => tap(MouseButtons.wheel); Future onMobileApps() async { sendMouse('down', MouseButtons.wheel); diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 5a5dcf623ee..645002686d9 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -9,7 +9,6 @@ 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/widgets.dart'; import 'package:flutter_hbb/common/widgets/peers_view.dart'; import 'package:flutter_hbb/consts.dart'; import 'package:flutter_hbb/models/ab_model.dart'; @@ -19,10 +18,12 @@ import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/group_model.dart'; import 'package:flutter_hbb/models/peer_model.dart'; import 'package:flutter_hbb/models/peer_tab_model.dart'; +import 'package:flutter_hbb/models/printer_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; import 'package:flutter_hbb/models/user_model.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/models/desktop_render_texture.dart'; +import 'package:flutter_hbb/models/terminal_model.dart'; import 'package:flutter_hbb/plugin/event.dart'; import 'package:flutter_hbb/plugin/manager.dart'; import 'package:flutter_hbb/plugin/widgets/desc_ui.dart'; @@ -34,6 +35,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:uuid/uuid.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:file_picker/file_picker.dart'; import '../common.dart'; import '../utils/image.dart' as img; @@ -59,6 +61,7 @@ class CachedPeerData { bool secure = false; bool direct = false; + String streamType = ''; CachedPeerData(); @@ -72,6 +75,7 @@ class CachedPeerData { 'permissions': permissions, 'secure': secure, 'direct': direct, + 'streamType': streamType, }); } @@ -90,6 +94,7 @@ class CachedPeerData { }); data.secure = map['secure']; data.direct = map['direct']; + data.streamType = map['streamType']; return data; } catch (e) { debugPrint('Failed to parse CachedPeerData: $e'); @@ -119,6 +124,8 @@ class FfiModel with ChangeNotifier { RxBool waitForFirstImage = true.obs; bool isRefreshing = false; + Timer? timerScreenshot; + Rect? get rect => _rect; bool get isOriginalResolutionSet => _pi.tryGetDisplayIfNotAllDisplay()?.isOriginalResolutionSet ?? false; @@ -216,29 +223,48 @@ class FfiModel with ChangeNotifier { _timer = null; clearPermissions(); waitForImageTimer?.cancel(); + timerScreenshot?.cancel(); } - setConnectionType(String peerId, bool secure, bool direct) { + setConnectionType( + String peerId, bool secure, bool direct, String streamType) { cachedPeerData.secure = secure; cachedPeerData.direct = direct; + cachedPeerData.streamType = streamType; _secure = secure; _direct = direct; try { var connectionType = ConnectionTypeState.find(peerId); connectionType.setSecure(secure); connectionType.setDirect(direct); + connectionType.setStreamType(streamType); } catch (e) { // } } - Widget? getConnectionImage() { + Widget? getConnectionImageText() { if (secure == null || direct == null) { return null; } else { final icon = '${secure == true ? 'secure' : 'insecure'}${direct == true ? '' : '_relay'}'; - return SvgPicture.asset('assets/$icon.svg', width: 48, height: 48); + final iconWidget = + SvgPicture.asset('assets/$icon.svg', width: 48, height: 48); + String connectionText = + getConnectionText(secure!, direct!, cachedPeerData.streamType); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + iconWidget, + SizedBox(height: 4), + Text( + connectionText, + style: TextStyle(fontSize: 12), + textAlign: TextAlign.center, + ), + ], + ); } } @@ -255,7 +281,7 @@ class FfiModel with ChangeNotifier { 'link': '', }, sessionId, peerId); updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId); - setConnectionType(peerId, data.secure, data.direct); + setConnectionType(peerId, data.secure, data.direct, data.streamType); await handlePeerInfo(data.peerInfo, peerId, true); for (final element in data.cursorDataList) { updateLastCursorId(element); @@ -284,8 +310,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'sync_platform_additions') { handlePlatformAdditions(evt, sessionId, peerId); } else if (name == 'connection_ready') { - setConnectionType( - peerId, evt['secure'] == 'true', evt['direct'] == 'true'); + setConnectionType(peerId, evt['secure'] == 'true', + evt['direct'] == 'true', evt['stream_type'] ?? ''); } else if (name == 'switch_display') { // switch display is kept for backward compatibility handleSwitchDisplay(evt, sessionId, peerId); @@ -307,6 +333,8 @@ class FfiModel with ChangeNotifier { } else if (name == 'chat_server_mode') { parent.target?.chatModel .receive(int.parse(evt['id'] as String), evt['text'] ?? ''); + } else if (name == 'terminal_response') { + parent.target?.routeTerminalResponse(evt); } else if (name == 'file_dir') { parent.target?.fileModel.receiveFileDir(evt); } else if (name == 'empty_dirs') { @@ -407,15 +435,261 @@ class FfiModel with ChangeNotifier { parent.target?.fileModel.sendEmptyDirs(evt); } } else if (name == "record_status") { - if (desktopType == DesktopType.remote || isMobile) { + if (desktopType == DesktopType.remote || + desktopType == DesktopType.viewCamera || + isMobile) { parent.target?.recordingModel.updateStatus(evt['start'] == 'true'); } + } else if (name == "printer_request") { + _handlePrinterRequest(evt, sessionId, peerId); + } else if (name == 'screenshot') { + _handleScreenshot(evt, sessionId, peerId); } else { debugPrint('Event is not handled in the fixed branch: $name'); } }; } + _handleScreenshot( + Map evt, SessionID sessionId, String peerId) { + timerScreenshot?.cancel(); + timerScreenshot = null; + final msg = evt['msg'] ?? ''; + final msgBoxType = 'custom-nook-nocancel-hasclose'; + final msgBoxTitle = 'Take screenshot'; + final dialogManager = parent.target!.dialogManager; + if (msg.isNotEmpty) { + msgBox(sessionId, msgBoxType, msgBoxTitle, msg, '', dialogManager); + } else { + final msgBoxText = 'screenshot-action-tip'; + + close() { + dialogManager.dismissAll(); + } + + saveAs() { + close(); + Future.delayed(Duration.zero, () async { + final ts = DateTime.now().millisecondsSinceEpoch ~/ 1000; + String? outputFile = await FilePicker.platform.saveFile( + dialogTitle: '${translate('Save as')}...', + fileName: 'screenshot_$ts.png', + allowedExtensions: ['png'], + type: FileType.custom, + ); + if (outputFile == null) { + bind.sessionHandleScreenshot(sessionId: sessionId, action: '2'); + } else { + final res = await bind.sessionHandleScreenshot( + sessionId: sessionId, action: '0:$outputFile'); + if (res.isNotEmpty) { + msgBox(sessionId, 'custom-nook-nocancel-hasclose-error', + 'Take screenshot', res, '', dialogManager); + } + } + }); + } + + copyToClipboard() { + bind.sessionHandleScreenshot(sessionId: sessionId, action: '1'); + close(); + } + + cancel() { + bind.sessionHandleScreenshot(sessionId: sessionId, action: '2'); + close(); + } + + final List buttons = [ + dialogButton('${translate('Save as')}...', onPressed: saveAs), + dialogButton('Copy to clipboard', onPressed: copyToClipboard), + dialogButton('Cancel', onPressed: cancel), + ]; + dialogManager.dismissAll(); + dialogManager.show( + (setState, close, context) => CustomAlertDialog( + title: null, + content: SelectionArea( + child: msgboxContent(msgBoxType, msgBoxTitle, msgBoxText)), + actions: buttons, + ), + tag: '$msgBoxType-$msgBoxTitle-$msgBoxTitle', + ); + } + } + + _handlePrinterRequest( + Map evt, SessionID sessionId, String peerId) { + final id = evt['id']; + final path = evt['path']; + final dialogManager = parent.target!.dialogManager; + dialogManager.show((setState, close, context) { + PrinterOptions printerOptions = PrinterOptions.load(); + final saveSettings = mainGetLocalBoolOptionSync(kKeyPrinterSave).obs; + final dontShowAgain = false.obs; + final Rx selectedPrinterName = printerOptions.printerName.obs; + final printerNames = printerOptions.printerNames; + final defaultOrSelectedGroupValue = + (printerOptions.action == kValuePrinterIncomingJobDismiss + ? kValuePrinterIncomingJobDefault + : printerOptions.action) + .obs; + + onRatioChanged(String? value) { + defaultOrSelectedGroupValue.value = + value ?? kValuePrinterIncomingJobDefault; + } + + onSubmit() { + final printerName = defaultOrSelectedGroupValue.isEmpty + ? '' + : selectedPrinterName.value; + bind.sessionPrinterResponse( + sessionId: sessionId, id: id, path: path, printerName: printerName); + if (saveSettings.value || dontShowAgain.value) { + bind.mainSetLocalOption(key: kKeyPrinterSelected, value: printerName); + bind.mainSetLocalOption( + key: kKeyPrinterIncomingJobAction, + value: defaultOrSelectedGroupValue.value); + } + if (dontShowAgain.value) { + mainSetLocalBoolOption(kKeyPrinterAllowAutoPrint, true); + } + close(); + } + + onCancel() { + if (dontShowAgain.value) { + bind.mainSetLocalOption( + key: kKeyPrinterIncomingJobAction, + value: kValuePrinterIncomingJobDismiss); + } + close(); + } + + final printerItemHeight = 30.0; + final selectionAreaHeight = + printerItemHeight * min(8.0, max(printerNames.length, 3.0)); + final content = Column( + children: [ + Text(translate('print-incoming-job-confirm-tip')), + Row( + children: [ + Obx(() => Radio( + value: kValuePrinterIncomingJobDefault, + groupValue: defaultOrSelectedGroupValue.value, + onChanged: onRatioChanged)), + GestureDetector( + child: Text(translate('use-the-default-printer-tip')), + onTap: () => onRatioChanged(kValuePrinterIncomingJobDefault)), + ], + ), + Column( + children: [ + Row(children: [ + Obx(() => Radio( + value: kValuePrinterIncomingJobSelected, + groupValue: defaultOrSelectedGroupValue.value, + onChanged: onRatioChanged)), + GestureDetector( + child: Text(translate('use-the-selected-printer-tip')), + onTap: () => + onRatioChanged(kValuePrinterIncomingJobSelected)), + ]), + SizedBox( + height: selectionAreaHeight, + width: 500, + child: ListView.builder( + itemBuilder: (context, index) { + return Obx(() => GestureDetector( + child: Container( + decoration: BoxDecoration( + color: selectedPrinterName.value == + printerNames[index] + ? (defaultOrSelectedGroupValue.value == + kValuePrinterIncomingJobSelected + ? MyTheme.button + : MyTheme.button.withOpacity(0.5)) + : Theme.of(context).cardColor, + borderRadius: BorderRadius.all( + Radius.circular(5.0), + ), + ), + key: ValueKey(printerNames[index]), + height: printerItemHeight, + child: Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 10.0), + child: Text( + printerNames[index], + style: TextStyle(fontSize: 14), + ), + ), + ), + ), + onTap: defaultOrSelectedGroupValue.value == + kValuePrinterIncomingJobSelected + ? () { + selectedPrinterName.value = + printerNames[index]; + } + : null, + )); + }, + itemCount: printerNames.length), + ), + ], + ), + Row( + children: [ + Obx(() => Checkbox( + value: saveSettings.value, + onChanged: (value) { + if (value != null) { + saveSettings.value = value; + mainSetLocalBoolOption(kKeyPrinterSave, value); + } + })), + GestureDetector( + child: Text(translate('save-settings-tip')), + onTap: () { + saveSettings.value = !saveSettings.value; + mainSetLocalBoolOption(kKeyPrinterSave, saveSettings.value); + }), + ], + ), + Row( + children: [ + Obx(() => Checkbox( + value: dontShowAgain.value, + onChanged: (value) { + if (value != null) { + dontShowAgain.value = value; + } + })), + GestureDetector( + child: Text(translate('dont-show-again-tip')), + onTap: () { + dontShowAgain.value = !dontShowAgain.value; + }), + ], + ), + ], + ); + return CustomAlertDialog( + title: Text(translate('Incoming Print Job')), + content: content, + actions: [ + dialogButton('OK', onPressed: onSubmit), + dialogButton('Cancel', onPressed: onCancel), + ], + onSubmit: onSubmit, + onCancel: onCancel, + ); + }); + } + _handleUseTextureRender( Map evt, SessionID sessionId, String peerId) { parent.target?.imageModel.setUseTextureRender(evt['v'] == 'Y'); @@ -501,7 +775,9 @@ class FfiModel with ChangeNotifier { final display = int.parse(evt['display']); if (_pi.currentDisplay != kAllDisplayValue) { - if (bind.peerGetDefaultSessionsCount(id: peerId) > 1) { + if (bind.peerGetSessionsCount( + id: peerId, connType: parent.target!.connType.index) > + 1) { if (display != _pi.currentDisplay) { return; } @@ -581,10 +857,16 @@ class FfiModel with ChangeNotifier { } else if (type == 'input-password') { enterPasswordDialog(sessionId, dialogManager); } else if (type == 'session-login' || type == 'session-re-login') { - enterUserLoginDialog(sessionId, dialogManager); - } else if (type == 'session-login-password' || - type == 'session-login-password') { - enterUserLoginAndPasswordDialog(sessionId, dialogManager); + enterUserLoginDialog(sessionId, dialogManager, 'login_linux_tip', true); + } else if (type == 'session-login-password') { + enterUserLoginAndPasswordDialog( + sessionId, dialogManager, 'login_linux_tip', true); + } else if (type == 'terminal-admin-login') { + enterUserLoginDialog( + sessionId, dialogManager, 'terminal-admin-login-tip', false); + } else if (type == 'terminal-admin-login-password') { + enterUserLoginAndPasswordDialog( + sessionId, dialogManager, 'terminal-admin-login-tip', false); } else if (type == 'restarting') { showMsgBox(sessionId, type, title, text, link, false, dialogManager, hasCancel: false); @@ -739,17 +1021,12 @@ class FfiModel with ChangeNotifier { String link, bool hasRetry, OverlayDialogManager dialogManager) { - if (text == 'no_need_privacy_mode_no_physical_displays_tip' || - text == 'Enter privacy mode') { - // There are display changes on the remote side, - // which will cause some messages to refresh the canvas and dismiss dialogs. - // So we add a delay here to ensure the dialog is displayed. - Future.delayed(Duration(milliseconds: 3000), () { - showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager); - }); - } else { + // There are display changes on the remote side, + // which will cause some messages to refresh the canvas and dismiss dialogs. + // So we add a delay here to ensure the dialog is displayed. + Future.delayed(Duration(milliseconds: 3000), () { showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager); - } + }); } _updateSessionWidthHeight(SessionID sessionId) { @@ -809,7 +1086,9 @@ class FfiModel with ChangeNotifier { _pi.primaryDisplay = currentDisplay; } - if (bind.peerGetDefaultSessionsCount(id: peerId) <= 1) { + if (bind.peerGetSessionsCount( + id: peerId, connType: parent.target!.connType.index) <= + 1) { _pi.currentDisplay = currentDisplay; } @@ -829,7 +1108,14 @@ class FfiModel with ChangeNotifier { } if (connType == ConnType.fileTransfer) { parent.target?.fileModel.onReady(); - } else if (connType == ConnType.defaultConn) { + } else if (connType == ConnType.terminal) { + // Call onReady on all registered terminal models + final models = parent.target?._terminalModels.values ?? []; + for (final model in models) { + model.onReady(); + } + } else if (connType == ConnType.defaultConn || + connType == ConnType.viewCamera) { List newDisplays = []; List displays = json.decode(evt['displays']); for (int i = 0; i < displays.length; ++i) { @@ -859,7 +1145,7 @@ class FfiModel with ChangeNotifier { bind.sessionGetToggleOptionSync( sessionId: sessionId, arg: kOptionToggleViewOnly)); } - if (connType == ConnType.defaultConn) { + if (connType == ConnType.defaultConn || connType == ConnType.viewCamera) { final platformAdditions = evt['platform_additions']; if (platformAdditions != null && platformAdditions != '') { try { @@ -1500,13 +1786,15 @@ class CanvasModel with ChangeNotifier { return max(bottom - MediaQueryData.fromView(ui.window).padding.top, 0); } + updateSize() => _size = getSize(); + updateViewStyle({refreshMousePos = true, notify = true}) async { final style = await bind.sessionGetViewStyle(sessionId: sessionId); if (style == null) { return; } - _size = getSize(); + updateSize(); final displayWidth = getDisplayWidth(); final displayHeight = getDisplayHeight(); final viewStyle = ViewStyle( @@ -1543,7 +1831,7 @@ class CanvasModel with ChangeNotifier { _resetCanvasOffset(int displayWidth, int displayHeight) { _x = (size.width - displayWidth * _scale) / 2; _y = (size.height - displayHeight * _scale) / 2; - if (isMobile && _lastViewStyle.style == kRemoteViewStyleOriginal) { + if (isMobile) { _moveToCenterCursor(); } } @@ -1736,7 +2024,8 @@ class CanvasModel with ChangeNotifier { _timerMobileFocusCanvasCursor?.cancel(); _timerMobileFocusCanvasCursor = Timer(Duration(milliseconds: 100), () async { - await updateViewStyle(refreshMousePos: false, notify: false); + updateSize(); + _resetCanvasOffset(getDisplayWidth(), getDisplayHeight()); notifyListeners(); }); } @@ -2427,6 +2716,8 @@ class CursorModel with ChangeNotifier { _x = -10000; _x = -10000; _image = null; + _firstUpdateMouseTime = null; + gotMouseControl = true; disposeImages(); _clearCache(); @@ -2571,7 +2862,15 @@ class ElevationModel with ChangeNotifier { onPortableServiceRunning(bool running) => _running = running; } -enum ConnType { defaultConn, fileTransfer, portForward, rdp } +// The index values of `ConnType` are same as rust protobuf. +enum ConnType { + defaultConn, + fileTransfer, + portForward, + rdp, + viewCamera, + terminal +} /// Flutter state manager and data communication with the Rust core. class FFI { @@ -2606,6 +2905,12 @@ class FFI { late final Peers favoritePeersModel; // global late final Peers lanPeersModel; // global + // Terminal model registry for multiple terminals + final Map _terminalModels = {}; + + // Getter for terminal models + Map get terminalModels => _terminalModels; + FFI(SessionID? sId) { sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId); imageModel = ImageModel(WeakReference(this)); @@ -2646,12 +2951,14 @@ class FFI { ffiModel.waitForImageTimer = null; } - /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward]. + /// Start with the given [id]. Only transfer file if [isFileTransfer], only view camera if [isViewCamera], only port forward if [isPortForward]. void start( String id, { bool isFileTransfer = false, + bool isViewCamera = false, bool isPortForward = false, bool isRdp = false, + bool isTerminal = false, String? switchUuid, String? password, bool? isSharedPassword, @@ -2664,11 +2971,22 @@ class FFI { closed = false; auditNote = ''; if (isMobile) mobileReset(); - assert(!(isFileTransfer && isPortForward), 'more than one connect type'); + assert( + (!(isPortForward && isViewCamera)) && + (!(isViewCamera && isPortForward)) && + (!(isPortForward && isFileTransfer)) && + (!(isTerminal && isFileTransfer)) && + (!(isTerminal && isViewCamera)) && + (!(isTerminal && isPortForward)), + 'more than one connect type'); if (isFileTransfer) { connType = ConnType.fileTransfer; + } else if (isViewCamera) { + connType = ConnType.viewCamera; } else if (isPortForward) { connType = ConnType.portForward; + } else if (isTerminal) { + connType = ConnType.terminal; } else { chatModel.resetClientMode(); connType = ConnType.defaultConn; @@ -2686,8 +3004,10 @@ class FFI { sessionId: sessionId, id: id, isFileTransfer: isFileTransfer, + isViewCamera: isViewCamera, isPortForward: isPortForward, isRdp: isRdp, + isTerminal: isTerminal, switchUuid: switchUuid ?? '', forceRelay: forceRelay ?? false, password: password ?? '', @@ -2701,7 +3021,10 @@ class FFI { return; } final addRes = bind.sessionAddExistedSync( - id: id, sessionId: sessionId, displays: Int32List.fromList(displays)); + id: id, + sessionId: sessionId, + displays: Int32List.fromList(displays), + isViewCamera: isViewCamera); if (addRes != '') { debugPrint( 'Unreachable, failed to add existed session to $id, $addRes'); @@ -2712,6 +3035,15 @@ class FFI { if (isDesktop && connType == ConnType.defaultConn) { textureModel.updateCurrentDisplay(display ?? 0); } + // FIXME: separate cameras displays or shift all indices. + if (isDesktop && connType == ConnType.viewCamera) { + // FIXME: currently the default 0 is not used. + textureModel.updateCurrentDisplay(display ?? 0); + } + + if (isDesktop) { + inputModel.updateTrackpadSpeed(); + } // CAUTION: `sessionStart()` and `sessionStartWithDisplays()` are an async functions. // Though the stream is returned immediately, the stream may not be ready. @@ -2855,6 +3187,11 @@ class FFI { Future close({bool closeSession = true}) async { closed = true; chatModel.close(); + // Close all terminal models + for (final model in _terminalModels.values) { + model.dispose(); + } + _terminalModels.clear(); if (imageModel.image != null && !isWebDesktop) { await setCanvasConfig( sessionId, @@ -2885,6 +3222,27 @@ class FFI { Future invokeMethod(String method, [dynamic arguments]) async { return await platformFFI.invokeMethod(method, arguments); } + + // Terminal model management + void registerTerminalModel(int terminalId, TerminalModel model) { + debugPrint('[FFI] Registering terminal model for terminal $terminalId'); + _terminalModels[terminalId] = model; + } + + void unregisterTerminalModel(int terminalId) { + debugPrint('[FFI] Unregistering terminal model for terminal $terminalId'); + _terminalModels.remove(terminalId); + } + + void routeTerminalResponse(Map evt) { + final int terminalId = TerminalModel.getTerminalIdFromEvt(evt); + + // Route to specific terminal model if it exists + final model = _terminalModels[terminalId]; + if (model != null) { + model.handleTerminalResponse(evt); + } + } } const kInvalidResolutionValue = -1; @@ -2930,7 +3288,8 @@ class Display { originalWidth == kVirtualDisplayResolutionValue && originalHeight == kVirtualDisplayResolutionValue; bool get isOriginalResolution => - width == originalWidth && height == originalHeight; + width == (originalWidth * scale).round() && + height == (originalHeight * scale).round(); } class Resolution { diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index c8d5085e897..337f532786e 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -60,14 +60,14 @@ class PlatformFFI { } bool registerEventHandler( - String eventName, String handlerName, HandleEvent handler) { + String eventName, String handlerName, HandleEvent handler, {bool replace = false}) { debugPrint('registerEventHandler $eventName $handlerName'); var handlers = _eventHandlers[eventName]; if (handlers == null) { _eventHandlers[eventName] = {handlerName: handler}; return true; } else { - if (handlers.containsKey(handlerName)) { + if (!replace && handlers.containsKey(handlerName)) { return false; } else { handlers[handlerName] = handler; diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart index 7ab5a2b803e..35236dd4c50 100644 --- a/flutter/lib/models/peer_model.dart +++ b/flutter/lib/models/peer_model.dart @@ -19,6 +19,7 @@ class Peer { String rdpUsername; bool online = false; String loginName; //login username + String device_group_name; bool? sameServer; String getId() { @@ -41,6 +42,7 @@ class Peer { rdpPort = json['rdpPort'] ?? '', rdpUsername = json['rdpUsername'] ?? '', loginName = json['loginName'] ?? '', + device_group_name = json['device_group_name'] ?? '', sameServer = json['same_server']; Map toJson() { @@ -57,6 +59,7 @@ class Peer { "rdpPort": rdpPort, "rdpUsername": rdpUsername, 'loginName': loginName, + 'device_group_name': device_group_name, 'same_server': sameServer, }; } @@ -83,6 +86,7 @@ class Peer { "hostname": hostname, "platform": platform, "login_name": loginName, + "device_group_name": device_group_name, }; } @@ -99,6 +103,7 @@ class Peer { required this.rdpPort, required this.rdpUsername, required this.loginName, + required this.device_group_name, this.sameServer, }); @@ -116,6 +121,7 @@ class Peer { rdpPort: '', rdpUsername: '', loginName: '', + device_group_name: '', ); bool equal(Peer other) { return id == other.id && @@ -129,6 +135,7 @@ class Peer { forceAlwaysRelay == other.forceAlwaysRelay && rdpPort == other.rdpPort && rdpUsername == other.rdpUsername && + device_group_name == other.device_group_name && loginName == other.loginName; } @@ -146,6 +153,7 @@ class Peer { rdpPort: other.rdpPort, rdpUsername: other.rdpUsername, loginName: other.loginName, + device_group_name: other.device_group_name, sameServer: other.sameServer); } @@ -157,6 +165,11 @@ class Peers extends ChangeNotifier { final String name; final String loadEvent; List peers = List.empty(growable: true); + // Part of the peers that are not in the rest peers list. + // When there're too many peers, we may want to load the front 100 peers first, + // so we can see peers in UI quickly. `restPeerIds` is the rest peers' ids. + // And then load all peers later. + List restPeerIds = List.empty(growable: true); final GetInitPeers? getInitPeers; UpdateEvent event = UpdateEvent.load; static const _cbQueryOnlines = 'callback_query_onlines'; @@ -230,6 +243,12 @@ class Peers extends ChangeNotifier { } else { peers = _decodePeers(evt['peers']); } + + restPeerIds = []; + if (evt['ids'] != null) { + restPeerIds = (evt['ids'] as String).split(','); + } + for (var peer in peers) { final state = onlineStates[peer.id]; peer.online = state != null && state != false; diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart index 83df1f05d6a..d152f7349e5 100644 --- a/flutter/lib/models/peer_tab_model.dart +++ b/flutter/lib/models/peer_tab_model.dart @@ -28,14 +28,14 @@ class PeerTabModel with ChangeNotifier { 'Favorites', 'Discovered', 'Address book', - 'Group', + 'Accessible devices', ]; static const List icons = [ Icons.access_time_filled, Icons.star, Icons.explore, IconFont.addressBook, - Icons.group, + IconFont.deviceGroupFill, ]; List isEnabled = List.from([ true, diff --git a/flutter/lib/models/printer_model.dart b/flutter/lib/models/printer_model.dart new file mode 100644 index 00000000000..8d0b3793257 --- /dev/null +++ b/flutter/lib/models/printer_model.dart @@ -0,0 +1,48 @@ +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; + +class PrinterOptions { + String action; + List printerNames; + String printerName; + + PrinterOptions( + {required this.action, + required this.printerNames, + required this.printerName}); + + static PrinterOptions load() { + var action = bind.mainGetLocalOption(key: kKeyPrinterIncomingJobAction); + if (![ + kValuePrinterIncomingJobDismiss, + kValuePrinterIncomingJobDefault, + kValuePrinterIncomingJobSelected + ].contains(action)) { + action = kValuePrinterIncomingJobDefault; + } + + final printerNames = getPrinterNames(); + var selectedPrinterName = bind.mainGetLocalOption(key: kKeyPrinterSelected); + if (!printerNames.contains(selectedPrinterName)) { + if (action == kValuePrinterIncomingJobSelected) { + action = kValuePrinterIncomingJobDefault; + bind.mainSetLocalOption( + key: kKeyPrinterIncomingJobAction, + value: kValuePrinterIncomingJobDefault); + if (printerNames.isEmpty) { + selectedPrinterName = ''; + } else { + selectedPrinterName = printerNames.first; + } + bind.mainSetLocalOption( + key: kKeyPrinterSelected, value: selectedPrinterName); + } + } + + return PrinterOptions( + action: action, + printerNames: printerNames, + printerName: selectedPrinterName); + } +} diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 8775764619e..c3e6fab71b7 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -36,6 +36,7 @@ class ServerModel with ChangeNotifier { int _connectStatus = 0; // Rendezvous Server status String _verificationMethod = ""; String _temporaryPasswordLength = ""; + bool _allowNumericOneTimePassword = false; String _approveMode = ""; int _zeroClientLengthCounter = 0; @@ -112,6 +113,12 @@ class ServerModel with ChangeNotifier { */ } + bool get allowNumericOneTimePassword => _allowNumericOneTimePassword; + switchAllowNumericOneTimePassword() async { + await mainSetBoolOption( + kOptionAllowNumericOneTimePassword, !_allowNumericOneTimePassword); + } + TextEditingController get serverId => _serverId; TextEditingController get serverPasswd => _serverPasswd; @@ -227,6 +234,8 @@ class ServerModel with ChangeNotifier { final temporaryPasswordLength = await bind.mainGetOption(key: "temporary-password-length"); final approveMode = await bind.mainGetOption(key: kOptionApproveMode); + final numericOneTimePassword = + await mainGetBoolOption(kOptionAllowNumericOneTimePassword); /* var hideCm = option2bool( 'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm')); @@ -265,6 +274,10 @@ class ServerModel with ChangeNotifier { _temporaryPasswordLength = temporaryPasswordLength; update = true; } + if (_allowNumericOneTimePassword != numericOneTimePassword) { + _allowNumericOneTimePassword = numericOneTimePassword; + update = true; + } /* if (_hideCm != hideCm) { _hideCm = hideCm; @@ -600,7 +613,13 @@ class ServerModel with ChangeNotifier { void showLoginDialog(Client client) { showClientDialog( client, - client.isFileTransfer ? "File Connection" : "Screen Connection", + client.isFileTransfer + ? "Transfer file" + : client.isViewCamera + ? "View camera" + : client.isTerminal + ? "Terminal" + : "Share screen", 'Do you accept?', 'android_new_connection_tip', () => sendLoginResponse(client, false), @@ -679,7 +698,7 @@ class ServerModel with ChangeNotifier { void sendLoginResponse(Client client, bool res) async { if (res) { bind.cmLoginRes(connId: client.id, res: res); - if (!client.isFileTransfer) { + if (!client.isFileTransfer && !client.isTerminal) { parent.target?.invokeMethod("start_capture"); } parent.target?.invokeMethod("cancel_notification", client.id); @@ -791,13 +810,17 @@ class ServerModel with ChangeNotifier { enum ClientType { remote, file, + camera, portForward, + terminal, } class Client { int id = 0; // client connections inner count id bool authorized = false; bool isFileTransfer = false; + bool isViewCamera = false; + bool isTerminal = false; String portForward = ""; String name = ""; String peerId = ""; // peer user's id,show at app @@ -815,13 +838,16 @@ class Client { RxInt unreadChatMessageCount = 0.obs; - Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, - this.keyboard, this.clipboard, this.audio); + Client(this.id, this.authorized, this.isFileTransfer, this.isViewCamera, + this.name, this.peerId, this.keyboard, this.clipboard, this.audio); Client.fromJson(Map json) { id = json['id']; authorized = json['authorized']; isFileTransfer = json['is_file_transfer']; + // TODO: no entry then default. + isViewCamera = json['is_view_camera']; + isTerminal = json['is_terminal'] ?? false; portForward = json['port_forward']; name = json['name']; peerId = json['peer_id']; @@ -843,6 +869,8 @@ class Client { data['id'] = id; data['authorized'] = authorized; data['is_file_transfer'] = isFileTransfer; + data['is_view_camera'] = isViewCamera; + data['is_terminal'] = isTerminal; data['port_forward'] = portForward; data['name'] = name; data['peer_id'] = peerId; @@ -863,6 +891,10 @@ class Client { ClientType type_() { if (isFileTransfer) { return ClientType.file; + } else if (isViewCamera) { + return ClientType.camera; + } else if (isTerminal) { + return ClientType.terminal; } else if (portForward.isNotEmpty) { return ClientType.portForward; } else { diff --git a/flutter/lib/models/terminal_model.dart b/flutter/lib/models/terminal_model.dart new file mode 100644 index 00000000000..ae64e818320 --- /dev/null +++ b/flutter/lib/models/terminal_model.dart @@ -0,0 +1,346 @@ +import 'dart:async'; +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_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/main.dart'; +import 'package:xterm/xterm.dart'; + +import 'model.dart'; +import 'platform_model.dart'; + +class TerminalModel with ChangeNotifier { + final String id; // peer id + final FFI parent; + final int terminalId; + late final Terminal terminal; + late final TerminalController terminalController; + + bool _terminalOpened = false; + bool get terminalOpened => _terminalOpened; + + bool _disposed = false; + + final _inputBuffer = []; + + bool get isPeerWindows => parent.ffiModel.pi.platform == kPeerPlatformWindows; + + Future _handleInput(String data) async { + // If we press the `Enter` button on Android, + // `data` can be '\r' or '\n' when using different keyboards. + // Android -> Windows. '\r' works, but '\n' does not. '\n' is just a newline. + // Android -> Linux. Both '\r' and '\n' work as expected (execute a command). + // So when we receive '\n', we may need to convert it to '\r' to ensure compatibility. + // Desktop -> Desktop works fine. + // Check if we are on mobile or web(mobile), and convert '\n' to '\r'. + final isMobileOrWebMobile = (isMobile || (isWeb && !isWebDesktop)); + if (isMobileOrWebMobile && isPeerWindows && data == '\n') { + data = '\r'; + } + if (_terminalOpened) { + // Send user input to remote terminal + try { + await bind.sessionSendTerminalInput( + sessionId: parent.sessionId, + terminalId: terminalId, + data: data, + ); + } catch (e) { + debugPrint('[TerminalModel] Error sending terminal input: $e'); + } + } else { + debugPrint('[TerminalModel] Terminal not opened yet, buffering input'); + _inputBuffer.add(data); + } + } + + TerminalModel(this.parent, [this.terminalId = 0]) : id = parent.id { + terminal = Terminal(maxLines: 10000); + terminalController = TerminalController(); + + // Setup terminal callbacks + terminal.onOutput = _handleInput; + + terminal.onResize = (w, h, pw, ph) async { + // Validate all dimensions before using them + if (w > 0 && h > 0 && pw > 0 && ph > 0) { + debugPrint( + '[TerminalModel] Terminal resized to ${w}x$h (pixel: ${pw}x$ph)'); + if (_terminalOpened) { + // Notify remote terminal of resize + try { + await bind.sessionResizeTerminal( + sessionId: parent.sessionId, + terminalId: terminalId, + rows: h, + cols: w, + ); + } catch (e) { + debugPrint('[TerminalModel] Error resizing terminal: $e'); + } + } + } else { + debugPrint( + '[TerminalModel] Invalid terminal dimensions: ${w}x$h (pixel: ${pw}x$ph)'); + } + }; + } + + void onReady() { + parent.dialogManager.dismissAll(); + + // Fire and forget - don't block onReady + openTerminal().catchError((e) { + debugPrint('[TerminalModel] Error opening terminal: $e'); + }); + } + + Future openTerminal() async { + if (_terminalOpened) return; + // Request the remote side to open a terminal with default shell + // The remote side will decide which shell to use based on its OS + + // Get terminal dimensions, ensuring they are valid + int rows = 24; + int cols = 80; + + if (terminal.viewHeight > 0) { + rows = terminal.viewHeight; + } + if (terminal.viewWidth > 0) { + cols = terminal.viewWidth; + } + + debugPrint( + '[TerminalModel] Opening terminal $terminalId, sessionId: ${parent.sessionId}, size: ${cols}x$rows'); + try { + await bind + .sessionOpenTerminal( + sessionId: parent.sessionId, + terminalId: terminalId, + rows: rows, + cols: cols, + ) + .timeout( + const Duration(seconds: 5), + onTimeout: () { + throw TimeoutException( + 'sessionOpenTerminal timed out after 5 seconds'); + }, + ); + debugPrint('[TerminalModel] sessionOpenTerminal called successfully'); + } catch (e) { + debugPrint('[TerminalModel] Error calling sessionOpenTerminal: $e'); + // Optionally show error to user + if (e is TimeoutException) { + terminal.write('Failed to open terminal: Connection timeout\r\n'); + } + } + } + + Future closeTerminal() async { + if (_terminalOpened) { + try { + await bind + .sessionCloseTerminal( + sessionId: parent.sessionId, + terminalId: terminalId, + ) + .timeout( + const Duration(seconds: 3), + onTimeout: () { + throw TimeoutException( + 'sessionCloseTerminal timed out after 3 seconds'); + }, + ); + debugPrint('[TerminalModel] sessionCloseTerminal called successfully'); + } catch (e) { + debugPrint('[TerminalModel] Error calling sessionCloseTerminal: $e'); + // Continue with cleanup even if close fails + } + _terminalOpened = false; + notifyListeners(); + } + } + + static int getTerminalIdFromEvt(Map evt) { + if (evt.containsKey('terminal_id')) { + final v = evt['terminal_id']; + if (v is int) { + // Desktop and mobile send terminal_id as an int + return v; + } else if (v is String) { + // Web sends terminal_id as a string + final parsed = int.tryParse(v); + if (parsed != null) { + return parsed; + } else { + debugPrint( + '[TerminalModel] Failed to parse terminal_id as integer: $v. Expected a numeric string.'); + return 0; + } + } else { + // Unexpected type, log and handle gracefully + debugPrint( + '[TerminalModel] Unexpected terminal_id type: ${v.runtimeType}, value: $v. Expected int or String.'); + return 0; + } + } else { + debugPrint('[TerminalModel] Event does not contain terminal_id'); + return 0; + } + } + + static bool getSuccessFromEvt(Map evt) { + if (evt.containsKey('success')) { + final v = evt['success']; + if (v is bool) { + // Desktop and mobile + return v; + } else if (v is String) { + // Web + return v.toLowerCase() == 'true'; + } else { + // Unexpected type, log and handle gracefully + debugPrint( + '[TerminalModel] Unexpected success type: ${v.runtimeType}, value: $v. Expected bool or String.'); + return false; + } + } else { + debugPrint('[TerminalModel] Event does not contain success'); + return false; + } + } + + void handleTerminalResponse(Map evt) { + final String? type = evt['type']; + final int evtTerminalId = getTerminalIdFromEvt(evt); + + // Only handle events for this terminal + if (evtTerminalId != terminalId) { + debugPrint( + '[TerminalModel] Ignoring event for terminal $evtTerminalId (not mine)'); + return; + } + + switch (type) { + case 'opened': + _handleTerminalOpened(evt); + break; + case 'data': + _handleTerminalData(evt); + break; + case 'closed': + _handleTerminalClosed(evt); + break; + case 'error': + _handleTerminalError(evt); + break; + } + } + + void _handleTerminalOpened(Map evt) { + final bool success = getSuccessFromEvt(evt); + final String message = evt['message'] ?? ''; + final String? serviceId = evt['service_id']; + + debugPrint( + '[TerminalModel] Terminal opened response: success=$success, message=$message, service_id=$serviceId'); + + if (success) { + _terminalOpened = true; + + // Service ID is now saved on the Rust side in handle_terminal_response + + // Process any buffered input + _processBufferedInputAsync().then((_) { + notifyListeners(); + }).catchError((e) { + debugPrint('[TerminalModel] Error processing buffered input: $e'); + notifyListeners(); + }); + + final persistentSessions = + evt['persistent_sessions'] as List? ?? []; + if (kWindowId != null && persistentSessions.isNotEmpty) { + DesktopMultiWindow.invokeMethod( + kWindowId!, + kWindowEventRestoreTerminalSessions, + jsonEncode({ + 'persistent_sessions': persistentSessions, + })); + } + } else { + terminal.write('Failed to open terminal: $message\r\n'); + } + } + + Future _processBufferedInputAsync() async { + final buffer = List.from(_inputBuffer); + _inputBuffer.clear(); + + for (final data in buffer) { + try { + await bind.sessionSendTerminalInput( + sessionId: parent.sessionId, + terminalId: terminalId, + data: data, + ); + } catch (e) { + debugPrint('[TerminalModel] Error sending buffered input: $e'); + } + } + } + + void _handleTerminalData(Map evt) { + final data = evt['data']; + + if (data != null) { + try { + String text = ''; + if (data is String) { + // Try to decode as base64 first + try { + final bytes = base64Decode(data); + text = utf8.decode(bytes); + } catch (e) { + // If base64 decode fails, treat as plain text + text = data; + } + } else if (data is List) { + // Handle if data comes as byte array + text = utf8.decode(List.from(data)); + } else { + debugPrint('[TerminalModel] Unknown data type: ${data.runtimeType}'); + return; + } + + terminal.write(text); + } catch (e) { + debugPrint('[TerminalModel] Failed to process terminal data: $e'); + } + } + } + + void _handleTerminalClosed(Map evt) { + final int exitCode = evt['exit_code'] ?? 0; + terminal.write('\r\nTerminal closed with exit code: $exitCode\r\n'); + _terminalOpened = false; + notifyListeners(); + } + + void _handleTerminalError(Map evt) { + final String message = evt['message'] ?? 'Unknown error'; + terminal.write('\r\nTerminal error: $message\r\n'); + } + + @override + void dispose() { + if (_disposed) return; + _disposed = true; + // Terminal cleanup is handled server-side when service closes + super.dispose(); + } +} diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart index 9d9c762d998..99a53806291 100644 --- a/flutter/lib/models/user_model.dart +++ b/flutter/lib/models/user_model.dart @@ -116,6 +116,10 @@ class UserModel { userName.value = user.name; isAdmin.value = user.isAdmin; bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user)); + if (isWeb) { + // ugly here, tmp solution + bind.mainSetLocalOption(key: 'verifier', value: user.verifier ?? ''); + } } // update ab and group status @@ -184,7 +188,9 @@ class UserModel { rethrow; } - if (loginResponse.user != null) { + final isLogInDone = loginResponse.type == HttpType.kAuthResTypeToken && + loginResponse.access_token != null; + if (isLogInDone && loginResponse.user != null) { _parseAndUpdateUser(loginResponse.user!); } diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index a4312d959c7..5241c3974ff 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -8,10 +8,12 @@ import 'dart:html'; import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter_hbb/common/widgets/login.dart'; import 'package:flutter_hbb/models/state_model.dart'; import 'package:flutter_hbb/web/bridge.dart'; import 'package:flutter_hbb/common.dart'; +import 'package:uuid/uuid.dart'; final List> mouseListeners = []; final List> keyListeners = []; @@ -49,14 +51,15 @@ class PlatformFFI { } bool registerEventHandler( - String eventName, String handlerName, HandleEvent handler) { + String eventName, String handlerName, HandleEvent handler, + {bool replace = false}) { debugPrint('registerEventHandler $eventName $handlerName'); var handlers = _eventHandlers[eventName]; if (handlers == null) { _eventHandlers[eventName] = {handlerName: handler}; return true; } else { - if (handlers.containsKey(handlerName)) { + if (!replace && handlers.containsKey(handlerName)) { return false; } else { handlers[handlerName] = handler; @@ -112,6 +115,17 @@ class PlatformFFI { context["onInitFinished"] = () { completer.complete(); }; + context['dialog'] = (type, title, text) { + final uuid = Uuid(); + msgBox(SessionID(uuid.v4()), type, title, text, '', gFFI.dialogManager); + }; + context['loginDialog'] = () { + loginDialog(); + }; + context['closeConnection'] = () { + gFFI.dialogManager.dismissAll(); + closeConnection(); + }; context.callMethod('init'); version = getByName('version'); window.onContextMenu.listen((event) { diff --git a/flutter/lib/plugin/utils/dialogs.dart b/flutter/lib/plugin/utils/dialogs.dart index f30248f7a53..6fdb86ab41c 100644 --- a/flutter/lib/plugin/utils/dialogs.dart +++ b/flutter/lib/plugin/utils/dialogs.dart @@ -2,16 +2,18 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/models/platform_model.dart'; void showPeerSelectionDialog( {bool singleSelection = false, - required Function(List) onPeersCallback}) { - final peers = bind.mainLoadRecentPeersSync(); + required Function(List) onPeersCallback}) async { + // load recent peers, we can directly use the peers in `gFFI.recentPeersModel`. + // The plugin is not used for now, so just left it empty here. + final peers = ''; if (peers.isEmpty) { - debugPrint("load recent peers sync failed."); + // debugPrint("load recent peers failed."); return; } + Map map = jsonDecode(peers); List peersList = map['peers'] ?? []; final selected = List.empty(growable: true); diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart index 70001ffdff4..3bbb292f449 100644 --- a/flutter/lib/utils/multi_window_manager.dart +++ b/flutter/lib/utils/multi_window_manager.dart @@ -11,7 +11,15 @@ import 'package:flutter_hbb/models/input_model.dart'; /// must keep the order // ignore: constant_identifier_names -enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown } +enum WindowType { + Main, + RemoteDesktop, + FileTransfer, + ViewCamera, + PortForward, + Terminal, + Unknown +} extension Index on int { WindowType get windowType { @@ -23,7 +31,11 @@ extension Index on int { case 2: return WindowType.FileTransfer; case 3: + return WindowType.ViewCamera; + case 4: return WindowType.PortForward; + case 5: + return WindowType.Terminal; default: return WindowType.Unknown; } @@ -50,31 +62,47 @@ class RustDeskMultiWindowManager { final List _windowActiveCallbacks = List.empty(growable: true); final List _remoteDesktopWindows = List.empty(growable: true); final List _fileTransferWindows = List.empty(growable: true); + final List _viewCameraWindows = List.empty(growable: true); final List _portForwardWindows = List.empty(growable: true); + final List _terminalWindows = List.empty(growable: true); - moveTabToNewWindow(int windowId, String peerId, String sessionId) async { + moveTabToNewWindow(int windowId, String peerId, String sessionId, + WindowType windowType) async { var params = { - 'type': WindowType.RemoteDesktop.index, + 'type': windowType.index, 'id': peerId, 'tab_window_id': windowId, 'session_id': sessionId, }; - await _newSession( - false, - WindowType.RemoteDesktop, - kWindowEventNewRemoteDesktop, - peerId, - _remoteDesktopWindows, - jsonEncode(params), - ); + if (windowType == WindowType.RemoteDesktop) { + await _newSession( + false, + WindowType.RemoteDesktop, + kWindowEventNewRemoteDesktop, + peerId, + _remoteDesktopWindows, + jsonEncode(params), + ); + } else if (windowType == WindowType.ViewCamera) { + await _newSession( + false, + WindowType.ViewCamera, + kWindowEventNewViewCamera, + peerId, + _viewCameraWindows, + jsonEncode(params), + ); + } } // This function must be called in the main window thread. // Because the _remoteDesktopWindows is managed in that thread. openMonitorSession(int windowId, String peerId, int display, int displayCount, - Rect? screenRect) async { - if (_remoteDesktopWindows.length > 1) { - for (final windowId in _remoteDesktopWindows) { + Rect? screenRect, int windowType) async { + final isCamera = windowType == WindowType.ViewCamera.index; + final windowIDs = isCamera ? _viewCameraWindows : _remoteDesktopWindows; + if (windowIDs.length > 1) { + for (final windowId in windowIDs) { if (await DesktopMultiWindow.invokeMethod( windowId, kWindowEventActiveDisplaySession, @@ -91,7 +119,7 @@ class RustDeskMultiWindowManager { ? List.generate(displayCount, (index) => index) : [display]; var params = { - 'type': WindowType.RemoteDesktop.index, + 'type': windowType, 'id': peerId, 'tab_window_id': windowId, 'display': display, @@ -107,10 +135,10 @@ class RustDeskMultiWindowManager { } await _newSession( false, - WindowType.RemoteDesktop, - kWindowEventNewRemoteDesktop, + windowType.windowType, + isCamera ? kWindowEventNewViewCamera : kWindowEventNewRemoteDesktop, peerId, - _remoteDesktopWindows, + windowIDs, jsonEncode(params), screenRect: screenRect, ); @@ -277,6 +305,27 @@ class RustDeskMultiWindowManager { ); } + Future newViewCamera( + String remoteId, { + String? password, + bool? isSharedPassword, + String? switchUuid, + bool? forceRelay, + String? connToken, + }) async { + return await newSession( + WindowType.ViewCamera, + kWindowEventNewViewCamera, + remoteId, + _viewCameraWindows, + password: password, + forceRelay: forceRelay, + switchUuid: switchUuid, + isSharedPassword: isSharedPassword, + connToken: connToken, + ); + } + Future newPortForward( String remoteId, bool isRDP, { @@ -298,6 +347,42 @@ class RustDeskMultiWindowManager { ); } + Future newTerminal( + String remoteId, { + String? password, + bool? isSharedPassword, + bool? forceRelay, + String? connToken, + }) async { + // Iterate through terminal windows in reverse order to prioritize + // the most recently added or used windows, as they are more likely + // to have an active session. + for (final windowId in _terminalWindows.reversed) { + if (await DesktopMultiWindow.invokeMethod( + windowId, kWindowEventActiveSession, remoteId)) { + return MultiWindowCallResult(windowId, null); + } + } + + // Terminal windows should always create new windows, not reuse + // This avoids the MissingPluginException when trying to invoke + // new_terminal on an inactive window + var params = { + "type": WindowType.Terminal.index, + "id": remoteId, + "password": password, + "forceRelay": forceRelay, + "isSharedPassword": isSharedPassword, + "connToken": connToken, + }; + final msg = jsonEncode(params); + + // Always create a new window for terminal + final windowId = await newSessionWindow( + WindowType.Terminal, remoteId, msg, _terminalWindows, false); + return MultiWindowCallResult(windowId, null); + } + Future call( WindowType type, String methodName, dynamic args) async { final wnds = _findWindowsByType(type); @@ -324,8 +409,12 @@ class RustDeskMultiWindowManager { return _remoteDesktopWindows; case WindowType.FileTransfer: return _fileTransferWindows; + case WindowType.ViewCamera: + return _viewCameraWindows; case WindowType.PortForward: return _portForwardWindows; + case WindowType.Terminal: + return _terminalWindows; case WindowType.Unknown: break; } @@ -342,9 +431,14 @@ class RustDeskMultiWindowManager { case WindowType.FileTransfer: _fileTransferWindows.clear(); break; + case WindowType.ViewCamera: + _viewCameraWindows.clear(); + break; case WindowType.PortForward: _portForwardWindows.clear(); break; + case WindowType.Terminal: + _terminalWindows.clear(); case WindowType.Unknown: break; } @@ -376,9 +470,13 @@ class RustDeskMultiWindowManager { if (windows.isEmpty) { return; } - for (final wId in windows) { - debugPrint("closing multi window, type: ${type.toString()} id: $wId"); - await saveWindowPosition(type, windowId: wId); + for (int i = 0; i < windows.length; i++) { + final wId = windows[i]; + final shouldSavePos = type != WindowType.Terminal || i == windows.length - 1; + if (shouldSavePos) { + debugPrint("closing multi window, type: ${type.toString()} id: $wId"); + await saveWindowPosition(type, windowId: wId); + } try { await WindowController.fromWindowId(wId).setPreventClose(false); await WindowController.fromWindowId(wId).close(); diff --git a/flutter/lib/web/bridge.dart b/flutter/lib/web/bridge.dart index dba7fc0941c..388fba5da22 100644 --- a/flutter/lib/web/bridge.dart +++ b/flutter/lib/web/bridge.dart @@ -60,7 +60,8 @@ class RustdeskImpl { throw UnimplementedError("hostStopSystemKeyPropagate"); } - int peerGetDefaultSessionsCount({required String id, dynamic hint}) { + int peerGetSessionsCount( + {required String id, required int connType, dynamic hint}) { return 0; } @@ -68,6 +69,7 @@ class RustdeskImpl { {required String id, required UuidValue sessionId, required Int32List displays, + required bool isViewCamera, dynamic hint}) { return ''; } @@ -76,8 +78,10 @@ class RustdeskImpl { {required UuidValue sessionId, required String id, required bool isFileTransfer, + required bool isViewCamera, required bool isPortForward, required bool isRdp, + required bool isTerminal, required String switchUuid, required bool forceRelay, required String password, @@ -90,7 +94,9 @@ class RustdeskImpl { 'id': id, 'password': password, 'is_shared_password': isSharedPassword, - 'isFileTransfer': isFileTransfer + 'isFileTransfer': isFileTransfer, + 'isViewCamera': isViewCamera, + 'isTerminal': isTerminal }) ]); } @@ -263,6 +269,16 @@ class RustdeskImpl { ])); } + Future sessionGetTrackpadSpeed( + {required UuidValue sessionId, dynamic hint}) { + throw UnimplementedError("sessionGetTrackpadSpeed"); + } + + Future sessionSetTrackpadSpeed( + {required UuidValue sessionId, required int value, dynamic hint}) { + throw UnimplementedError("sessionSetTrackpadSpeed"); + } + Future sessionGetScrollStyle( {required UuidValue sessionId, dynamic hint}) { return Future(() => @@ -892,8 +908,18 @@ class RustdeskImpl { return js.context.callMethod('getByName', ['option:local', key]); } + // Do not return the real environment variables. + // Use the global variable as the environment variable in web. String mainGetEnv({required String key, dynamic hint}) { - throw UnimplementedError("mainGetEnv"); + return js.context.callMethod('getByName', ['envvar', key]); + } + + // Use the global variable as the environment variable in web. + void mainSetEnv({required String key, String? value, dynamic hint}) { + js.context.callMethod('setByName', [ + 'envvar', + jsonEncode({'name': key, 'value': value}) + ]); } Future mainSetLocalOption( @@ -1516,15 +1542,20 @@ class RustdeskImpl { Future mainAccountAuth( {required String op, required bool rememberMe, dynamic hint}) { - throw UnimplementedError("mainAccountAuth"); + return Future(() => js.context.callMethod('setByName', [ + 'account_auth', + jsonEncode({'op': op, 'remember': rememberMe}) + ])); } Future mainAccountAuthCancel({dynamic hint}) { - throw UnimplementedError("mainAccountAuthCancel"); + return Future( + () => js.context.callMethod('setByName', ['account_auth_cancel'])); } Future mainAccountAuthResult({dynamic hint}) { - throw UnimplementedError("mainAccountAuthResult"); + return Future( + () => js.context.callMethod('getByName', ['account_auth_result'])); } Future mainOnMainWindowClose({dynamic hint}) { @@ -1848,5 +1879,105 @@ class RustdeskImpl { throw UnimplementedError("sessionGetConnToken"); } + String mainGetPrinterNames({dynamic hint}) { + return ''; + } + + Future sessionPrinterResponse( + {required UuidValue sessionId, + required int id, + required String path, + required String printerName, + dynamic hint}) { + throw UnimplementedError("sessionPrinterResponse"); + } + + Future mainGetCommon({required String key, dynamic hint}) { + throw UnimplementedError("mainGetCommon"); + } + + String mainGetCommonSync({required String key, dynamic hint}) { + throw UnimplementedError("mainGetCommonSync"); + } + + Future mainSetCommon( + {required String key, required String value, dynamic hint}) { + throw UnimplementedError("mainSetCommon"); + } + + Future sessionHandleScreenshot( + {required UuidValue sessionId, required String action, dynamic hint}) { + throw UnimplementedError("sessionHandleScreenshot"); + } + + String? sessionGetCommonSync( + {required UuidValue sessionId, + required String key, + required String param, + dynamic hint}) { + throw UnimplementedError("sessionGetCommonSync"); + } + + Future sessionTakeScreenshot( + {required UuidValue sessionId, required int display, dynamic hint}) { + throw UnimplementedError("sessionTakeScreenshot"); + } + + Future sessionOpenTerminal( + {required UuidValue sessionId, + required int terminalId, + required int rows, + required int cols, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'open_terminal', + jsonEncode({ + 'terminal_id': terminalId, + 'rows': rows, + 'cols': cols, + }) + ])); + } + + Future sessionSendTerminalInput( + {required UuidValue sessionId, + required int terminalId, + required String data, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'send_terminal_input', + jsonEncode({ + 'terminal_id': terminalId, + 'data': data, + }) + ])); + } + + Future sessionResizeTerminal( + {required UuidValue sessionId, + required int terminalId, + required int rows, + required int cols, + dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'resize_terminal', + jsonEncode({ + 'terminal_id': terminalId, + 'rows': rows, + 'cols': cols, + }) + ])); + } + + Future sessionCloseTerminal( + {required UuidValue sessionId, required int terminalId, dynamic hint}) { + return Future(() => js.context.callMethod('setByName', [ + 'close_terminal', + jsonEncode({ + 'terminal_id': terminalId, + }) + ])); + } + void dispose() {} } diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock index a29674fece3..0083448428e 100644 --- a/flutter/macos/Podfile.lock +++ b/flutter/macos/Podfile.lock @@ -10,6 +10,11 @@ PODS: - flutter_custom_cursor (0.0.1): - FlutterMacOS - FlutterMacOS (1.0.0) + - FMDB (2.7.12): + - FMDB/standard (= 2.7.12) + - FMDB/Core (2.7.12) + - FMDB/standard (2.7.12): + - FMDB/Core - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -17,9 +22,9 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS - - sqflite (0.0.3): - - Flutter + - sqflite (0.0.2): - FlutterMacOS + - FMDB (>= 2.7.5) - texture_rgba_renderer (0.0.1): - FlutterMacOS - uni_links_desktop (0.0.1): @@ -46,7 +51,7 @@ DEPENDENCIES: - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - texture_rgba_renderer (from `Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos`) - uni_links_desktop (from `Flutter/ephemeral/.symlinks/plugins/uni_links_desktop/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -55,6 +60,10 @@ DEPENDENCIES: - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) - window_size (from `Flutter/ephemeral/.symlinks/plugins/window_size/macos`) +SPEC REPOS: + trunk: + - FMDB + EXTERNAL SOURCES: desktop_drop: :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos @@ -75,7 +84,7 @@ EXTERNAL SOURCES: screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos texture_rgba_renderer: :path: Flutter/ephemeral/.symlinks/plugins/texture_rgba_renderer/macos uni_links_desktop: @@ -92,24 +101,25 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: - desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 - desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f - file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 - flutter_custom_cursor: 629957115075c672287bd0fa979d863ccf6024f7 + desktop_drop: e0b672a7d84c0a6cbc378595e82cdb15f2970a43 + desktop_multi_window: 93667594ccc4b88d91a97972fd3b1b89667fa80a + device_info_plus: b0fafc687fb901e2af612763340f1b0d4352f8e5 + file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31 + flutter_custom_cursor: 37e588711a2746f5cf48adb58b582cacff11c0c6 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec - texture_rgba_renderer: cbed959a3c127122194a364e14b8577bd62dc8f2 - uni_links_desktop: 45900fb319df48fcdea2df0756e9c2626696b026 - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 - video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 - wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 - window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 - window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 + FMDB: 728731dd336af3936ce00f91d9d8495f5718a0e6 + package_info_plus: 122abb51244f66eead59ce7c9c200d6b53111779 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + screen_retriever: 4f97c103641aab8ce183fa5af3b87029df167936 + sqflite: c73556b2499b92f0b6e6946abe4a4084510cdf90 + texture_rgba_renderer: 6661f577ea5d4990e964c7e3840e544ac798e6da + uni_links_desktop: 34322c2646e4c9abc69b62e1865f9782d2850ba2 + url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 + video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + wakelock_plus: 21ddc249ac4b8d018838dbdabd65c5976c308497 + window_manager: 1d01fa7ac65a6e6f83b965471b1a7fdd3f06166c + window_size: 4bd15034e6e3d0720fd77928a7c42e5492cfece9 PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj index f38badcbb36..c41bfa11744 100644 --- a/flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -433,7 +433,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = HZF9JMC8YN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -579,7 +579,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = HZF9JMC8YN; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -609,7 +609,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = HZF9JMC8YN; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift index 3498decd37a..46372a5822f 100644 --- a/flutter/macos/Runner/AppDelegate.swift +++ b/flutter/macos/Runner/AppDelegate.swift @@ -1,7 +1,7 @@ import Cocoa import FlutterMacOS -@NSApplicationMain +@main class AppDelegate: FlutterAppDelegate { var launched = false; override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { diff --git a/flutter/macos/Runner/Configs/AppInfo.xcconfig b/flutter/macos/Runner/Configs/AppInfo.xcconfig index f095b674e4a..eabc428e5ec 100644 --- a/flutter/macos/Runner/Configs/AppInfo.xcconfig +++ b/flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -11,4 +11,4 @@ PRODUCT_NAME = RustDesk PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2024 Purslane Ltd. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2025 Purslane Ltd. All rights reserved. diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 8888f9e5734..c6f8aa1c20a 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -5,10 +5,15 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051 + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "64.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" after_layout: dependency: transitive description: @@ -21,10 +26,10 @@ packages: dependency: transitive description: name: analyzer - sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.7.0" animations: dependency: transitive description: @@ -45,18 +50,18 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" auto_size_text: dependency: "direct main" description: @@ -69,10 +74,10 @@ packages: dependency: "direct main" description: name: auto_size_text_field - sha256: d47c81ffa9b61d219f6c50492dc03ea28fa9346561b2ec33b46ccdc000ddb0aa + sha256: "41c90b2270e38edc6ce5c02e5a17737a863e65e246bdfc94565a38f3ec399144" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.4" back_button_interceptor: dependency: "direct main" description: @@ -85,10 +90,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" bot_toast: dependency: "direct main" description: @@ -125,10 +130,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -141,18 +146,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -165,10 +170,10 @@ packages: dependency: transitive description: name: built_value - sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6 + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.10.1" cached_network_image: dependency: transitive description: @@ -189,10 +194,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" characters: dependency: transitive description: @@ -205,10 +210,10 @@ packages: dependency: transitive description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -221,10 +226,10 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -237,10 +242,10 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: transitive description: @@ -261,42 +266,42 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cross_file: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" dart_style: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.7" dash_chat_2: dependency: "direct main" description: @@ -310,10 +315,10 @@ packages: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" debounce_throttle: dependency: "direct main" description: @@ -335,7 +340,7 @@ packages: description: path: "." ref: HEAD - resolved-ref: "4f562ab49d289cfa36bfda7cff12746ec0200033" + resolved-ref: b47e8385e5a75d38319ad706a64b0ead3108b093 url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window" source: git version: "0.1.0" @@ -351,10 +356,10 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.2" draggable_float_widget: dependency: "direct main" description: @@ -380,6 +385,14 @@ packages: url: "https://github.com/rustdesk-org/dynamic_layouts.git" source: git version: "0.0.1+1" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" extended_text: dependency: "direct main" description: @@ -392,10 +405,10 @@ packages: dependency: transitive description: name: extended_text_library - sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829" + sha256: "13d99f8a10ead472d5e2cf4770d3d047203fe5054b152e9eb5dc692a71befbba" url: "https://pub.dev" source: hosted - version: "12.0.0" + version: "12.0.1" external_path: dependency: "direct main" description: @@ -440,18 +453,18 @@ packages: dependency: transitive description: name: file_selector_linux - sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" url: "https://pub.dev" source: hosted - version: "0.9.2+1" + version: "0.9.3+2" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6 + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.4+2" file_selector_platform_interface: dependency: transitive description: @@ -464,34 +477,34 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+4" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flex_color_picker: dependency: "direct main" description: name: flex_color_picker - sha256: "0871edc170153cfc3de316d30625f40a85daecfa76ce541641f3cc0ec7757cbf" + sha256: "12dc855ae8ef5491f529b1fc52c655f06dcdf4114f1f7fdecafa41eec2ec8d79" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.6.0" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f" + sha256: "7639d2c86268eff84a909026eb169f008064af0fb3696a651b24b0fa24a40334" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "3.4.1" flutter: dependency: "direct main" description: flutter @@ -525,8 +538,8 @@ packages: dependency: "direct main" description: path: "." - ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" - resolved-ref: "2ded7f146437a761ffe6981e2f742038f85ca68d" + ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87" + resolved-ref: "08a471bb8ceccdd50483c81cdfa8b81b07b14b87" url: "https://github.com/rustdesk-org/flutter_gpu_texture_renderer" source: git version: "0.0.1" @@ -608,7 +621,7 @@ packages: source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: - dependency: transitive + dependency: "direct overridden" description: name: flutter_plugin_android_lifecycle sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da @@ -627,10 +640,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -640,74 +653,82 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.5.7" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" get: dependency: "direct main" description: name: get - sha256: e4e7335ede17452b391ed3b2ede016545706c01a02292a6c97619705e7d2a85e + sha256: c79eeb4339f1f3deffd9ec912f8a923834bec55f7b49c9e882b8fef2c139d425 url: "https://pub.dev" source: hosted - version: "4.6.6" + version: "4.7.2" glob: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 + url: "https://pub.dev" + source: hosted + version: "6.2.1" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" html: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.6" http: dependency: "direct main" description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: @@ -728,10 +749,10 @@ packages: dependency: "direct main" description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.3.0" image_picker: dependency: "direct main" description: @@ -744,50 +765,50 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1" + sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a" url: "https://pub.dev" source: hosted - version: "0.8.9+3" + version: "0.8.12+21" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.6" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3 + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" url: "https://pub.dev" source: hosted - version: "0.8.9+1" + version: "0.8.12+2" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + sha256: "34a65f6740df08bbbeb0a1abd8e6d32107941fd4868f67a507b25601651022c9" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.10.1" image_picker_windows: dependency: transitive description: @@ -808,10 +829,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -824,10 +845,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" lints: dependency: transitive description: @@ -840,18 +861,26 @@ packages: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -872,10 +901,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.0" nested: dependency: transitive description: @@ -888,18 +917,18 @@ packages: dependency: transitive description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" package_config: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" package_info_plus: dependency: "direct main" description: @@ -936,34 +965,34 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -984,10 +1013,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" pedantic: dependency: transitive description: @@ -1000,10 +1029,10 @@ packages: dependency: "direct main" description: name: percent_indicator - sha256: c37099ad833a883c9d71782321cb65c3a848c21b6939b6185f0ff6640d05814c + sha256: "157d29133bbc6ecb11f923d36e7960a96a3f28837549a20b65e5135729f0f9fd" url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.2.5" petitparser: dependency: transitive description: @@ -1016,10 +1045,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1040,50 +1069,50 @@ packages: dependency: "direct main" description: name: provider - sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096" + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.5" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.4.0" pull_down_button: dependency: "direct main" description: name: pull_down_button - sha256: "235b302701ce029fd9e9470975069376a6700935bb47a5f1b3ec8a5efba07e6f" + sha256: "48b928203afdeafa4a8be5dc96980523bc8a2ddbd04569f766071a722be22379" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.4" puppeteer: dependency: transitive description: name: puppeteer - sha256: eedeaae6ec5d2e54f9ae22ab4d6b3dda2e8791c356cc783046d06c287ffe11d8 + sha256: "7a990c68d33882b642214c351f66492d9a738afa4226a098ab70642357337fa2" url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.16.0" qr: dependency: transitive description: name: qr - sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" qr_code_scanner: dependency: "direct main" description: @@ -1104,10 +1133,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" rxdart: dependency: transitive description: @@ -1152,10 +1181,10 @@ packages: dependency: transitive description: name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" shelf_web_socket: dependency: transitive description: @@ -1189,82 +1218,82 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sqflite: - dependency: transitive + dependency: "direct main" description: name: sqflite - sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6 + sha256: a9a8c6dfdf315f87f2a23a7bad2b60c8d5af0f88a5fde92cf9205202770c2753 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.2.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4+6" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.6" texture_rgba_renderer: dependency: "direct main" description: @@ -1278,18 +1307,18 @@ packages: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" toggle_switch: dependency: "direct main" description: name: toggle_switch - sha256: "9e6af1f0c5a97d9de41109dc7b9e1b3bbe73417f89b10e0e44dc834fb493d4cb" + sha256: dca04512d7c23ed320d6c5ede1211a404f177d54d353bf785b07d15546a86ce5 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.0" tuple: dependency: "direct main" description: @@ -1302,10 +1331,10 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" uni_links: dependency: "direct main" description: @@ -1351,66 +1380,66 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.14" url_launcher_ios: dependency: "direct main" description: name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.2" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -1423,26 +1452,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.18" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.10+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1455,42 +1484,42 @@ packages: dependency: transitive description: name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 + sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14" url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.9.5" video_player_android: dependency: transitive description: name: video_player_android - sha256: "7f8f25d7ad56819a82b2948357f3c3af071f6a678db33833b26ec36bbc221316" + sha256: "391e092ba4abe2f93b3e625bd6b6a6ec7d7414279462c1c0ee42b5ab8d0a0898" url: "https://pub.dev" source: hosted - version: "2.4.11" + version: "2.7.16" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f" url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.7.1" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" + sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" video_player_web: dependency: transitive description: name: video_player_web - sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb" + sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.5" visibility_detector: dependency: "direct main" description: @@ -1503,50 +1532,50 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: "104d94837bb28c735894dcd592877e990149c380e6358b00c04398ca1426eed4" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.1" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207 url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.3" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.2" web: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" win32: dependency: "direct main" description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.10.1" win32_registry: dependency: transitive description: @@ -1577,10 +1606,10 @@ packages: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1589,30 +1618,46 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + xterm: + dependency: "direct main" + description: + name: xterm + sha256: "168dfedca77cba33fdb6f52e2cd001e9fde216e398e89335c19b524bb22da3a2" + url: "https://pub.dev" + source: hosted + version: "4.0.0" yaml: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" yaml_edit: dependency: transitive description: name: yaml_edit - sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" + sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.2" + zmodem: + dependency: transitive + description: + name: zmodem + sha256: "3b7e5b29f3a7d8aee472029b05165a68438eff2f3f7766edf13daba1e297adbf" + url: "https://pub.dev" + source: hosted + version: "0.0.6" zxing2: dependency: "direct main" description: name: zxing2 - sha256: a042961441bd400f59595f9125ef5fca4c888daf0ea59c17f41e0e151f8a12b5 + sha256: "2677c49a3b9ca9457cb1d294fd4bd5041cac6aab8cdb07b216ba4e98945c684f" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.4" sdks: dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 1776db7a5e7..d8e1aff2cad 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers -version: 1.3.5+54 +version: 1.4.1+59 environment: sdk: '^3.1.0' @@ -35,7 +35,7 @@ dependencies: wakelock_plus: ^1.1.3 #firebase_analytics: ^9.1.5 package_info_plus: ^4.2.0 - url_launcher: ^6.2.1 + url_launcher: ^6.3.1 url_launcher_ios: ^6.3.2 toggle_switch: ^2.1.0 dash_chat_2: @@ -94,7 +94,7 @@ dependencies: flutter_gpu_texture_renderer: git: url: https://github.com/rustdesk-org/flutter_gpu_texture_renderer - ref: 2ded7f146437a761ffe6981e2f742038f85ca68d + ref: 08a471bb8ceccdd50483c81cdfa8b81b07b14b87 uuid: ^3.0.7 auto_size_text_field: ^2.2.1 flex_color_picker: ^3.3.0 @@ -106,6 +106,9 @@ dependencies: device_info_plus: ^9.1.0 qr_flutter: ^4.1.0 extended_text: 14.0.0 + xterm: 4.0.0 + sqflite: 2.2.0 + google_fonts: ^6.2.1 dev_dependencies: icons_launcher: ^2.0.4 @@ -118,7 +121,8 @@ dev_dependencies: dependency_overrides: intl: ^0.19.0 - + flutter_plugin_android_lifecycle: 2.0.17 + # rerun: flutter pub run flutter_launcher_icons flutter_icons: image_path: "../res/icon.png" @@ -161,6 +165,12 @@ flutter: - family: AddressBook fonts: - asset: assets/address_book.ttf + - family: DeviceGroup + fonts: + - asset: assets/device_group.ttf + - family: More + fonts: + - asset: assets/more.ttf # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. @@ -187,4 +197,3 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages - diff --git a/flutter/test/cm_test.dart b/flutter/test/cm_test.dart index 21b87b84895..342764b4a63 100644 --- a/flutter/test/cm_test.dart +++ b/flutter/test/cm_test.dart @@ -10,10 +10,10 @@ import 'package:get/get.dart'; import 'package:window_manager/window_manager.dart'; final testClients = [ - Client(0, false, false, "UserAAAAAA", "123123123", true, false, false), - Client(1, false, false, "UserBBBBB", "221123123", true, false, false), - Client(2, false, false, "UserC", "331123123", true, false, false), - Client(3, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false) + Client(0, false, false, false, "UserAAAAAA", "123123123", true, false, false, false), + Client(1, false, false, false, "UserBBBBB", "221123123", true, false, false, false), + Client(2, false, false, false, "UserC", "331123123", true, false, false, false), + Client(3, false, false, false, "UserDDDDDDDDDDDd", "441123123", true, false, false, false) ]; /// flutter run -d {platform} -t test/cm_test.dart to test cm diff --git a/flutter/web/v1/.gitignore b/flutter/web/v1/.gitignore deleted file mode 100644 index 6290a3f6319..00000000000 --- a/flutter/web/v1/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -assets -js/src/gen_js_from_hbb.ts -js/src/message.ts -js/src/rendezvous.ts -ogvjs* -libopus.js -libopus.wasm -yuv-canvas* -node_modules diff --git a/flutter/web/v1/README.md b/flutter/web/v1/README.md deleted file mode 100644 index b9e2fc5c096..00000000000 --- a/flutter/web/v1/README.md +++ /dev/null @@ -1 +0,0 @@ -v1 is not compatible with current Flutter source code. \ No newline at end of file diff --git a/flutter/web/v1/index.html b/flutter/web/v1/index.html deleted file mode 100644 index b466df66225..00000000000 --- a/flutter/web/v1/index.html +++ /dev/null @@ -1,183 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - RustDesk - - - - - - - - - - -
-
-
- - - - - - - - - diff --git a/flutter/web/v1/js/.gitattributes b/flutter/web/v1/js/.gitattributes deleted file mode 100644 index 176a458f94e..00000000000 --- a/flutter/web/v1/js/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text=auto diff --git a/flutter/web/v1/js/.gitignore b/flutter/web/v1/js/.gitignore deleted file mode 100644 index 8737dbba591..00000000000 --- a/flutter/web/v1/js/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -node_modules -.DS_Store -dist -dist-ssr -*.local -*log -ogvjs -.vscode -.yarn diff --git a/flutter/web/v1/js/.yarnrc.yml b/flutter/web/v1/js/.yarnrc.yml deleted file mode 100644 index 3186f3f0795..00000000000 --- a/flutter/web/v1/js/.yarnrc.yml +++ /dev/null @@ -1 +0,0 @@ -nodeLinker: node-modules diff --git a/flutter/web/v1/js/gen_js_from_hbb.py b/flutter/web/v1/js/gen_js_from_hbb.py deleted file mode 100755 index cfa95ffe0a1..00000000000 --- a/flutter/web/v1/js/gen_js_from_hbb.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 - -import re -import os -import glob -from tabnanny import check - -def pad_start(s, n, c = ' '): - if len(s) >= n: - return s - return c * (n - len(s)) + s - -def safe_unicode(s): - res = "" - for c in s: - res += r"\u{}".format(pad_start(hex(ord(c))[2:], 4, '0')) - return res - -def main(): - print('export const LANGS = {') - for fn in glob.glob('../../../src/lang/*'): - lang = os.path.basename(fn)[:-3] - if lang == 'template': continue - print(' %s: {'%lang) - for ln in open(fn, encoding='utf-8'): - ln = ln.strip() - if ln.startswith('("'): - toks = ln.split('", "') - assert(len(toks) == 2) - a = toks[0][2:] - b = toks[1][:-3] - print(' "%s": "%s",'%(safe_unicode(a), safe_unicode(b))) - print(' },') - print('}') - check_if_retry = ['', False] - KEY_MAP = ['', False] - for ln in open('../../../src/client.rs', encoding='utf-8'): - ln = ln.strip() - if 'check_if_retry' in ln: - check_if_retry[1] = True - continue - if ln.startswith('}') and check_if_retry[1]: - check_if_retry[1] = False - continue - if check_if_retry[1]: - ln = removeComment(ln) - check_if_retry[0] += ln + '\n' - if 'KEY_MAP' in ln: - KEY_MAP[1] = True - continue - if '.collect' in ln and KEY_MAP[1]: - KEY_MAP[1] = False - continue - if KEY_MAP[1] and ln.startswith('('): - ln = removeComment(ln) - toks = ln.split('", Key::') - assert(len(toks) == 2) - a = toks[0][2:] - b = toks[1].replace('ControlKey(ControlKey::', '').replace("Chr('", '').replace("' as _)),", '').replace(')),', '') - KEY_MAP[0] += ' "%s": "%s",\n'%(a, b) - print() - print('export function checkIfRetry(msgtype: string, title: string, text: string, retry_for_relay: boolean) {') - print(' return %s'%check_if_retry[0].replace('to_lowercase', 'toLowerCase').replace('contains', 'indexOf').replace('!', '').replace('")', '") < 0')) - print(';}') - print() - print('export const KEY_MAP: any = {') - print(KEY_MAP[0]) - print('}') - 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) - -main() diff --git a/flutter/web/v1/js/index.html b/flutter/web/v1/js/index.html deleted file mode 100644 index 0ae0a24103f..00000000000 --- a/flutter/web/v1/js/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - Vite App - - -
- - - diff --git a/flutter/web/v1/js/package.json b/flutter/web/v1/js/package.json deleted file mode 100644 index 15e0e75b89d..00000000000 --- a/flutter/web/v1/js/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "web_hbb", - "version": "1.0.0", - "scripts": { - "dev": "vite", - "build": "./gen_js_from_hbb.py > src/gen_js_from_hbb.ts && ./ts_proto.py && tsc && vite build", - "preview": "vite preview" - }, - "devDependencies": { - "typescript": "^4.4.4", - "vite": "^2.7.2" - }, - "dependencies": { - "fast-sha256": "^1.3.0", - "libsodium": "^0.7.9", - "libsodium-wrappers": "^0.7.9", - "pcm-player": "^0.0.11", - "ts-proto": "^1.101.0", - "wasm-feature-detect": "^1.2.11", - "zstddec": "^0.0.2" - } -} diff --git a/flutter/web/v1/js/src/codec.js b/flutter/web/v1/js/src/codec.js deleted file mode 100644 index 27c9565ec70..00000000000 --- a/flutter/web/v1/js/src/codec.js +++ /dev/null @@ -1,43 +0,0 @@ -// example: https://github.com/rgov/js-theora-decoder/blob/main/index.html -// https://github.com/brion/ogv.js/releases, yarn add has no simd -// dev: copy decoder files from node/ogv/dist/* to project dir -// dist: .... to dist -/* - OGVDemuxerOggW: 'ogv-demuxer-ogg-wasm.js', - OGVDemuxerWebMW: 'ogv-demuxer-webm-wasm.js', - OGVDecoderAudioOpusW: 'ogv-decoder-audio-opus-wasm.js', - OGVDecoderAudioVorbisW: 'ogv-decoder-audio-vorbis-wasm.js', - OGVDecoderVideoTheoraW: 'ogv-decoder-video-theora-wasm.js', - OGVDecoderVideoVP8W: 'ogv-decoder-video-vp8-wasm.js', - OGVDecoderVideoVP8MTW: 'ogv-decoder-video-vp8-mt-wasm.js', - OGVDecoderVideoVP9W: 'ogv-decoder-video-vp9-wasm.js', - OGVDecoderVideoVP9SIMDW: 'ogv-decoder-video-vp9-simd-wasm.js', - OGVDecoderVideoVP9MTW: 'ogv-decoder-video-vp9-mt-wasm.js', - OGVDecoderVideoVP9SIMDMTW: 'ogv-decoder-video-vp9-simd-mt-wasm.js', - OGVDecoderVideoAV1W: 'ogv-decoder-video-av1-wasm.js', - OGVDecoderVideoAV1SIMDW: 'ogv-decoder-video-av1-simd-wasm.js', - OGVDecoderVideoAV1MTW: 'ogv-decoder-video-av1-mt-wasm.js', - OGVDecoderVideoAV1SIMDMTW: 'ogv-decoder-video-av1-simd-mt-wasm.js', -*/ -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, - // currently available in Firefox and Chrome with experimental flags enabled. - // æ‰€æœ‰ä¸»æµæµè§ˆå™¨å‡é»˜è®¤äºŽ2018å¹´1月5æ—¥ç¦ç”¨SharedArrayBuffer - const isSIMD = await simd(); - console.log('isSIMD: ' + isSIMD); - window.OGVLoader.loadClass( - isSIMD ? "OGVDecoderVideoVP9SIMDW" : "OGVDecoderVideoVP9W", - (videoCodecClass) => { - window.videoCodecClass = videoCodecClass; - videoCodecClass({ videoFormat: {} }).then((decoder) => { - decoder.init(() => { - callback(decoder); - }) - }) - }, - { worker: true, threading: true } - ); -} \ No newline at end of file diff --git a/flutter/web/v1/js/src/common.ts b/flutter/web/v1/js/src/common.ts deleted file mode 100644 index 8da049a4db5..00000000000 --- a/flutter/web/v1/js/src/common.ts +++ /dev/null @@ -1,77 +0,0 @@ -import * as zstd from "zstddec"; -import { KeyEvent, controlKeyFromJSON, ControlKey } from "./message"; -import { KEY_MAP, LANGS } from "./gen_js_from_hbb"; - -let decompressor: zstd.ZSTDDecoder; - -export async function initZstd() { - const tmp = new zstd.ZSTDDecoder(); - await tmp.init(); - console.log("zstd ready"); - decompressor = tmp; -} - -export async function decompress(compressedArray: Uint8Array) { - const MAX = 1024 * 1024 * 64; - const MIN = 1024 * 1024; - let n = 30 * compressedArray.length; - if (n > MAX) { - n = MAX; - } - if (n < MIN) { - n = MIN; - } - try { - if (!decompressor) { - await initZstd(); - } - return decompressor.decode(compressedArray, n); - } catch (e) { - console.error("decompress failed: " + e); - return undefined; - } -} - -const LANG = getLang(); - -export function translate(locale: string, text: string): string { - const lang = LANG || locale.substring(locale.length - 2).toLowerCase(); - let en = LANGS.en as any; - let dict = (LANGS as any)[lang]; - if (!dict) dict = en; - let res = dict[text]; - if (!res && lang != "en") res = en[text]; - return res || text; -} - -const zCode = "z".charCodeAt(0); -const aCode = "a".charCodeAt(0); - -export function mapKey(name: string, isDesktop: Boolean) { - const tmp = KEY_MAP[name] || name; - if (tmp.length == 1) { - const chr = tmp.charCodeAt(0); - if (!isDesktop && (chr > zCode || chr < aCode)) - return KeyEvent.fromPartial({ unicode: chr }); - else return KeyEvent.fromPartial({ chr }); - } - const control_key = controlKeyFromJSON(tmp); - if (control_key == ControlKey.UNRECOGNIZED) { - console.error("Unknown control key " + tmp); - } - return KeyEvent.fromPartial({ control_key }); -} - -export async function sleep(ms: number) { - await new Promise((r) => setTimeout(r, ms)); -} - -function getLang(): string { - try { - const queryString = window.location.search; - const urlParams = new URLSearchParams(queryString); - return urlParams.get("lang") || ""; - } catch (e) { - return ""; - } -} diff --git a/flutter/web/v1/js/src/connection.ts b/flutter/web/v1/js/src/connection.ts deleted file mode 100644 index b0c479c90cc..00000000000 --- a/flutter/web/v1/js/src/connection.ts +++ /dev/null @@ -1,773 +0,0 @@ -import Websock from "./websock"; -import * as message from "./message.js"; -import * as rendezvous from "./rendezvous.js"; -import { loadVp9 } from "./codec"; -import * as sha256 from "fast-sha256"; -import * as globals from "./globals"; -import { decompress, mapKey, sleep } from "./common"; - -const PORT = 21116; -const HOSTS = [ - "rs-sg.rustdesk.com", - "rs-cn.rustdesk.com", - "rs-us.rustdesk.com", -]; -let HOST = localStorage.getItem("rendezvous-server") || HOSTS[0]; -const SCHEMA = "ws://"; - -type MsgboxCallback = (type: string, title: string, text: string) => void; -type DrawCallback = (data: Uint8Array) => void; -//const cursorCanvas = document.createElement("canvas"); - -export default class Connection { - _msgs: any[]; - _ws: Websock | undefined; - _interval: any; - _id: string; - _hash: message.Hash | undefined; - _msgbox: MsgboxCallback; - _draw: DrawCallback; - _peerInfo: message.PeerInfo | undefined; - _firstFrame: Boolean | undefined; - _videoDecoder: any; - _password: Uint8Array | undefined; - _options: any; - _videoTestSpeed: number[]; - //_cursors: { [name: number]: any }; - - constructor() { - this._msgbox = globals.msgbox; - this._draw = globals.draw; - this._msgs = []; - this._id = ""; - this._videoTestSpeed = [0, 0]; - //this._cursors = {}; - } - - async start(id: string) { - try { - await this._start(id); - } catch (e: any) { - this.msgbox( - "error", - "Connection Error", - e.type == "close" ? "Reset by the peer" : String(e) - ); - } - } - - async _start(id: string) { - if (!this._options) { - this._options = globals.getPeers()[id] || {}; - } - if (!this._password) { - const p = this.getOption("password"); - if (p) { - try { - this._password = Uint8Array.from(JSON.parse("[" + p + "]")); - } catch (e) { - console.error(e); - } - } - } - this._interval = setInterval(() => { - while (this._msgs.length) { - this._ws?.sendMessage(this._msgs[0]); - this._msgs.splice(0, 1); - } - }, 1); - this.loadVideoDecoder(); - const uri = getDefaultUri(); - const ws = new Websock(uri, true); - this._ws = ws; - this._id = id; - console.log( - new Date() + ": Connecting to rendezvous server: " + uri + ", for " + id - ); - await ws.open(); - 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({ - id, - licence_key: localStorage.getItem("key") || undefined, - conn_type, - nat_type, - token: localStorage.getItem("access_token") || undefined, - }); - ws.sendRendezvous({ punch_hole_request }); - const msg = (await ws.next()) as rendezvous.RendezvousMessage; - ws.close(); - console.log(new Date() + ": Got relay response"); - const phr = msg.punch_hole_response; - const rr = msg.relay_response; - if (phr) { - if (phr?.other_failure) { - this.msgbox("error", "Error", phr?.other_failure); - return; - } - if (phr.failure != rendezvous.PunchHoleResponse_Failure.UNRECOGNIZED) { - switch (phr?.failure) { - case rendezvous.PunchHoleResponse_Failure.ID_NOT_EXIST: - this.msgbox("error", "Error", "ID does not exist"); - break; - case rendezvous.PunchHoleResponse_Failure.OFFLINE: - this.msgbox("error", "Error", "Remote desktop is offline"); - break; - case rendezvous.PunchHoleResponse_Failure.LICENSE_MISMATCH: - this.msgbox("error", "Error", "Key mismatch"); - break; - case rendezvous.PunchHoleResponse_Failure.LICENSE_OVERUSE: - this.msgbox("error", "Error", "Key overuse"); - break; - } - } - } else if (rr) { - if (!rr.version) { - this.msgbox("error", "Error", "Remote version is low, not support web"); - return; - } - await this.connectRelay(rr); - } - } - - async connectRelay(rr: rendezvous.RelayResponse) { - const pk = rr.pk; - let uri = rr.relay_server; - if (uri) { - uri = getrUriFromRs(uri, true, 2); - } else { - uri = getDefaultUri(true); - } - const uuid = rr.uuid; - console.log(new Date() + ": Connecting to relay server: " + uri); - const ws = new Websock(uri, false); - await ws.open(); - console.log(new Date() + ": Connected to relay server"); - this._ws = ws; - const request_relay = rendezvous.RequestRelay.fromPartial({ - licence_key: localStorage.getItem("key") || undefined, - uuid, - }); - ws.sendRendezvous({ request_relay }); - const secure = (await this.secure(pk)) || false; - globals.pushEvent("connection_ready", { secure, direct: false }); - await this.msgLoop(); - } - - async secure(pk: Uint8Array | undefined) { - if (pk) { - const RS_PK = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; - try { - pk = await globals.verify(pk, localStorage.getItem("key") || RS_PK); - if (pk) { - const idpk = message.IdPk.decode(pk); - if (idpk.id == this._id) { - pk = idpk.pk; - } - } - if (pk?.length != 32) { - pk = undefined; - } - } catch (e) { - console.error(e); - pk = undefined; - } - if (!pk) - console.error( - "Handshake failed: invalid public key from rendezvous server" - ); - } - if (!pk) { - // send an empty message out in case server is setting up secure and waiting for first message - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - const msg = (await this._ws?.next()) as message.Message; - let signedId: any = msg?.signed_id; - if (!signedId) { - console.error("Handshake failed: invalid message type"); - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - try { - signedId = await globals.verify(signedId.id, Uint8Array.from(pk!)); - } catch (e) { - console.error(e); - // fall back to non-secure connection in case pk mismatch - console.error("pk mismatch, fall back to non-secure"); - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - const idpk = message.IdPk.decode(signedId); - const id = idpk.id; - const theirPk = idpk.pk; - if (id != this._id!) { - console.error("Handshake failed: sign failure"); - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - if (theirPk.length != 32) { - console.error( - "Handshake failed: invalid public box key length from peer" - ); - const public_key = message.PublicKey.fromPartial({}); - this._ws?.sendMessage({ public_key }); - return; - } - const [mySk, asymmetric_value] = globals.genBoxKeyPair(); - const secret_key = globals.genSecretKey(); - const symmetric_value = globals.seal(secret_key, theirPk, mySk); - const public_key = message.PublicKey.fromPartial({ - asymmetric_value, - symmetric_value, - }); - this._ws?.sendMessage({ public_key }); - this._ws?.setSecretKey(secret_key); - console.log("secured"); - return true; - } - - async msgLoop() { - while (true) { - const msg = (await this._ws?.next()) as message.Message; - if (msg?.hash) { - this._hash = msg?.hash; - if (!this._password) - this.msgbox("input-password", "Password Required", ""); - this.login(); - } else if (msg?.test_delay) { - const test_delay = msg?.test_delay; - console.log(test_delay); - if (!test_delay.from_client) { - this._ws?.sendMessage({ test_delay }); - } - } else if (msg?.login_response) { - const r = msg?.login_response; - if (r.error) { - if (r.error == "Wrong Password") { - this._password = undefined; - this.msgbox( - "re-input-password", - r.error, - "Do you want to enter again?" - ); - } else { - this.msgbox("error", "Login Error", r.error); - } - } else if (r.peer_info) { - this.handlePeerInfo(r.peer_info); - } - } else if (msg?.video_frame) { - this.handleVideoFrame(msg?.video_frame!); - } else if (msg?.clipboard) { - const cb = msg?.clipboard; - if (cb.compress) { - const c = await decompress(cb.content); - if (!c) continue; - cb.content = c; - } - try { - globals.copyToClipboard(new TextDecoder().decode(cb.content)); - } catch (e) { - console.error(e); - } - // globals.pushEvent("clipboard", cb); - } else if (msg?.cursor_data) { - const cd = msg?.cursor_data; - const c = await decompress(cd.colors); - if (!c) continue; - cd.colors = c; - globals.pushEvent("cursor_data", cd); - /* - let ctx = cursorCanvas.getContext("2d"); - cursorCanvas.width = cd.width; - cursorCanvas.height = cd.height; - let imgData = new ImageData( - new Uint8ClampedArray(c), - cd.width, - cd.height - ); - ctx?.clearRect(0, 0, cd.width, cd.height); - ctx?.putImageData(imgData, 0, 0); - let url = cursorCanvas.toDataURL(); - const img = document.createElement("img"); - img.src = url; - this._cursors[cd.id] = img; - //cursorCanvas.width /= 2.; - //cursorCanvas.height /= 2.; - //ctx?.drawImage(img, cursorCanvas.width, cursorCanvas.height); - url = cursorCanvas.toDataURL(); - document.body.style.cursor = - "url(" + url + ")" + cd.hotx + " " + cd.hoty + ", default"; - console.log(document.body.style.cursor); - */ - } else if (msg?.cursor_id) { - globals.pushEvent("cursor_id", { id: msg?.cursor_id }); - } else if (msg?.cursor_position) { - globals.pushEvent("cursor_position", msg?.cursor_position); - } else if (msg?.misc) { - if (!this.handleMisc(msg?.misc)) break; - } else if (msg?.audio_frame) { - globals.playAudio(msg?.audio_frame.data); - } - } - } - - msgbox(type_: string, title: string, text: string) { - this._msgbox?.(type_, title, text); - } - - draw(frame: any) { - this._draw?.(frame); - globals.draw(frame); - } - - close() { - this._msgs = []; - clearInterval(this._interval); - this._ws?.close(); - this._videoDecoder?.close(); - } - - refresh() { - const misc = message.Misc.fromPartial({ refresh_video: true }); - this._ws?.sendMessage({ misc }); - } - - setMsgbox(callback: MsgboxCallback) { - this._msgbox = callback; - } - - setDraw(callback: DrawCallback) { - this._draw = callback; - } - - login(password: string | undefined = undefined) { - if (password) { - const salt = this._hash?.salt; - let p = hash([password, salt!]); - this._password = p; - const challenge = this._hash?.challenge; - p = hash([p, challenge!]); - this.msgbox("connecting", "Connecting...", "Logging in..."); - this._sendLoginMessage(p); - } else { - let p = this._password; - if (p) { - const challenge = this._hash?.challenge; - p = hash([p, challenge!]); - } - this._sendLoginMessage(p); - } - } - - async reconnect() { - this.close(); - await this.start(this._id); - } - - _sendLoginMessage(password: Uint8Array | undefined = undefined) { - const login_request = message.LoginRequest.fromPartial({ - username: this._id!, - my_id: "web", // to-do - my_name: "web", // to-do - password, - option: this.getOptionMessage(), - video_ack_required: true, - }); - this._ws?.sendMessage({ login_request }); - } - - getOptionMessage(): message.OptionMessage | undefined { - let n = 0; - const msg = message.OptionMessage.fromPartial({}); - const q = this.getImageQualityEnum(this.getImageQuality(), true); - const yes = message.OptionMessage_BoolOption.Yes; - if (q != undefined) { - msg.image_quality = q; - n += 1; - } - if (this._options["show-remote-cursor"]) { - msg.show_remote_cursor = yes; - n += 1; - } - if (this._options["lock-after-session-end"]) { - msg.lock_after_session_end = yes; - n += 1; - } - if (this._options["privacy-mode"]) { - msg.privacy_mode = yes; - n += 1; - } - if (this._options["disable-audio"]) { - msg.disable_audio = yes; - n += 1; - } - if (this._options["disable-clipboard"]) { - msg.disable_clipboard = yes; - n += 1; - } - return n > 0 ? msg : undefined; - } - - sendVideoReceived() { - const misc = message.Misc.fromPartial({ video_received: true }); - this._ws?.sendMessage({ misc }); - } - - handleVideoFrame(vf: message.VideoFrame) { - if (!this._firstFrame) { - this.msgbox("", "", ""); - this._firstFrame = true; - } - if (vf.vp9s) { - const dec = this._videoDecoder; - var tm = new Date().getTime(); - var i = 0; - const n = vf.vp9s?.frames.length; - vf.vp9s.frames.forEach((f) => { - dec.processFrame(f.data.slice(0).buffer, (ok: any) => { - i++; - if (i == n) this.sendVideoReceived(); - if (ok && dec.frameBuffer && n == i) { - this.draw(dec.frameBuffer); - const now = new Date().getTime(); - var elapsed = now - tm; - this._videoTestSpeed[1] += elapsed; - this._videoTestSpeed[0] += 1; - if (this._videoTestSpeed[0] >= 30) { - console.log( - "video decoder: " + - parseInt( - "" + this._videoTestSpeed[1] / this._videoTestSpeed[0] - ) - ); - this._videoTestSpeed = [0, 0]; - } - } - }); - }); - } - } - - handlePeerInfo(pi: message.PeerInfo) { - this._peerInfo = pi; - if (pi.displays.length == 0) { - this.msgbox("error", "Remote Error", "No Display"); - return; - } - this.msgbox("success", "Successful", "Connected, waiting for image..."); - globals.pushEvent("peer_info", pi); - const p = this.shouldAutoLogin(); - if (p) this.inputOsPassword(p); - const username = this.getOption("info")?.username; - if (username && !pi.username) pi.username = username; - this.setOption("info", pi); - if (this.getRemember()) { - if (this._password?.length) { - const p = this._password.toString(); - if (p != this.getOption("password")) { - this.setOption("password", p); - console.log("remember password of " + this._id); - } - } - } else { - this.setOption("password", undefined); - } - } - - shouldAutoLogin(): string { - const l = this.getOption("lock-after-session-end"); - const a = !!this.getOption("auto-login"); - const p = this.getOption("os-password"); - if (p && l && a) { - return p; - } - return ""; - } - - handleMisc(misc: message.Misc) { - if (misc.audio_format) { - globals.initAudio( - misc.audio_format.channels, - misc.audio_format.sample_rate - ); - } else if (misc.chat_message) { - globals.pushEvent("chat", { text: misc.chat_message.text }); - } else if (misc.permission_info) { - const p = misc.permission_info; - console.info("Change permission " + p.permission + " -> " + p.enabled); - let name; - switch (p.permission) { - case message.PermissionInfo_Permission.Keyboard: - name = "keyboard"; - break; - case message.PermissionInfo_Permission.Clipboard: - name = "clipboard"; - break; - case message.PermissionInfo_Permission.Audio: - name = "audio"; - break; - default: - return; - } - globals.pushEvent("permission", { [name]: p.enabled }); - } else if (misc.switch_display) { - this.loadVideoDecoder(); - globals.pushEvent("switch_display", misc.switch_display); - } else if (misc.close_reason) { - this.msgbox("error", "Connection Error", misc.close_reason); - this.close(); - return false; - } - return true; - } - - getRemember(): Boolean { - return this._options["remember"] || false; - } - - setRemember(v: Boolean) { - this.setOption("remember", v); - } - - getOption(name: string): any { - return this._options[name]; - } - - setOption(name: string, value: any) { - if (value == undefined) { - delete this._options[name]; - } else { - this._options[name] = value; - } - this._options["tm"] = new Date().getTime(); - const peers = globals.getPeers(); - peers[this._id] = this._options; - localStorage.setItem("peers", JSON.stringify(peers)); - } - - inputKey( - name: string, - down: boolean, - press: boolean, - alt: Boolean, - ctrl: Boolean, - shift: Boolean, - command: Boolean - ) { - const key_event = mapKey(name, globals.isDesktop()); - if (!key_event) return; - if (alt && (name == "VK_MENU" || name == "RAlt")) { - alt = false; - } - if (ctrl && (name == "VK_CONTROL" || name == "RControl")) { - ctrl = false; - } - if (shift && (name == "VK_SHIFT" || name == "RShift")) { - shift = false; - } - if (command && (name == "Meta" || name == "RWin")) { - command = false; - } - key_event.down = down; - key_event.press = press; - key_event.modifiers = this.getMod(alt, ctrl, shift, command); - this._ws?.sendMessage({ key_event }); - } - - ctrlAltDel() { - const key_event = message.KeyEvent.fromPartial({ down: true }); - if (this._peerInfo?.platform == "Windows") { - key_event.control_key = message.ControlKey.CtrlAltDel; - } else { - key_event.control_key = message.ControlKey.Delete; - key_event.modifiers = this.getMod(true, true, false, false); - } - this._ws?.sendMessage({ key_event }); - } - - inputString(seq: string) { - const key_event = message.KeyEvent.fromPartial({ seq }); - this._ws?.sendMessage({ key_event }); - } - - switchDisplay(display: number) { - const switch_display = message.SwitchDisplay.fromPartial({ display }); - const misc = message.Misc.fromPartial({ switch_display }); - this._ws?.sendMessage({ misc }); - } - - async inputOsPassword(seq: string) { - this.inputMouse(); - await sleep(50); - this.inputMouse(0, 3, 3); - await sleep(50); - this.inputMouse(1 | (1 << 3)); - this.inputMouse(2 | (1 << 3)); - await sleep(1200); - const key_event = message.KeyEvent.fromPartial({ press: true, seq }); - this._ws?.sendMessage({ key_event }); - } - - lockScreen() { - const key_event = message.KeyEvent.fromPartial({ - down: true, - control_key: message.ControlKey.LockScreen, - }); - this._ws?.sendMessage({ key_event }); - } - - getMod(alt: Boolean, ctrl: Boolean, shift: Boolean, command: Boolean) { - const mod: message.ControlKey[] = []; - if (alt) mod.push(message.ControlKey.Alt); - if (ctrl) mod.push(message.ControlKey.Control); - if (shift) mod.push(message.ControlKey.Shift); - if (command) mod.push(message.ControlKey.Meta); - return mod; - } - - inputMouse( - mask: number = 0, - x: number = 0, - y: number = 0, - alt: Boolean = false, - ctrl: Boolean = false, - shift: Boolean = false, - command: Boolean = false - ) { - const mouse_event = message.MouseEvent.fromPartial({ - mask, - x, - y, - modifiers: this.getMod(alt, ctrl, shift, command), - }); - this._ws?.sendMessage({ mouse_event }); - } - - toggleOption(name: string) { - const v = !this._options[name]; - const option = message.OptionMessage.fromPartial({}); - const v2 = v - ? message.OptionMessage_BoolOption.Yes - : message.OptionMessage_BoolOption.No; - switch (name) { - case "show-remote-cursor": - option.show_remote_cursor = v2; - break; - case "disable-audio": - option.disable_audio = v2; - break; - case "disable-clipboard": - option.disable_clipboard = v2; - break; - case "lock-after-session-end": - option.lock_after_session_end = v2; - break; - case "privacy-mode": - option.privacy_mode = v2; - break; - case "block-input": - option.block_input = message.OptionMessage_BoolOption.Yes; - break; - case "unblock-input": - option.block_input = message.OptionMessage_BoolOption.No; - break; - default: - return; - } - if (name.indexOf("block-input") < 0) this.setOption(name, v); - const misc = message.Misc.fromPartial({ option }); - this._ws?.sendMessage({ misc }); - } - - getImageQuality() { - return this.getOption("image-quality"); - } - - getImageQualityEnum( - value: string, - ignoreDefault: Boolean - ): message.ImageQuality | undefined { - switch (value) { - case "low": - return message.ImageQuality.Low; - case "best": - return message.ImageQuality.Best; - case "balanced": - return ignoreDefault ? undefined : message.ImageQuality.Balanced; - default: - return undefined; - } - } - - setImageQuality(value: string) { - this.setOption("image-quality", value); - const image_quality = this.getImageQualityEnum(value, false); - if (image_quality == undefined) return; - const option = message.OptionMessage.fromPartial({ image_quality }); - const misc = message.Misc.fromPartial({ option }); - this._ws?.sendMessage({ misc }); - } - - loadVideoDecoder() { - this._videoDecoder?.close(); - loadVp9((decoder: any) => { - this._videoDecoder = decoder; - console.log("vp9 loaded"); - console.log(decoder); - }); - } -} - -function testDelay() { - var nearest = ""; - HOSTS.forEach((host) => { - const now = new Date().getTime(); - new Websock(getrUriFromRs(host), true).open().then(() => { - console.log("latency of " + host + ": " + (new Date().getTime() - now)); - if (!nearest) { - HOST = host; - localStorage.setItem("rendezvous-server", host); - } - }); - }); -} - -testDelay(); - -function getDefaultUri(isRelay: Boolean = false): string { - const host = localStorage.getItem("custom-rendezvous-server"); - return getrUriFromRs(host || HOST, isRelay); -} - -function getrUriFromRs( - uri: string, - isRelay: Boolean = false, - roffset: number = 0 -): string { - if (uri.indexOf(":") > 0) { - const tmp = uri.split(":"); - const port = parseInt(tmp[1]); - uri = tmp[0] + ":" + (port + (isRelay ? roffset || 3 : 2)); - } else { - uri += ":" + (PORT + (isRelay ? 3 : 2)); - } - return SCHEMA + uri; -} - -function hash(datas: (string | Uint8Array)[]): Uint8Array { - const hasher = new sha256.Hash(); - datas.forEach((data) => { - if (typeof data == "string") { - data = new TextEncoder().encode(data); - } - return hasher.update(data); - }); - return hasher.digest(); -} diff --git a/flutter/web/v1/js/src/globals.js b/flutter/web/v1/js/src/globals.js deleted file mode 100644 index 953add18d01..00000000000 --- a/flutter/web/v1/js/src/globals.js +++ /dev/null @@ -1,383 +0,0 @@ -import Connection from "./connection"; -import _sodium from "libsodium-wrappers"; -import { CursorData } from "./message"; -import { loadVp9 } from "./codec"; -import { checkIfRetry, version } from "./gen_js_from_hbb"; -import { initZstd, translate } from "./common"; -import PCMPlayer from "pcm-player"; - -window.curConn = undefined; -window.isMobile = () => { - return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) - || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0, 4)); -} - -export function isDesktop() { - return !isMobile(); -} - -export function msgbox(type, title, text) { - if (!type || (type == 'error' && !text)) return; - const text2 = text.toLowerCase(); - var hasRetry = checkIfRetry(type, title, text) ? 'true' : ''; - onGlobalEvent(JSON.stringify({ name: 'msgbox', type, title, text, hasRetry })); -} - -function jsonfyForDart(payload) { - var tmp = {}; - for (const [key, value] of Object.entries(payload)) { - if (!key) continue; - tmp[key] = value instanceof Uint8Array ? '[' + value.toString() + ']' : JSON.stringify(value); - } - return tmp; -} - -export function pushEvent(name, payload) { - payload = jsonfyForDart(payload); - payload.name = name; - onGlobalEvent(JSON.stringify(payload)); -} - -let yuvWorker; -let yuvCanvas; -let gl; -let pixels; -let flipPixels; -let oldSize; -if (YUVCanvas.WebGLFrameSink.isAvailable()) { - var canvas = document.createElement('canvas'); - yuvCanvas = YUVCanvas.attach(canvas, { webGL: true }); - gl = canvas.getContext("webgl"); -} else { - yuvWorker = new Worker("./yuv.js"); -} -let testSpeed = [0, 0]; - -export function draw(frame) { - if (yuvWorker) { - // frame's (y/u/v).bytes already detached, can not transferrable any more. - yuvWorker.postMessage(frame); - } else { - var tm0 = new Date().getTime(); - yuvCanvas.drawFrame(frame); - var width = canvas.width; - var height = canvas.height; - var size = width * height * 4; - if (size != oldSize) { - pixels = new Uint8Array(size); - flipPixels = new Uint8Array(size); - oldSize = size; - } - gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - const row = width * 4; - const end = (height - 1) * row; - for (let i = 0; i < size; i += row) { - flipPixels.set(pixels.subarray(i, i + row), end - i); - } - onRgba(flipPixels); - testSpeed[1] += new Date().getTime() - tm0; - testSpeed[0] += 1; - if (testSpeed[0] > 30) { - console.log('gl: ' + parseInt('' + testSpeed[1] / testSpeed[0])); - testSpeed = [0, 0]; - } - } - /* - var testCanvas = document.getElementById("test-yuv-decoder-canvas"); - if (testCanvas && currentFrame) { - var ctx = testCanvas.getContext("2d"); - testCanvas.width = frame.format.displayWidth; - testCanvas.height = frame.format.displayHeight; - var img = ctx.createImageData(testCanvas.width, testCanvas.height); - img.data.set(currentFrame); - ctx.putImageData(img, 0, 0); - } - */ -} - -export function sendOffCanvas(c) { - let canvas = c.transferControlToOffscreen(); - yuvWorker.postMessage({ canvas }, [canvas]); -} - -export function setConn(conn) { - window.curConn = conn; -} - -export function getConn() { - return window.curConn; -} - -export async function startConn(id) { - setByName('remote_id', id); - await curConn.start(id); -} - -export function close() { - getConn()?.close(); - setConn(undefined); -} - -export function newConn() { - window.curConn?.close(); - const conn = new Connection(); - setConn(conn); - return conn; -} - -let sodium; -export async function verify(signed, pk) { - if (!sodium) { - await _sodium.ready; - sodium = _sodium; - } - if (typeof pk == 'string') { - pk = decodeBase64(pk); - } - return sodium.crypto_sign_open(signed, pk); -} - -export function decodeBase64(pk) { - return sodium.from_base64(pk, sodium.base64_variants.ORIGINAL); -} - -export function genBoxKeyPair() { - const pair = sodium.crypto_box_keypair(); - const sk = pair.privateKey; - const pk = pair.publicKey; - return [sk, pk]; -} - -export function genSecretKey() { - return sodium.crypto_secretbox_keygen(); -} - -export function seal(unsigned, theirPk, ourSk) { - const nonce = Uint8Array.from(Array(24).fill(0)); - return sodium.crypto_box_easy(unsigned, nonce, theirPk, ourSk); -} - -function makeOnce(value) { - var byteArray = Array(24).fill(0); - - for (var index = 0; index < byteArray.length && value > 0; index++) { - var byte = value & 0xff; - byteArray[index] = byte; - value = (value - byte) / 256; - } - - return Uint8Array.from(byteArray); -}; - -export function encrypt(unsigned, nonce, key) { - return sodium.crypto_secretbox_easy(unsigned, makeOnce(nonce), key); -} - -export function decrypt(signed, nonce, key) { - return sodium.crypto_secretbox_open_easy(signed, makeOnce(nonce), key); -} - -window.setByName = (name, value) => { - switch (name) { - case 'remote_id': - localStorage.setItem('remote-id', value); - break; - case 'connect': - newConn(); - startConn(value); - break; - case 'login': - value = JSON.parse(value); - curConn.setRemember(value.remember == 'true'); - curConn.login(value.password); - break; - case 'close': - close(); - break; - case 'refresh': - curConn.refresh(); - break; - case 'reconnect': - curConn.reconnect(); - break; - case 'toggle_option': - curConn.toggleOption(value); - break; - case 'image_quality': - curConn.setImageQuality(value); - break; - case 'lock_screen': - curConn.lockScreen(); - break; - case 'ctrl_alt_del': - curConn.ctrlAltDel(); - break; - case 'switch_display': - curConn.switchDisplay(value); - break; - case 'remove': - const peers = getPeers(); - delete peers[value]; - localStorage.setItem('peers', JSON.stringify(peers)); - break; - case 'input_key': - value = JSON.parse(value); - curConn.inputKey(value.name, value.down == 'true', value.press == 'true', value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true'); - break; - case 'input_string': - curConn.inputString(value); - break; - case 'send_mouse': - let mask = 0; - value = JSON.parse(value); - switch (value.type) { - case 'down': - mask = 1; - break; - case 'up': - mask = 2; - break; - case 'wheel': - mask = 3; - break; - } - switch (value.buttons) { - case 'left': - mask |= 1 << 3; - break; - case 'right': - mask |= 2 << 3; - break; - case 'wheel': - mask |= 4 << 3; - } - curConn.inputMouse(mask, parseInt(value.x || '0'), parseInt(value.y || '0'), value.alt == 'true', value.ctrl == 'true', value.shift == 'true', value.command == 'true'); - break; - case 'option': - value = JSON.parse(value); - localStorage.setItem(value.name, value.value); - break; - case 'peer_option': - value = JSON.parse(value); - curConn.setOption(value.name, value.value); - break; - case 'input_os_password': - curConn.inputOsPassword(value); - break; - default: - break; - } -} - -window.getByName = (name, arg) => { - let v = _getByName(name, arg); - if (typeof v == 'string' || v instanceof String) return v; - if (v == undefined || v == null) return ''; - return JSON.stringify(v); -} - -function getPeersForDart() { - const peers = []; - for (const [id, value] of Object.entries(getPeers())) { - if (!id) continue; - const tm = value['tm']; - const info = value['info']; - if (!tm || !info) continue; - peers.push([tm, id, info]); - } - return peers.sort().reverse().map(x => x.slice(1)); -} - -function _getByName(name, arg) { - switch (name) { - case 'peers': - return getPeersForDart(); - case 'remote_id': - return localStorage.getItem('remote-id'); - case 'remember': - return curConn.getRemember(); - case 'toggle_option': - return curConn.getOption(arg) || false; - case 'option': - return localStorage.getItem(arg); - case 'image_quality': - return curConn.getImageQuality(); - case 'translate': - arg = JSON.parse(arg); - return translate(arg.locale, arg.text); - case 'peer_option': - return curConn.getOption(arg); - case 'test_if_valid_server': - break; - case 'version': - return version; - } - return ''; -} - -let opusWorker = new Worker("./libopus.js"); -let pcmPlayer; - -export function initAudio(channels, sampleRate) { - pcmPlayer = newAudioPlayer(channels, sampleRate); - opusWorker.postMessage({ channels, sampleRate }); -} - -export function playAudio(packet) { - opusWorker.postMessage(packet, [packet.buffer]); -} - -window.init = async () => { - if (yuvWorker) { - yuvWorker.onmessage = (e) => { - onRgba(e.data); - } - } - opusWorker.onmessage = (e) => { - pcmPlayer.feed(e.data); - } - loadVp9(() => { }); - await initZstd(); - console.log('init done'); -} - -export function getPeers() { - try { - return JSON.parse(localStorage.getItem('peers')) || {}; - } catch (e) { - return {}; - } -} - -function newAudioPlayer(channels, sampleRate) { - return new PCMPlayer({ - channels, - sampleRate, - flushingTime: 2000 - }); -} - -export function copyToClipboard(text) { - if (window.clipboardData && window.clipboardData.setData) { - // Internet Explorer-specific code path to prevent textarea being shown while dialog is visible. - return window.clipboardData.setData("Text", text); - - } - else if (document.queryCommandSupported && document.queryCommandSupported("copy")) { - var textarea = document.createElement("textarea"); - textarea.textContent = text; - textarea.style.position = "fixed"; // Prevent scrolling to bottom of page in Microsoft Edge. - document.body.appendChild(textarea); - textarea.select(); - try { - return document.execCommand("copy"); // Security exception may be thrown by some browsers. - } - catch (ex) { - console.warn("Copy to clipboard failed.", ex); - // return prompt("Copy to clipboard: Ctrl+C, Enter", text); - } - finally { - document.body.removeChild(textarea); - } - } -} \ No newline at end of file diff --git a/flutter/web/v1/js/src/main.ts b/flutter/web/v1/js/src/main.ts deleted file mode 100644 index 2be877f580f..00000000000 --- a/flutter/web/v1/js/src/main.ts +++ /dev/null @@ -1,2 +0,0 @@ -import "./globals"; -import "./ui"; \ No newline at end of file diff --git a/flutter/web/v1/js/src/style.css b/flutter/web/v1/js/src/style.css deleted file mode 100644 index 852de7aa2ae..00000000000 --- a/flutter/web/v1/js/src/style.css +++ /dev/null @@ -1,8 +0,0 @@ -#app { - font-family: Avenir, Helvetica, Arial, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - text-align: center; - color: #2c3e50; - margin-top: 60px; -} diff --git a/flutter/web/v1/js/src/ui.js b/flutter/web/v1/js/src/ui.js deleted file mode 100644 index 4463340228e..00000000000 --- a/flutter/web/v1/js/src/ui.js +++ /dev/null @@ -1,108 +0,0 @@ -import "./style.css"; -import "./connection"; -import * as globals from "./globals"; - -const app = document.querySelector('#app'); - -if (app) { - app.innerHTML = ` -
- - - - -
Host:
Key:
Id:
- - - -`; - - let player; - window.init(); - - document.body.onload = () => { - const host = document.querySelector('#host'); - host.value = localStorage.getItem('custom-rendezvous-server'); - const id = document.querySelector('#id'); - id.value = localStorage.getItem('id'); - const key = document.querySelector('#key'); - key.value = localStorage.getItem('key'); - player = YUVCanvas.attach(document.getElementById('player')); - // globals.sendOffCanvas(document.getElementById('player')); - }; - - window.connect = () => { - const host = document.querySelector('#host'); - localStorage.setItem('custom-rendezvous-server', host.value); - const id = document.querySelector('#id'); - localStorage.setItem('id', id.value); - const key = document.querySelector('#key'); - localStorage.setItem('key', key.value); - const func = async () => { - const conn = globals.newConn(); - conn.setMsgbox(msgbox); - conn.setDraw((f) => { - /* - if (!(document.getElementById('player').width > 0)) { - document.getElementById('player').width = f.format.displayWidth; - document.getElementById('player').height = f.format.displayHeight; - } - */ - globals.draw(f); - player.drawFrame(f); - }); - document.querySelector('div#status').style.display = 'block'; - document.querySelector('div#connect').style.display = 'none'; - document.querySelector('div#text').innerHTML = 'Connecting ...'; - await conn.start(id.value); - }; - func(); - } - - function msgbox(type, title, text) { - if (!globals.getConn()) return; - if (type == 'input-password') { - document.querySelector('div#status').style.display = 'none'; - document.querySelector('div#password').style.display = 'block'; - } else if (!type) { - document.querySelector('div#canvas').style.display = 'block'; - document.querySelector('div#password').style.display = 'none'; - document.querySelector('div#status').style.display = 'none'; - } else if (type == 'error') { - document.querySelector('div#status').style.display = 'block'; - document.querySelector('div#canvas').style.display = 'none'; - document.querySelector('div#text').innerHTML = '
' + text + '
'; - } else { - document.querySelector('div#password').style.display = 'none'; - document.querySelector('div#status').style.display = 'block'; - document.querySelector('div#text').innerHTML = '
' + text + '
'; - } - } - - window.cancel = () => { - globals.close(); - document.querySelector('div#connect').style.display = 'block'; - document.querySelector('div#password').style.display = 'none'; - document.querySelector('div#status').style.display = 'none'; - document.querySelector('div#canvas').style.display = 'none'; - } - - window.confirm = () => { - const password = document.querySelector('input#password').value; - if (password) { - document.querySelector('div#password').style.display = 'none'; - globals.getConn().login(password); - } - } -} \ No newline at end of file diff --git a/flutter/web/v1/js/src/vite-env.d.ts b/flutter/web/v1/js/src/vite-env.d.ts deleted file mode 100644 index 151aa6856ff..00000000000 --- a/flutter/web/v1/js/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// \ No newline at end of file diff --git a/flutter/web/v1/js/src/websock.ts b/flutter/web/v1/js/src/websock.ts deleted file mode 100644 index 6f05e6f6bd1..00000000000 --- a/flutter/web/v1/js/src/websock.ts +++ /dev/null @@ -1,183 +0,0 @@ -import * as message from "./message.js"; -import * as rendezvous from "./rendezvous.js"; -import * as globals from "./globals"; - -type Keys = "message" | "open" | "close" | "error"; - -export default class Websock { - _websocket: WebSocket; - _eventHandlers: { [key in Keys]: Function }; - _buf: (rendezvous.RendezvousMessage | message.Message)[]; - _status: any; - _latency: number; - _secretKey: [Uint8Array, number, number] | undefined; - _uri: string; - _isRendezvous: boolean; - - constructor(uri: string, isRendezvous: boolean = true) { - this._eventHandlers = { - message: (_: any) => {}, - open: () => {}, - close: () => {}, - error: () => {}, - }; - this._uri = uri; - this._status = ""; - this._buf = []; - this._websocket = new WebSocket(uri); - this._websocket.onmessage = this._recv_message.bind(this); - this._websocket.binaryType = "arraybuffer"; - this._latency = new Date().getTime(); - this._isRendezvous = isRendezvous; - } - - latency(): number { - return this._latency; - } - - setSecretKey(key: Uint8Array) { - this._secretKey = [key, 0, 0]; - } - - sendMessage(json: message.DeepPartial) { - let data = message.Message.encode( - message.Message.fromPartial(json) - ).finish(); - let k = this._secretKey; - if (k) { - k[1] += 1; - data = globals.encrypt(data, k[1], k[0]); - } - this._websocket.send(data); - } - - sendRendezvous(data: rendezvous.DeepPartial) { - this._websocket.send( - rendezvous.RendezvousMessage.encode( - rendezvous.RendezvousMessage.fromPartial(data) - ).finish() - ); - } - - parseMessage(data: Uint8Array) { - return message.Message.decode(data); - } - - parseRendezvous(data: Uint8Array) { - return rendezvous.RendezvousMessage.decode(data); - } - - // Event Handlers - off(evt: Keys) { - this._eventHandlers[evt] = () => {}; - } - - on(evt: Keys, handler: Function) { - this._eventHandlers[evt] = handler; - } - - async open(timeout: number = 12000): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => { - if (this._status != "open") { - reject(this._status || "Timeout"); - } - }, timeout); - this._websocket.onopen = () => { - this._latency = new Date().getTime() - this._latency; - this._status = "open"; - console.debug(">> WebSock.onopen"); - if (this._websocket?.protocol) { - console.info( - "Server choose sub-protocol: " + this._websocket.protocol - ); - } - - this._eventHandlers.open(); - console.info("WebSock.onopen"); - resolve(this); - }; - this._websocket.onclose = (e) => { - if (this._status == "open") { - // e.code 1000 means that the connection was closed normally. - // - } - this._status = e; - console.error("WebSock.onclose: "); - console.error(e); - this._eventHandlers.close(e); - reject("Reset by the peer"); - }; - this._websocket.onerror = (e: any) => { - if (!this._status) { - reject("Failed to connect to " + (this._isRendezvous ? "rendezvous" : "relay") + " server"); - return; - } - this._status = e; - console.error("WebSock.onerror: ") - console.error(e); - this._eventHandlers.error(e); - }; - }); - } - - async next( - timeout = 12000 - ): Promise { - const func = ( - resolve: (value: rendezvous.RendezvousMessage | message.Message) => void, - reject: (reason: any) => void, - tm0: number - ) => { - if (this._buf.length) { - resolve(this._buf[0]); - this._buf.splice(0, 1); - } else { - if (this._status != "open") { - reject(this._status); - return; - } - if (new Date().getTime() > tm0 + timeout) { - reject("Timeout"); - } else { - setTimeout(() => func(resolve, reject, tm0), 1); - } - } - }; - return new Promise((resolve, reject) => { - func(resolve, reject, new Date().getTime()); - }); - } - - close() { - this._status = ""; - if (this._websocket) { - if ( - this._websocket.readyState === WebSocket.OPEN || - this._websocket.readyState === WebSocket.CONNECTING - ) { - console.info("Closing WebSocket connection"); - this._websocket.close(); - } - - this._websocket.onmessage = () => {}; - } - } - - _recv_message(e: any) { - if (e.data instanceof window.ArrayBuffer) { - let bytes = new Uint8Array(e.data); - const k = this._secretKey; - if (k) { - k[2] += 1; - bytes = globals.decrypt(bytes, k[2], k[0]); - } - this._buf.push( - this._isRendezvous - ? this.parseRendezvous(bytes) - : this.parseMessage(bytes) - ); - } - this._eventHandlers.message(e.data); - } -} diff --git a/flutter/web/v1/js/ts_proto.py b/flutter/web/v1/js/ts_proto.py deleted file mode 100755 index 62a73fe7c12..00000000000 --- a/flutter/web/v1/js/ts_proto.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python - -import os - -path = os.path.abspath(os.path.join(os.getcwd(), '..', '..', '..', 'libs', 'hbb_common', 'protos')) - -if os.name == 'nt': - cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path - print(cmd) - os.system(cmd) - cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=protoc-gen-ts_proto=.\node_modules\.bin\protoc-gen-ts_proto.cmd -I "%s" --ts_proto_out=./src/ message.proto'%path - print(cmd) - os.system(cmd) -else: - cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ rendezvous.proto'%path - print(cmd) - os.system(cmd) - cmd = r'protoc --ts_proto_opt=esModuleInterop=true --ts_proto_opt=snakeToCamel=false --plugin=./node_modules/.bin/protoc-gen-ts_proto -I "%s" --ts_proto_out=./src/ message.proto'%path - print(cmd) - os.system(cmd) diff --git a/flutter/web/v1/js/tsconfig.json b/flutter/web/v1/js/tsconfig.json deleted file mode 100644 index ca949de6ad2..00000000000 --- a/flutter/web/v1/js/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "allowJs": true, - "lib": [ - "ESNext", - "DOM" - ], - "moduleResolution": "Node", - "strict": true, - "sourceMap": true, - "resolveJsonModule": true, - "esModuleInterop": true, - "noEmit": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noImplicitReturns": true - }, - "include": [ - "./src" - ] -} \ No newline at end of file diff --git a/flutter/web/v1/js/vite.config.js b/flutter/web/v1/js/vite.config.js deleted file mode 100644 index 22c51fa54a0..00000000000 --- a/flutter/web/v1/js/vite.config.js +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'vite'; - -export default defineConfig({ - build: { - manifest: false, - rollupOptions: { - output: { - entryFileNames: `[name].js`, - chunkFileNames: `[name].js`, - assetFileNames: `[name].[ext]`, - } - } - }, -}) \ No newline at end of file diff --git a/flutter/web/v1/js/yarn.lock b/flutter/web/v1/js/yarn.lock deleted file mode 100644 index d5566487944..00000000000 --- a/flutter/web/v1/js/yarn.lock +++ /dev/null @@ -1,1494 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 6 - cacheKey: 8 - -"@gar/promisify@npm:^1.1.3": - version: 1.1.3 - resolution: "@gar/promisify@npm:1.1.3" - checksum: 4059f790e2d07bf3c3ff3e0fec0daa8144fe35c1f6e0111c9921bd32106adaa97a4ab096ad7dab1e28ee6a9060083c4d1a4ada42a7f5f3f7a96b8812e2b757c1 - languageName: node - linkType: hard - -"@npmcli/fs@npm:^2.1.0": - version: 2.1.0 - resolution: "@npmcli/fs@npm:2.1.0" - dependencies: - "@gar/promisify": ^1.1.3 - semver: ^7.3.5 - checksum: 6ec6d678af6da49f9dac50cd882d7f661934dd278972ffbaacde40d9eaa2871292d634000a0cca9510f6fc29855fbd4af433e1adbff90a524ec3eaf140f1219b - languageName: node - linkType: hard - -"@npmcli/move-file@npm:^2.0.0": - version: 2.0.0 - resolution: "@npmcli/move-file@npm:2.0.0" - dependencies: - mkdirp: ^1.0.4 - rimraf: ^3.0.2 - checksum: 1388777b507b0c592d53f41b9d182e1a8de7763bc625fc07999b8edbc22325f074e5b3ec90af79c89d6987fdb2325bc66d59f483258543c14a43661621f841b0 - languageName: node - linkType: hard - -"@protobufjs/aspromise@npm:^1.1.1, @protobufjs/aspromise@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/aspromise@npm:1.1.2" - checksum: 011fe7ef0826b0fd1a95935a033a3c0fd08483903e1aa8f8b4e0704e3233406abb9ee25350ec0c20bbecb2aad8da0dcea58b392bbd77d6690736f02c143865d2 - languageName: node - linkType: hard - -"@protobufjs/base64@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/base64@npm:1.1.2" - checksum: 67173ac34de1e242c55da52c2f5bdc65505d82453893f9b51dc74af9fe4c065cf4a657a4538e91b0d4a1a1e0a0642215e31894c31650ff6e3831471061e1ee9e - languageName: node - linkType: hard - -"@protobufjs/codegen@npm:^2.0.4": - version: 2.0.4 - resolution: "@protobufjs/codegen@npm:2.0.4" - checksum: 59240c850b1d3d0b56d8f8098dd04787dcaec5c5bd8de186fa548de86b86076e1c50e80144b90335e705a044edf5bc8b0998548474c2a10a98c7e004a1547e4b - languageName: node - linkType: hard - -"@protobufjs/eventemitter@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/eventemitter@npm:1.1.0" - checksum: 0369163a3d226851682f855f81413cbf166cd98f131edb94a0f67f79e75342d86e89df9d7a1df08ac28be2bc77e0a7f0200526bb6c2a407abbfee1f0262d5fd7 - languageName: node - linkType: hard - -"@protobufjs/fetch@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/fetch@npm:1.1.0" - dependencies: - "@protobufjs/aspromise": ^1.1.1 - "@protobufjs/inquire": ^1.1.0 - checksum: 3fce7e09eb3f1171dd55a192066450f65324fd5f7cc01a431df01bb00d0a895e6bfb5b0c5561ce157ee1d886349c90703d10a4e11a1a256418ff591b969b3477 - languageName: node - linkType: hard - -"@protobufjs/float@npm:^1.0.2": - version: 1.0.2 - resolution: "@protobufjs/float@npm:1.0.2" - checksum: 5781e1241270b8bd1591d324ca9e3a3128d2f768077a446187a049e36505e91bc4156ed5ac3159c3ce3d2ba3743dbc757b051b2d723eea9cd367bfd54ab29b2f - languageName: node - linkType: hard - -"@protobufjs/inquire@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/inquire@npm:1.1.0" - checksum: ca06f02eaf65ca36fb7498fc3492b7fc087bfcc85c702bac5b86fad34b692bdce4990e0ef444c1e2aea8c034227bd1f0484be02810d5d7e931c55445555646f4 - languageName: node - linkType: hard - -"@protobufjs/path@npm:^1.1.2": - version: 1.1.2 - resolution: "@protobufjs/path@npm:1.1.2" - checksum: 856eeb532b16a7aac071cacde5c5620df800db4c80cee6dbc56380524736205aae21e5ae47739114bf669ab5e8ba0e767a282ad894f3b5e124197cb9224445ee - languageName: node - linkType: hard - -"@protobufjs/pool@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/pool@npm:1.1.0" - checksum: d6a34fbbd24f729e2a10ee915b74e1d77d52214de626b921b2d77288bd8f2386808da2315080f2905761527cceffe7ec34c7647bd21a5ae41a25e8212ff79451 - languageName: node - linkType: hard - -"@protobufjs/utf8@npm:^1.1.0": - version: 1.1.0 - resolution: "@protobufjs/utf8@npm:1.1.0" - checksum: f9bf3163d13aaa3b6f5e6fbf37a116e094ea021c0e1f2a7ccd0e12a29e2ce08dafba4e8b36e13f8ed7397e1591610ce880ed1289af4d66cf4ace8a36a9557278 - languageName: node - linkType: hard - -"@tootallnate/once@npm:2": - version: 2.0.0 - resolution: "@tootallnate/once@npm:2.0.0" - checksum: ad87447820dd3f24825d2d947ebc03072b20a42bfc96cbafec16bff8bbda6c1a81fcb0be56d5b21968560c5359a0af4038a68ba150c3e1694fe4c109a063bed8 - languageName: node - linkType: hard - -"@types/long@npm:^4.0.1": - version: 4.0.1 - resolution: "@types/long@npm:4.0.1" - checksum: ff9653c33f5000d0f131fd98a950a0343e2e33107dd067a97ac4a3b9678e1a2e39ea44772ad920f54ef6e8f107f76bc92c2584ba905a0dc4253282a4101166d0 - languageName: node - linkType: hard - -"@types/node@npm:>=13.7.0": - version: 17.0.8 - resolution: "@types/node@npm:17.0.8" - checksum: f4cadeb9e602027520abc88c77142697e33cf6ac98bb02f8b595a398603cbd33df1f94d01c055c9f13cde0c8eaafc5e396ca72645458d42b4318b845bc7f1d0f - languageName: node - linkType: hard - -"@types/object-hash@npm:^1.3.0": - version: 1.3.4 - resolution: "@types/object-hash@npm:1.3.4" - checksum: fe4aa041427f3c69cbcf63434af6e788329b40d7208b30aa845cfc1aa6bf9b0c11b23fa33a567d85ba7f2574a95c3b4a227f4b9b9b55da1eaea68ab94b4058d9 - languageName: node - linkType: hard - -"abbrev@npm:1": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: a4a97ec07d7ea112c517036882b2ac22f3109b7b19077dc656316d07d308438aac28e4d9746dc4d84bf6b1e75b4a7b0a5f3cb30592419f128ca9a8cee3bcfa17 - languageName: node - linkType: hard - -"agent-base@npm:6, agent-base@npm:^6.0.2": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: 4 - checksum: f52b6872cc96fd5f622071b71ef200e01c7c4c454ee68bc9accca90c98cfb39f2810e3e9aa330435835eedc8c23f4f8a15267f67c6e245d2b33757575bdac49d - languageName: node - linkType: hard - -"agentkeepalive@npm:^4.2.1": - version: 4.2.1 - resolution: "agentkeepalive@npm:4.2.1" - dependencies: - debug: ^4.1.0 - depd: ^1.1.2 - humanize-ms: ^1.2.1 - checksum: 39cb49ed8cf217fd6da058a92828a0a84e0b74c35550f82ee0a10e1ee403c4b78ade7948be2279b188b7a7303f5d396ea2738b134731e464bf28de00a4f72a18 - languageName: node - linkType: hard - -"aggregate-error@npm:^3.0.0": - version: 3.1.0 - resolution: "aggregate-error@npm:3.1.0" - dependencies: - clean-stack: ^2.0.0 - indent-string: ^4.0.0 - checksum: 1101a33f21baa27a2fa8e04b698271e64616b886795fd43c31068c07533c7b3facfcaf4e9e0cab3624bd88f729a592f1c901a1a229c9e490eafce411a8644b79 - languageName: node - linkType: hard - -"ansi-regex@npm:^5.0.1": - version: 5.0.1 - resolution: "ansi-regex@npm:5.0.1" - checksum: 2aa4bb54caf2d622f1afdad09441695af2a83aa3fe8b8afa581d205e57ed4261c183c4d3877cee25794443fde5876417d859c108078ab788d6af7e4fe52eb66b - languageName: node - linkType: hard - -"aproba@npm:^1.0.3 || ^2.0.0": - version: 2.0.0 - resolution: "aproba@npm:2.0.0" - checksum: 5615cadcfb45289eea63f8afd064ab656006361020e1735112e346593856f87435e02d8dcc7ff0d11928bc7d425f27bc7c2a84f6c0b35ab0ff659c814c138a24 - languageName: node - linkType: hard - -"are-we-there-yet@npm:^3.0.0": - version: 3.0.0 - resolution: "are-we-there-yet@npm:3.0.0" - dependencies: - delegates: ^1.0.0 - readable-stream: ^3.6.0 - checksum: 348edfdd931b0b50868b55402c01c3f64df1d4c229ab6f063539a5025fd6c5f5bb8a0cab409bbed8d75d34762d22aa91b7c20b4204eb8177063158d9ba792981 - languageName: node - linkType: hard - -"balanced-match@npm:^1.0.0": - version: 1.0.2 - resolution: "balanced-match@npm:1.0.2" - checksum: 9706c088a283058a8a99e0bf91b0a2f75497f185980d9ffa8b304de1d9e58ebda7c72c07ebf01dadedaac5b2907b2c6f566f660d62bd336c3468e960403b9d65 - languageName: node - linkType: hard - -"brace-expansion@npm:^1.1.7": - version: 1.1.11 - resolution: "brace-expansion@npm:1.1.11" - dependencies: - balanced-match: ^1.0.0 - concat-map: 0.0.1 - checksum: faf34a7bb0c3fcf4b59c7808bc5d2a96a40988addf2e7e09dfbb67a2251800e0d14cd2bfc1aa79174f2f5095c54ff27f46fb1289fe2d77dac755b5eb3434cc07 - languageName: node - linkType: hard - -"brace-expansion@npm:^2.0.1": - version: 2.0.1 - resolution: "brace-expansion@npm:2.0.1" - dependencies: - balanced-match: ^1.0.0 - checksum: a61e7cd2e8a8505e9f0036b3b6108ba5e926b4b55089eeb5550cd04a471fe216c96d4fe7e4c7f995c728c554ae20ddfc4244cad10aef255e72b62930afd233d1 - languageName: node - linkType: hard - -"cacache@npm:^16.0.2": - version: 16.0.7 - resolution: "cacache@npm:16.0.7" - dependencies: - "@npmcli/fs": ^2.1.0 - "@npmcli/move-file": ^2.0.0 - chownr: ^2.0.0 - fs-minipass: ^2.1.0 - glob: ^8.0.1 - infer-owner: ^1.0.4 - lru-cache: ^7.7.1 - minipass: ^3.1.6 - minipass-collect: ^1.0.2 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - mkdirp: ^1.0.4 - p-map: ^4.0.0 - promise-inflight: ^1.0.1 - rimraf: ^3.0.2 - ssri: ^9.0.0 - tar: ^6.1.11 - unique-filename: ^1.1.1 - checksum: 2155b099b7e0f0369fb1155ca4673532ca7efe2ebdbec63acca8743580b8446b5d4fd7184626b1cb059001af77b981cdc67035c7855544d365d4f048eafca2ca - languageName: node - linkType: hard - -"chownr@npm:^2.0.0": - version: 2.0.0 - resolution: "chownr@npm:2.0.0" - checksum: c57cf9dd0791e2f18a5ee9c1a299ae6e801ff58fee96dc8bfd0dcb4738a6ce58dd252a3605b1c93c6418fe4f9d5093b28ffbf4d66648cb2a9c67eaef9679be2f - languageName: node - linkType: hard - -"clean-stack@npm:^2.0.0": - version: 2.2.0 - resolution: "clean-stack@npm:2.2.0" - checksum: 2ac8cd2b2f5ec986a3c743935ec85b07bc174d5421a5efc8017e1f146a1cf5f781ae962618f416352103b32c9cd7e203276e8c28241bbe946160cab16149fb68 - languageName: node - linkType: hard - -"color-support@npm:^1.1.3": - version: 1.1.3 - resolution: "color-support@npm:1.1.3" - bin: - color-support: bin.js - checksum: 9b7356817670b9a13a26ca5af1c21615463b500783b739b7634a0c2047c16cef4b2865d7576875c31c3cddf9dd621fa19285e628f20198b233a5cfdda6d0793b - languageName: node - linkType: hard - -"concat-map@npm:0.0.1": - version: 0.0.1 - resolution: "concat-map@npm:0.0.1" - checksum: 902a9f5d8967a3e2faf138d5cb784b9979bad2e6db5357c5b21c568df4ebe62bcb15108af1b2253744844eb964fc023fbd9afbbbb6ddd0bcc204c6fb5b7bf3af - languageName: node - linkType: hard - -"console-control-strings@npm:^1.1.0": - version: 1.1.0 - resolution: "console-control-strings@npm:1.1.0" - checksum: 8755d76787f94e6cf79ce4666f0c5519906d7f5b02d4b884cf41e11dcd759ed69c57da0670afd9236d229a46e0f9cf519db0cd829c6dca820bb5a5c3def584ed - languageName: node - linkType: hard - -"dataloader@npm:^1.4.0": - version: 1.4.0 - resolution: "dataloader@npm:1.4.0" - checksum: e2c93d43afde68980efc0cd9ff48e9851116e27a9687f863e02b56d46f7e7868cc762cd6dcbaf4197e1ca850a03651510c165c2ae24b8e9843fd894002ad0e20 - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.3.3": - version: 4.3.4 - resolution: "debug@npm:4.3.4" - dependencies: - ms: 2.1.2 - peerDependenciesMeta: - supports-color: - optional: true - checksum: 3dbad3f94ea64f34431a9cbf0bafb61853eda57bff2880036153438f50fb5a84f27683ba0d8e5426bf41a8c6ff03879488120cf5b3a761e77953169c0600a708 - languageName: node - linkType: hard - -"delegates@npm:^1.0.0": - version: 1.0.0 - resolution: "delegates@npm:1.0.0" - checksum: a51744d9b53c164ba9c0492471a1a2ffa0b6727451bdc89e31627fdf4adda9d51277cfcbfb20f0a6f08ccb3c436f341df3e92631a3440226d93a8971724771fd - languageName: node - linkType: hard - -"depd@npm:^1.1.2": - version: 1.1.2 - resolution: "depd@npm:1.1.2" - checksum: 6b406620d269619852885ce15965272b829df6f409724415e0002c8632ab6a8c0a08ec1f0bd2add05dc7bd7507606f7e2cc034fa24224ab829580040b835ecd9 - languageName: node - linkType: hard - -"emoji-regex@npm:^8.0.0": - version: 8.0.0 - resolution: "emoji-regex@npm:8.0.0" - checksum: d4c5c39d5a9868b5fa152f00cada8a936868fd3367f33f71be515ecee4c803132d11b31a6222b2571b1e5f7e13890156a94880345594d0ce7e3c9895f560f192 - languageName: node - linkType: hard - -"encoding@npm:^0.1.13": - version: 0.1.13 - resolution: "encoding@npm:0.1.13" - dependencies: - iconv-lite: ^0.6.2 - checksum: bb98632f8ffa823996e508ce6a58ffcf5856330fde839ae42c9e1f436cc3b5cc651d4aeae72222916545428e54fd0f6aa8862fd8d25bdbcc4589f1e3f3715e7f - languageName: node - linkType: hard - -"env-paths@npm:^2.2.0": - version: 2.2.1 - resolution: "env-paths@npm:2.2.1" - checksum: 65b5df55a8bab92229ab2b40dad3b387fad24613263d103a97f91c9fe43ceb21965cd3392b1ccb5d77088021e525c4e0481adb309625d0cb94ade1d1fb8dc17e - languageName: node - linkType: hard - -"err-code@npm:^2.0.2": - version: 2.0.3 - resolution: "err-code@npm:2.0.3" - checksum: 8b7b1be20d2de12d2255c0bc2ca638b7af5171142693299416e6a9339bd7d88fc8d7707d913d78e0993176005405a236b066b45666b27b797252c771156ace54 - languageName: node - linkType: hard - -"esbuild-android-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-android-arm64@npm:0.13.15" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-darwin-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-darwin-64@npm:0.13.15" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"esbuild-darwin-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-darwin-arm64@npm:0.13.15" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-freebsd-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-freebsd-64@npm:0.13.15" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-freebsd-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-freebsd-arm64@npm:0.13.15" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-linux-32@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-32@npm:0.13.15" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"esbuild-linux-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-64@npm:0.13.15" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"esbuild-linux-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-arm64@npm:0.13.15" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"esbuild-linux-arm@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-arm@npm:0.13.15" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"esbuild-linux-mips64le@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-mips64le@npm:0.13.15" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - -"esbuild-linux-ppc64le@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-linux-ppc64le@npm:0.13.15" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - -"esbuild-netbsd-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-netbsd-64@npm:0.13.15" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-openbsd-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-openbsd-64@npm:0.13.15" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"esbuild-sunos-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-sunos-64@npm:0.13.15" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - -"esbuild-windows-32@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-windows-32@npm:0.13.15" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"esbuild-windows-64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-windows-64@npm:0.13.15" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"esbuild-windows-arm64@npm:0.13.15": - version: 0.13.15 - resolution: "esbuild-windows-arm64@npm:0.13.15" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"esbuild@npm:^0.13.12": - version: 0.13.15 - resolution: "esbuild@npm:0.13.15" - dependencies: - esbuild-android-arm64: 0.13.15 - esbuild-darwin-64: 0.13.15 - esbuild-darwin-arm64: 0.13.15 - esbuild-freebsd-64: 0.13.15 - esbuild-freebsd-arm64: 0.13.15 - esbuild-linux-32: 0.13.15 - esbuild-linux-64: 0.13.15 - esbuild-linux-arm: 0.13.15 - esbuild-linux-arm64: 0.13.15 - esbuild-linux-mips64le: 0.13.15 - esbuild-linux-ppc64le: 0.13.15 - esbuild-netbsd-64: 0.13.15 - esbuild-openbsd-64: 0.13.15 - esbuild-sunos-64: 0.13.15 - esbuild-windows-32: 0.13.15 - esbuild-windows-64: 0.13.15 - esbuild-windows-arm64: 0.13.15 - dependenciesMeta: - esbuild-android-arm64: - optional: true - esbuild-darwin-64: - optional: true - esbuild-darwin-arm64: - optional: true - esbuild-freebsd-64: - optional: true - esbuild-freebsd-arm64: - optional: true - esbuild-linux-32: - optional: true - esbuild-linux-64: - optional: true - esbuild-linux-arm: - optional: true - esbuild-linux-arm64: - optional: true - esbuild-linux-mips64le: - optional: true - esbuild-linux-ppc64le: - optional: true - esbuild-netbsd-64: - optional: true - esbuild-openbsd-64: - optional: true - esbuild-sunos-64: - optional: true - esbuild-windows-32: - optional: true - esbuild-windows-64: - optional: true - esbuild-windows-arm64: - optional: true - bin: - esbuild: bin/esbuild - checksum: d5fac8f28a6328592e45f9d49a7e98420cf2c2a3ff5a753bbf011ab79bcb5c062209ef862d3ae0875d8f2a50d40c112b0224e8b419a7cbffc6e2b02cee11ef7e - languageName: node - linkType: hard - -"fast-sha256@npm:^1.3.0": - version: 1.3.0 - resolution: "fast-sha256@npm:1.3.0" - checksum: 2b0bea7d3a9955e67abd2d3fbef4ce57f7dbb75708fc206d14973bd1d97aaf35b5c0a59c1d65be6f755df43d73b7657b9eac4fb3c2d58e6849966db1ef1fa186 - languageName: node - linkType: hard - -"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": - version: 2.1.0 - resolution: "fs-minipass@npm:2.1.0" - dependencies: - minipass: ^3.0.0 - checksum: 1b8d128dae2ac6cc94230cc5ead341ba3e0efaef82dab46a33d171c044caaa6ca001364178d42069b2809c35a1c3c35079a32107c770e9ffab3901b59af8c8b1 - languageName: node - linkType: hard - -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 99ddea01a7e75aa276c250a04eedeffe5662bce66c65c07164ad6264f9de18fb21be9433ead460e54cff20e31721c811f4fb5d70591799df5f85dce6d6746fd0 - languageName: node - linkType: hard - -"fsevents@npm:~2.3.2": - version: 2.3.2 - resolution: "fsevents@npm:2.3.2" - dependencies: - node-gyp: latest - checksum: 97ade64e75091afee5265e6956cb72ba34db7819b4c3e94c431d4be2b19b8bb7a2d4116da417950c3425f17c8fe693d25e20212cac583ac1521ad066b77ae31f - conditions: os=darwin - languageName: node - linkType: hard - -"fsevents@patch:fsevents@~2.3.2#~builtin": - version: 2.3.2 - resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin::version=2.3.2&hash=18f3a7" - dependencies: - node-gyp: latest - conditions: os=darwin - languageName: node - linkType: hard - -"function-bind@npm:^1.1.1": - version: 1.1.1 - resolution: "function-bind@npm:1.1.1" - checksum: b32fbaebb3f8ec4969f033073b43f5c8befbb58f1a79e12f1d7490358150359ebd92f49e72ff0144f65f2c48ea2a605bff2d07965f548f6474fd8efd95bf361a - languageName: node - linkType: hard - -"gauge@npm:^4.0.3": - version: 4.0.4 - resolution: "gauge@npm:4.0.4" - dependencies: - aproba: ^1.0.3 || ^2.0.0 - color-support: ^1.1.3 - console-control-strings: ^1.1.0 - has-unicode: ^2.0.1 - signal-exit: ^3.0.7 - string-width: ^4.2.3 - strip-ansi: ^6.0.1 - wide-align: ^1.1.5 - checksum: 788b6bfe52f1dd8e263cda800c26ac0ca2ff6de0b6eee2fe0d9e3abf15e149b651bd27bf5226be10e6e3edb5c4e5d5985a5a1a98137e7a892f75eff76467ad2d - languageName: node - linkType: hard - -"glob@npm:^7.1.3, glob@npm:^7.1.4": - version: 7.2.3 - resolution: "glob@npm:7.2.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^3.1.1 - once: ^1.3.0 - path-is-absolute: ^1.0.0 - checksum: 29452e97b38fa704dabb1d1045350fb2467cf0277e155aa9ff7077e90ad81d1ea9d53d3ee63bd37c05b09a065e90f16aec4a65f5b8de401d1dac40bc5605d133 - languageName: node - linkType: hard - -"glob@npm:^8.0.1": - version: 8.0.3 - resolution: "glob@npm:8.0.3" - dependencies: - fs.realpath: ^1.0.0 - inflight: ^1.0.4 - inherits: 2 - minimatch: ^5.0.1 - once: ^1.3.0 - checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5 - languageName: node - linkType: hard - -"graceful-fs@npm:^4.2.6": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 3f109d70ae123951905d85032ebeae3c2a5a7a997430df00ea30df0e3a6c60cf6689b109654d6fdacd28810a053348c4d14642da1d075049e6be1ba5216218da - languageName: node - linkType: hard - -"has-unicode@npm:^2.0.1": - version: 2.0.1 - resolution: "has-unicode@npm:2.0.1" - checksum: 1eab07a7436512db0be40a710b29b5dc21fa04880b7f63c9980b706683127e3c1b57cb80ea96d47991bdae2dfe479604f6a1ba410106ee1046a41d1bd0814400 - languageName: node - linkType: hard - -"has@npm:^1.0.3": - version: 1.0.3 - resolution: "has@npm:1.0.3" - dependencies: - function-bind: ^1.1.1 - checksum: b9ad53d53be4af90ce5d1c38331e712522417d017d5ef1ebd0507e07c2fbad8686fffb8e12ddecd4c39ca9b9b47431afbb975b8abf7f3c3b82c98e9aad052792 - languageName: node - linkType: hard - -"http-cache-semantics@npm:^4.1.0": - version: 4.1.0 - resolution: "http-cache-semantics@npm:4.1.0" - checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 - languageName: node - linkType: hard - -"http-proxy-agent@npm:^5.0.0": - version: 5.0.0 - resolution: "http-proxy-agent@npm:5.0.0" - dependencies: - "@tootallnate/once": 2 - agent-base: 6 - debug: 4 - checksum: e2ee1ff1656a131953839b2a19cd1f3a52d97c25ba87bd2559af6ae87114abf60971e498021f9b73f9fd78aea8876d1fb0d4656aac8a03c6caa9fc175f22b786 - languageName: node - linkType: hard - -"https-proxy-agent@npm:^5.0.0": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" - dependencies: - agent-base: 6 - debug: 4 - checksum: 571fccdf38184f05943e12d37d6ce38197becdd69e58d03f43637f7fa1269cf303a7d228aa27e5b27bbd3af8f09fd938e1c91dcfefff2df7ba77c20ed8dfc765 - languageName: node - linkType: hard - -"humanize-ms@npm:^1.2.1": - version: 1.2.1 - resolution: "humanize-ms@npm:1.2.1" - dependencies: - ms: ^2.0.0 - checksum: 9c7a74a2827f9294c009266c82031030eae811ca87b0da3dceb8d6071b9bde22c9f3daef0469c3c533cc67a97d8a167cd9fc0389350e5f415f61a79b171ded16 - languageName: node - linkType: hard - -"iconv-lite@npm:^0.6.2": - version: 0.6.3 - resolution: "iconv-lite@npm:0.6.3" - dependencies: - safer-buffer: ">= 2.1.2 < 3.0.0" - checksum: 3f60d47a5c8fc3313317edfd29a00a692cc87a19cac0159e2ce711d0ebc9019064108323b5e493625e25594f11c6236647d8e256fbe7a58f4a3b33b89e6d30bf - languageName: node - linkType: hard - -"imurmurhash@npm:^0.1.4": - version: 0.1.4 - resolution: "imurmurhash@npm:0.1.4" - checksum: 7cae75c8cd9a50f57dadd77482359f659eaebac0319dd9368bcd1714f55e65badd6929ca58569da2b6494ef13fdd5598cd700b1eba23f8b79c5f19d195a3ecf7 - languageName: node - linkType: hard - -"indent-string@npm:^4.0.0": - version: 4.0.0 - resolution: "indent-string@npm:4.0.0" - checksum: 824cfb9929d031dabf059bebfe08cf3137365e112019086ed3dcff6a0a7b698cb80cf67ccccde0e25b9e2d7527aa6cc1fed1ac490c752162496caba3e6699612 - languageName: node - linkType: hard - -"infer-owner@npm:^1.0.4": - version: 1.0.4 - resolution: "infer-owner@npm:1.0.4" - checksum: 181e732764e4a0611576466b4b87dac338972b839920b2a8cde43642e4ed6bd54dc1fb0b40874728f2a2df9a1b097b8ff83b56d5f8f8e3927f837fdcb47d8a89 - languageName: node - linkType: hard - -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: ^1.3.0 - wrappy: 1 - checksum: f4f76aa072ce19fae87ce1ef7d221e709afb59d445e05d47fba710e85470923a75de35bfae47da6de1b18afc3ce83d70facf44cfb0aff89f0a3f45c0a0244dfd - languageName: node - linkType: hard - -"inherits@npm:2, inherits@npm:^2.0.3": - version: 2.0.4 - resolution: "inherits@npm:2.0.4" - checksum: 4a48a733847879d6cf6691860a6b1e3f0f4754176e4d71494c41f3475553768b10f84b5ce1d40fbd0e34e6bfbb864ee35858ad4dd2cf31e02fc4a154b724d7f1 - languageName: node - linkType: hard - -"ip@npm:^1.1.5": - version: 1.1.8 - resolution: "ip@npm:1.1.8" - checksum: a2ade53eb339fb0cbe9e69a44caab10d6e3784662285eb5d2677117ee4facc33a64679051c35e0dfdb1a3983a51ce2f5d2cb36446d52e10d01881789b76e28fb - languageName: node - linkType: hard - -"is-core-module@npm:^2.8.0": - version: 2.8.1 - resolution: "is-core-module@npm:2.8.1" - dependencies: - has: ^1.0.3 - checksum: 418b7bc10768a73c41c7ef497e293719604007f88934a6ffc5f7c78702791b8528102fb4c9e56d006d69361549b3d9519440214a74aefc7e0b79e5e4411d377f - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^3.0.0": - version: 3.0.0 - resolution: "is-fullwidth-code-point@npm:3.0.0" - checksum: 44a30c29457c7fb8f00297bce733f0a64cd22eca270f83e58c105e0d015e45c019491a4ab2faef91ab51d4738c670daff901c799f6a700e27f7314029e99e348 - languageName: node - linkType: hard - -"is-lambda@npm:^1.0.1": - version: 1.0.1 - resolution: "is-lambda@npm:1.0.1" - checksum: 93a32f01940220532e5948538699ad610d5924ac86093fcee83022252b363eb0cc99ba53ab084a04e4fb62bf7b5731f55496257a4c38adf87af9c4d352c71c35 - languageName: node - linkType: hard - -"isexe@npm:^2.0.0": - version: 2.0.0 - resolution: "isexe@npm:2.0.0" - checksum: 26bf6c5480dda5161c820c5b5c751ae1e766c587b1f951ea3fcfc973bafb7831ae5b54a31a69bd670220e42e99ec154475025a468eae58ea262f813fdc8d1c62 - languageName: node - linkType: hard - -"libsodium-wrappers@npm:^0.7.9": - version: 0.7.9 - resolution: "libsodium-wrappers@npm:0.7.9" - dependencies: - libsodium: ^0.7.0 - checksum: b5b1b9e1b4aa5662e07df244934125f9e3cd2ba7fe0ec45191a5ffc822d22f4d2f6e09e42d91c30c4f48ca0c7f810a176fdf5e32eed6722d7d82a2a719459f56 - languageName: node - linkType: hard - -"libsodium@npm:^0.7.0, libsodium@npm:^0.7.9": - version: 0.7.9 - resolution: "libsodium@npm:0.7.9" - checksum: 1c922c9972cf97ddb7207ee4f810dd291e0610dd57ea0e47f2343968392546aaa629945a2fb39ae5f19d067f6ed0bb7330f32cc9a680a847a662e9a210ce7bfb - languageName: node - linkType: hard - -"lodash@npm:^4.17.15": - version: 4.17.21 - resolution: "lodash@npm:4.17.21" - checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 - languageName: node - linkType: hard - -"long@npm:^4.0.0": - version: 4.0.0 - resolution: "long@npm:4.0.0" - checksum: 16afbe8f749c7c849db1f4de4e2e6a31ac6e617cead3bdc4f9605cb703cd20e1e9fc1a7baba674ffcca57d660a6e5b53a9e236d7b25a295d3855cca79cc06744 - languageName: node - linkType: hard - -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: ^4.0.0 - checksum: f97f499f898f23e4585742138a22f22526254fdba6d75d41a1c2526b3b6cc5747ef59c5612ba7375f42aca4f8461950e925ba08c991ead0651b4918b7c978297 - languageName: node - linkType: hard - -"lru-cache@npm:^7.7.1": - version: 7.10.1 - resolution: "lru-cache@npm:7.10.1" - checksum: e8b190d71ed0fcd7b29c71a3e9b01f851c92d1ef8865ff06b5581ca991db1e5e006920ed4da8b56da1910664ed51abfd76c46fb55e82ac252ff6c970ff910d72 - languageName: node - linkType: hard - -"make-fetch-happen@npm:^10.0.3": - version: 10.1.3 - resolution: "make-fetch-happen@npm:10.1.3" - dependencies: - agentkeepalive: ^4.2.1 - cacache: ^16.0.2 - http-cache-semantics: ^4.1.0 - http-proxy-agent: ^5.0.0 - https-proxy-agent: ^5.0.0 - is-lambda: ^1.0.1 - lru-cache: ^7.7.1 - minipass: ^3.1.6 - minipass-collect: ^1.0.2 - minipass-fetch: ^2.0.3 - minipass-flush: ^1.0.5 - minipass-pipeline: ^1.2.4 - negotiator: ^0.6.3 - promise-retry: ^2.0.1 - socks-proxy-agent: ^6.1.1 - ssri: ^9.0.0 - checksum: 14b9bc5fb65a1a1f53b4579c947d1ebdb18db71eb0b35a2eab612e9642a14127917528fe4ffb2c37aaa0d27dfd7507e4044e6e2e47b43985e8fa18722f535b8f - languageName: node - linkType: hard - -"minimatch@npm:^3.1.1": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: ^1.1.7 - checksum: c154e566406683e7bcb746e000b84d74465b3a832c45d59912b9b55cd50dee66e5c4b1e5566dba26154040e51672f9aa450a9aef0c97cfc7336b78b7afb9540a - languageName: node - linkType: hard - -"minimatch@npm:^5.0.1": - version: 5.1.0 - resolution: "minimatch@npm:5.1.0" - dependencies: - brace-expansion: ^2.0.1 - checksum: 15ce53d31a06361e8b7a629501b5c75491bc2b59712d53e802b1987121d91b433d73fcc5be92974fde66b2b51d8fb28d75a9ae900d249feb792bb1ba2a4f0a90 - languageName: node - linkType: hard - -"minipass-collect@npm:^1.0.2": - version: 1.0.2 - resolution: "minipass-collect@npm:1.0.2" - dependencies: - minipass: ^3.0.0 - checksum: 14df761028f3e47293aee72888f2657695ec66bd7d09cae7ad558da30415fdc4752bbfee66287dcc6fd5e6a2fa3466d6c484dc1cbd986525d9393b9523d97f10 - languageName: node - linkType: hard - -"minipass-fetch@npm:^2.0.3": - version: 2.1.0 - resolution: "minipass-fetch@npm:2.1.0" - dependencies: - encoding: ^0.1.13 - minipass: ^3.1.6 - minipass-sized: ^1.0.3 - minizlib: ^2.1.2 - dependenciesMeta: - encoding: - optional: true - checksum: 1334732859a3f7959ed22589bafd9c40384b885aebb5932328071c33f86b3eb181d54c86919675d1825ab5f1c8e4f328878c863873258d113c29d79a4b0c9c9f - languageName: node - linkType: hard - -"minipass-flush@npm:^1.0.5": - version: 1.0.5 - resolution: "minipass-flush@npm:1.0.5" - dependencies: - minipass: ^3.0.0 - checksum: 56269a0b22bad756a08a94b1ffc36b7c9c5de0735a4dd1ab2b06c066d795cfd1f0ac44a0fcae13eece5589b908ecddc867f04c745c7009be0b566421ea0944cf - languageName: node - linkType: hard - -"minipass-pipeline@npm:^1.2.4": - version: 1.2.4 - resolution: "minipass-pipeline@npm:1.2.4" - dependencies: - minipass: ^3.0.0 - checksum: b14240dac0d29823c3d5911c286069e36d0b81173d7bdf07a7e4a91ecdef92cdff4baaf31ea3746f1c61e0957f652e641223970870e2353593f382112257971b - languageName: node - linkType: hard - -"minipass-sized@npm:^1.0.3": - version: 1.0.3 - resolution: "minipass-sized@npm:1.0.3" - dependencies: - minipass: ^3.0.0 - checksum: 79076749fcacf21b5d16dd596d32c3b6bf4d6e62abb43868fac21674078505c8b15eaca4e47ed844985a4514854f917d78f588fcd029693709417d8f98b2bd60 - languageName: node - linkType: hard - -"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6": - version: 3.1.6 - resolution: "minipass@npm:3.1.6" - dependencies: - yallist: ^4.0.0 - checksum: 57a04041413a3531a65062452cb5175f93383ef245d6f4a2961d34386eb9aa8ac11ac7f16f791f5e8bbaf1dfb1ef01596870c88e8822215db57aa591a5bb0a77 - languageName: node - linkType: hard - -"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": - version: 2.1.2 - resolution: "minizlib@npm:2.1.2" - dependencies: - minipass: ^3.0.0 - yallist: ^4.0.0 - checksum: f1fdeac0b07cf8f30fcf12f4b586795b97be856edea22b5e9072707be51fc95d41487faec3f265b42973a304fe3a64acd91a44a3826a963e37b37bafde0212c3 - languageName: node - linkType: hard - -"mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": - version: 1.0.4 - resolution: "mkdirp@npm:1.0.4" - bin: - mkdirp: bin/cmd.js - checksum: a96865108c6c3b1b8e1d5e9f11843de1e077e57737602de1b82030815f311be11f96f09cce59bd5b903d0b29834733e5313f9301e3ed6d6f6fba2eae0df4298f - languageName: node - linkType: hard - -"ms@npm:2.1.2": - version: 2.1.2 - resolution: "ms@npm:2.1.2" - checksum: 673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f - languageName: node - linkType: hard - -"ms@npm:^2.0.0": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d - languageName: node - linkType: hard - -"nanoid@npm:^3.1.30": - version: 3.2.0 - resolution: "nanoid@npm:3.2.0" - bin: - nanoid: bin/nanoid.cjs - checksum: 3d1d5a69fea84e538057cf64106e713931c4ef32af344068ecff153ff91252f39b0f2b472e09b0dfff43ac3cf520c92938d90e6455121fe93976e23660f4fccc - languageName: node - linkType: hard - -"negotiator@npm:^0.6.3": - version: 0.6.3 - resolution: "negotiator@npm:0.6.3" - checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9 - languageName: node - linkType: hard - -"node-gyp@npm:latest": - version: 9.0.0 - resolution: "node-gyp@npm:9.0.0" - dependencies: - env-paths: ^2.2.0 - glob: ^7.1.4 - graceful-fs: ^4.2.6 - make-fetch-happen: ^10.0.3 - nopt: ^5.0.0 - npmlog: ^6.0.0 - rimraf: ^3.0.2 - semver: ^7.3.5 - tar: ^6.1.2 - which: ^2.0.2 - bin: - node-gyp: bin/node-gyp.js - checksum: 4d8ef8860f7e4f4d86c91db3f519d26ed5cc23b48fe54543e2afd86162b4acbd14f21de42a5db344525efb69a991e021b96a68c70c6e2d5f4a5cb770793da6d3 - languageName: node - linkType: hard - -"nopt@npm:^5.0.0": - version: 5.0.0 - resolution: "nopt@npm:5.0.0" - dependencies: - abbrev: 1 - bin: - nopt: bin/nopt.js - checksum: d35fdec187269503843924e0114c0c6533fb54bbf1620d0f28b4b60ba01712d6687f62565c55cc20a504eff0fbe5c63e22340c3fad549ad40469ffb611b04f2f - languageName: node - linkType: hard - -"npmlog@npm:^6.0.0": - version: 6.0.2 - resolution: "npmlog@npm:6.0.2" - dependencies: - are-we-there-yet: ^3.0.0 - console-control-strings: ^1.1.0 - gauge: ^4.0.3 - set-blocking: ^2.0.0 - checksum: ae238cd264a1c3f22091cdd9e2b106f684297d3c184f1146984ecbe18aaa86343953f26b9520dedd1b1372bc0316905b736c1932d778dbeb1fcf5a1001390e2a - languageName: node - linkType: hard - -"object-hash@npm:^1.3.1": - version: 1.3.1 - resolution: "object-hash@npm:1.3.1" - checksum: fdcb957a2f15a9060e30655a9f683ba1fc25dfb8809a73d32e9634bec385a2f1d686c707ac1e5f69fb773bc12df03fb64c77ce3faeed83e35f4eb1946cb1989e - languageName: node - linkType: hard - -"once@npm:^1.3.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" - dependencies: - wrappy: 1 - checksum: cd0a88501333edd640d95f0d2700fbde6bff20b3d4d9bdc521bdd31af0656b5706570d6c6afe532045a20bb8dc0849f8332d6f2a416e0ba6d3d3b98806c7db68 - languageName: node - linkType: hard - -"p-map@npm:^4.0.0": - version: 4.0.0 - resolution: "p-map@npm:4.0.0" - dependencies: - aggregate-error: ^3.0.0 - checksum: cb0ab21ec0f32ddffd31dfc250e3afa61e103ef43d957cc45497afe37513634589316de4eb88abdfd969fe6410c22c0b93ab24328833b8eb1ccc087fc0442a1c - languageName: node - linkType: hard - -"path-is-absolute@npm:^1.0.0": - version: 1.0.1 - resolution: "path-is-absolute@npm:1.0.1" - checksum: 060840f92cf8effa293bcc1bea81281bd7d363731d214cbe5c227df207c34cd727430f70c6037b5159c8a870b9157cba65e775446b0ab06fd5ecc7e54615a3b8 - languageName: node - linkType: hard - -"path-parse@npm:^1.0.7": - version: 1.0.7 - resolution: "path-parse@npm:1.0.7" - checksum: 49abf3d81115642938a8700ec580da6e830dde670be21893c62f4e10bd7dd4c3742ddc603fe24f898cba7eb0c6bc1777f8d9ac14185d34540c6d4d80cd9cae8a - languageName: node - linkType: hard - -"pcm-player@npm:^0.0.11": - version: 0.0.11 - resolution: "pcm-player@npm:0.0.11" - checksum: 8b02471e5788f23dbc965e577656a36c77d8a92f9481ddacac2d46c52755653e61bdb7a41698cee1a29e9df0cf8d853495b3968551b025c601ac4a7bf8139d81 - languageName: node - linkType: hard - -"picocolors@npm:^1.0.0": - version: 1.0.0 - resolution: "picocolors@npm:1.0.0" - checksum: a2e8092dd86c8396bdba9f2b5481032848525b3dc295ce9b57896f931e63fc16f79805144321f72976383fc249584672a75cc18d6777c6b757603f372f745981 - languageName: node - linkType: hard - -"postcss@npm:^8.4.5": - version: 8.4.5 - resolution: "postcss@npm:8.4.5" - dependencies: - nanoid: ^3.1.30 - picocolors: ^1.0.0 - source-map-js: ^1.0.1 - checksum: b78abdd89c10f7b48f4bdcd376104a19d6e9c7495ab521729bdb3df315af6c211360e9f06887ad3bc0ab0f61a04b91d68ea11462997c79cced58b9ccd66fac07 - languageName: node - linkType: hard - -"prettier@npm:^2.5.1": - version: 2.5.1 - resolution: "prettier@npm:2.5.1" - bin: - prettier: bin-prettier.js - checksum: 21b9408476ea1c544b0e45d51ceb94a84789ff92095abb710942d780c862d0daebdb29972d47f6b4d0f7ebbfb0ffbf56cc2cfa3e3e9d1cca54864af185b15b66 - languageName: node - linkType: hard - -"promise-inflight@npm:^1.0.1": - version: 1.0.1 - resolution: "promise-inflight@npm:1.0.1" - checksum: 22749483091d2c594261517f4f80e05226d4d5ecc1fc917e1886929da56e22b5718b7f2a75f3807e7a7d471bc3be2907fe92e6e8f373ddf5c64bae35b5af3981 - languageName: node - linkType: hard - -"promise-retry@npm:^2.0.1": - version: 2.0.1 - resolution: "promise-retry@npm:2.0.1" - dependencies: - err-code: ^2.0.2 - retry: ^0.12.0 - checksum: f96a3f6d90b92b568a26f71e966cbbc0f63ab85ea6ff6c81284dc869b41510e6cdef99b6b65f9030f0db422bf7c96652a3fff9f2e8fb4a0f069d8f4430359429 - languageName: node - linkType: hard - -"protobufjs@npm:^6.8.8": - version: 6.11.2 - resolution: "protobufjs@npm:6.11.2" - dependencies: - "@protobufjs/aspromise": ^1.1.2 - "@protobufjs/base64": ^1.1.2 - "@protobufjs/codegen": ^2.0.4 - "@protobufjs/eventemitter": ^1.1.0 - "@protobufjs/fetch": ^1.1.0 - "@protobufjs/float": ^1.0.2 - "@protobufjs/inquire": ^1.1.0 - "@protobufjs/path": ^1.1.2 - "@protobufjs/pool": ^1.1.0 - "@protobufjs/utf8": ^1.1.0 - "@types/long": ^4.0.1 - "@types/node": ">=13.7.0" - long: ^4.0.0 - bin: - pbjs: bin/pbjs - pbts: bin/pbts - checksum: 80e9d9610c3eb66f9eae4e44a1ae30381cedb721b7d5f635d781fe4c507e2c77bb7c879addcd1dda79733d3ae589d9e66fd18d42baf99b35df7382a0f9920795 - languageName: node - linkType: hard - -"readable-stream@npm:^3.6.0": - version: 3.6.0 - resolution: "readable-stream@npm:3.6.0" - dependencies: - inherits: ^2.0.3 - string_decoder: ^1.1.1 - util-deprecate: ^1.0.1 - checksum: d4ea81502d3799439bb955a3a5d1d808592cf3133350ed352aeaa499647858b27b1c4013984900238b0873ec8d0d8defce72469fb7a83e61d53f5ad61cb80dc8 - languageName: node - linkType: hard - -"resolve@npm:^1.20.0": - version: 1.21.0 - resolution: "resolve@npm:1.21.0" - dependencies: - is-core-module: ^2.8.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: d7d9092a5c04a048bea16c7e5a2eb605ac3e8363a0cc5644de1fde17d5028e8d5f4343aab1d99bd327b98e91a66ea83e242718150c64dfedcb96e5e7aad6c4f5 - languageName: node - linkType: hard - -"resolve@patch:resolve@^1.20.0#~builtin": - version: 1.21.0 - resolution: "resolve@patch:resolve@npm%3A1.21.0#~builtin::version=1.21.0&hash=07638b" - dependencies: - is-core-module: ^2.8.0 - path-parse: ^1.0.7 - supports-preserve-symlinks-flag: ^1.0.0 - bin: - resolve: bin/resolve - checksum: a0a4d1f7409e73190f31f901f8a619960bb3bd4ae38ba3a54c7ea7e1c87758d28a73256bb8d6a35996a903d1bf14f53883f0dcac6c571c063cb8162d813ad26e - languageName: node - linkType: hard - -"retry@npm:^0.12.0": - version: 0.12.0 - resolution: "retry@npm:0.12.0" - checksum: 623bd7d2e5119467ba66202d733ec3c2e2e26568074923bc0585b6b99db14f357e79bdedb63cab56cec47491c4a0da7e6021a7465ca6dc4f481d3898fdd3158c - languageName: node - linkType: hard - -"rimraf@npm:^3.0.2": - version: 3.0.2 - resolution: "rimraf@npm:3.0.2" - dependencies: - glob: ^7.1.3 - bin: - rimraf: bin.js - checksum: 87f4164e396f0171b0a3386cc1877a817f572148ee13a7e113b238e48e8a9f2f31d009a92ec38a591ff1567d9662c6b67fd8818a2dbbaed74bc26a87a2a4a9a0 - languageName: node - linkType: hard - -"rollup@npm:^2.59.0": - version: 2.64.0 - resolution: "rollup@npm:2.64.0" - dependencies: - fsevents: ~2.3.2 - dependenciesMeta: - fsevents: - optional: true - bin: - rollup: dist/bin/rollup - checksum: dc5b28538002ed635ea54af4c2ced05c52146322c61dbe0e84f294ee62e4f232a15760fdcef9bbeb742883edf9bf093ace5389bbdd816d18b9f5740555135180 - languageName: node - linkType: hard - -"safe-buffer@npm:~5.2.0": - version: 5.2.1 - resolution: "safe-buffer@npm:5.2.1" - checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491 - languageName: node - linkType: hard - -"safer-buffer@npm:>= 2.1.2 < 3.0.0": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: cab8f25ae6f1434abee8d80023d7e72b598cf1327164ddab31003c51215526801e40b66c5e65d658a0af1e9d6478cadcb4c745f4bd6751f97d8644786c0978b0 - languageName: node - linkType: hard - -"semver@npm:^7.3.5": - version: 7.3.7 - resolution: "semver@npm:7.3.7" - dependencies: - lru-cache: ^6.0.0 - bin: - semver: bin/semver.js - checksum: 2fa3e877568cd6ce769c75c211beaed1f9fce80b28338cadd9d0b6c40f2e2862bafd62c19a6cff42f3d54292b7c623277bcab8816a2b5521cf15210d43e75232 - languageName: node - linkType: hard - -"set-blocking@npm:^2.0.0": - version: 2.0.0 - resolution: "set-blocking@npm:2.0.0" - checksum: 6e65a05f7cf7ebdf8b7c75b101e18c0b7e3dff4940d480efed8aad3a36a4005140b660fa1d804cb8bce911cac290441dc728084a30504d3516ac2ff7ad607b02 - languageName: node - linkType: hard - -"signal-exit@npm:^3.0.7": - version: 3.0.7 - resolution: "signal-exit@npm:3.0.7" - checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 - languageName: node - linkType: hard - -"smart-buffer@npm:^4.2.0": - version: 4.2.0 - resolution: "smart-buffer@npm:4.2.0" - checksum: b5167a7142c1da704c0e3af85c402002b597081dd9575031a90b4f229ca5678e9a36e8a374f1814c8156a725d17008ae3bde63b92f9cfd132526379e580bec8b - languageName: node - linkType: hard - -"socks-proxy-agent@npm:^6.1.1": - version: 6.2.0 - resolution: "socks-proxy-agent@npm:6.2.0" - dependencies: - agent-base: ^6.0.2 - debug: ^4.3.3 - socks: ^2.6.2 - checksum: 6723fd64fb50334e2b340fd0a80fd8488ffc5bc43d85b7cf1d25612044f814dd7d6ea417fd47602159941236f7f4bd15669fa5d7e1f852598a31288e1a43967b - languageName: node - linkType: hard - -"socks@npm:^2.6.2": - version: 2.6.2 - resolution: "socks@npm:2.6.2" - dependencies: - ip: ^1.1.5 - smart-buffer: ^4.2.0 - checksum: dd9194293059d737759d5c69273850ad4149f448426249325c4bea0e340d1cf3d266c3b022694b0dcf5d31f759de23657244c481fc1e8322add80b7985c36b5e - languageName: node - linkType: hard - -"source-map-js@npm:^1.0.1": - version: 1.0.1 - resolution: "source-map-js@npm:1.0.1" - checksum: 22606113d62bbd468712b0cb0c46e9a8629de7eb081049c62a04d977a211abafd7d61455617f8b78daba0b6c0c7e7c88f8c6b5aaeacffac0a6676ecf5caac5ce - languageName: node - linkType: hard - -"ssri@npm:^9.0.0": - version: 9.0.0 - resolution: "ssri@npm:9.0.0" - dependencies: - minipass: ^3.1.1 - checksum: bf33174232d07cc64e77ab1c51b55d28352273380c503d35642a19627e88a2c5f160039bb0a28608a353485075dda084dbf0390c7070f9f284559eb71d01b84b - languageName: node - linkType: hard - -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.2.3": - version: 4.2.3 - resolution: "string-width@npm:4.2.3" - dependencies: - emoji-regex: ^8.0.0 - is-fullwidth-code-point: ^3.0.0 - strip-ansi: ^6.0.1 - checksum: e52c10dc3fbfcd6c3a15f159f54a90024241d0f149cf8aed2982a2d801d2e64df0bf1dc351cf8e95c3319323f9f220c16e740b06faecd53e2462df1d2b5443fb - languageName: node - linkType: hard - -"string_decoder@npm:^1.1.1": - version: 1.3.0 - resolution: "string_decoder@npm:1.3.0" - dependencies: - safe-buffer: ~5.2.0 - checksum: 8417646695a66e73aefc4420eb3b84cc9ffd89572861fe004e6aeb13c7bc00e2f616247505d2dbbef24247c372f70268f594af7126f43548565c68c117bdeb56 - languageName: node - linkType: hard - -"strip-ansi@npm:^6.0.1": - version: 6.0.1 - resolution: "strip-ansi@npm:6.0.1" - dependencies: - ansi-regex: ^5.0.1 - checksum: f3cd25890aef3ba6e1a74e20896c21a46f482e93df4a06567cebf2b57edabb15133f1f94e57434e0a958d61186087b1008e89c94875d019910a213181a14fc8c - languageName: node - linkType: hard - -"supports-preserve-symlinks-flag@npm:^1.0.0": - version: 1.0.0 - resolution: "supports-preserve-symlinks-flag@npm:1.0.0" - checksum: 53b1e247e68e05db7b3808b99b892bd36fb096e6fba213a06da7fab22045e97597db425c724f2bbd6c99a3c295e1e73f3e4de78592289f38431049e1277ca0ae - languageName: node - linkType: hard - -"tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.1.11 - resolution: "tar@npm:6.1.11" - dependencies: - chownr: ^2.0.0 - fs-minipass: ^2.0.0 - minipass: ^3.0.0 - minizlib: ^2.1.1 - mkdirp: ^1.0.3 - yallist: ^4.0.0 - checksum: a04c07bb9e2d8f46776517d4618f2406fb977a74d914ad98b264fc3db0fe8224da5bec11e5f8902c5b9bcb8ace22d95fbe3c7b36b8593b7dfc8391a25898f32f - languageName: node - linkType: hard - -"ts-poet@npm:^4.5.0": - version: 4.10.0 - resolution: "ts-poet@npm:4.10.0" - dependencies: - lodash: ^4.17.15 - prettier: ^2.5.1 - checksum: ffb3890a429f7ab59d96a7a17d9cc161bce786695af0fd77156e5779cfaeda92eaae4f15995a8c71a83cfb528d61bd2518bae19c4adec147bff8dce6f27c57d3 - languageName: node - linkType: hard - -"ts-proto-descriptors@npm:^1.2.1": - version: 1.3.1 - resolution: "ts-proto-descriptors@npm:1.3.1" - dependencies: - long: ^4.0.0 - protobufjs: ^6.8.8 - checksum: ef8acf9231375dd00cfa667c688746ae24fb8012a3875d1447cb6a6e9e0311150681719072716f58a24b1df801bcc35e56faca11ea4bac1f8146038b524b93c4 - languageName: node - linkType: hard - -"ts-proto@npm:^1.101.0": - version: 1.101.0 - resolution: "ts-proto@npm:1.101.0" - dependencies: - "@types/object-hash": ^1.3.0 - dataloader: ^1.4.0 - object-hash: ^1.3.1 - protobufjs: ^6.8.8 - ts-poet: ^4.5.0 - ts-proto-descriptors: ^1.2.1 - bin: - protoc-gen-ts_proto: protoc-gen-ts_proto - checksum: d404e34cad4fc5fb19271f7f257ff177d0ebac22ceca3b287927566a3ecda2f350b8592851d27415f6ec645525eae4ab40291ce3a6a3e151bb004478a1fe634a - languageName: node - linkType: hard - -"typescript@npm:^4.4.4": - version: 4.5.4 - resolution: "typescript@npm:4.5.4" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 59f3243f9cd6fe3161e6150ff6bf795fc843b4234a655dbd938a310515e0d61afd1ac942799e7415e4334255e41c2c49b7dd5d9fd38a17acd25a6779ca7e0961 - languageName: node - linkType: hard - -"typescript@patch:typescript@^4.4.4#~builtin": - version: 4.5.4 - resolution: "typescript@patch:typescript@npm%3A4.5.4#~builtin::version=4.5.4&hash=bda367" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: eda87927f9cfb94aca9b5e47842daf37347ad3073133e17f556fbb6c18f3493c5b551eedab0f4b26774235ddb7acbe0087250d5285f72ce6819a0891dd5a74ed - languageName: node - linkType: hard - -"unique-filename@npm:^1.1.1": - version: 1.1.1 - resolution: "unique-filename@npm:1.1.1" - dependencies: - unique-slug: ^2.0.0 - checksum: cf4998c9228cc7647ba7814e255dec51be43673903897b1786eff2ac2d670f54d4d733357eb08dea969aa5e6875d0e1bd391d668fbdb5a179744e7c7551a6f80 - languageName: node - linkType: hard - -"unique-slug@npm:^2.0.0": - version: 2.0.2 - resolution: "unique-slug@npm:2.0.2" - dependencies: - imurmurhash: ^0.1.4 - checksum: 5b6876a645da08d505dedb970d1571f6cebdf87044cb6b740c8dbb24f0d6e1dc8bdbf46825fd09f994d7cf50760e6f6e063cfa197d51c5902c00a861702eb75a - languageName: node - linkType: hard - -"util-deprecate@npm:^1.0.1": - version: 1.0.2 - resolution: "util-deprecate@npm:1.0.2" - checksum: 474acf1146cb2701fe3b074892217553dfcf9a031280919ba1b8d651a068c9b15d863b7303cb15bd00a862b498e6cf4ad7b4a08fb134edd5a6f7641681cb54a2 - languageName: node - linkType: hard - -"vite@npm:^2.7.2": - version: 2.7.12 - resolution: "vite@npm:2.7.12" - dependencies: - esbuild: ^0.13.12 - fsevents: ~2.3.2 - postcss: ^8.4.5 - resolve: ^1.20.0 - rollup: ^2.59.0 - peerDependencies: - less: "*" - sass: "*" - stylus: "*" - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - less: - optional: true - sass: - optional: true - stylus: - optional: true - bin: - vite: bin/vite.js - checksum: 56d62ae8131b02891f2dbd81f26a3ca28a02bfe390f9cb4e0c2d8dc831c2e2f8264dd3c45b14c7dd48e79d83d323a35148f92729e1f3385fae04fcd691f3f985 - languageName: node - linkType: hard - -"wasm-feature-detect@npm:^1.2.11": - version: 1.2.11 - resolution: "wasm-feature-detect@npm:1.2.11" - checksum: e7f28f5e6ca0722ba059e200c47a944ebd7570027a3ac5600b7178ee9bf950fe5280a68b5e3b5f29930407cc1214695ca10ea36a3d995d3445f4e34db58a8505 - languageName: node - linkType: hard - -"web_hbb@workspace:.": - version: 0.0.0-use.local - resolution: "web_hbb@workspace:." - dependencies: - fast-sha256: ^1.3.0 - libsodium: ^0.7.9 - libsodium-wrappers: ^0.7.9 - pcm-player: ^0.0.11 - ts-proto: ^1.101.0 - typescript: ^4.4.4 - vite: ^2.7.2 - wasm-feature-detect: ^1.2.11 - zstddec: ^0.0.2 - languageName: unknown - linkType: soft - -"which@npm:^2.0.2": - version: 2.0.2 - resolution: "which@npm:2.0.2" - dependencies: - isexe: ^2.0.0 - bin: - node-which: ./bin/node-which - checksum: 1a5c563d3c1b52d5f893c8b61afe11abc3bab4afac492e8da5bde69d550de701cf9806235f20a47b5c8fa8a1d6a9135841de2596535e998027a54589000e66d1 - languageName: node - linkType: hard - -"wide-align@npm:^1.1.5": - version: 1.1.5 - resolution: "wide-align@npm:1.1.5" - dependencies: - string-width: ^1.0.2 || 2 || 3 || 4 - checksum: d5fc37cd561f9daee3c80e03b92ed3e84d80dde3365a8767263d03dacfc8fa06b065ffe1df00d8c2a09f731482fcacae745abfbb478d4af36d0a891fad4834d3 - languageName: node - linkType: hard - -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 159da4805f7e84a3d003d8841557196034155008f817172d4e986bd591f74aa82aa7db55929a54222309e01079a65a92a9e6414da5a6aa4b01ee44a511ac3ee5 - languageName: node - linkType: hard - -"yallist@npm:^4.0.0": - version: 4.0.0 - resolution: "yallist@npm:4.0.0" - checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5 - languageName: node - linkType: hard - -"zstddec@npm:^0.0.2": - version: 0.0.2 - resolution: "zstddec@npm:0.0.2" - checksum: 107334442a34590173cda03614006337712658fd043fa79f72bd486de527e2a16da474d7b20d4a171f086b334c2ad8a72afb634776d79bc2c36aee065babe31b - languageName: node - linkType: hard diff --git a/flutter/web/v1/libs/firebase-analytics.js b/flutter/web/v1/libs/firebase-analytics.js deleted file mode 100644 index 9b9a02b093f..00000000000 --- a/flutter/web/v1/libs/firebase-analytics.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(require("@firebase/app")):"function"==typeof define&&define.amd?define(["@firebase/app"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).firebase)}(this,function(mt){"use strict";try{!function(){function e(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=e(mt),r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)};var s=function(){return(s=Object.assign||function(e){for(var t,n=1,r=arguments.length;na[0]&&t[1]=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function u(e,t){for(var n=0,r=t.length,i=e.length;n"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new f(o,e,i)},n);function n(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var y=/\{\$([^}]+)}/g,b=1e3,w=2,I=144e5,_=.5;function E(e,t,n){void 0===n&&(n=w);n=(t=void 0===t?b:t)*Math.pow(n,e),e=Math.round(_*n*(Math.random()-.5)*2);return Math.min(I,n+e)}var T=(S.prototype.setInstantiationMode=function(e){return this.instantiationMode=e,this},S.prototype.setMultipleInstances=function(e){return this.multipleInstances=e,this},S.prototype.setServiceProps=function(e){return this.serviceProps=e,this},S.prototype.setInstanceCreatedCallback=function(e){return this.onInstanceCreated=e,this},S);function S(e,t,n){this.name=e,this.instanceFactory=t,this.type=n,this.multipleInstances=!1,this.serviceProps={},this.instantiationMode="LAZY",this.onInstanceCreated=null}function C(n){return new Promise(function(e,t){n.onsuccess=function(){e(n.result)},n.onerror=function(){t(n.error)}})}function O(n,r,i){var o,e=new Promise(function(e,t){C(o=n[r].apply(n,i)).then(e,t)});return e.request=o,e}function N(e,n,t){t.forEach(function(t){Object.defineProperty(e.prototype,t,{get:function(){return this[n][t]},set:function(e){this[n][t]=e}})})}function D(t,n,r,e){e.forEach(function(e){e in r.prototype&&(t.prototype[e]=function(){return O(this[n],e,arguments)})})}function P(t,n,r,e){e.forEach(function(e){e in r.prototype&&(t.prototype[e]=function(){return this[n][e].apply(this[n],arguments)})})}function A(e,r,t,n){n.forEach(function(n){n in t.prototype&&(e.prototype[n]=function(){return e=this[r],(t=O(e,n,arguments)).then(function(e){if(e)return new k(e,t.request)});var e,t})})}function x(e){this._index=e}function k(e,t){this._cursor=e,this._request=t}function j(e){this._store=e}function L(n){this._tx=n,this.complete=new Promise(function(e,t){n.oncomplete=function(){e()},n.onerror=function(){t(n.error)},n.onabort=function(){t(n.error)}})}function R(e,t,n){this._db=e,this.oldVersion=t,this.transaction=new L(n)}function F(e){this._db=e}N(x,"_index",["name","keyPath","multiEntry","unique"]),D(x,"_index",IDBIndex,["get","getKey","getAll","getAllKeys","count"]),A(x,"_index",IDBIndex,["openCursor","openKeyCursor"]),N(k,"_cursor",["direction","key","primaryKey","value"]),D(k,"_cursor",IDBCursor,["update","delete"]),["advance","continue","continuePrimaryKey"].forEach(function(n){n in IDBCursor.prototype&&(k.prototype[n]=function(){var t=this,e=arguments;return Promise.resolve().then(function(){return t._cursor[n].apply(t._cursor,e),C(t._request).then(function(e){if(e)return new k(e,t._request)})})})}),j.prototype.createIndex=function(){return new x(this._store.createIndex.apply(this._store,arguments))},j.prototype.index=function(){return new x(this._store.index.apply(this._store,arguments))},N(j,"_store",["name","keyPath","indexNames","autoIncrement"]),D(j,"_store",IDBObjectStore,["put","add","delete","clear","get","getAll","getKey","getAllKeys","count"]),A(j,"_store",IDBObjectStore,["openCursor","openKeyCursor"]),P(j,"_store",IDBObjectStore,["deleteIndex"]),L.prototype.objectStore=function(){return new j(this._tx.objectStore.apply(this._tx,arguments))},N(L,"_tx",["objectStoreNames","mode"]),P(L,"_tx",IDBTransaction,["abort"]),R.prototype.createObjectStore=function(){return new j(this._db.createObjectStore.apply(this._db,arguments))},N(R,"_db",["name","version","objectStoreNames"]),P(R,"_db",IDBDatabase,["deleteObjectStore","close"]),F.prototype.transaction=function(){return new L(this._db.transaction.apply(this._db,arguments))},N(F,"_db",["name","version","objectStoreNames"]),P(F,"_db",IDBDatabase,["close"]),["openCursor","openKeyCursor"].forEach(function(i){[j,x].forEach(function(e){i in e.prototype&&(e.prototype[i.replace("open","iterate")]=function(){var e=(n=arguments,Array.prototype.slice.call(n)),t=e[e.length-1],n=this._store||this._index,r=n[i].apply(n,e.slice(0,-1));r.onsuccess=function(){t(r.result)}})})}),[x,j].forEach(function(e){e.prototype.getAll||(e.prototype.getAll=function(e,n){var r=this,i=[];return new Promise(function(t){r.iterateCursor(e,function(e){e?(i.push(e.value),void 0===n||i.length!=n?e.continue():t(i)):t(i)})})})});var M="0.4.32",B=1e4,H="w:"+M,q="FIS_v2",V="https://firebaseinstallations.googleapis.com/v1",G=36e5,K=((Re={})["missing-app-config-values"]='Missing App configuration value: "{$valueName}"',Re["not-registered"]="Firebase Installation is not registered.",Re["installation-not-found"]="Firebase Installation not found.",Re["request-failed"]='{$requestName} request failed with error "{$serverCode} {$serverStatus}: {$serverMessage}"',Re["app-offline"]="Could not process request. Application offline.",Re["delete-pending-registration"]="Can't delete installation while there is a pending registration request.",Re),U=new m("installations","Installations",K);function W(e){return e instanceof f&&e.code.includes("request-failed")}function $(e){e=e.projectId;return V+"/projects/"+e+"/installations"}function z(e){return{token:e.token,requestStatus:2,expiresIn:(e=e.expiresIn,Number(e.replace("s","000"))),creationTime:Date.now()}}function J(n,r){return p(this,void 0,void 0,function(){var t;return h(this,function(e){switch(e.label){case 0:return[4,r.json()];case 1:return t=e.sent(),t=t.error,[2,U.create("request-failed",{requestName:n,serverCode:t.code,serverMessage:t.message,serverStatus:t.status})]}})})}function Y(e){e=e.apiKey;return new Headers({"Content-Type":"application/json",Accept:"application/json","x-goog-api-key":e})}function X(e,t){t=t.refreshToken,e=Y(e);return e.append("Authorization",q+" "+t),e}function Z(n){return p(this,void 0,void 0,function(){var t;return h(this,function(e){switch(e.label){case 0:return[4,n()];case 1:return 500<=(t=e.sent()).status&&t.status<600?[2,n()]:[2,t]}})})}function Q(t){return new Promise(function(e){setTimeout(e,t)})}function ee(e){return btoa(String.fromCharCode.apply(String,u([],function(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||0a[0]&&t[1]=e.length?void 0:e)&&e[r++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function f(e,t){var n="function"==typeof Symbol&&e[Symbol.iterator];if(!n)return e;var r,i,o=n.call(e),a=[];try{for(;(void 0===t||0"})):"Error",e=this.serviceName+": "+e+" ("+o+").";return new c(o,e,i)},v);function v(e,t,n){this.service=e,this.serviceName=t,this.errors=n}var m=/\{\$([^}]+)}/g;function y(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function g(e,t){t=new b(e,t);return t.subscribe.bind(t)}var b=(I.prototype.next=function(t){this.forEachObserver(function(e){e.next(t)})},I.prototype.error=function(t){this.forEachObserver(function(e){e.error(t)}),this.close(t)},I.prototype.complete=function(){this.forEachObserver(function(e){e.complete()}),this.close()},I.prototype.subscribe=function(e,t,n){var r,i=this;if(void 0===e&&void 0===t&&void 0===n)throw new Error("Missing Observer.");void 0===(r=function(e,t){if("object"!=typeof e||null===e)return!1;for(var n=0,r=t;n=(null!=o?o:e.logLevel)&&a({level:R[t].toLowerCase(),message:i,args:n,type:e.name})}}(n[e])}var H=((H={})["no-app"]="No Firebase App '{$appName}' has been created - call Firebase App.initializeApp()",H["bad-app-name"]="Illegal App name: '{$appName}",H["duplicate-app"]="Firebase App named '{$appName}' already exists",H["app-deleted"]="Firebase App named '{$appName}' already deleted",H["invalid-app-argument"]="firebase.{$appName}() takes either no argument or a Firebase App instance.",H["invalid-log-argument"]="First argument to `onLog` must be null or a function.",H),V=new d("app","Firebase",H),B="@firebase/app",M="[DEFAULT]",U=((H={})[B]="fire-core",H["@firebase/analytics"]="fire-analytics",H["@firebase/app-check"]="fire-app-check",H["@firebase/auth"]="fire-auth",H["@firebase/database"]="fire-rtdb",H["@firebase/functions"]="fire-fn",H["@firebase/installations"]="fire-iid",H["@firebase/messaging"]="fire-fcm",H["@firebase/performance"]="fire-perf",H["@firebase/remote-config"]="fire-rc",H["@firebase/storage"]="fire-gcs",H["@firebase/firestore"]="fire-fst",H["fire-js"]="fire-js",H["firebase-wrapper"]="fire-js-all",H),W=new z("@firebase/app"),G=(Object.defineProperty($.prototype,"automaticDataCollectionEnabled",{get:function(){return this.checkDestroyed_(),this.automaticDataCollectionEnabled_},set:function(e){this.checkDestroyed_(),this.automaticDataCollectionEnabled_=e},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"name",{get:function(){return this.checkDestroyed_(),this.name_},enumerable:!1,configurable:!0}),Object.defineProperty($.prototype,"options",{get:function(){return this.checkDestroyed_(),this.options_},enumerable:!1,configurable:!0}),$.prototype.delete=function(){var t=this;return new Promise(function(e){t.checkDestroyed_(),e()}).then(function(){return t.firebase_.INTERNAL.removeApp(t.name_),Promise.all(t.container.getProviders().map(function(e){return e.delete()}))}).then(function(){t.isDeleted_=!0})},$.prototype._getService=function(e,t){void 0===t&&(t=M),this.checkDestroyed_();var n=this.container.getProvider(e);return n.isInitialized()||"EXPLICIT"!==(null===(e=n.getComponent())||void 0===e?void 0:e.instantiationMode)||n.initialize(),n.getImmediate({identifier:t})},$.prototype._removeServiceInstance=function(e,t){void 0===t&&(t=M),this.container.getProvider(e).clearInstance(t)},$.prototype._addComponent=function(t){try{this.container.addComponent(t)}catch(e){W.debug("Component "+t.name+" failed to register with FirebaseApp "+this.name,e)}},$.prototype._addOrOverwriteComponent=function(e){this.container.addOrOverwriteComponent(e)},$.prototype.toJSON=function(){return{name:this.name,automaticDataCollectionEnabled:this.automaticDataCollectionEnabled,options:this.options}},$.prototype.checkDestroyed_=function(){if(this.isDeleted_)throw V.create("app-deleted",{appName:this.name_})},$);function $(e,t,n){var r=this;this.firebase_=n,this.isDeleted_=!1,this.name_=t.name,this.automaticDataCollectionEnabled_=t.automaticDataCollectionEnabled||!1,this.options_=h(void 0,e),this.container=new S(t.name),this._addComponent(new O("app",function(){return r},"PUBLIC")),this.firebase_.INTERNAL.components.forEach(function(e){return r._addComponent(e)})}G.prototype.name&&G.prototype.options||G.prototype.delete||console.log("dc");var K="8.10.1";function Y(a){var s={},l=new Map,c={__esModule:!0,initializeApp:function(e,t){void 0===t&&(t={});"object"==typeof t&&null!==t||(t={name:t});var n=t;void 0===n.name&&(n.name=M);t=n.name;if("string"!=typeof t||!t)throw V.create("bad-app-name",{appName:String(t)});if(y(s,t))throw V.create("duplicate-app",{appName:t});n=new a(e,n,c);return s[t]=n},app:u,registerVersion:function(e,t,n){var r=null!==(i=U[e])&&void 0!==i?i:e;n&&(r+="-"+n);var i=r.match(/\s|\//),e=t.match(/\s|\//);i||e?(n=['Unable to register library "'+r+'" with version "'+t+'":'],i&&n.push('library name "'+r+'" contains illegal characters (whitespace or "/")'),i&&e&&n.push("and"),e&&n.push('version name "'+t+'" contains illegal characters (whitespace or "/")'),W.warn(n.join(" "))):o(new O(r+"-version",function(){return{library:r,version:t}},"VERSION"))},setLogLevel:T,onLog:function(e,t){if(null!==e&&"function"!=typeof e)throw V.create("invalid-log-argument");x(e,t)},apps:null,SDK_VERSION:K,INTERNAL:{registerComponent:o,removeApp:function(e){delete s[e]},components:l,useAsService:function(e,t){return"serverAuth"!==t?t:null}}};function u(e){if(!y(s,e=e||M))throw V.create("no-app",{appName:e});return s[e]}function o(n){var e,r=n.name;if(l.has(r))return W.debug("There were multiple attempts to register component "+r+"."),"PUBLIC"===n.type?c[r]:null;l.set(r,n),"PUBLIC"===n.type&&(e=function(e){if("function"!=typeof(e=void 0===e?u():e)[r])throw V.create("invalid-app-argument",{appName:r});return e[r]()},void 0!==n.serviceProps&&h(e,n.serviceProps),c[r]=e,a.prototype[r]=function(){for(var e=[],t=0;t 30) { - console.log('yuv: ' + parseInt('' + testSpeed[1] / testSpeed[0])); - testSpeed = [0, 0]; - } - return out; -} - -var currentFrame; -self.addEventListener('message', (e) => { - currentFrame = e.data; -}); - -function run() { - if (currentFrame) { - self.postMessage(I420ToARGB(currentFrame)); - currentFrame = undefined; - } - setTimeout(run, 1); -} - -run(); \ No newline at end of file diff --git a/flutter/web/v1/yuv.wasm b/flutter/web/v1/yuv.wasm deleted file mode 100644 index f203c685c93..00000000000 Binary files a/flutter/web/v1/yuv.wasm and /dev/null differ diff --git a/flutter/web/v2/README.md b/flutter/web/v2/README.md deleted file mode 100644 index 7c128776cee..00000000000 --- a/flutter/web/v2/README.md +++ /dev/null @@ -1 +0,0 @@ -Under dev. \ No newline at end of file diff --git a/flutter/windows/runner/Runner.rc b/flutter/windows/runner/Runner.rc index 7e0fe9b31f6..ab1b7e06fed 100644 --- a/flutter/windows/runner/Runner.rc +++ b/flutter/windows/runner/Runner.rc @@ -93,7 +93,7 @@ BEGIN VALUE "FileDescription", "RustDesk Remote Desktop" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "InternalName", "rustdesk" "\0" - VALUE "LegalCopyright", "Copyright © 2024 Purslane Ltd. All rights reserved." "\0" + VALUE "LegalCopyright", "Copyright © 2025 Purslane Ltd. All rights reserved." "\0" VALUE "OriginalFilename", "rustdesk.exe" "\0" VALUE "ProductName", "RustDesk" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" diff --git a/libs/clipboard/Cargo.toml b/libs/clipboard/Cargo.toml index c3673a9bd79..afe2f2f3137 100644 --- a/libs/clipboard/Cargo.toml +++ b/libs/clipboard/Cargo.toml @@ -34,7 +34,6 @@ parking_lot = {version = "0.12"} [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies] rand = {version = "0.8", optional = true} -fuser = {version = "0.13", optional = true} libc = {version = "0.2", optional = true} dashmap = {version ="5.5", optional = true} utf16string = {version = "0.2", optional = true} @@ -44,6 +43,15 @@ once_cell = {version = "1.18", optional = true} percent-encoding = {version ="2.3", optional = true} x11-clipboard = {git="https://github.com/clslaid/x11-clipboard", branch = "feat/store-batch", optional = true} x11rb = {version = "0.12", features = ["all-extensions"], optional = true} +fuser = {version = "0.15", default-features = false, optional = true} [target.'cfg(target_os = "macos")'.dependencies] cacao = {git="https://github.com/clslaid/cacao", branch = "feat/set-file-urls", optional = true} +# Use `relax-void-encoding`, as that allows us to pass `c_void` instead of implementing `Encode` correctly for `&CGImageRef` +objc2 = { version = "0.5.1", features = ["relax-void-encoding"] } +objc2-foundation = { version = "0.2.0", features = ["NSArray", "NSString", "NSEnumerator", "NSGeometry", "NSProgress"] } +objc2-app-kit = { version = "0.2.0", features = ["NSPasteboard", "NSPasteboardItem", "NSImage", "NSFilePromiseProvider"] } +uuid = { version = "1.3", features = ["v4"] } +fsevent = "2.1.2" +dirs = "5.0" +xattr = "1.4.0" diff --git a/libs/clipboard/src/context_send.rs b/libs/clipboard/src/context_send.rs index f3606509f01..caa9d4a4883 100644 --- a/libs/clipboard/src/context_send.rs +++ b/libs/clipboard/src/context_send.rs @@ -1,22 +1,29 @@ use hbb_common::{log, ResultType}; -use std::sync::Mutex; +use std::{ops::Deref, sync::Mutex}; use crate::CliprdrServiceContext; const CLIPBOARD_RESPONSE_WAIT_TIMEOUT_SECS: u32 = 30; lazy_static::lazy_static! { - static ref CONTEXT_SEND: ContextSend = ContextSend{addr: Mutex::new(None)}; + static ref CONTEXT_SEND: ContextSend = ContextSend::default(); } -pub struct ContextSend { - addr: Mutex>>, +#[derive(Default)] +pub struct ContextSend(Mutex>>); + +impl Deref for ContextSend { + type Target = Mutex>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } } impl ContextSend { #[inline] pub fn is_enabled() -> bool { - CONTEXT_SEND.addr.lock().unwrap().is_some() + CONTEXT_SEND.lock().unwrap().is_some() } pub fn set_is_stopped() { @@ -24,7 +31,7 @@ impl ContextSend { } pub fn enable(enabled: bool) { - let mut lock = CONTEXT_SEND.addr.lock().unwrap(); + let mut lock = CONTEXT_SEND.lock().unwrap(); if enabled { if lock.is_some() { return; @@ -49,7 +56,7 @@ impl ContextSend { /// make sure the clipboard context is enabled. pub fn make_sure_enabled() -> ResultType<()> { - let mut lock = CONTEXT_SEND.addr.lock().unwrap(); + let mut lock = CONTEXT_SEND.lock().unwrap(); if lock.is_some() { return Ok(()); } @@ -63,7 +70,7 @@ impl ContextSend { pub fn proc) -> ResultType<()>>( f: F, ) -> ResultType<()> { - let mut lock = CONTEXT_SEND.addr.lock().unwrap(); + let mut lock = CONTEXT_SEND.lock().unwrap(); match lock.as_mut() { Some(context) => f(context), None => Ok(()), diff --git a/libs/clipboard/src/lib.rs b/libs/clipboard/src/lib.rs index 30055740ed8..f28fe083da8 100644 --- a/libs/clipboard/src/lib.rs +++ b/libs/clipboard/src/lib.rs @@ -1,24 +1,32 @@ -#[allow(dead_code)] -use std::{ - path::PathBuf, - sync::{Arc, Mutex, RwLock}, -}; +use std::sync::{Arc, Mutex, RwLock}; -#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] -use hbb_common::{allow_err, bail}; +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] +use hbb_common::ResultType; +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +use hbb_common::{allow_err, log}; use hbb_common::{ lazy_static, tokio::sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, Mutex as TokioMutex, }, - ResultType, }; use serde_derive::{Deserialize, Serialize}; use thiserror::Error; +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] pub mod context_send; pub mod platform; +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] pub use context_send::*; #[cfg(target_os = "windows")] @@ -28,8 +36,19 @@ const ERR_CODE_INVALID_PARAMETER: u32 = 0x00000002; #[cfg(target_os = "windows")] const ERR_CODE_SEND_MSG: u32 = 0x00000003; +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] pub(crate) use platform::create_cliprdr_context; +pub struct ProgressPercent { + pub percent: f64, + pub is_canceled: bool, + pub is_failed: bool, +} + +// to-do: This trait may be removed, because unix file copy paste does not need it. /// Ability to handle Clipboard File from remote rustdesk client /// /// # Note @@ -41,9 +60,12 @@ pub trait CliprdrServiceContext: Send + Sync { fn set_is_stopped(&mut self) -> Result<(), CliprdrError>; /// clear the content on clipboard fn empty_clipboard(&mut self, conn_id: i32) -> Result; - /// run as a server for clipboard RPC fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError>; + /// get the progress of the paste task. + fn get_progress_percent(&self) -> Option; + /// cancel the paste task. + fn cancel(&mut self); } #[derive(Error, Debug)] @@ -62,10 +84,12 @@ pub enum CliprdrError { ConversionFailure, #[error("failure to read clipboard")] OpenClipboard, - #[error("failure to read file metadata or content")] - FileError { path: PathBuf, err: std::io::Error }, - #[error("invalid request")] + #[error("failure to read file metadata or content, path: {path}, err: {err}")] + FileError { path: String, err: std::io::Error }, + #[error("invalid request: {description}")] InvalidRequest { description: String }, + #[error("common request: {description}")] + CommonError { description: String }, #[error("unknown cliprdr error")] Unknown(u32), } @@ -107,6 +131,7 @@ pub enum ClipboardFile { stream_id: i32, requested_data: Vec, }, + TryEmpty, } struct MsgChannel { @@ -198,42 +223,67 @@ pub fn get_rx_cliprdr_server(conn_id: i32) -> Arc ResultType<()> { +pub fn send_data(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> { #[cfg(target_os = "windows")] return send_data_to_channel(conn_id, data); #[cfg(not(target_os = "windows"))] if conn_id == 0 { - send_data_to_all(data); + let _ = send_data_to_all(data); + Ok(()) } else { - send_data_to_channel(conn_id, data); + send_data_to_channel(conn_id, data) } } -#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste",))] + #[inline] -fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> ResultType<()> { +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] +fn send_data_to_channel(conn_id: i32, data: ClipboardFile) -> Result<(), CliprdrError> { if let Some(msg_channel) = VEC_MSG_CHANNEL .read() .unwrap() .iter() .find(|x| x.conn_id == conn_id) { - msg_channel.sender.send(data)?; - Ok(()) + msg_channel + .sender + .send(data) + .map_err(|e| CliprdrError::CommonError { + description: e.to_string(), + }) } else { - bail!("conn_id not found"); + Err(CliprdrError::InvalidRequest { + description: "conn_id not found".to_string(), + }) + } +} + +#[inline] +#[cfg(target_os = "windows")] +pub fn send_data_exclude(conn_id: i32, data: ClipboardFile) { + // Need more tests to see if it's necessary to handle the error. + for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { + if msg_channel.conn_id != conn_id { + allow_err!(msg_channel.sender.send(data.clone())); + } } } -#[cfg(feature = "unix-file-copy-paste")] #[inline] -fn send_data_to_all(data: ClipboardFile) -> ResultType<()> { +#[cfg(feature = "unix-file-copy-paste")] +fn send_data_to_all(data: ClipboardFile) { // Need more tests to see if it's necessary to handle the error. for msg_channel in VEC_MSG_CHANNEL.read().unwrap().iter() { allow_err!(msg_channel.sender.send(data.clone())); } - Ok(()) } #[cfg(test)] diff --git a/libs/clipboard/src/platform/mod.rs b/libs/clipboard/src/platform/mod.rs index 5db27112973..f54f4021b61 100644 --- a/libs/clipboard/src/platform/mod.rs +++ b/libs/clipboard/src/platform/mod.rs @@ -1,6 +1,3 @@ -#[cfg(any(target_os = "linux", target_os = "macos"))] -use crate::{CliprdrError, CliprdrServiceContext}; - #[cfg(target_os = "windows")] pub mod windows; #[cfg(target_os = "windows")] @@ -16,76 +13,14 @@ pub fn create_cliprdr_context( } #[cfg(feature = "unix-file-copy-paste")] -#[cfg(any(target_os = "linux", target_os = "macos"))] -/// use FUSE for file pasting on these platforms -pub mod fuse; -#[cfg(feature = "unix-file-copy-paste")] -#[cfg(any(target_os = "linux", target_os = "macos"))] pub mod unix; -#[cfg(any(target_os = "linux", target_os = "macos"))] + +#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] pub fn create_cliprdr_context( _enable_files: bool, _enable_others: bool, _response_wait_timeout_secs: u32, ) -> crate::ResultType> { - #[cfg(feature = "unix-file-copy-paste")] - { - use std::{fs::Permissions, os::unix::prelude::PermissionsExt}; - - use hbb_common::{config::APP_NAME, log}; - - if !_enable_files { - return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); - } - - let timeout = std::time::Duration::from_secs(_response_wait_timeout_secs as u64); - - let app_name = APP_NAME.read().unwrap().clone(); - - let mnt_path = format!("/tmp/{}/{}", app_name, "cliprdr"); - - // this function must be called after the main IPC is up - std::fs::create_dir(&mnt_path).ok(); - std::fs::set_permissions(&mnt_path, Permissions::from_mode(0o777)).ok(); - - log::info!("clear previously mounted cliprdr FUSE"); - if let Err(e) = std::process::Command::new("umount").arg(&mnt_path).status() { - log::warn!("umount {:?} may fail: {:?}", mnt_path, e); - } - - let unix_ctx = unix::ClipboardContext::new(timeout, mnt_path.parse()?)?; - log::debug!("start cliprdr FUSE"); - unix_ctx.run()?; - - Ok(Box::new(unix_ctx) as Box<_>) - } - - #[cfg(not(feature = "unix-file-copy-paste"))] - return Ok(Box::new(DummyCliprdrContext {}) as Box<_>); -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -struct DummyCliprdrContext {} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -impl CliprdrServiceContext for DummyCliprdrContext { - fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { - Ok(()) - } - fn empty_clipboard(&mut self, _conn_id: i32) -> Result { - Ok(true) - } - fn server_clip_file( - &mut self, - _conn_id: i32, - _msg: crate::ClipboardFile, - ) -> Result<(), crate::CliprdrError> { - Ok(()) - } + let boxed = unix::macos::pasteboard_context::create_pasteboard_context()? as Box<_>; + Ok(boxed) } - -#[cfg(feature = "unix-file-copy-paste")] -#[cfg(any(target_os = "linux", target_os = "macos"))] -// begin of epoch used by microsoft -// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00 -const LDAP_EPOCH_DELTA: u64 = 116444772610000000; diff --git a/libs/clipboard/src/platform/unix/filetype.rs b/libs/clipboard/src/platform/unix/filetype.rs new file mode 100644 index 00000000000..8436ba05ee2 --- /dev/null +++ b/libs/clipboard/src/platform/unix/filetype.rs @@ -0,0 +1,188 @@ +use super::{FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_UNIX_MODE, LDAP_EPOCH_DELTA}; +use crate::CliprdrError; +use hbb_common::{ + bytes::{Buf, Bytes}, + log, +}; +use serde_derive::{Deserialize, Serialize}; +use std::{ + path::PathBuf, + time::{Duration, SystemTime}, +}; +use utf16string::WStr; + +#[cfg(target_os = "linux")] +pub type Inode = u64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum FileType { + File, + Directory, + // todo: support symlink + Symlink, +} + +/// read only permission +pub const PERM_READ: u16 = 0o444; +/// read and write permission +pub const PERM_RW: u16 = 0o644; +/// only self can read and readonly +pub const PERM_SELF_RO: u16 = 0o400; +/// rwx +pub const PERM_RWX: u16 = 0o755; +#[allow(dead_code)] +/// max length of file name +pub const MAX_NAME_LEN: usize = 255; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FileDescription { + pub conn_id: i32, + pub name: PathBuf, + pub kind: FileType, + pub atime: SystemTime, + pub last_modified: SystemTime, + pub last_metadata_changed: SystemTime, + pub creation_time: SystemTime, + pub size: u64, + pub perm: u16, +} + +impl FileDescription { + fn parse_file_descriptor( + bytes: &mut Bytes, + conn_id: i32, + ) -> Result { + let flags = bytes.get_u32_le(); + // skip reserved 32 bytes + bytes.advance(32); + let attributes = bytes.get_u32_le(); + + // in original specification, this is 16 bytes reserved + // we use the last 4 bytes to store the file mode + // skip reserved 12 bytes + bytes.advance(12); + let perm = bytes.get_u32_le() as u16; + + // last write time from 1601-01-01 00:00:00, in 100ns + let last_write_time = bytes.get_u64_le(); + // file size + let file_size_high = bytes.get_u32_le(); + let file_size_low = bytes.get_u32_le(); + // utf16 file name, double \0 terminated, in 520 bytes block + // read with another pointer, and advance the main pointer + let block = bytes.clone(); + bytes.advance(520); + + let block = &block[..520]; + let wstr = WStr::from_utf16le(block).map_err(|e| { + log::error!("cannot convert file descriptor path: {:?}", e); + CliprdrError::ConversionFailure + })?; + + let from_unix = flags & FLAGS_FD_UNIX_MODE != 0; + + let valid_attributes = flags & FLAGS_FD_ATTRIBUTES != 0; + if !valid_attributes { + return Err(CliprdrError::InvalidRequest { + description: "file description must have valid attributes".to_string(), + }); + } + + // todo: check normal, hidden, system, readonly, archive... + let directory = attributes & 0x10 != 0; + let normal = attributes == 0x80; + let hidden = attributes & 0x02 != 0; + let readonly = attributes & 0x01 != 0; + + let perm = if from_unix { + // as is + perm + // cannot set as is... + } else if normal { + PERM_RWX + } else if readonly { + PERM_READ + } else if hidden { + PERM_SELF_RO + } else if directory { + PERM_RWX + } else { + PERM_RW + }; + + let kind = if directory { + FileType::Directory + } else { + FileType::File + }; + + // to-do: use `let valid_size = flags & FLAGS_FD_SIZE != 0;` + // We use `true` to for compatibility with Windows. + // let valid_size = flags & FLAGS_FD_SIZE != 0; + let valid_size = true; + let size = if valid_size { + ((file_size_high as u64) << 32) + file_size_low as u64 + } else { + 0 + }; + + let valid_write_time = flags & FLAGS_FD_LAST_WRITE != 0; + let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA { + let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100; + let last_write_time = Duration::from_nanos(last_write_time); + SystemTime::UNIX_EPOCH + last_write_time + } else { + SystemTime::UNIX_EPOCH + }; + + let name = wstr.to_utf8().replace('\\', "/"); + let name = PathBuf::from(name.trim_end_matches('\0')); + + let desc = FileDescription { + conn_id, + name, + kind, + atime: last_modified, + last_modified, + last_metadata_changed: last_modified, + creation_time: last_modified, + size, + perm, + }; + + Ok(desc) + } + + /// parse file descriptions from a format data response PDU + /// which containing a CSPTR_FILEDESCRIPTORW indicated format data + pub fn parse_file_descriptors( + file_descriptor_pdu: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let mut data = Bytes::from(file_descriptor_pdu); + if data.remaining() < 4 { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with infficient length".to_string(), + }); + } + + let count = data.get_u32_le() as usize; + if data.remaining() == 0 && count == 0 { + return Ok(Vec::new()); + } + + if data.remaining() != 592 * count { + return Err(CliprdrError::InvalidRequest { + description: "file descriptor request with invalid length".to_string(), + }); + } + + let mut files = Vec::with_capacity(count); + for _ in 0..count { + let desc = Self::parse_file_descriptor(&mut data, conn_id)?; + files.push(desc); + } + + Ok(files) + } +} diff --git a/libs/clipboard/src/platform/fuse.rs b/libs/clipboard/src/platform/unix/fuse/cs.rs similarity index 82% rename from libs/clipboard/src/platform/fuse.rs rename to libs/clipboard/src/platform/unix/fuse/cs.rs index c5fe60f56e3..0f1cf873936 100644 --- a/libs/clipboard/src/platform/fuse.rs +++ b/libs/clipboard/src/platform/unix/fuse/cs.rs @@ -31,33 +31,29 @@ use std::{ }; use fuser::{ReplyDirectory, FUSE_ROOT_ID}; -use hbb_common::{ - bytes::{Buf, Bytes}, - log, -}; +use hbb_common::log; use parking_lot::{Condvar, Mutex}; -use utf16string::WStr; - -use crate::{send_data, ClipboardFile, CliprdrError}; -use super::LDAP_EPOCH_DELTA; +use crate::{ + platform::unix::{ + filetype::{FileDescription, FileType, Inode, MAX_NAME_LEN, PERM_RWX}, + BLOCK_SIZE, + }, + send_data, ClipboardFile, CliprdrError, +}; /// fuse server ready retry max times const READ_RETRY: i32 = 3; -/// block size for fuse, align to our asynchronic request size over FileContentsRequest. -pub const BLOCK_SIZE: u32 = 4 * 1024 * 1024; - -/// read only permission -const PERM_READ: u16 = 0o444; -/// read and write permission -const PERM_RW: u16 = 0o644; -/// only self can read and readonly -const PERM_SELF_RO: u16 = 0o400; -/// rwx -const PERM_RWX: u16 = 0o755; -/// max length of file name -const MAX_NAME_LEN: usize = 255; +impl From for fuser::FileType { + fn from(value: FileType) -> Self { + match value { + FileType::File => Self::RegularFile, + FileType::Directory => Self::Directory, + FileType::Symlink => Self::Symlink, + } + } +} /// fuse client /// this is a proxy to the fuse server @@ -150,9 +146,15 @@ impl fuser::Filesystem for FuseClient { server.release(req, ino, fh, _flags, _lock_owner, _flush, reply) } - fn getattr(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { + fn getattr( + &mut self, + req: &fuser::Request<'_>, + ino: u64, + fh: Option, + reply: fuser::ReplyAttr, + ) { let mut server = self.server.lock(); - server.getattr(req, ino, reply) + server.getattr(req, ino, fh, reply) } fn statfs(&mut self, req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyStatfs) { @@ -247,7 +249,6 @@ impl fuser::Filesystem for FuseServer { if parent_entry.attributes.kind != FileType::Directory { log::error!("fuse: parent is not a directory"); - reply.error(libc::ENOTDIR); return; } @@ -480,7 +481,13 @@ impl fuser::Filesystem for FuseServer { reply.ok(); } - fn getattr(&mut self, _req: &fuser::Request<'_>, ino: u64, reply: fuser::ReplyAttr) { + fn getattr( + &mut self, + _req: &fuser::Request<'_>, + ino: u64, + _fh: Option, + reply: fuser::ReplyAttr, + ) { let files = &self.files; let Some(entry) = files.get(ino as usize - 1) else { reply.error(libc::ENOENT); @@ -527,14 +534,6 @@ impl FuseServer { size: u32, ) -> Result, std::io::Error> { // todo: async and concurrent read, generate stream_id per request - log::debug!( - "reading {:?} offset {} size {} on stream: {}", - node.name, - offset, - size, - node.stream_id - ); - let cb_requested = unsafe { // convert `size` from u32 to i32 // yet with same bit representation @@ -554,16 +553,14 @@ impl FuseServer { clip_data_id: 0, }; - send_data(node.conn_id, request.clone()); - - log::debug!( - "waiting for read reply for {:?} on stream: {}", - node.name, - node.stream_id - ); + send_data(node.conn_id, request.clone()).map_err(|e| { + log::error!("failed to send file list to channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; let mut retry_times = 0; + // to-do: more tests needed loop { let reply = self.rx.recv_timeout(self.timeout).map_err(|e| { log::error!("failed to receive file list from channel: {:?}", e); @@ -590,7 +587,10 @@ impl FuseServer { )); } - send_data(node.conn_id, request.clone()); + send_data(node.conn_id, request.clone()).map_err(|e| { + log::error!("failed to send file list to channel: {:?}", e); + std::io::Error::new(std::io::ErrorKind::Other, e) + })?; continue; } return Ok(requested_data); @@ -605,160 +605,6 @@ impl FuseServer { } } } - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FileDescription { - pub conn_id: i32, - pub name: PathBuf, - pub kind: FileType, - pub atime: SystemTime, - pub last_modified: SystemTime, - pub last_metadata_changed: SystemTime, - pub creation_time: SystemTime, - - pub size: u64, - - pub perm: u16, -} - -impl FileDescription { - fn parse_file_descriptor( - bytes: &mut Bytes, - conn_id: i32, - ) -> Result { - let flags = bytes.get_u32_le(); - // skip reserved 32 bytes - bytes.advance(32); - let attributes = bytes.get_u32_le(); - - // in original specification, this is 16 bytes reserved - // we use the last 4 bytes to store the file mode - // skip reserved 12 bytes - bytes.advance(12); - let perm = bytes.get_u32_le() as u16; - - // last write time from 1601-01-01 00:00:00, in 100ns - let last_write_time = bytes.get_u64_le(); - // file size - let file_size_high = bytes.get_u32_le(); - let file_size_low = bytes.get_u32_le(); - // utf16 file name, double \0 terminated, in 520 bytes block - // read with another pointer, and advance the main pointer - let block = bytes.clone(); - bytes.advance(520); - - let block = &block[..520]; - let wstr = WStr::from_utf16le(block).map_err(|e| { - log::error!("cannot convert file descriptor path: {:?}", e); - CliprdrError::ConversionFailure - })?; - - let from_unix = flags & 0x08 != 0; - - let valid_attributes = flags & 0x04 != 0; - if !valid_attributes { - return Err(CliprdrError::InvalidRequest { - description: "file description must have valid attributes".to_string(), - }); - } - - // todo: check normal, hidden, system, readonly, archive... - let directory = attributes & 0x10 != 0; - let normal = attributes == 0x80; - let hidden = attributes & 0x02 != 0; - let readonly = attributes & 0x01 != 0; - - let perm = if from_unix { - // as is - perm - // cannot set as is... - } else if normal { - PERM_RWX - } else if readonly { - PERM_READ - } else if hidden { - PERM_SELF_RO - } else if directory { - PERM_RWX - } else { - PERM_RW - }; - - let kind = if directory { - FileType::Directory - } else { - FileType::File - }; - - let valid_size = flags & 0x40 != 0; - let size = if valid_size { - ((file_size_high as u64) << 32) + file_size_low as u64 - } else { - 0 - }; - - let valid_write_time = flags & 0x20 != 0; - let last_modified = if valid_write_time && last_write_time >= LDAP_EPOCH_DELTA { - let last_write_time = (last_write_time - LDAP_EPOCH_DELTA) * 100; - let last_write_time = Duration::from_nanos(last_write_time); - SystemTime::UNIX_EPOCH + last_write_time - } else { - SystemTime::UNIX_EPOCH - }; - - let name = wstr.to_utf8().replace('\\', "/"); - let name = PathBuf::from(name.trim_end_matches('\0')); - - let desc = FileDescription { - conn_id, - name, - kind, - atime: last_modified, - last_modified, - last_metadata_changed: last_modified, - - creation_time: last_modified, - size, - perm, - }; - - Ok(desc) - } - - /// parse file descriptions from a format data response PDU - /// which containing a CSPTR_FILEDESCRIPTORW indicated format data - pub fn parse_file_descriptors( - file_descriptor_pdu: Vec, - conn_id: i32, - ) -> Result, CliprdrError> { - let mut data = Bytes::from(file_descriptor_pdu); - if data.remaining() < 4 { - return Err(CliprdrError::InvalidRequest { - description: "file descriptor request with infficient length".to_string(), - }); - } - - let count = data.get_u32_le() as usize; - if data.remaining() == 0 && count == 0 { - return Ok(Vec::new()); - } - - if data.remaining() != 592 * count { - return Err(CliprdrError::InvalidRequest { - description: "file descriptor request with invalid length".to_string(), - }); - } - - let mut files = Vec::with_capacity(count); - for _ in 0..count { - let desc = Self::parse_file_descriptor(&mut data, conn_id)?; - files.push(desc); - } - - Ok(files) - } -} - /// a node in the FUSE file tree #[derive(Debug)] struct FuseNode { @@ -881,7 +727,7 @@ impl FuseNode { format!("invalid file name {}", file.name.display()), ); CliprdrError::FileError { - path: file.name.clone(), + path: file.name.to_string_lossy().to_string(), err, } })?; @@ -902,26 +748,6 @@ impl FuseNode { } } -pub type Inode = u64; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FileType { - File, - Directory, - // todo: support symlink - Symlink, -} - -impl From for fuser::FileType { - fn from(value: FileType) -> Self { - match value { - FileType::File => Self::RegularFile, - FileType::Directory => Self::Directory, - FileType::Symlink => Self::Symlink, - } - } -} - #[derive(Debug, Clone)] pub struct InodeAttributes { inode: Inode, @@ -1064,8 +890,6 @@ impl FileHandles { #[cfg(test)] mod fuse_test { - use std::str::FromStr; - use super::*; // todo: more tests needed! diff --git a/libs/clipboard/src/platform/unix/fuse/mod.rs b/libs/clipboard/src/platform/unix/fuse/mod.rs new file mode 100644 index 00000000000..df743004fb2 --- /dev/null +++ b/libs/clipboard/src/platform/unix/fuse/mod.rs @@ -0,0 +1,225 @@ +mod cs; + +use super::filetype::FileDescription; +use crate::{ClipboardFile, CliprdrError}; +use cs::FuseServer; +use fuser::MountOption; +use hbb_common::{config::APP_NAME, log}; +use parking_lot::Mutex; +use std::{ + path::PathBuf, + sync::{mpsc::Sender, Arc}, + time::Duration, +}; + +lazy_static::lazy_static! { + static ref FUSE_MOUNT_POINT_CLIENT: Arc = { + let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-client"); + // No need to run `canonicalize()` here. + Arc::new(mnt_path) + }; + + static ref FUSE_MOUNT_POINT_SERVER: Arc = { + let mnt_path = format!("/tmp/{}/{}", APP_NAME.read().unwrap(), "cliprdr-server"); + // No need to run `canonicalize()` here. + Arc::new(mnt_path) + }; + + static ref FUSE_CONTEXT_CLIENT: Arc>> = Arc::new(Mutex::new(None)); + static ref FUSE_CONTEXT_SERVER: Arc>> = Arc::new(Mutex::new(None)); +} + +static FUSE_TIMEOUT: Duration = Duration::from_secs(3); + +pub fn get_exclude_paths(is_client: bool) -> Arc { + if is_client { + FUSE_MOUNT_POINT_CLIENT.clone() + } else { + FUSE_MOUNT_POINT_SERVER.clone() + } +} + +pub fn is_fuse_context_inited(is_client: bool) -> bool { + if is_client { + FUSE_CONTEXT_CLIENT.lock().is_some() + } else { + FUSE_CONTEXT_SERVER.lock().is_some() + } +} + +pub fn init_fuse_context(is_client: bool) -> Result<(), CliprdrError> { + let mut fuse_context_lock = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + if fuse_context_lock.is_some() { + return Ok(()); + } + let mount_point = if is_client { + FUSE_MOUNT_POINT_CLIENT.clone() + } else { + FUSE_MOUNT_POINT_SERVER.clone() + }; + + let mount_point = std::path::PathBuf::from(&*mount_point); + let (server, tx) = FuseServer::new(FUSE_TIMEOUT); + let server = Arc::new(Mutex::new(server)); + + prepare_fuse_mount_point(&mount_point); + let mnt_opts = [ + MountOption::FSName("rustdesk-cliprdr-fs".to_string()), + MountOption::NoAtime, + MountOption::RO, + ]; + log::info!("mounting clipboard FUSE to {}", mount_point.display()); + // to-do: ignore the error if the mount point is already mounted + // Because the sciter version uses separate processes as the controlling side. + let session = fuser::spawn_mount2( + FuseServer::client(server.clone()), + mount_point.clone(), + &mnt_opts, + ) + .map_err(|e| { + log::error!("failed to mount cliprdr fuse: {:?}", e); + CliprdrError::CliprdrInit + })?; + let session = Mutex::new(Some(session)); + + let ctx = FuseContext { + server, + tx, + mount_point, + session, + conn_id: 0, + }; + *fuse_context_lock = Some(ctx); + Ok(()) +} + +pub fn uninit_fuse_context(is_client: bool) { + uninit_fuse_context_(is_client) +} + +pub fn format_data_response_to_urls( + is_client: bool, + format_data: Vec, + conn_id: i32, +) -> Result, CliprdrError> { + let mut ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_mut() + .ok_or(CliprdrError::CliprdrInit)? + .format_data_response_to_urls(format_data, conn_id) +} + +pub fn handle_file_content_response( + is_client: bool, + clip: ClipboardFile, +) -> Result<(), CliprdrError> { + // we don't know its corresponding request, no resend can be performed + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .ok_or(CliprdrError::CliprdrInit)? + .tx + .send(clip) + .map_err(|e| { + log::error!("failed to send file contents response to fuse: {:?}", e); + CliprdrError::ClipboardInternalError + })?; + Ok(()) +} + +pub fn empty_local_files(is_client: bool, conn_id: i32) -> bool { + let ctx = if is_client { + FUSE_CONTEXT_CLIENT.lock() + } else { + FUSE_CONTEXT_SERVER.lock() + }; + ctx.as_ref() + .map(|c| c.empty_local_files(conn_id)) + .unwrap_or(false) +} + +struct FuseContext { + server: Arc>, + tx: Sender, + mount_point: PathBuf, + // stores fuse background session handle + session: Mutex>, + // Indicates the connection ID of that set the clipboard content + conn_id: i32, +} + +// this function must be called after the main IPC is up +fn prepare_fuse_mount_point(mount_point: &PathBuf) { + use std::{ + fs::{self, Permissions}, + os::unix::prelude::PermissionsExt, + }; + + fs::create_dir(mount_point).ok(); + fs::set_permissions(mount_point, Permissions::from_mode(0o777)).ok(); + + if let Err(e) = std::process::Command::new("umount") + .arg(mount_point) + .status() + { + log::warn!("umount {:?} may fail: {:?}", mount_point, e); + } +} + +fn uninit_fuse_context_(is_client: bool) { + if is_client { + let _ = FUSE_CONTEXT_CLIENT.lock().take(); + } else { + let _ = FUSE_CONTEXT_SERVER.lock().take(); + } +} + +impl Drop for FuseContext { + fn drop(&mut self) { + self.session.lock().take().map(|s| s.join()); + log::info!("unmounting clipboard FUSE from {}", self.mount_point.display()); + } +} + +impl FuseContext { + pub fn empty_local_files(&self, conn_id: i32) -> bool { + if conn_id != 0 && self.conn_id != conn_id { + return false; + } + let mut fuse_guard = self.server.lock(); + let _ = fuse_guard.load_file_list(vec![]); + true + } + + pub fn format_data_response_to_urls( + &mut self, + format_data: Vec, + conn_id: i32, + ) -> Result, CliprdrError> { + let files = FileDescription::parse_file_descriptors(format_data, conn_id)?; + + let paths = { + let mut fuse_guard = self.server.lock(); + fuse_guard.load_file_list(files)?; + self.conn_id = conn_id; + + fuse_guard.list_root() + }; + + let prefix = self.mount_point.clone(); + Ok(paths + .into_iter() + .map(|p| prefix.join(p).to_string_lossy().to_string()) + .collect()) + } +} diff --git a/libs/clipboard/src/platform/unix/local_file.rs b/libs/clipboard/src/platform/unix/local_file.rs index e24712efa4d..11d62cad8ee 100644 --- a/libs/clipboard/src/platform/unix/local_file.rs +++ b/libs/clipboard/src/platform/unix/local_file.rs @@ -1,38 +1,29 @@ +use super::{BLOCK_SIZE, LDAP_EPOCH_DELTA}; +use crate::{ + platform::unix::{ + FLAGS_FD_ATTRIBUTES, FLAGS_FD_LAST_WRITE, FLAGS_FD_PROGRESSUI, FLAGS_FD_SIZE, + FLAGS_FD_UNIX_MODE, + }, + CliprdrError, +}; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; use std::{ collections::HashSet, fs::File, io::{BufRead, BufReader, Read, Seek}, os::unix::prelude::PermissionsExt, - path::PathBuf, + path::{Path, PathBuf}, sync::atomic::{AtomicU64, Ordering}, time::SystemTime, }; - -use hbb_common::{ - bytes::{BufMut, BytesMut}, - log, -}; use utf16string::WString; -use crate::{ - platform::{fuse::BLOCK_SIZE, LDAP_EPOCH_DELTA}, - CliprdrError, -}; - -/// has valid file attributes -const FLAGS_FD_ATTRIBUTES: u32 = 0x04; -/// has valid file size -const FLAGS_FD_SIZE: u32 = 0x40; -/// has valid last write time -const FLAGS_FD_LAST_WRITE: u32 = 0x20; -/// show progress -const FLAGS_FD_PROGRESSUI: u32 = 0x4000; -/// transferred from unix, contains file mode -/// P.S. this flag is not used in windows -const FLAGS_FD_UNIX_MODE: u32 = 0x08; - #[derive(Debug)] pub(super) struct LocalFile { + pub relative_root: PathBuf, pub path: PathBuf, pub handle: Option>, @@ -51,9 +42,9 @@ pub(super) struct LocalFile { } impl LocalFile { - pub fn try_open(path: &PathBuf) -> Result { + pub fn try_open(relative_root: &Path, path: &Path) -> Result { let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { - path: path.clone(), + path: path.to_string_lossy().to_string(), err: e, })?; let size = mt.len() as u64; @@ -79,7 +70,8 @@ impl LocalFile { Ok(Self { name, - path: path.clone(), + relative_root: relative_root.to_path_buf(), + path: path.to_path_buf(), handle, offset, size, @@ -121,7 +113,12 @@ impl LocalFile { let size_high = (self.size >> 32) as u32; let size_low = (self.size & (u32::MAX as u64)) as u32; - let path = self.path.to_string_lossy().to_string(); + let path = self + .path + .strip_prefix(&self.relative_root) + .unwrap_or(&self.path) + .to_string_lossy() + .into_owned(); let wstr: WString = WString::from(&path); let name = wstr.as_bytes(); @@ -172,12 +169,12 @@ impl LocalFile { pub fn load_handle(&mut self) -> Result<(), CliprdrError> { if !self.is_dir && self.handle.is_none() { let handle = std::fs::File::open(&self.path).map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; let mut reader = BufReader::with_capacity(BLOCK_SIZE as usize * 2, handle); reader.fill_buf().map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; self.handle = Some(reader); @@ -188,20 +185,25 @@ impl LocalFile { pub fn read_exact_at(&mut self, buf: &mut [u8], offset: u64) -> Result<(), CliprdrError> { self.load_handle()?; - let handle = self.handle.as_mut()?; + let Some(handle) = self.handle.as_mut() else { + return Err(CliprdrError::FileError { + path: self.path.to_string_lossy().to_string(), + err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle not found"), + }); + }; if offset != self.offset.load(Ordering::Relaxed) { handle .seek(std::io::SeekFrom::Start(offset)) .map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; } handle .read_exact(buf) .map_err(|e| CliprdrError::FileError { - path: self.path.clone(), + path: self.path.to_string_lossy().to_string(), err: e, })?; let new_offset = offset + (buf.len() as u64); @@ -219,7 +221,8 @@ impl LocalFile { pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, CliprdrError> { fn constr_file_lst( - path: &PathBuf, + relative_root: &Path, + path: &Path, file_list: &mut Vec, visited: &mut HashSet, ) -> Result<(), CliprdrError> { @@ -227,22 +230,28 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, C if visited.contains(path) { return Ok(()); } - visited.insert(path.clone()); + visited.insert(path.to_path_buf()); - let local_file = LocalFile::try_open(path)?; + let local_file = LocalFile::try_open(relative_root, path)?; file_list.push(local_file); let mt = std::fs::metadata(path).map_err(|e| CliprdrError::FileError { - path: path.clone(), + path: path.to_string_lossy().to_string(), err: e, })?; if mt.is_dir() { - let dir = std::fs::read_dir(path)?; + let dir = std::fs::read_dir(path).map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; for entry in dir { - let entry = entry?; + let entry = entry.map_err(|e| CliprdrError::FileError { + path: path.to_string_lossy().to_string(), + err: e, + })?; let path = entry.path(); - constr_file_lst(&path, file_list, visited)?; + constr_file_lst(relative_root, &path, file_list, visited)?; } } Ok(()) @@ -251,8 +260,18 @@ pub(super) fn construct_file_list(paths: &[PathBuf]) -> Result, C let mut file_list = Vec::new(); let mut visited = HashSet::new(); + let relative_root = paths + .first() + .ok_or(CliprdrError::InvalidRequest { + description: "empty file list".to_string(), + })? + .parent() + .ok_or(CliprdrError::InvalidRequest { + description: "empty parent".to_string(), + })? + .to_path_buf(); for path in paths { - constr_file_lst(path, &mut file_list, &mut visited)?; + constr_file_lst(&relative_root, path, &mut file_list, &mut visited)?; } Ok(file_list) } @@ -263,7 +282,7 @@ mod file_list_test { use hbb_common::bytes::{BufMut, BytesMut}; - use crate::{platform::fuse::FileDescription, CliprdrError}; + use crate::{platform::unix::filetype::FileDescription, CliprdrError}; use super::LocalFile; @@ -277,6 +296,7 @@ mod file_list_test { #[inline] fn generate_file(path: &str, name: &str, is_dir: bool) -> LocalFile { LocalFile { + relative_root: PathBuf::from("."), path: PathBuf::from(path), handle: None, name: name.to_string(), diff --git a/libs/clipboard/src/platform/unix/macos/README.md b/libs/clipboard/src/platform/unix/macos/README.md new file mode 100644 index 00000000000..5c1cc5c909e --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/README.md @@ -0,0 +1,25 @@ +# File pate on macOS + +MacOS cannot use `fuse` because of [macfuse is not supported by default](https://github.com/macfuse/macfuse/wiki/Getting-Started#enabling-support-for-third-party-kernel-extensions-apple-silicon-macs). + +1. Use a temporary file `/tmp/rustdesk_` as a placeholder in the pasteboard. +2. Uses `fsevent` to observe files paste operation. Then perform pasting files. + +## Files + +### `pasteboard_context.rs` + +The context manager of the paste operations. + +### `item_data_provider.rs` + +1. Set pasteboard item. +2. Create temp file in `/tmp/.rustdesk_*`. + +### `paste_observer.rs` + +Use `fsevent` to observe the paste operation with the source file `/tmp/.rustdesk_*`. + +### `paste_task.rs` + +Perform the paste. diff --git a/libs/clipboard/src/platform/unix/macos/item_data_provider.rs b/libs/clipboard/src/platform/unix/macos/item_data_provider.rs new file mode 100644 index 00000000000..95036312ed6 --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/item_data_provider.rs @@ -0,0 +1,77 @@ +use super::pasteboard_context::{PasteObserverInfo, TEMP_FILE_PREFIX}; +use objc2::{ + declare_class, msg_send_id, mutability, + rc::Id, + runtime::{NSObject, NSObjectProtocol}, + ClassType, DeclaredClass, +}; +use objc2_app_kit::{ + NSPasteboard, NSPasteboardItem, NSPasteboardItemDataProvider, NSPasteboardType, + NSPasteboardTypeFileURL, +}; +use objc2_foundation::NSString; +use std::{io::Result, sync::mpsc::Sender}; + +pub(super) struct Ivars { + task_info: PasteObserverInfo, + tx: Sender>, +} + +declare_class!( + pub(super) struct PasteboardFileUrlProvider; + + unsafe impl ClassType for PasteboardFileUrlProvider { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + const NAME: &'static str = "PasteboardFileUrlProvider"; + } + + impl DeclaredClass for PasteboardFileUrlProvider { + type Ivars = Ivars; + } + + unsafe impl NSObjectProtocol for PasteboardFileUrlProvider {} + + unsafe impl NSPasteboardItemDataProvider for PasteboardFileUrlProvider { + #[method(pasteboard:item:provideDataForType:)] + #[allow(non_snake_case)] + unsafe fn pasteboard_item_provideDataForType( + &self, + _pasteboard: Option<&NSPasteboard>, + item: &NSPasteboardItem, + r#type: &NSPasteboardType, + ) { + if r#type == NSPasteboardTypeFileURL { + let path = format!("/tmp/{}{}", TEMP_FILE_PREFIX, uuid::Uuid::new_v4().to_string()); + match std::fs::File::create(&path) { + Ok(_) => { + let url = format!("file:///{}", &path); + item.setString_forType(&NSString::from_str(&url), &NSPasteboardTypeFileURL); + let mut task_info = self.ivars().task_info.clone(); + task_info.source_path = path; + self.ivars().tx.send(Ok(task_info)).ok(); + } + Err(e) => { + self.ivars().tx.send(Err(e)).ok(); + } + } + } + } + + // #[method(pasteboardFinishedWithDataProvider:)] + // unsafe fn pasteboardFinishedWithDataProvider(&self, pasteboard: &NSPasteboard) { + // } + } + + unsafe impl PasteboardFileUrlProvider {} +); + +pub(super) fn create_pasteboard_file_url_provider( + task_info: PasteObserverInfo, + tx: Sender>, +) -> Id { + let provider = PasteboardFileUrlProvider::alloc(); + let provider = provider.set_ivars(Ivars { task_info, tx }); + let provider: Id = unsafe { msg_send_id![super(provider), init] }; + provider +} diff --git a/libs/clipboard/src/platform/unix/macos/mod.rs b/libs/clipboard/src/platform/unix/macos/mod.rs new file mode 100644 index 00000000000..8b114aa17a8 --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/mod.rs @@ -0,0 +1,14 @@ +mod item_data_provider; +mod paste_observer; +mod paste_task; +pub mod pasteboard_context; + +pub fn should_handle_msg(msg: &crate::ClipboardFile) -> bool { + matches!( + msg, + crate::ClipboardFile::FormatList { .. } + | crate::ClipboardFile::FormatDataResponse { .. } + | crate::ClipboardFile::FileContentsResponse { .. } + | crate::ClipboardFile::TryEmpty + ) +} diff --git a/libs/clipboard/src/platform/unix/macos/paste-files-macos.png b/libs/clipboard/src/platform/unix/macos/paste-files-macos.png new file mode 100644 index 00000000000..73e4e3f0b69 Binary files /dev/null and b/libs/clipboard/src/platform/unix/macos/paste-files-macos.png differ diff --git a/libs/clipboard/src/platform/unix/macos/paste_observer.rs b/libs/clipboard/src/platform/unix/macos/paste_observer.rs new file mode 100644 index 00000000000..01e8b6c10df --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/paste_observer.rs @@ -0,0 +1,179 @@ +use super::pasteboard_context::PasteObserverInfo; +use fsevent::{self, StreamFlags}; +use hbb_common::{bail, log, ResultType}; +use std::{ + sync::{ + mpsc::{channel, Receiver, RecvTimeoutError, Sender}, + Arc, Mutex, + }, + thread, + time::Duration, +}; + +enum FseventControl { + Start, + Stop, + Exit, +} + +struct FseventThreadInfo { + tx: Sender, + handle: thread::JoinHandle<()>, +} + +pub struct PasteObserver { + exit: Arc>, + observer_info: Arc>>, + tx_handle_fsevent_thread: Option, + handle_observer_thread: Option>, +} + +impl Drop for PasteObserver { + fn drop(&mut self) { + *self.exit.lock().unwrap() = true; + if let Some(handle_observer_thread) = self.handle_observer_thread.take() { + handle_observer_thread.join().ok(); + } + if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.take() { + tx_handle_fsevent_thread.tx.send(FseventControl::Exit).ok(); + tx_handle_fsevent_thread.handle.join().ok(); + } + } +} + +impl PasteObserver { + const OBSERVE_TIMEOUT: Duration = Duration::from_secs(30); + + pub fn new() -> Self { + Self { + exit: Arc::new(Mutex::new(false)), + observer_info: Default::default(), + tx_handle_fsevent_thread: None, + handle_observer_thread: None, + } + } + + pub fn init(&mut self, cb_pasted: fn(&PasteObserverInfo) -> ()) -> ResultType<()> { + let Some(home_dir) = dirs::home_dir() else { + bail!("No home dir is set, do not observe."); + }; + + let (tx_observer, rx_observer) = channel::(); + let handle_observer = Self::init_thread_observer( + self.exit.clone(), + self.observer_info.clone(), + rx_observer, + cb_pasted, + ); + self.handle_observer_thread = Some(handle_observer); + let (tx_control, rx_control) = channel::(); + let handle_fsevent = Self::init_thread_fsevent( + home_dir.to_string_lossy().to_string(), + tx_observer, + rx_control, + ); + self.tx_handle_fsevent_thread = Some(FseventThreadInfo { + tx: tx_control, + handle: handle_fsevent, + }); + Ok(()) + } + + #[inline] + fn get_file_from_path(path: &String) -> String { + let last_slash = path.rfind('/').or_else(|| path.rfind('\\')); + match last_slash { + Some(index) => path[index + 1..].to_string(), + None => path.clone(), + } + } + + fn init_thread_observer( + exit: Arc>, + observer_info: Arc>>, + rx_observer: Receiver, + cb_pasted: fn(&PasteObserverInfo) -> (), + ) -> thread::JoinHandle<()> { + thread::spawn(move || loop { + match rx_observer.recv_timeout(Duration::from_millis(300)) { + Ok(event) => { + if (event.flag & StreamFlags::ITEM_CREATED) != StreamFlags::NONE + && (event.flag & StreamFlags::ITEM_REMOVED) == StreamFlags::NONE + && (event.flag & StreamFlags::IS_FILE) != StreamFlags::NONE + { + let source_file = observer_info + .lock() + .unwrap() + .as_ref() + .map(|x| Self::get_file_from_path(&x.source_path)); + if let Some(source_file) = source_file { + let file = Self::get_file_from_path(&event.path); + if source_file == file { + if let Some(observer_info) = observer_info.lock().unwrap().as_mut() + { + observer_info.target_path = event.path.clone(); + cb_pasted(observer_info); + } + } + } + } + } + Err(_) => { + if *(exit.lock().unwrap()) { + break; + } + } + } + }) + } + + fn new_fsevent(home_dir: String, tx_observer: Sender) -> fsevent::FsEvent { + let mut evt = fsevent::FsEvent::new(vec![home_dir.to_string()]); + evt.observe_async(tx_observer).ok(); + evt + } + + fn init_thread_fsevent( + home_dir: String, + tx_observer: Sender, + rx_control: Receiver, + ) -> thread::JoinHandle<()> { + log::debug!("fsevent observe dir: {}", &home_dir); + thread::spawn(move || { + let mut fsevent = None; + loop { + match rx_control.recv_timeout(Self::OBSERVE_TIMEOUT) { + Ok(FseventControl::Start) => { + if fsevent.is_none() { + fsevent = + Some(Self::new_fsevent(home_dir.clone(), tx_observer.clone())); + } + } + Ok(FseventControl::Stop) | Err(RecvTimeoutError::Timeout) => { + let _ = fsevent.as_mut().map(|e| e.shutdown_observe()); + fsevent = None; + } + Ok(FseventControl::Exit) | Err(RecvTimeoutError::Disconnected) => { + break; + } + } + } + log::info!("fsevent thread exit"); + let _ = fsevent.as_mut().map(|e| e.shutdown_observe()); + }) + } + + pub fn start(&mut self, observer_info: PasteObserverInfo) { + if let Some(tx_handle_fsevent_thread) = self.tx_handle_fsevent_thread.as_ref() { + self.observer_info.lock().unwrap().replace(observer_info); + tx_handle_fsevent_thread.tx.send(FseventControl::Start).ok(); + } + } + + pub fn stop(&mut self) { + if let Some(tx_handle_fsevent_thread) = &self.tx_handle_fsevent_thread { + self.observer_info = Default::default(); + tx_handle_fsevent_thread.tx.send(FseventControl::Stop).ok(); + } + } +} diff --git a/libs/clipboard/src/platform/unix/macos/paste_task.rs b/libs/clipboard/src/platform/unix/macos/paste_task.rs new file mode 100644 index 00000000000..33a11ed6c6e --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/paste_task.rs @@ -0,0 +1,639 @@ +use crate::{ + platform::unix::{FileDescription, FileType, BLOCK_SIZE}, + send_data, ClipboardFile, CliprdrError, ProgressPercent, +}; +use hbb_common::{allow_err, log, tokio::time::Instant}; +use std::{ + cmp::min, + fs::{File, FileTimes}, + io::{BufWriter, Write}, + os::macos::fs::FileTimesExt, + path::{Path, PathBuf}, + sync::{ + mpsc::{Receiver, RecvTimeoutError}, + Arc, Mutex, + }, + thread, + time::{Duration, SystemTime}, +}; + +const RECV_RETRY_TIMES: usize = 3; + +const DOWNLOAD_EXTENSION: &str = "rddownload"; +const RECEIVE_WAIT_TIMEOUT: Duration = Duration::from_millis(5_000); + +// https://stackoverflow.com/a/15112784/1926020 +// "1984-01-24 08:00:00 +0000" +const TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED: u64 = 443779200; +const ATTR_PROGRESS_FRACTION_COMPLETED: &str = "com.apple.progress.fractionCompleted"; + +pub struct FileContentsResponse { + pub conn_id: i32, + pub msg_flags: i32, + pub stream_id: i32, + pub requested_data: Vec, +} + +#[derive(Debug)] +struct PasteTaskProgress { + // Use list index to identify the file + // `list_index` is also used as the stream id + list_index: i32, + offset: u64, + total_size: u64, + current_size: u64, + last_sent_time: Instant, + download_file_index: i32, + download_file_size: u64, + download_file_path: String, + download_file_current_size: u64, + file_handle: Option>, + error: Option, + is_canceled: bool, +} + +struct PasteTaskHandle { + progress: PasteTaskProgress, + target_dir: PathBuf, + files: Vec, +} + +pub struct PasteTask { + exit: Arc>, + handle: Arc>>, + handle_worker: Option>, +} + +impl Drop for PasteTask { + fn drop(&mut self) { + *self.exit.lock().unwrap() = true; + if let Some(handle_worker) = self.handle_worker.take() { + handle_worker.join().ok(); + } + } +} + +impl PasteTask { + const INVALID_FILE_INDEX: i32 = -1; + + pub fn new(rx_file_contents: Receiver) -> Self { + let exit = Arc::new(Mutex::new(false)); + let handle = Arc::new(Mutex::new(None)); + let handle_worker = + Self::init_worker_thread(exit.clone(), handle.clone(), rx_file_contents); + Self { + handle, + exit, + handle_worker: Some(handle_worker), + } + } + + pub fn start(&mut self, target_dir: PathBuf, files: Vec) { + let mut task_lock = self.handle.lock().unwrap(); + if task_lock + .as_ref() + .map(|x| !x.is_finished()) + .unwrap_or(false) + { + log::error!("Previous paste task is not finished, ignore new request."); + return; + } + let total_size = files.iter().map(|f| f.size).sum(); + let mut task_handle = PasteTaskHandle { + progress: PasteTaskProgress { + list_index: -1, + offset: 0, + total_size, + current_size: 0, + last_sent_time: Instant::now(), + download_file_index: Self::INVALID_FILE_INDEX, + download_file_size: 0, + download_file_path: "".to_owned(), + download_file_current_size: 0, + file_handle: None, + error: None, + is_canceled: false, + }, + target_dir, + files, + }; + task_handle.update_next(0).ok(); + if task_handle.is_finished() { + task_handle.on_finished(); + } else { + if let Err(e) = task_handle.send_file_contents_request() { + log::error!("Failed to send file contents request, error: {}", &e); + task_handle.on_error(e); + } + } + *task_lock = Some(task_handle); + } + + pub fn cancel(&self) { + let mut task_handle = self.handle.lock().unwrap(); + if let Some(task_handle) = task_handle.as_mut() { + task_handle.progress.is_canceled = true; + task_handle.on_cancelled(); + } + } + + fn init_worker_thread( + exit: Arc>, + handle: Arc>>, + rx_file_contents: Receiver, + ) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut retry_count = 0; + loop { + if *exit.lock().unwrap() { + break; + } + + match rx_file_contents.recv_timeout(Duration::from_millis(300)) { + Ok(file_contents) => { + let mut task_lock = handle.lock().unwrap(); + let Some(task_handle) = task_lock.as_mut() else { + continue; + }; + if task_handle.is_finished() { + continue; + } + + if file_contents.stream_id != task_handle.progress.list_index { + // ignore invalid stream id + continue; + } else if file_contents.msg_flags != 0x01 { + retry_count += 1; + if retry_count > RECV_RETRY_TIMES { + task_handle.progress.error = Some(CliprdrError::InvalidRequest { + description: format!( + "Failed to read file contents, stream id: {}, msg_flags: {}", + file_contents.stream_id, + file_contents.msg_flags + ), + }); + } + } else { + let resp_list_index = file_contents.stream_id; + let Some(file) = &task_handle.files.get(resp_list_index as usize) + else { + // unreachable + // Because `task_handle.progress.list_index >= task_handle.files.len()` should always be false + log::warn!( + "Invalid response list index: {}, file length: {}", + resp_list_index, + task_handle.files.len() + ); + continue; + }; + if file.conn_id != file_contents.conn_id { + // unreachable + // We still add log here to make sure we can see the error message when it happens. + log::error!( + "Invalid response conn id: {}, expected: {}", + file_contents.conn_id, + file.conn_id + ); + continue; + } + + if let Err(e) = task_handle.handle_file_contents_response(file_contents) + { + log::error!("Failed to handle file contents response: {}", &e); + task_handle.on_error(e); + } + } + + if !task_handle.is_finished() { + if let Err(e) = task_handle.send_file_contents_request() { + log::error!("Failed to send file contents request: {}", &e); + task_handle.on_error(e); + } + } else { + retry_count = 0; + task_handle.on_finished(); + } + } + Err(RecvTimeoutError::Timeout) => { + let mut task_lock = handle.lock().unwrap(); + if let Some(task_handle) = task_lock.as_mut() { + if task_handle.check_receive_timemout() { + retry_count = 0; + task_handle.on_finished(); + } + } + } + Err(RecvTimeoutError::Disconnected) => { + break; + } + } + } + }) + } + + pub fn is_finished(&self) -> bool { + self.handle + .lock() + .unwrap() + .as_ref() + .map(|handle| handle.is_finished()) + .unwrap_or(true) + } + + pub fn progress_percent(&self) -> Option { + self.handle + .lock() + .unwrap() + .as_ref() + .map(|handle| handle.progress_percent()) + } +} + +impl PasteTaskHandle { + fn update_next(&mut self, size: u64) -> Result<(), CliprdrError> { + if self.is_finished() { + return Ok(()); + } + self.progress.current_size += size; + + let is_start = self.progress.list_index == -1; + if is_start || (self.progress.offset + size) >= self.progress.download_file_size { + if !is_start { + self.on_done(); + } + for i in (self.progress.list_index + 1)..self.files.len() as i32 { + let Some(file_desc) = self.files.get(i as usize) else { + return Err(CliprdrError::InvalidRequest { + description: format!("Invalid file index: {}", i), + }); + }; + match file_desc.kind { + FileType::File => { + if file_desc.size == 0 { + if let Some(new_file_path) = + Self::get_new_filename(&self.target_dir, file_desc) + { + if let Ok(f) = std::fs::File::create(&new_file_path) { + f.set_len(0).ok(); + Self::set_file_metadata(&f, file_desc); + } + }; + } else { + self.progress.list_index = i; + self.progress.offset = 0; + self.open_new_writer()?; + break; + } + } + FileType::Directory => { + let path = self.target_dir.join(&file_desc.name); + if !path.exists() { + std::fs::create_dir_all(path).ok(); + } + } + FileType::Symlink => { + // to-do: handle symlink + } + } + } + } else { + self.progress.offset += size; + self.progress.download_file_current_size += size; + self.update_progress_completed(None); + } + if self.progress.file_handle.is_none() { + self.progress.list_index = self.files.len() as i32; + self.progress.offset = 0; + self.progress.download_file_size = 0; + self.progress.download_file_current_size = 0; + } + Ok(()) + } + + fn start_progress_completed(&self) { + if let Some(file) = self.progress.file_handle.as_ref() { + let creation_time = + SystemTime::UNIX_EPOCH + Duration::from_secs(TIMESTAMP_FOR_FILE_PROGRESS_COMPLETED); + file.get_ref() + .set_times(FileTimes::new().set_created(creation_time)) + .ok(); + xattr::set( + &self.progress.download_file_path, + ATTR_PROGRESS_FRACTION_COMPLETED, + "0.0".as_bytes(), + ) + .ok(); + } + } + + fn update_progress_completed(&mut self, fraction_completed: Option) { + let fraction_completed = fraction_completed.unwrap_or_else(|| { + let current_size = self.progress.download_file_current_size as f64; + let total_size = self.progress.download_file_size as f64; + if total_size > 0.0 { + current_size / total_size + } else { + 1.0 + } + }); + xattr::set( + &self.progress.download_file_path, + ATTR_PROGRESS_FRACTION_COMPLETED, + &fraction_completed.to_string().as_bytes(), + ) + .ok(); + } + + #[inline] + fn remove_progress_completed(path: &str) { + if !path.is_empty() { + xattr::remove(path, ATTR_PROGRESS_FRACTION_COMPLETED).ok(); + } + } + + fn open_new_writer(&mut self) -> Result<(), CliprdrError> { + let Some(file) = &self.files.get(self.progress.list_index as usize) else { + return Err(CliprdrError::InvalidRequest { + description: format!( + "Invalid file index: {}, file count: {}", + self.progress.list_index, + self.files.len() + ), + }); + }; + + let original_file_path = self + .target_dir + .join(&file.name) + .to_string_lossy() + .to_string(); + let Some(download_file_path) = Self::get_first_filename( + format!("{}.{}", original_file_path, DOWNLOAD_EXTENSION), + file.kind, + ) else { + return Err(CliprdrError::CommonError { + description: format!("Failed to get download file path: {}", original_file_path), + }); + }; + let Some(download_path_parent) = Path::new(&download_file_path).parent() else { + return Err(CliprdrError::CommonError { + description: format!( + "Failed to get parent of the download file path: {}", + original_file_path + ), + }); + }; + if !download_path_parent.exists() { + if let Err(e) = std::fs::create_dir_all(download_path_parent) { + return Err(CliprdrError::FileError { + path: download_path_parent.to_string_lossy().to_string(), + err: e, + }); + } + } + match std::fs::File::create(&download_file_path) { + Ok(handle) => { + let writer = BufWriter::with_capacity(BLOCK_SIZE as usize * 2, handle); + self.progress.download_file_index = self.progress.list_index; + self.progress.download_file_size = file.size; + self.progress.download_file_path = download_file_path; + self.progress.download_file_current_size = 0; + self.progress.file_handle = Some(writer); + self.start_progress_completed(); + } + Err(e) => { + self.progress.error = Some(CliprdrError::FileError { + path: download_file_path, + err: e, + }); + } + }; + Ok(()) + } + + fn get_first_filename(path: String, r#type: FileType) -> Option { + let p = Path::new(&path); + if !p.exists() { + return Some(path); + } else { + for i in 1..9999999 { + let new_path = match r#type { + FileType::File => { + if let Some(ext) = p.extension() { + let new_name = format!( + "{}-{}.{}", + p.file_stem().unwrap_or_default().to_string_lossy(), + i, + ext.to_string_lossy() + ); + p.with_file_name(new_name).to_string_lossy().to_string() + } else { + format!("{} ({})", path, i) + } + } + FileType::Directory => format!("{} ({})", path, i), + FileType::Symlink => { + // to-do: handle symlink + return None; + } + }; + if !Path::new(&new_path).exists() { + return Some(new_path); + } + } + } + // unreachable + None + } + + fn progress_percent(&self) -> ProgressPercent { + let percent = self.progress.current_size as f64 / self.progress.total_size as f64; + ProgressPercent { + percent, + is_canceled: self.progress.is_canceled, + is_failed: self.progress.error.is_some(), + } + } + + fn is_finished(&self) -> bool { + self.progress.is_canceled + || self.progress.error.is_some() + || self.progress.list_index >= self.files.len() as i32 + } + + fn check_receive_timemout(&mut self) -> bool { + if !self.is_finished() { + if self.progress.last_sent_time.elapsed() > RECEIVE_WAIT_TIMEOUT { + self.progress.error = Some(CliprdrError::InvalidRequest { + description: "Failed to read file contents".to_string(), + }); + return true; + } + } + false + } + + fn on_finished(&mut self) { + if self.progress.error.is_some() { + self.on_cancelled(); + } else { + self.on_done(); + } + if self.progress.current_size != self.progress.total_size { + self.progress.error = Some(CliprdrError::InvalidRequest { + description: "Failed to download all files".to_string(), + }); + } + } + + fn on_error(&mut self, error: CliprdrError) { + self.progress.error = Some(error); + self.on_cancelled(); + } + + fn on_cancelled(&mut self) { + self.progress.file_handle = None; + std::fs::remove_file(&self.progress.download_file_path).ok(); + } + + fn on_done(&mut self) { + self.update_progress_completed(Some(1.0)); + Self::remove_progress_completed(&self.progress.download_file_path); + + let Some(file) = self.progress.file_handle.as_mut() else { + return; + }; + if self.progress.download_file_index == PasteTask::INVALID_FILE_INDEX { + return; + } + + if let Err(e) = file.flush() { + log::error!("Failed to flush file: {:?}", e); + } + self.progress.file_handle = None; + + let Some(file_desc) = self.files.get(self.progress.download_file_index as usize) else { + // unreachable + log::error!( + "Failed to get file description: {}", + self.progress.download_file_index + ); + return; + }; + let Some(rename_to_path) = Self::get_new_filename(&self.target_dir, file_desc) else { + return; + }; + match std::fs::rename(&self.progress.download_file_path, &rename_to_path) { + Ok(_) => Self::set_file_metadata2(&rename_to_path, file_desc), + Err(e) => { + log::error!("Failed to rename file: {:?}", e); + } + } + self.progress.download_file_path = "".to_owned(); + self.progress.download_file_index = PasteTask::INVALID_FILE_INDEX; + } + + fn get_new_filename(target_dir: &PathBuf, file_desc: &FileDescription) -> Option { + let mut rename_to_path = target_dir + .join(&file_desc.name) + .to_string_lossy() + .to_string(); + if Path::new(&rename_to_path).exists() { + let Some(new_path) = Self::get_first_filename(rename_to_path.clone(), file_desc.kind) + else { + log::error!("Failed to get new file name: {}", &rename_to_path); + return None; + }; + rename_to_path = new_path; + } + Some(rename_to_path) + } + + #[inline] + fn set_file_metadata(f: &File, file_desc: &FileDescription) { + let times = FileTimes::new() + .set_accessed(file_desc.atime) + .set_modified(file_desc.last_modified) + .set_created(file_desc.creation_time); + f.set_times(times).ok(); + } + + #[inline] + fn set_file_metadata2(path: &str, file_desc: &FileDescription) { + let times = FileTimes::new() + .set_accessed(file_desc.atime) + .set_modified(file_desc.last_modified) + .set_created(file_desc.creation_time); + File::options() + .write(true) + .open(path) + .map(|f| f.set_times(times)) + .ok(); + } + + fn send_file_contents_request(&mut self) -> Result<(), CliprdrError> { + if self.is_finished() { + return Ok(()); + } + + let stream_id = self.progress.list_index; + let list_index = self.progress.list_index; + let Some(file) = &self.files.get(list_index as usize) else { + // unreachable + return Err(CliprdrError::InvalidRequest { + description: format!("Invalid file index: {}", list_index), + }); + }; + let cb_requested = min(BLOCK_SIZE as u64, file.size - self.progress.offset); + let conn_id = file.conn_id; + + let (n_position_high, n_position_low) = ( + (self.progress.offset >> 32) as i32, + (self.progress.offset & (u32::MAX as u64)) as i32, + ); + let request = ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags: 2, + n_position_low, + n_position_high, + cb_requested: cb_requested as _, + have_clip_data_id: false, + clip_data_id: 0, + }; + allow_err!(send_data(conn_id, request)); + self.progress.last_sent_time = Instant::now(); + + Ok(()) + } + + fn handle_file_contents_response( + &mut self, + file_contents: FileContentsResponse, + ) -> Result<(), CliprdrError> { + if let Some(file) = self.progress.file_handle.as_mut() { + let data = file_contents.requested_data.as_slice(); + let mut write_len = 0; + while write_len < data.len() { + match file.write(&data[write_len..]) { + Ok(len) => { + write_len += len; + } + Err(e) => { + return Err(CliprdrError::FileError { + path: self.progress.download_file_path.clone(), + err: e, + }); + } + } + } + self.update_next(write_len as _)?; + } else { + return Err(CliprdrError::FileError { + path: self.progress.download_file_path.clone(), + err: std::io::Error::new(std::io::ErrorKind::NotFound, "file handle is not opened"), + }); + } + Ok(()) + } +} diff --git a/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs b/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs new file mode 100644 index 00000000000..4c747409363 --- /dev/null +++ b/libs/clipboard/src/platform/unix/macos/pasteboard_context.rs @@ -0,0 +1,460 @@ +use super::{ + item_data_provider::create_pasteboard_file_url_provider, + paste_observer::PasteObserver, + paste_task::{FileContentsResponse, PasteTask}, +}; +use crate::{ + platform::unix::{ + filetype::FileDescription, FILECONTENTS_FORMAT_NAME, FILEDESCRIPTORW_FORMAT_NAME, + }, + send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ProgressPercent, +}; +use hbb_common::{allow_err, bail, log, ResultType}; +use objc2::{msg_send_id, rc::autoreleasepool, rc::Id, runtime::ProtocolObject, ClassType}; +use objc2_app_kit::{NSPasteboard, NSPasteboardTypeFileURL}; +use objc2_foundation::{NSArray, NSString}; +use std::{ + io, + path::Path, + sync::{ + mpsc::{channel, Receiver, RecvTimeoutError, Sender}, + Arc, Mutex, + }, + thread, + time::Duration, +}; + +lazy_static::lazy_static! { + static ref PASTE_OBSERVER_INFO: Arc>> = Default::default(); +} + +pub const TEMP_FILE_PREFIX: &str = ".rustdesk_"; + +#[derive(Default, Debug, Clone, PartialEq)] +pub(super) struct PasteObserverInfo { + pub file_descriptor_id: i32, + pub conn_id: i32, + pub source_path: String, + pub target_path: String, +} + +impl PasteObserverInfo { + fn exit_msg() -> Self { + Self::default() + } +} + +struct ContextInfo { + tx: Sender>, + handle: thread::JoinHandle<()>, +} + +pub struct PasteboardContext { + pasteboard: Id, + observer: Arc>, + tx_handle: Option, + tx_remove_file: Option>, + remove_file_handle: Option>, + tx_paste_task: Sender, + paste_task: Arc>, +} + +unsafe impl Send for PasteboardContext {} +unsafe impl Sync for PasteboardContext {} + +impl Drop for PasteboardContext { + fn drop(&mut self) { + self.observer.lock().unwrap().stop(); + if let Some(tx_handle) = self.tx_handle.take() { + if tx_handle.tx.send(Ok(PasteObserverInfo::exit_msg())).is_ok() { + tx_handle.handle.join().ok(); + } + } + } +} + +impl CliprdrServiceContext for PasteboardContext { + fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { + Ok(()) + } + + fn empty_clipboard(&mut self, conn_id: i32) -> Result { + Ok(self.empty_clipboard_(conn_id)) + } + + fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + self.server_clip_file_(conn_id, msg) + } + + fn get_progress_percent(&self) -> Option { + self.paste_task.lock().unwrap().progress_percent() + } + + fn cancel(&mut self) { + self.paste_task.lock().unwrap().cancel(); + } +} + +impl PasteboardContext { + fn init(&mut self) { + let (tx_remove_file, rx_remove_file) = channel(); + let handle_remove_file = Self::init_thread_remove_file(rx_remove_file); + self.tx_remove_file = Some(tx_remove_file.clone()); + self.remove_file_handle = Some(handle_remove_file); + + let (tx, rx) = channel(); + let observer: Arc> = self.observer.clone(); + let handle = Self::init_thread_observer(tx_remove_file, rx, observer); + self.tx_handle = Some(ContextInfo { tx, handle }); + } + + fn init_thread_observer( + tx_remove_file: Sender, + rx: Receiver>, + observer: Arc>, + ) -> thread::JoinHandle<()> { + let exit_msg = PasteObserverInfo::exit_msg(); + thread::spawn(move || loop { + match rx.recv() { + Ok(Ok(task_info)) => { + if task_info == exit_msg { + log::debug!("pasteboard item data provider: exit"); + break; + } + tx_remove_file.send(task_info.source_path.clone()).ok(); + observer.lock().unwrap().start(task_info); + } + Ok(Err(e)) => { + log::error!("pasteboard item data provider, inner error: {e}"); + } + Err(e) => { + log::error!("pasteboard item data provider, error: {e}"); + break; + } + } + }) + } + + fn init_thread_remove_file(rx: Receiver) -> thread::JoinHandle<()> { + thread::spawn(move || { + let mut cur_file: Option = None; + loop { + match rx.recv_timeout(Duration::from_secs(30)) { + Ok(path) => { + if let Some(file) = cur_file.take() { + if !file.is_empty() { + std::fs::remove_file(&file).ok(); + } + } + if !path.is_empty() { + cur_file = Some(path); + } + } + Err(e) => { + if let Some(file) = cur_file.take() { + if !file.is_empty() { + std::fs::remove_file(&file).ok(); + } + } + if e == RecvTimeoutError::Disconnected { + break; + } + } + } + } + }) + } + + // Just removing the file can also make paste option in the context menu disappear. + fn empty_clipboard_(&mut self, _conn_id: i32) -> bool { + self.tx_remove_file + .as_ref() + .map(|tx| tx.send("".to_string()).ok()); + true + } + + fn temp_files_count() -> usize { + let mut count = 0; + if let Ok(entries) = std::fs::read_dir("/tmp") { + for entry in entries { + if let Ok(entry) = entry { + let path = entry.path(); + if path.is_file() { + if let Some(file_name) = path.file_name() { + if let Some(file_name_str) = file_name.to_str() { + if file_name_str.starts_with(TEMP_FILE_PREFIX) { + count += 1; + } + } + } + } + } + } + } + count + } + + fn server_clip_file_(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { + match msg { + ClipboardFile::FormatList { format_list } => { + let temp_files = Self::temp_files_count(); + if temp_files >= 3 { + // The temp files should be 0 or 1 in normal case. + // We should not continue to paste files if there are more than 3 temp files. + return Err(CliprdrError::CommonError { + description: format!( + "too many temp files, current: {}, limit: {}", + temp_files, 3 + ), + }); + } + + let task_lock = self.paste_task.lock().unwrap(); + if !task_lock.is_finished() { + return Err(CliprdrError::CommonError { + description: "previous file paste task is not finished".to_string(), + }); + } + self.handle_format_list(conn_id, format_list)?; + } + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + self.handle_format_data_response(conn_id, msg_flags, format_data)?; + } + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + requested_data, + } => { + self.handle_file_contents_response(conn_id, msg_flags, stream_id, requested_data)?; + } + ClipboardFile::TryEmpty => self.handle_try_empty(conn_id), + _ => {} + } + Ok(()) + } + + fn handle_format_list( + &self, + conn_id: i32, + format_list: Vec<(i32, String)>, + ) -> Result<(), CliprdrError> { + if let Some(tx_handle) = self.tx_handle.as_ref() { + if !format_list + .iter() + .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) + .map(|(id, _)| *id) + .is_some() + { + return Err(CliprdrError::CommonError { + description: "no file contents format found".to_string(), + }); + }; + let Some(file_descriptor_id) = format_list + .iter() + .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) + .map(|(id, _)| *id) + else { + return Err(CliprdrError::CommonError { + description: "no file descriptor format found".to_string(), + }); + }; + + autoreleasepool(|_| self.set_clipboard_item(tx_handle, conn_id, file_descriptor_id))?; + } else { + return Err(CliprdrError::CommonError { + description: "pasteboard context is not inited".to_string(), + }); + } + Ok(()) + } + + fn set_clipboard_item( + &self, + tx_handle: &ContextInfo, + conn_id: i32, + file_descriptor_id: i32, + ) -> Result<(), CliprdrError> { + let tx = tx_handle.tx.clone(); + let provider = create_pasteboard_file_url_provider( + PasteObserverInfo { + file_descriptor_id, + conn_id, + source_path: "".to_string(), + target_path: "".to_string(), + }, + tx, + ); + unsafe { + let types = NSArray::from_vec(vec![NSString::from_str( + &NSPasteboardTypeFileURL.to_string(), + )]); + let item = objc2_app_kit::NSPasteboardItem::new(); + item.setDataProvider_forTypes(&ProtocolObject::from_id(provider), &types); + self.pasteboard.clearContents(); + if !self + .pasteboard + .writeObjects(&Id::cast(NSArray::from_vec(vec![item]))) + { + return Err(CliprdrError::CommonError { + description: "failed to write objects".to_string(), + }); + } + } + Ok(()) + } + + fn handle_format_data_response( + &self, + conn_id: i32, + msg_flags: i32, + format_data: Vec, + ) -> Result<(), CliprdrError> { + log::debug!("handle format data response, msg_flags: {msg_flags}"); + if msg_flags != 0x1 { + // return failure message? + } + + let mut task_lock = self.paste_task.lock().unwrap(); + let target_dir = PASTE_OBSERVER_INFO + .lock() + .unwrap() + .as_ref() + .map(|task| task.target_path.clone()); + // unreachable in normal case + let Some(target_dir) = target_dir.as_ref().map(|d| Path::new(d).parent()).flatten() else { + return Err(CliprdrError::CommonError { + description: "failed to get parent path".to_string(), + }); + }; + // unreachable in normal case + if !target_dir.exists() { + return Err(CliprdrError::CommonError { + description: "target path does not exist".to_string(), + }); + } + let target_dir = target_dir.to_owned(); + match FileDescription::parse_file_descriptors(format_data, conn_id) { + Ok(files) => { + task_lock.start(target_dir, files); + Ok(()) + } + Err(e) => { + PASTE_OBSERVER_INFO + .lock() + .unwrap() + .replace(PasteObserverInfo::default()); + Err(e) + } + } + } + + fn handle_file_contents_response( + &self, + conn_id: i32, + msg_flags: i32, + stream_id: i32, + requested_data: Vec, + ) -> Result<(), CliprdrError> { + log::debug!("handle file contents response"); + self.tx_paste_task + .send(FileContentsResponse { + conn_id, + msg_flags, + stream_id, + requested_data, + }) + .ok(); + Ok(()) + } + + fn handle_try_empty(&mut self, conn_id: i32) { + log::debug!("empty_clipboard called"); + let ret = self.empty_clipboard_(conn_id); + log::debug!( + "empty_clipboard called, conn_id {}, return {}", + conn_id, + ret + ); + } +} + +fn handle_paste_result(task_info: &PasteObserverInfo) { + log::info!( + "file {} is pasted to {}", + &task_info.source_path, + &task_info.target_path + ); + if Path::new(&task_info.target_path).parent().is_none() { + log::error!( + "failed to get parent path of {}, no need to perform pasting", + &task_info.target_path + ); + return; + } + + PASTE_OBSERVER_INFO + .lock() + .unwrap() + .replace(task_info.clone()); + // to-do: add a timeout to clear data in `PASTE_OBSERVER_INFO`. + std::fs::remove_file(&task_info.source_path).ok(); + std::fs::remove_file(&task_info.target_path).ok(); + let data = ClipboardFile::FormatDataRequest { + requested_format_id: task_info.file_descriptor_id, + }; + allow_err!(send_data(task_info.conn_id as _, data)); +} + +#[inline] +pub fn create_pasteboard_context() -> ResultType> { + let pasteboard: Option> = + unsafe { msg_send_id![NSPasteboard::class(), generalPasteboard] }; + let Some(pasteboard) = pasteboard else { + bail!("failed to get general pasteboard"); + }; + let mut observer = PasteObserver::new(); + observer.init(handle_paste_result)?; + let (tx, rx) = channel(); + let mut context = Box::new(PasteboardContext { + pasteboard, + observer: Arc::new(Mutex::new(observer)), + tx_handle: None, + tx_remove_file: None, + remove_file_handle: None, + tx_paste_task: tx, + paste_task: Arc::new(Mutex::new(PasteTask::new(rx))), + }); + context.init(); + Ok(context) +} + +#[cfg(test)] +mod tests { + #[test] + fn test_temp_files_count() { + let mut c = super::PasteboardContext::temp_files_count(); + + let mut created_files = vec![]; + for _ in 0..10 { + let path = format!( + "/tmp/{}{}", + super::TEMP_FILE_PREFIX, + uuid::Uuid::new_v4().to_string() + ); + if std::fs::File::create(&path).is_ok() { + created_files.push(path); + c += 1; + } + } + + assert_eq!(c, super::PasteboardContext::temp_files_count()); + + // Clean up the created files. + for file in created_files { + std::fs::remove_file(&file).ok(); + } + } +} diff --git a/libs/clipboard/src/platform/unix/mod.rs b/libs/clipboard/src/platform/unix/mod.rs index 9a086109473..de5917f495b 100644 --- a/libs/clipboard/src/platform/unix/mod.rs +++ b/libs/clipboard/src/platform/unix/mod.rs @@ -1,48 +1,42 @@ -use std::{ - path::PathBuf, - sync::{mpsc::Sender, Arc}, - time::Duration, -}; - use dashmap::DashMap; -use fuser::MountOption; -use hbb_common::{ - bytes::{BufMut, BytesMut}, - log, -}; use lazy_static::lazy_static; -use parking_lot::Mutex; - -use crate::{ - platform::{fuse::FileDescription, unix::local_file::construct_file_list}, - send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, -}; - -use self::local_file::LocalFile; -#[cfg(target_os = "linux")] -use self::url::{encode_path_to_uri, parse_plain_uri_list}; - -use super::fuse::FuseServer; +mod filetype; +pub use filetype::{FileDescription, FileType}; +/// use FUSE for file pasting on these platforms #[cfg(target_os = "linux")] -/// clipboard implementation of x11 -pub mod x11; - +pub mod fuse; #[cfg(target_os = "macos")] -/// clipboard implementation of macos -pub mod ns_clipboard; +pub mod macos; pub mod local_file; - -#[cfg(target_os = "linux")] -pub mod url; +pub mod serv_files; + +/// has valid file attributes +pub const FLAGS_FD_ATTRIBUTES: u32 = 0x04; +/// has valid file size +pub const FLAGS_FD_SIZE: u32 = 0x40; +/// has valid last write time +pub const FLAGS_FD_LAST_WRITE: u32 = 0x20; +/// show progress +pub const FLAGS_FD_PROGRESSUI: u32 = 0x4000; +/// transferred from unix, contains file mode +/// P.S. this flag is not used in windows +pub const FLAGS_FD_UNIX_MODE: u32 = 0x08; // not actual format id, just a placeholder -const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; -const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW"; +pub const FILEDESCRIPTOR_FORMAT_ID: i32 = 49334; +pub const FILEDESCRIPTORW_FORMAT_NAME: &str = "FileGroupDescriptorW"; // not actual format id, just a placeholder -const FILECONTENTS_FORMAT_ID: i32 = 49267; -const FILECONTENTS_FORMAT_NAME: &str = "FileContents"; +pub const FILECONTENTS_FORMAT_ID: i32 = 49267; +pub const FILECONTENTS_FORMAT_NAME: &str = "FileContents"; + +/// block size for fuse, align to our asynchronic request size over FileContentsRequest. +pub(crate) const BLOCK_SIZE: u32 = 4 * 1024 * 1024; + +// begin of epoch used by microsoft +// 1601-01-01 00:00:00 + LDAP_EPOCH_DELTA*(100 ns) = 1970-01-01 00:00:00 +const LDAP_EPOCH_DELTA: u64 = 116444772610000000; lazy_static! { static ref REMOTE_FORMAT_MAP: DashMap = DashMap::from_iter( @@ -58,541 +52,7 @@ lazy_static! { ); } -fn get_local_format(remote_id: i32) -> Option { +#[inline] +pub fn get_local_format(remote_id: i32) -> Option { REMOTE_FORMAT_MAP.get(&remote_id).map(|s| s.clone()) } - -fn add_remote_format(local_name: &str, remote_id: i32) { - REMOTE_FORMAT_MAP.insert(remote_id, local_name.to_string()); -} - -trait SysClipboard: Send + Sync { - fn start(&self); - - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError>; - fn get_file_list(&self) -> Vec; -} - -#[cfg(target_os = "linux")] -fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, CliprdrError> { - #[cfg(feature = "wayland")] - { - unimplemented!() - } - #[cfg(not(feature = "wayland"))] - { - use x11::*; - let x11_clip = X11Clipboard::new(ignore_path)?; - Ok(Box::new(x11_clip) as Box<_>) - } -} - -#[cfg(target_os = "macos")] -fn get_sys_clipboard(ignore_path: &PathBuf) -> Result, CliprdrError> { - use ns_clipboard::*; - let ns_pb = NsPasteboard::new(ignore_path)?; - Ok(Box::new(ns_pb) as Box<_>) -} - -#[derive(Debug)] -enum FileContentsRequest { - Size { - stream_id: i32, - file_idx: usize, - }, - - Range { - stream_id: i32, - file_idx: usize, - offset: u64, - length: u64, - }, -} - -pub struct ClipboardContext { - pub fuse_mount_point: PathBuf, - /// stores fuse background session handle - fuse_handle: Mutex>, - - /// a sender of clipboard file contents pdu to fuse server - fuse_tx: Sender, - fuse_server: Arc>, - - clipboard: Arc, - local_files: Mutex>, -} - -impl ClipboardContext { - pub fn new(timeout: Duration, mount_path: PathBuf) -> Result { - // assert mount path exists - let fuse_mount_point = mount_path.canonicalize().map_err(|e| { - log::error!("failed to canonicalize mount path: {:?}", e); - CliprdrError::CliprdrInit - })?; - - let (fuse_server, fuse_tx) = FuseServer::new(timeout); - - let fuse_server = Arc::new(Mutex::new(fuse_server)); - - let clipboard = get_sys_clipboard(&fuse_mount_point)?; - let clipboard = Arc::from(clipboard) as Arc<_>; - let local_files = Mutex::new(vec![]); - - Ok(Self { - fuse_mount_point, - fuse_server, - fuse_tx, - fuse_handle: Mutex::new(None), - clipboard, - local_files, - }) - } - - pub fn run(&self) -> Result<(), CliprdrError> { - if !self.is_stopped() { - return Ok(()); - } - - let mut fuse_handle = self.fuse_handle.lock(); - - let mount_path = &self.fuse_mount_point; - - let mnt_opts = [ - MountOption::FSName("rustdesk-cliprdr-fs".to_string()), - MountOption::NoAtime, - MountOption::RO, - ]; - log::info!( - "mounting clipboard FUSE to {}", - self.fuse_mount_point.display() - ); - - let new_handle = fuser::spawn_mount2( - FuseServer::client(self.fuse_server.clone()), - mount_path, - &mnt_opts, - ) - .map_err(|e| { - log::error!("failed to mount cliprdr fuse: {:?}", e); - CliprdrError::CliprdrInit - })?; - *fuse_handle = Some(new_handle); - - let clipboard = self.clipboard.clone(); - - std::thread::spawn(move || { - log::debug!("start listening clipboard"); - clipboard.start(); - }); - - Ok(()) - } - - /// set clipboard data from file list - pub fn set_clipboard(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { - let prefix = self.fuse_mount_point.clone(); - let paths: Vec = paths.iter().cloned().map(|p| prefix.join(p)).collect(); - log::debug!("setting clipboard with paths: {:?}", paths); - self.clipboard.set_file_list(&paths)?; - log::debug!("clipboard set, paths: {:?}", paths); - Ok(()) - } - - fn serve_file_contents( - &self, - conn_id: i32, - request: FileContentsRequest, - ) -> Result<(), CliprdrError> { - let mut file_list = self.local_files.lock(); - - let (file_idx, file_contents_resp) = match request { - FileContentsRequest::Size { - stream_id, - file_idx, - } => { - log::debug!("file contents (size) requested from conn: {}", conn_id); - let Some(file) = file_list.get(file_idx) else { - log::error!( - "invalid file index {} requested from conn: {}", - file_idx, - conn_id - ); - resp_file_contents_fail(conn_id, stream_id); - - return Err(CliprdrError::InvalidRequest { - description: format!( - "invalid file index {} requested from conn: {}", - file_idx, conn_id - ), - }); - }; - - log::debug!( - "conn {} requested file-{}: {}", - conn_id, - file_idx, - file.name - ); - - let size = file.size; - ( - file_idx, - ClipboardFile::FileContentsResponse { - msg_flags: 0x1, - stream_id, - requested_data: size.to_le_bytes().to_vec(), - }, - ) - } - FileContentsRequest::Range { - stream_id, - file_idx, - offset, - length, - } => { - log::debug!( - "file contents (range from {} length {}) request from conn: {}", - offset, - length, - conn_id - ); - let Some(file) = file_list.get_mut(file_idx) else { - log::error!( - "invalid file index {} requested from conn: {}", - file_idx, - conn_id - ); - resp_file_contents_fail(conn_id, stream_id); - return Err(CliprdrError::InvalidRequest { - description: format!( - "invalid file index {} requested from conn: {}", - file_idx, conn_id - ), - }); - }; - log::debug!( - "conn {} requested file-{}: {}", - conn_id, - file_idx, - file.name - ); - - if offset > file.size { - log::error!("invalid reading offset requested from conn: {}", conn_id); - resp_file_contents_fail(conn_id, stream_id); - - return Err(CliprdrError::InvalidRequest { - description: format!( - "invalid reading offset requested from conn: {}", - conn_id - ), - }); - } - let read_size = if offset + length > file.size { - file.size - offset - } else { - length - }; - - let mut buf = vec![0u8; read_size as usize]; - - file.read_exact_at(&mut buf, offset)?; - - ( - file_idx, - ClipboardFile::FileContentsResponse { - msg_flags: 0x1, - stream_id, - requested_data: buf, - }, - ) - } - }; - - send_data(conn_id, file_contents_resp); - log::debug!("file contents sent to conn: {}", conn_id); - // hot reload next file - for next_file in file_list.iter_mut().skip(file_idx + 1) { - if !next_file.is_dir { - next_file.load_handle()?; - break; - } - } - Ok(()) - } -} - -fn resp_file_contents_fail(conn_id: i32, stream_id: i32) { - let resp = ClipboardFile::FileContentsResponse { - msg_flags: 0x2, - stream_id, - requested_data: vec![], - }; - send_data(conn_id, resp) -} - -impl ClipboardContext { - pub fn is_stopped(&self) -> bool { - self.fuse_handle.lock().is_none() - } - - pub fn sync_local_files(&self) -> Result<(), CliprdrError> { - let mut local_files = self.local_files.lock(); - let clipboard_files = self.clipboard.get_file_list(); - let local_file_list: Vec = local_files.iter().map(|f| f.path.clone()).collect(); - if local_file_list == clipboard_files { - return Ok(()); - } - let new_files = construct_file_list(&clipboard_files)?; - *local_files = new_files; - Ok(()) - } - - pub fn serve(&self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { - log::debug!("serve clipboard file from conn: {}", conn_id); - if self.is_stopped() { - log::debug!("cliprdr stopped, restart it"); - self.run()?; - } - match msg { - ClipboardFile::NotifyCallback { .. } => { - unreachable!() - } - ClipboardFile::MonitorReady => { - log::debug!("server_monitor_ready called"); - - self.send_file_list(conn_id)?; - - Ok(()) - } - - ClipboardFile::FormatList { format_list } => { - log::debug!("server_format_list called"); - // filter out "FileGroupDescriptorW" and "FileContents" - let fmt_lst: Vec<(i32, String)> = format_list - .into_iter() - .filter(|(_, name)| { - name == FILEDESCRIPTORW_FORMAT_NAME || name == FILECONTENTS_FORMAT_NAME - }) - .collect(); - if fmt_lst.len() != 2 { - log::debug!("no supported formats"); - return Ok(()); - } - log::debug!("supported formats: {:?}", fmt_lst); - let file_contents_id = fmt_lst - .iter() - .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) - .map(|(id, _)| *id)?; - let file_descriptor_id = fmt_lst - .iter() - .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) - .map(|(id, _)| *id)?; - - add_remote_format(FILECONTENTS_FORMAT_NAME, file_contents_id); - add_remote_format(FILEDESCRIPTORW_FORMAT_NAME, file_descriptor_id); - - // sync file system from peer - let data = ClipboardFile::FormatDataRequest { - requested_format_id: file_descriptor_id, - }; - send_data(conn_id, data); - - Ok(()) - } - ClipboardFile::FormatListResponse { msg_flags } => { - log::debug!("server_format_list_response called"); - if msg_flags != 0x1 { - send_format_list(conn_id) - } else { - Ok(()) - } - } - ClipboardFile::FormatDataRequest { - requested_format_id, - } => { - log::debug!("server_format_data_request called"); - let Some(format) = get_local_format(requested_format_id) else { - log::error!( - "got unsupported format data request: id={} from conn={}", - requested_format_id, - conn_id - ); - resp_format_data_failure(conn_id); - return Ok(()); - }; - - if format == FILEDESCRIPTORW_FORMAT_NAME { - self.send_file_list(conn_id)?; - } else if format == FILECONTENTS_FORMAT_NAME { - log::error!( - "try to read file contents with FormatDataRequest from conn={}", - conn_id - ); - resp_format_data_failure(conn_id); - } else { - log::error!( - "got unsupported format data request: id={} from conn={}", - requested_format_id, - conn_id - ); - resp_format_data_failure(conn_id); - } - Ok(()) - } - ClipboardFile::FormatDataResponse { - msg_flags, - format_data, - } => { - log::debug!( - "server_format_data_response called, msg_flags={}", - msg_flags - ); - - if msg_flags != 0x1 { - resp_format_data_failure(conn_id); - return Ok(()); - } - - log::debug!("parsing file descriptors"); - // this must be a file descriptor format data - let files = FileDescription::parse_file_descriptors(format_data, conn_id)?; - - let paths = { - let mut fuse_guard = self.fuse_server.lock(); - fuse_guard.load_file_list(files)?; - - fuse_guard.list_root() - }; - - log::debug!("load file list: {:?}", paths); - self.set_clipboard(&paths)?; - Ok(()) - } - ClipboardFile::FileContentsResponse { .. } => { - log::debug!("server_file_contents_response called"); - // we don't know its corresponding request, no resend can be performed - self.fuse_tx.send(msg).map_err(|e| { - log::error!("failed to send file contents response to fuse: {:?}", e); - CliprdrError::ClipboardInternalError - })?; - Ok(()) - } - ClipboardFile::FileContentsRequest { - stream_id, - list_index, - dw_flags, - n_position_low, - n_position_high, - cb_requested, - .. - } => { - log::debug!("server_file_contents_request called"); - let fcr = if dw_flags == 0x1 { - FileContentsRequest::Size { - stream_id, - file_idx: list_index as usize, - } - } else if dw_flags == 0x2 { - let offset = (n_position_high as u64) << 32 | n_position_low as u64; - let length = cb_requested as u64; - - FileContentsRequest::Range { - stream_id, - file_idx: list_index as usize, - offset, - length, - } - } else { - log::error!("got invalid FileContentsRequest from conn={}", conn_id); - resp_file_contents_fail(conn_id, stream_id); - return Ok(()); - }; - - self.serve_file_contents(conn_id, fcr) - } - } - } - - fn send_file_list(&self, conn_id: i32) -> Result<(), CliprdrError> { - self.sync_local_files()?; - - let file_list = self.local_files.lock(); - send_file_list(&*file_list, conn_id) - } -} - -impl CliprdrServiceContext for ClipboardContext { - fn set_is_stopped(&mut self) -> Result<(), CliprdrError> { - // unmount the fuse - if let Some(fuse_handle) = self.fuse_handle.lock().take() { - fuse_handle.join(); - } - // we don't stop the clipboard, keep listening in case of restart - Ok(()) - } - - fn empty_clipboard(&mut self, _conn_id: i32) -> Result { - self.clipboard.set_file_list(&[])?; - Ok(true) - } - - fn server_clip_file(&mut self, conn_id: i32, msg: ClipboardFile) -> Result<(), CliprdrError> { - self.serve(conn_id, msg) - } -} - -fn resp_format_data_failure(conn_id: i32) { - let data = ClipboardFile::FormatDataResponse { - msg_flags: 0x2, - format_data: vec![], - }; - send_data(conn_id, data) -} - -fn send_format_list(conn_id: i32) -> Result<(), CliprdrError> { - log::debug!("send format list to remote, conn={}", conn_id); - let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID) - .unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string()); - let fc_format_name = - get_local_format(FILECONTENTS_FORMAT_ID).unwrap_or(FILECONTENTS_FORMAT_NAME.to_string()); - let format_list = ClipboardFile::FormatList { - format_list: vec![ - (FILEDESCRIPTOR_FORMAT_ID, fd_format_name), - (FILECONTENTS_FORMAT_ID, fc_format_name), - ], - }; - - send_data(conn_id, format_list); - log::debug!("format list to remote dispatched, conn={}", conn_id); - Ok(()) -} - -fn build_file_list_pdu(files: &[LocalFile]) -> Vec { - let mut data = BytesMut::with_capacity(4 + 592 * files.len()); - data.put_u32_le(files.len() as u32); - for file in files.iter() { - data.put(file.as_bin().as_slice()); - } - - data.to_vec() -} - -fn send_file_list(files: &[LocalFile], conn_id: i32) -> Result<(), CliprdrError> { - log::debug!( - "send file list to remote, conn={}, list={:?}", - conn_id, - files.iter().map(|f| f.path.display()).collect::>() - ); - - let format_data = build_file_list_pdu(files); - - send_data( - conn_id, - ClipboardFile::FormatDataResponse { - msg_flags: 1, - format_data, - }, - ); - Ok(()) -} diff --git a/libs/clipboard/src/platform/unix/ns_clipboard.rs b/libs/clipboard/src/platform/unix/ns_clipboard.rs deleted file mode 100644 index 32c60a4643f..00000000000 --- a/libs/clipboard/src/platform/unix/ns_clipboard.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::{collections::BTreeSet, path::PathBuf}; - -use cacao::pasteboard::{Pasteboard, PasteboardName}; -use hbb_common::log; -use parking_lot::Mutex; - -use crate::{platform::unix::send_format_list, CliprdrError}; - -use super::SysClipboard; - -#[inline] -fn wait_file_list() -> Option> { - let pb = Pasteboard::named(PasteboardName::General); - pb.get_file_urls() - .ok() - .map(|v| v.into_iter().map(|nsurl| nsurl.pathbuf()).collect()) -} - -#[inline] -fn set_file_list(file_list: &[PathBuf]) -> Result<(), CliprdrError> { - let pb = Pasteboard::named(PasteboardName::General); - pb.set_files(file_list.to_vec()) - .map_err(|_| CliprdrError::ClipboardInternalError) -} - -pub struct NsPasteboard { - ignore_path: PathBuf, - - former_file_list: Mutex>, -} - -impl NsPasteboard { - pub fn new(ignore_path: &PathBuf) -> Result { - Ok(Self { - ignore_path: ignore_path.to_owned(), - former_file_list: Mutex::new(vec![]), - }) - } -} - -impl SysClipboard for NsPasteboard { - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { - *self.former_file_list.lock() = paths.to_vec(); - set_file_list(paths) - } - - fn start(&self) { - { - *self.former_file_list.lock() = vec![]; - } - - loop { - let file_list = match wait_file_list() { - Some(v) => v, - None => { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - }; - - let filtered = file_list - .into_iter() - .filter(|pb| !pb.starts_with(&self.ignore_path)) - .collect::>(); - - if filtered.is_empty() { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - { - let mut former = self.former_file_list.lock(); - - let filtered_st: BTreeSet<_> = filtered.iter().collect(); - let former_st = former.iter().collect::>(); - if filtered_st == former_st { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - *former = filtered; - } - - if let Err(e) = send_format_list(0) { - log::warn!("failed to send format list: {}", e); - break; - } - - std::thread::sleep(std::time::Duration::from_millis(100)); - } - log::debug!("stop listening file related atoms on clipboard"); - } - - fn get_file_list(&self) -> Vec { - self.former_file_list.lock().clone() - } -} diff --git a/libs/clipboard/src/platform/unix/serv_files.rs b/libs/clipboard/src/platform/unix/serv_files.rs new file mode 100644 index 00000000000..a401e0b5cac --- /dev/null +++ b/libs/clipboard/src/platform/unix/serv_files.rs @@ -0,0 +1,231 @@ +use super::local_file::LocalFile; +use crate::{platform::unix::local_file::construct_file_list, ClipboardFile, CliprdrError}; +use hbb_common::{ + bytes::{BufMut, BytesMut}, + log, +}; +use parking_lot::Mutex; +use std::{path::PathBuf, sync::Arc}; + +lazy_static::lazy_static! { + // local files are cached, this value should not be changed when copying files + // Because `CliprdrFileContentsRequest` only contains the index of the file in the list. + // We need to keep the file list in the same order as the remote side. + // We may add a `FileId` field to `CliprdrFileContentsRequest` in the future. + static ref CLIP_FILES: Arc> = Default::default(); +} + +#[derive(Debug)] +enum FileContentsRequest { + Size { + stream_id: i32, + file_idx: usize, + }, + + Range { + stream_id: i32, + file_idx: usize, + offset: u64, + length: u64, + }, +} + +#[derive(Default)] +struct ClipFiles { + files: Vec, + file_list: Vec, + files_pdu: Vec, +} + +impl ClipFiles { + fn clear(&mut self) { + self.files.clear(); + self.file_list.clear(); + self.files_pdu.clear(); + } + + fn sync_files(&mut self, clipboard_files: &[String]) -> Result<(), CliprdrError> { + let clipboard_paths = clipboard_files + .iter() + .map(|s| PathBuf::from(s)) + .collect::>(); + self.file_list = construct_file_list(&clipboard_paths)?; + self.files = clipboard_files.to_vec(); + Ok(()) + } + + fn build_file_list_pdu(&mut self) { + let mut data = BytesMut::with_capacity(4 + 592 * self.file_list.len()); + data.put_u32_le(self.file_list.len() as u32); + for file in self.file_list.iter() { + data.put(file.as_bin().as_slice()); + } + self.files_pdu = data.to_vec() + } + + fn serve_file_contents( + &mut self, + conn_id: i32, + request: FileContentsRequest, + ) -> Result { + let (file_idx, file_contents_resp) = match request { + FileContentsRequest::Size { + stream_id, + file_idx, + } => { + log::debug!("file contents (size) requested from conn: {}", conn_id); + let Some(file) = self.file_list.get(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + let size = file.size; + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: size.to_le_bytes().to_vec(), + }, + ) + } + FileContentsRequest::Range { + stream_id, + file_idx, + offset, + length, + } => { + log::debug!( + "file contents (range from {} length {}) request from conn: {}", + offset, + length, + conn_id + ); + let Some(file) = self.file_list.get_mut(file_idx) else { + log::error!( + "invalid file index {} requested from conn: {}", + file_idx, + conn_id + ); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid file index {} requested from conn: {}", + file_idx, conn_id + ), + }); + }; + log::debug!( + "conn {} requested file-{}: {}", + conn_id, + file_idx, + file.name + ); + + if offset > file.size { + log::error!("invalid reading offset requested from conn: {}", conn_id); + return Err(CliprdrError::InvalidRequest { + description: format!( + "invalid reading offset requested from conn: {}", + conn_id + ), + }); + } + let read_size = if offset + length > file.size { + file.size - offset + } else { + length + }; + + let mut buf = vec![0u8; read_size as usize]; + + file.read_exact_at(&mut buf, offset)?; + + ( + file_idx, + ClipboardFile::FileContentsResponse { + msg_flags: 0x1, + stream_id, + requested_data: buf, + }, + ) + } + }; + + log::debug!("file contents sent to conn: {}", conn_id); + // hot reload next file + for next_file in self.file_list.iter_mut().skip(file_idx + 1) { + if !next_file.is_dir { + next_file.load_handle()?; + break; + } + } + Ok(file_contents_resp) + } +} + +#[inline] +pub fn clear_files() { + CLIP_FILES.lock().clear(); +} + +pub fn read_file_contents( + conn_id: i32, + stream_id: i32, + list_index: i32, + dw_flags: i32, + n_position_low: i32, + n_position_high: i32, + cb_requested: i32, +) -> Result { + let fcr = if dw_flags == 0x1 { + FileContentsRequest::Size { + stream_id, + file_idx: list_index as usize, + } + } else if dw_flags == 0x2 { + let offset = (n_position_high as u64) << 32 | n_position_low as u64; + let length = cb_requested as u64; + + FileContentsRequest::Range { + stream_id, + file_idx: list_index as usize, + offset, + length, + } + } else { + return Err(CliprdrError::InvalidRequest { + description: format!("got invalid FileContentsRequest, dw_flats: {dw_flags}"), + }); + }; + + CLIP_FILES.lock().serve_file_contents(conn_id, fcr) +} + +pub fn sync_files(files: &[String]) -> Result<(), CliprdrError> { + let mut files_lock = CLIP_FILES.lock(); + if files_lock.files == files { + return Ok(()); + } + files_lock.sync_files(files)?; + Ok(files_lock.build_file_list_pdu()) +} + +pub fn get_file_list_pdu() -> Vec { + CLIP_FILES.lock().files_pdu.clone() +} diff --git a/libs/clipboard/src/platform/unix/url.rs b/libs/clipboard/src/platform/unix/url.rs deleted file mode 100644 index 2ae520f4dfc..00000000000 --- a/libs/clipboard/src/platform/unix/url.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::path::{Path, PathBuf}; - -use crate::CliprdrError; - -// on x11, path will be encode as -// "/home/rustdesk/pictures/🖼ï¸.png" -> "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" -// url encode and decode is needed -const ENCODE_SET: percent_encoding::AsciiSet = percent_encoding::CONTROLS.add(b' ').remove(b'/'); - -pub(super) fn encode_path_to_uri(path: &PathBuf) -> io::Result { - let encoded = - percent_encoding::percent_encode(path.to_str()?.as_bytes(), &ENCODE_SET).to_string(); - format!("file://{}", encoded) -} - -pub(super) fn parse_uri_to_path(encoded_uri: &str) -> Result { - let encoded_path = encoded_uri.trim_start_matches("file://"); - let path_str = percent_encoding::percent_decode_str(encoded_path) - .decode_utf8() - .map_err(|_| CliprdrError::ConversionFailure)?; - let path_str = path_str.to_string(); - - Ok(Path::new(&path_str).to_path_buf()) -} - -// helper parse function -// convert 'text/uri-list' data to a list of valid Paths -// # Note -// - none utf8 data will lead to error -pub(super) fn parse_plain_uri_list(v: Vec) -> Result, CliprdrError> { - let text = String::from_utf8(v).map_err(|_| CliprdrError::ConversionFailure)?; - parse_uri_list(&text) -} - -// helper parse function -// convert 'text/uri-list' data to a list of valid Paths -// # Note -// - none utf8 data will lead to error -pub(super) fn parse_uri_list(text: &str) -> Result, CliprdrError> { - let mut list = Vec::new(); - - for line in text.lines() { - if !line.starts_with("file://") { - continue; - } - let decoded = parse_uri_to_path(line)?; - list.push(decoded) - } - Ok(list) -} - -#[cfg(test)] -mod uri_test { - #[test] - fn test_conversion() { - let path = std::path::PathBuf::from("/home/rustdesk/pictures/🖼ï¸.png"); - let uri = super::encode_path_to_uri(&path).unwrap(); - assert_eq!( - uri, - "file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png" - ); - let convert_back = super::parse_uri_to_path(&uri).unwrap(); - assert_eq!(path, convert_back); - } - - #[test] - fn parse_list() { - let uri_list = r#"file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png -file:///home/rustdesk/pictures/%F0%9F%96%BC%EF%B8%8F.png -"#; - let list = super::parse_uri_list(uri_list.into()).unwrap(); - assert!(list.len() == 2); - assert_eq!(list[0], list[1]); - } -} diff --git a/libs/clipboard/src/platform/unix/x11.rs b/libs/clipboard/src/platform/unix/x11.rs deleted file mode 100644 index 41b64264044..00000000000 --- a/libs/clipboard/src/platform/unix/x11.rs +++ /dev/null @@ -1,168 +0,0 @@ -use std::{collections::BTreeSet, path::PathBuf}; - -use hbb_common::log; -use once_cell::sync::OnceCell; -use parking_lot::Mutex; -use x11_clipboard::Clipboard; -use x11rb::protocol::xproto::Atom; - -use crate::{platform::unix::send_format_list, CliprdrError}; - -use super::{encode_path_to_uri, parse_plain_uri_list, SysClipboard}; - -static X11_CLIPBOARD: OnceCell = OnceCell::new(); - -fn get_clip() -> Result<&'static Clipboard, CliprdrError> { - X11_CLIPBOARD.get_or_try_init(|| Clipboard::new().map_err(|_| CliprdrError::CliprdrInit)) -} - -pub struct X11Clipboard { - ignore_path: PathBuf, - text_uri_list: Atom, - gnome_copied_files: Atom, - nautilus_clipboard: Atom, - - former_file_list: Mutex>, -} - -impl X11Clipboard { - pub fn new(ignore_path: &PathBuf) -> Result { - let clipboard = get_clip()?; - let text_uri_list = clipboard - .setter - .get_atom("text/uri-list") - .map_err(|_| CliprdrError::CliprdrInit)?; - let gnome_copied_files = clipboard - .setter - .get_atom("x-special/gnome-copied-files") - .map_err(|_| CliprdrError::CliprdrInit)?; - let nautilus_clipboard = clipboard - .setter - .get_atom("x-special/nautilus-clipboard") - .map_err(|_| CliprdrError::CliprdrInit)?; - Ok(Self { - ignore_path: ignore_path.to_owned(), - text_uri_list, - gnome_copied_files, - nautilus_clipboard, - former_file_list: Mutex::new(vec![]), - }) - } - - fn load(&self, target: Atom) -> Result, CliprdrError> { - let clip = get_clip()?.setter.atoms.clipboard; - let prop = get_clip()?.setter.atoms.property; - // NOTE: - // # why not use `load_wait` - // load_wait is likely to wait forever, which is not what we want - let res = get_clip()?.load_wait(clip, target, prop); - match res { - Ok(res) => Ok(res), - Err(x11_clipboard::error::Error::UnexpectedType(_)) => Ok(vec![]), - Err(x11_clipboard::error::Error::Timeout) => { - log::debug!("x11 clipboard get content timeout."); - Err(CliprdrError::ClipboardInternalError) - } - Err(e) => { - log::debug!("x11 clipboard get content fail: {:?}", e); - Err(CliprdrError::ClipboardInternalError) - } - } - } - - fn store_batch(&self, batch: Vec<(Atom, Vec)>) -> Result<(), CliprdrError> { - let clip = get_clip()?.setter.atoms.clipboard; - log::debug!("try to store clipboard content"); - get_clip()? - .store_batch(clip, batch) - .map_err(|_| CliprdrError::ClipboardInternalError) - } - - fn wait_file_list(&self) -> Result>, CliprdrError> { - let v = self.load(self.text_uri_list)?; - let p = parse_plain_uri_list(v)?; - Ok(Some(p)) - } -} - -impl SysClipboard for X11Clipboard { - fn set_file_list(&self, paths: &[PathBuf]) -> Result<(), CliprdrError> { - *self.former_file_list.lock() = paths.to_vec(); - - let uri_list: Vec = { - let mut v = Vec::new(); - for path in paths { - v.push(encode_path_to_uri(path)?); - } - v - }; - let uri_list = uri_list.join("\n"); - let text_uri_list_data = uri_list.as_bytes().to_vec(); - let gnome_copied_files_data = ["copy\n".as_bytes(), uri_list.as_bytes()].concat(); - let batch = vec![ - (self.text_uri_list, text_uri_list_data), - (self.gnome_copied_files, gnome_copied_files_data.clone()), - (self.nautilus_clipboard, gnome_copied_files_data), - ]; - self.store_batch(batch) - .map_err(|_| CliprdrError::ClipboardInternalError) - } - - fn start(&self) { - { - // clear cached file list - *self.former_file_list.lock() = vec![]; - } - loop { - let sth = match self.wait_file_list() { - Ok(sth) => sth, - Err(e) => { - log::warn!("failed to get file list from clipboard: {}", e); - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - }; - - let Some(paths) = sth else { - // just sleep - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - }; - - let filtered = paths - .into_iter() - .filter(|pb| !pb.starts_with(&self.ignore_path)) - .collect::>(); - - if filtered.is_empty() { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - { - let mut former = self.former_file_list.lock(); - - let filtered_st: BTreeSet<_> = filtered.iter().collect(); - let former_st = former.iter().collect::>(); - if filtered_st == former_st { - std::thread::sleep(std::time::Duration::from_millis(100)); - continue; - } - - *former = filtered; - } - - if let Err(e) = send_format_list(0) { - log::warn!("failed to send format list: {}", e); - break; - } - - std::thread::sleep(std::time::Duration::from_millis(100)); - } - log::debug!("stop listening file related atoms on clipboard"); - } - - fn get_file_list(&self) -> Vec { - self.former_file_list.lock().clone() - } -} diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs index 5d1aa086ddb..3734406e0c9 100644 --- a/libs/clipboard/src/platform/windows.rs +++ b/libs/clipboard/src/platform/windows.rs @@ -6,10 +6,11 @@ #![allow(deref_nullptr)] use crate::{ - allow_err, send_data, ClipboardFile, CliprdrError, CliprdrServiceContext, ResultType, - ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, + send_data, send_data_exclude, ClipboardFile, CliprdrError, CliprdrServiceContext, + ProgressPercent, ResultType, ERR_CODE_INVALID_PARAMETER, ERR_CODE_SEND_MSG, + ERR_CODE_SERVER_FUNCTION_NONE, VEC_MSG_CHANNEL, }; -use hbb_common::log; +use hbb_common::{allow_err, log}; use std::{ boxed::Box, ffi::{CStr, CString}, @@ -602,6 +603,12 @@ impl CliprdrServiceContext for CliprdrClientContext { let ret = server_clip_file(self, conn_id, msg); ret_to_result(ret) } + + fn get_progress_percent(&self) -> Option { + None + } + + fn cancel(&mut self) {} } fn ret_to_result(ret: u32) -> Result<(), CliprdrError> { @@ -614,6 +621,7 @@ fn ret_to_result(ret: u32) -> Result<(), CliprdrError> { e => Err(CliprdrError::Unknown(e)), } } + pub fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool { unsafe { TRUE == empty_cliprdr(context, conn_id as u32) } } @@ -643,6 +651,7 @@ pub fn server_clip_file( conn_id, &format_list ); + send_data_exclude(conn_id as _, ClipboardFile::TryEmpty); ret = server_format_list(context, conn_id, format_list); log::debug!( "server_format_list called, conn_id {}, return {}", @@ -740,6 +749,15 @@ pub fn server_clip_file( ret ); } + ClipboardFile::TryEmpty => { + log::debug!("empty_clipboard called"); + let ret = empty_clipboard(context, conn_id); + log::debug!( + "empty_clipboard called, conn_id {}, return {}", + conn_id, + ret + ); + } } ret } diff --git a/libs/clipboard/src/windows/wf_cliprdr.c b/libs/clipboard/src/windows/wf_cliprdr.c index 6f8381a6ad4..e065be215c3 100644 --- a/libs/clipboard/src/windows/wf_cliprdr.c +++ b/libs/clipboard/src/windows/wf_cliprdr.c @@ -269,6 +269,7 @@ static UINT cliprdr_send_request_filecontents(wfClipboard *clipboard, UINT32 con DWORD positionlow, ULONG request); static BOOL is_file_descriptor_from_remote(); +static BOOL is_set_by_instance(wfClipboard *clipboard); static void CliprdrDataObject_Delete(CliprdrDataObject *instance); @@ -600,8 +601,11 @@ static CliprdrStream *CliprdrStream_New(UINT32 connID, ULONG index, void *pData, clipboard->req_fdata = NULL; } } - else + else { + instance->m_lSize.QuadPart = + ((UINT64)instance->m_Dsc.nFileSizeHigh << 32) | instance->m_Dsc.nFileSizeLow; success = TRUE; + } } } @@ -1745,8 +1749,7 @@ static LRESULT CALLBACK cliprdr_proc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM DEBUG_CLIPRDR("info: WM_CLIPBOARDUPDATE"); // if (clipboard->sync) { - if ((GetClipboardOwner() != clipboard->hwnd) && - (S_FALSE == OleIsCurrentClipboard(clipboard->data_obj))) + if (!is_set_by_instance(clipboard)) { if (clipboard->hmem) { @@ -2086,6 +2089,8 @@ static FILEDESCRIPTORW *wf_cliprdr_get_file_descriptor(WCHAR *file_name, size_t return NULL; } + // to-do: use `fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI`. + // We keep `fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI` for compatibility. // fd->dwFlags = FD_ATTRIBUTES | FD_FILESIZE | FD_WRITESTIME | FD_PROGRESSUI; fd->dwFlags = FD_ATTRIBUTES | FD_WRITESTIME | FD_PROGRESSUI; fd->dwFileAttributes = GetFileAttributesW(file_name); @@ -2849,6 +2854,31 @@ wf_cliprdr_server_file_contents_request(CliprdrClientContext *context, goto exit; } + // If the clipboard is set by the instance, or the file descriptor is from remote, + // we should not process the request. + // Because this may be the following cases: + // 1. `A` -> `B`, `C` + // 2. Copy in `A` + // 3. Copy in `B` + // 4. Paste in `C` + // In this case, `C` should not get the file content from `A`. The clipboard is set by `B`. + // + // Or + // 1. `B` -> `A` -> `C` + // 2. Copy in `A` + // 2. Copy in `B` + // 3. Paste in `C` + // In this case, `C` should not get the file content from `A`. The clipboard is set by `B`. + // + // We can simply notify `C` to clear the clipboard when `A` received copy message from `B`, + // if connections are in the same process. + // But if connections are in different processes, it is not easy to notify the other process. + // So we just ignore the request from `C` in this case. + if (is_set_by_instance(clipboard) || is_file_descriptor_from_remote()) { + rc = ERROR_INTERNAL_ERROR; + goto exit; + } + cbRequested = fileContentsRequest->cbRequested; if (fileContentsRequest->dwFlags == FILECONTENTS_SIZE) cbRequested = sizeof(UINT64); @@ -3089,6 +3119,14 @@ wf_cliprdr_server_file_contents_response(CliprdrClientContext *context, return rc; } +BOOL is_set_by_instance(wfClipboard *clipboard) +{ + if (GetClipboardOwner() == clipboard->hwnd || S_OK == OleIsCurrentClipboard(clipboard->data_obj)) { + return TRUE; + } + return FALSE; +} + BOOL is_file_descriptor_from_remote() { UINT fsid = 0; @@ -3175,7 +3213,7 @@ BOOL wf_cliprdr_uninit(wfClipboard *clipboard, CliprdrClientContext *cliprdr) /* discard all contexts in clipboard */ if (try_open_clipboard(clipboard->hwnd)) { - if (is_file_descriptor_from_remote()) + if (is_set_by_instance(clipboard) || is_file_descriptor_from_remote()) { if (!EmptyClipboard()) { diff --git a/libs/enigo/src/macos/macos_impl.rs b/libs/enigo/src/macos/macos_impl.rs index e7d7d9e8d33..d85f3576f51 100644 --- a/libs/enigo/src/macos/macos_impl.rs +++ b/libs/enigo/src/macos/macos_impl.rs @@ -141,8 +141,27 @@ impl Enigo { self.flags |= flag; } - fn post(&self, event: CGEvent) { - if !self.ignore_flags { + // Just check F11 for minimal changes. + // Since enigo (legacy mode) is deprecated, it is currently in maintenance only. + fn post(&self, event: CGEvent, keycode: Option) { + if keycode == Some(kVK_F11) { + // Some key events require the flags to work. + // We can't simply set the flag to `CGEventFlags::CGEventFlagNull`. + // eg. `F11` requires flags `CGEventFlags::CGEventFlagSecondaryFn | 0x20000000` to work. + self.post_event(event, false); + } else { + // macOS system may use the previous event flag to generate the next event. + // Only found this issue when locking the screen. + // When we use enigo to lock the screen, the next mouse event will have the flag + // `CGEventFlagControl | CGEventFlagCommand | 0x20000000`. + // The key event will also have the flag `CGEventFlagControl | CGEventFlagCommand | 0x20000000`. + // Therefore, we need to set the flag to `event.set_flags(self.flags)` to avoid this. + self.post_event(event, true); + } + } + + fn post_event(&self, event: CGEvent, force_flags: bool) { + if !self.ignore_flags && (force_flags || self.flags != CGEventFlags::CGEventFlagNull) { event.set_flags(self.flags); } event.set_integer_value_field(EventField::EVENT_SOURCE_USER_DATA, ENIGO_INPUT_EXTRA_VALUE); @@ -204,7 +223,7 @@ impl MouseControllable for Enigo { if let Ok(event) = CGEvent::new_mouse_event(src.clone(), event_type, dest, CGMouseButton::Left) { - self.post(event); + self.post(event, None); } } } @@ -269,7 +288,7 @@ impl MouseControllable for Enigo { if let Some(v) = btn_value { event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); } - self.post(event); + self.post(event, None); } } Ok(()) @@ -308,7 +327,7 @@ impl MouseControllable for Enigo { if let Some(v) = btn_value { event.set_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER, v); } - self.post(event); + self.post(event, None); } } } @@ -394,7 +413,7 @@ impl KeyboardControllable for Enigo { if let Some(src) = self.event_source.as_ref() { if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), 0, true) { event.set_string(cluster); - self.post(event); + self.post(event, None); } } } @@ -408,11 +427,11 @@ impl KeyboardControllable for Enigo { if let Some(src) = self.event_source.as_ref() { if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, true) { - self.post(event); + self.post(event, Some(keycode)); } if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), keycode, false) { - self.post(event); + self.post(event, Some(keycode)); } } } @@ -424,18 +443,17 @@ impl KeyboardControllable for Enigo { } if let Some(src) = self.event_source.as_ref() { if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, true) { - self.post(event); + self.post(event, Some(code)); } } Ok(()) } fn key_up(&mut self, key: Key) { + let code = self.key_to_keycode(key); if let Some(src) = self.event_source.as_ref() { - if let Ok(event) = - CGEvent::new_keyboard_event(src.clone(), self.key_to_keycode(key), false) - { - self.post(event); + if let Ok(event) = CGEvent::new_keyboard_event(src.clone(), code, false) { + self.post(event, Some(code)); } } } diff --git a/libs/hbb_common b/libs/hbb_common new file mode 160000 index 00000000000..f91459c4ab8 --- /dev/null +++ b/libs/hbb_common @@ -0,0 +1 @@ +Subproject commit f91459c4ab80fc3cfdef0882b2af51f984bc914c diff --git a/libs/hbb_common/.gitignore b/libs/hbb_common/.gitignore deleted file mode 100644 index 693699042b1..00000000000 --- a/libs/hbb_common/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/target -**/*.rs.bk -Cargo.lock diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml deleted file mode 100644 index 259d01e9dd4..00000000000 --- a/libs/hbb_common/Cargo.toml +++ /dev/null @@ -1,65 +0,0 @@ -[package] -name = "hbb_common" -version = "0.1.0" -authors = ["open-trade "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -flexi_logger = { version = "0.27", features = ["async"] } -protobuf = { version = "3.4", features = ["with-bytes"] } -tokio = { version = "1.38", features = ["full"] } -tokio-util = { version = "0.7", features = ["full"] } -futures = "0.3" -bytes = { version = "1.6", features = ["serde"] } -log = "0.4" -env_logger = "0.10" -socket2 = { version = "0.3", features = ["reuseport"] } -zstd = "0.13" -anyhow = "1.0" -futures-util = "0.3" -directories-next = "2.0" -rand = "0.8" -serde_derive = "1.0" -serde = "1.0" -serde_json = "1.0" -lazy_static = "1.4" -confy = { git = "https://github.com/rustdesk-org/confy" } -dirs-next = "2.0" -filetime = "0.2" -sodiumoxide = "0.2" -regex = "1.8" -tokio-socks = { git = "https://github.com/rustdesk-org/tokio-socks" } -chrono = "0.4" -backtrace = "0.3" -libc = "0.2" -dlopen = "0.1" -toml = "0.7" -uuid = { version = "1.3", features = ["v4"] } -# new sysinfo issue: https://github.com/rustdesk/rustdesk/pull/6330#issuecomment-2270871442 -sysinfo = { git = "https://github.com/rustdesk-org/sysinfo", branch = "rlim_max" } -thiserror = "1.0" -httparse = "1.5" -base64 = "0.22" -url = "2.2" - -[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] -mac_address = "1.1" -machine-uid = { git = "https://github.com/rustdesk-org/machine-uid" } -[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies] -tokio-rustls = { version = "0.26", features = ["logging", "tls12", "ring"], default-features = false } -rustls-platform-verifier = "0.3.1" -rustls-pki-types = "1.4" -[target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] -tokio-native-tls ="0.3" - -[build-dependencies] -protobuf-codegen = { version = "3.4" } - -[target.'cfg(target_os = "windows")'.dependencies] -winapi = { version = "0.3", features = ["winuser", "synchapi", "pdh", "memoryapi", "sysinfoapi"] } - -[target.'cfg(target_os = "macos")'.dependencies] -osascript = "0.3" - diff --git a/libs/hbb_common/build.rs b/libs/hbb_common/build.rs deleted file mode 100644 index 5ebc3a28706..00000000000 --- a/libs/hbb_common/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -fn main() { - 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(out_dir) - .inputs(["protos/rendezvous.proto", "protos/message.proto"]) - .include("protos") - .customize(protobuf_codegen::Customize::default().tokio_bytes(true)) - .run() - .expect("Codegen failed."); -} diff --git a/libs/hbb_common/examples/config.rs b/libs/hbb_common/examples/config.rs deleted file mode 100644 index 95169df8e2c..00000000000 --- a/libs/hbb_common/examples/config.rs +++ /dev/null @@ -1,5 +0,0 @@ -extern crate hbb_common; - -fn main() { - println!("{:?}", hbb_common::config::PeerConfig::load("455058072")); -} diff --git a/libs/hbb_common/examples/system_message.rs b/libs/hbb_common/examples/system_message.rs deleted file mode 100644 index 0be78842868..00000000000 --- a/libs/hbb_common/examples/system_message.rs +++ /dev/null @@ -1,20 +0,0 @@ -extern crate hbb_common; -#[cfg(target_os = "linux")] -use hbb_common::platform::linux; -#[cfg(target_os = "macos")] -use hbb_common::platform::macos; - -fn main() { - #[cfg(target_os = "linux")] - let res = linux::system_message("test title", "test message", true); - #[cfg(target_os = "macos")] - let res = macos::alert( - "System Preferences".to_owned(), - "warning".to_owned(), - "test title".to_owned(), - "test message".to_owned(), - ["Ok".to_owned()].to_vec(), - ); - #[cfg(any(target_os = "linux", target_os = "macos"))] - println!("result {:?}", &res); -} diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto deleted file mode 100644 index d4601c0f98e..00000000000 --- a/libs/hbb_common/protos/message.proto +++ /dev/null @@ -1,861 +0,0 @@ -syntax = "proto3"; -package hbb; - -message EncodedVideoFrame { - bytes data = 1; - bool key = 2; - int64 pts = 3; -} - -message EncodedVideoFrames { repeated EncodedVideoFrame frames = 1; } - -message RGB { bool compress = 1; } - -// planes data send directly in binary for better use arraybuffer on web -message YUV { - bool compress = 1; - int32 stride = 2; -} - -enum Chroma { - I420 = 0; - I444 = 1; -} - -message VideoFrame { - oneof union { - EncodedVideoFrames vp9s = 6; - RGB rgb = 7; - YUV yuv = 8; - EncodedVideoFrames h264s = 10; - EncodedVideoFrames h265s = 11; - EncodedVideoFrames vp8s = 12; - EncodedVideoFrames av1s = 13; - } - int32 display = 14; -} - -message IdPk { - string id = 1; - bytes pk = 2; -} - -message DisplayInfo { - sint32 x = 1; - sint32 y = 2; - int32 width = 3; - int32 height = 4; - string name = 5; - bool online = 6; - bool cursor_embedded = 7; - Resolution original_resolution = 8; - double scale = 9; -} - -message PortForward { - string host = 1; - int32 port = 2; -} - -message FileTransfer { - string dir = 1; - bool show_hidden = 2; -} - -message OSLogin { - string username = 1; - string password = 2; -} - -message LoginRequest { - string username = 1; - bytes password = 2; - string my_id = 4; - string my_name = 5; - OptionMessage option = 6; - oneof union { - FileTransfer file_transfer = 7; - PortForward port_forward = 8; - } - bool video_ack_required = 9; - uint64 session_id = 10; - string version = 11; - OSLogin os_login = 12; - string my_platform = 13; - bytes hwid = 14; -} - -message Auth2FA { - string code = 1; - bytes hwid = 2; -} - -message ChatMessage { string text = 1; } - -message Features { - bool privacy_mode = 1; -} - -message CodecAbility { - bool vp8 = 1; - bool vp9 = 2; - bool av1 = 3; - bool h264 = 4; - bool h265 = 5; -} - -message SupportedEncoding { - bool h264 = 1; - bool h265 = 2; - bool vp8 = 3; - bool av1 = 4; - CodecAbility i444 = 5; -} - -message PeerInfo { - string username = 1; - string hostname = 2; - string platform = 3; - repeated DisplayInfo displays = 4; - int32 current_display = 5; - bool sas_enabled = 6; - string version = 7; - Features features = 9; - SupportedEncoding encoding = 10; - SupportedResolutions resolutions = 11; - // Use JSON's key-value format which is friendly for peer to handle. - // NOTE: Only support one-level dictionaries (for peer to update), and the key is of type string. - string platform_additions = 12; - WindowsSessions windows_sessions = 13; -} - -message WindowsSession { - uint32 sid = 1; - string name = 2; -} - -message LoginResponse { - oneof union { - string error = 1; - PeerInfo peer_info = 2; - } - bool enable_trusted_devices = 3; -} - -message TouchScaleUpdate { - // The delta scale factor relative to the previous scale. - // delta * 1000 - // 0 means scale end - int32 scale = 1; -} - -message TouchPanStart { - int32 x = 1; - int32 y = 2; -} - -message TouchPanUpdate { - // The delta x position relative to the previous position. - int32 x = 1; - // The delta y position relative to the previous position. - int32 y = 2; -} - -message TouchPanEnd { - int32 x = 1; - int32 y = 2; -} - -message TouchEvent { - oneof union { - TouchScaleUpdate scale_update = 1; - TouchPanStart pan_start = 2; - TouchPanUpdate pan_update = 3; - TouchPanEnd pan_end = 4; - } -} - -message PointerDeviceEvent { - oneof union { - TouchEvent touch_event = 1; - } - repeated ControlKey modifiers = 2; -} - -message MouseEvent { - int32 mask = 1; - sint32 x = 2; - sint32 y = 3; - repeated ControlKey modifiers = 4; -} - -enum KeyboardMode{ - Legacy = 0; - Map = 1; - Translate = 2; - Auto = 3; -} - -enum ControlKey { - Unknown = 0; - Alt = 1; - Backspace = 2; - CapsLock = 3; - Control = 4; - Delete = 5; - DownArrow = 6; - End = 7; - Escape = 8; - F1 = 9; - F10 = 10; - F11 = 11; - F12 = 12; - F2 = 13; - F3 = 14; - F4 = 15; - F5 = 16; - F6 = 17; - F7 = 18; - F8 = 19; - F9 = 20; - Home = 21; - LeftArrow = 22; - /// meta key (also known as "windows"; "super"; and "command") - Meta = 23; - /// option key on macOS (alt key on Linux and Windows) - Option = 24; // deprecated, use Alt instead - PageDown = 25; - PageUp = 26; - Return = 27; - RightArrow = 28; - Shift = 29; - Space = 30; - Tab = 31; - UpArrow = 32; - Numpad0 = 33; - Numpad1 = 34; - Numpad2 = 35; - Numpad3 = 36; - Numpad4 = 37; - Numpad5 = 38; - Numpad6 = 39; - Numpad7 = 40; - Numpad8 = 41; - Numpad9 = 42; - Cancel = 43; - Clear = 44; - Menu = 45; // deprecated, use Alt instead - Pause = 46; - Kana = 47; - Hangul = 48; - Junja = 49; - Final = 50; - Hanja = 51; - Kanji = 52; - Convert = 53; - Select = 54; - Print = 55; - Execute = 56; - Snapshot = 57; - Insert = 58; - Help = 59; - Sleep = 60; - Separator = 61; - Scroll = 62; - NumLock = 63; - RWin = 64; - Apps = 65; - Multiply = 66; - Add = 67; - Subtract = 68; - Decimal = 69; - Divide = 70; - Equals = 71; - NumpadEnter = 72; - RShift = 73; - RControl = 74; - RAlt = 75; - VolumeMute = 76; // mainly used on mobile devices as controlled side - VolumeUp = 77; - VolumeDown = 78; - Power = 79; // mainly used on mobile devices as controlled side - CtrlAltDel = 100; - LockScreen = 101; -} - -message KeyEvent { - // `down` indicates the key's state(down or up). - bool down = 1; - // `press` indicates a click event(down and up). - bool press = 2; - oneof union { - ControlKey control_key = 3; - // position key code. win: scancode, linux: key code, macos: key code - uint32 chr = 4; - uint32 unicode = 5; - string seq = 6; - // high word. virtual keycode - // low word. unicode - uint32 win2win_hotkey = 7; - } - repeated ControlKey modifiers = 8; - KeyboardMode mode = 9; -} - -message CursorData { - uint64 id = 1; - sint32 hotx = 2; - sint32 hoty = 3; - int32 width = 4; - int32 height = 5; - bytes colors = 6; -} - -message CursorPosition { - sint32 x = 1; - sint32 y = 2; -} - -message Hash { - string salt = 1; - string challenge = 2; -} - -enum ClipboardFormat { - Text = 0; - Rtf = 1; - Html = 2; - ImageRgba = 21; - ImagePng = 22; - ImageSvg = 23; - Special = 31; -} - -message Clipboard { - bool compress = 1; - bytes content = 2; - int32 width = 3; - int32 height = 4; - ClipboardFormat format = 5; - // Special format name, only used when format is Special. - string special_name = 6; -} - -message MultiClipboards { repeated Clipboard clipboards = 1; } - -enum FileType { - Dir = 0; - DirLink = 2; - DirDrive = 3; - File = 4; - FileLink = 5; -} - -message FileEntry { - FileType entry_type = 1; - string name = 2; - bool is_hidden = 3; - uint64 size = 4; - uint64 modified_time = 5; -} - -message FileDirectory { - int32 id = 1; - string path = 2; - repeated FileEntry entries = 3; -} - -message ReadDir { - string path = 1; - bool include_hidden = 2; -} - -message ReadEmptyDirs { - string path = 1; - bool include_hidden = 2; -} - -message ReadEmptyDirsResponse { - string path = 1; - repeated FileDirectory empty_dirs = 2; -} - -message ReadAllFiles { - int32 id = 1; - string path = 2; - bool include_hidden = 3; -} - -message FileRename { - int32 id = 1; - string path = 2; - string new_name = 3; -} - -message FileAction { - oneof union { - ReadDir read_dir = 1; - FileTransferSendRequest send = 2; - FileTransferReceiveRequest receive = 3; - FileDirCreate create = 4; - FileRemoveDir remove_dir = 5; - FileRemoveFile remove_file = 6; - ReadAllFiles all_files = 7; - FileTransferCancel cancel = 8; - FileTransferSendConfirmRequest send_confirm = 9; - FileRename rename = 10; - ReadEmptyDirs read_empty_dirs = 11; - } -} - -message FileTransferCancel { int32 id = 1; } - -message FileResponse { - oneof union { - FileDirectory dir = 1; - FileTransferBlock block = 2; - FileTransferError error = 3; - FileTransferDone done = 4; - FileTransferDigest digest = 5; - ReadEmptyDirsResponse empty_dirs = 6; - } -} - -message FileTransferDigest { - int32 id = 1; - sint32 file_num = 2; - uint64 last_modified = 3; - uint64 file_size = 4; - bool is_upload = 5; - bool is_identical = 6; -} - -message FileTransferBlock { - int32 id = 1; - sint32 file_num = 2; - bytes data = 3; - bool compressed = 4; - uint32 blk_id = 5; -} - -message FileTransferError { - int32 id = 1; - string error = 2; - sint32 file_num = 3; -} - -message FileTransferSendRequest { - int32 id = 1; - string path = 2; - bool include_hidden = 3; - int32 file_num = 4; -} - -message FileTransferSendConfirmRequest { - int32 id = 1; - sint32 file_num = 2; - oneof union { - bool skip = 3; - uint32 offset_blk = 4; - } -} - -message FileTransferDone { - int32 id = 1; - sint32 file_num = 2; -} - -message FileTransferReceiveRequest { - int32 id = 1; - string path = 2; // path written to - repeated FileEntry files = 3; - int32 file_num = 4; - uint64 total_size = 5; -} - -message FileRemoveDir { - int32 id = 1; - string path = 2; - bool recursive = 3; -} - -message FileRemoveFile { - int32 id = 1; - string path = 2; - sint32 file_num = 3; -} - -message FileDirCreate { - int32 id = 1; - string path = 2; -} - -// main logic from freeRDP -message CliprdrMonitorReady { -} - -message CliprdrFormat { - int32 id = 2; - string format = 3; -} - -message CliprdrServerFormatList { - repeated CliprdrFormat formats = 2; -} - -message CliprdrServerFormatListResponse { - int32 msg_flags = 2; -} - -message CliprdrServerFormatDataRequest { - int32 requested_format_id = 2; -} - -message CliprdrServerFormatDataResponse { - int32 msg_flags = 2; - bytes format_data = 3; -} - -message CliprdrFileContentsRequest { - int32 stream_id = 2; - int32 list_index = 3; - int32 dw_flags = 4; - int32 n_position_low = 5; - int32 n_position_high = 6; - int32 cb_requested = 7; - bool have_clip_data_id = 8; - int32 clip_data_id = 9; -} - -message CliprdrFileContentsResponse { - int32 msg_flags = 3; - int32 stream_id = 4; - bytes requested_data = 5; -} - -message Cliprdr { - oneof union { - CliprdrMonitorReady ready = 1; - CliprdrServerFormatList format_list = 2; - CliprdrServerFormatListResponse format_list_response = 3; - CliprdrServerFormatDataRequest format_data_request = 4; - CliprdrServerFormatDataResponse format_data_response = 5; - CliprdrFileContentsRequest file_contents_request = 6; - CliprdrFileContentsResponse file_contents_response = 7; - } -} - -message Resolution { - int32 width = 1; - int32 height = 2; -} - -message DisplayResolution { - int32 display = 1; - Resolution resolution = 2; -} - -message SupportedResolutions { repeated Resolution resolutions = 1; } - -message SwitchDisplay { - int32 display = 1; - sint32 x = 2; - sint32 y = 3; - int32 width = 4; - int32 height = 5; - bool cursor_embedded = 6; - SupportedResolutions resolutions = 7; - // Do not care about the origin point for now. - Resolution original_resolution = 8; -} - -message CaptureDisplays { - repeated int32 add = 1; - repeated int32 sub = 2; - repeated int32 set = 3; -} - -message ToggleVirtualDisplay { - int32 display = 1; - bool on = 2; -} - -message TogglePrivacyMode { - string impl_key = 1; - bool on = 2; -} - -message PermissionInfo { - enum Permission { - Keyboard = 0; - Clipboard = 2; - Audio = 3; - File = 4; - Restart = 5; - Recording = 6; - BlockInput = 7; - } - - Permission permission = 1; - bool enabled = 2; -} - -enum ImageQuality { - NotSet = 0; - Low = 2; - Balanced = 3; - Best = 4; -} - -message SupportedDecoding { - enum PreferCodec { - Auto = 0; - VP9 = 1; - H264 = 2; - H265 = 3; - VP8 = 4; - AV1 = 5; - } - - int32 ability_vp9 = 1; - int32 ability_h264 = 2; - int32 ability_h265 = 3; - PreferCodec prefer = 4; - int32 ability_vp8 = 5; - int32 ability_av1 = 6; - CodecAbility i444 = 7; - Chroma prefer_chroma = 8; -} - -message OptionMessage { - enum BoolOption { - NotSet = 0; - No = 1; - Yes = 2; - } - ImageQuality image_quality = 1; - BoolOption lock_after_session_end = 2; - BoolOption show_remote_cursor = 3; - BoolOption privacy_mode = 4; - BoolOption block_input = 5; - int32 custom_image_quality = 6; - BoolOption disable_audio = 7; - BoolOption disable_clipboard = 8; - BoolOption enable_file_transfer = 9; - SupportedDecoding supported_decoding = 10; - int32 custom_fps = 11; - BoolOption disable_keyboard = 12; -// Position 13 is used for Resolution. Remove later. -// Resolution custom_resolution = 13; -// BoolOption support_windows_specific_session = 14; - // starting from 15 please, do not use removed fields - BoolOption follow_remote_cursor = 15; - BoolOption follow_remote_window = 16; -} - -message TestDelay { - int64 time = 1; - bool from_client = 2; - uint32 last_delay = 3; - uint32 target_bitrate = 4; -} - -message PublicKey { - bytes asymmetric_value = 1; - bytes symmetric_value = 2; -} - -message SignedId { bytes id = 1; } - -message AudioFormat { - uint32 sample_rate = 1; - uint32 channels = 2; -} - -message AudioFrame { - bytes data = 1; -} - -// Notify peer to show message box. -message MessageBox { - // Message type. Refer to flutter/lib/common.dart/msgBox(). - string msgtype = 1; - string title = 2; - // English - string text = 3; - // If not empty, msgbox provides a button to following the link. - // The link here can't be directly http url. - // It must be the key of http url configed in peer side or "rustdesk://*" (jump in app). - string link = 4; -} - -message BackNotification { - // no need to consider block input by someone else - enum BlockInputState { - BlkStateUnknown = 0; - BlkOnSucceeded = 2; - BlkOnFailed = 3; - BlkOffSucceeded = 4; - BlkOffFailed = 5; - } - enum PrivacyModeState { - PrvStateUnknown = 0; - // Privacy mode on by someone else - PrvOnByOther = 2; - // Privacy mode is not supported on the remote side - PrvNotSupported = 3; - // Privacy mode on by self - PrvOnSucceeded = 4; - // Privacy mode on by self, but denied - PrvOnFailedDenied = 5; - // Some plugins are not found - PrvOnFailedPlugin = 6; - // Privacy mode on by self, but failed - PrvOnFailed = 7; - // Privacy mode off by self - PrvOffSucceeded = 8; - // Ctrl + P - PrvOffByPeer = 9; - // Privacy mode off by self, but failed - PrvOffFailed = 10; - PrvOffUnknown = 11; - } - - oneof union { - PrivacyModeState privacy_mode_state = 1; - BlockInputState block_input_state = 2; - } - // Supplementary message, for "PrvOnFailed" and "PrvOffFailed" - string details = 3; - // The key of the implementation - string impl_key = 4; -} - -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 PluginRequest { - string id = 1; - bytes content = 2; -} - -message PluginFailure { - string id = 1; - string name = 2; - string msg = 3; -} - -message WindowsSessions { - repeated WindowsSession sessions = 1; - uint32 current_sid = 2; -} - -// Query messages from peer. -message MessageQuery { - // The SwitchDisplay message of the target display. - // If the target display is not found, the message will be ignored. - int32 switch_display = 1; -} - -message Misc { - oneof union { - ChatMessage chat_message = 4; - SwitchDisplay switch_display = 5; - PermissionInfo permission_info = 6; - OptionMessage option = 7; - AudioFormat audio_format = 8; - string close_reason = 9; - bool refresh_video = 10; - bool video_received = 12; - BackNotification back_notification = 13; - bool restart_remote_device = 14; - bool uac = 15; - bool foreground_window_elevated = 16; - bool stop_service = 17; - ElevationRequest elevation_request = 18; - string elevation_response = 19; - bool portable_service_running = 20; - SwitchSidesRequest switch_sides_request = 21; - SwitchBack switch_back = 22; - // Deprecated since 1.2.4, use `change_display_resolution` (36) instead. - // But we must keep it for compatibility when peer version < 1.2.4. - Resolution change_resolution = 24; - PluginRequest plugin_request = 25; - PluginFailure plugin_failure = 26; - uint32 full_speed_fps = 27; // deprecated - uint32 auto_adjust_fps = 28; - bool client_record_status = 29; - CaptureDisplays capture_displays = 30; - int32 refresh_video_display = 31; - ToggleVirtualDisplay toggle_virtual_display = 32; - TogglePrivacyMode toggle_privacy_mode = 33; - SupportedEncoding supported_encoding = 34; - uint32 selected_sid = 35; - DisplayResolution change_display_resolution = 36; - MessageQuery message_query = 37; - int32 follow_current_display = 38; - } -} - -message VoiceCallRequest { - int64 req_timestamp = 1; - // Indicates whether the request is a connect action or a disconnect action. - bool is_connect = 2; -} - -message VoiceCallResponse { - bool accepted = 1; - int64 req_timestamp = 2; // Should copy from [VoiceCallRequest::req_timestamp]. - int64 ack_timestamp = 3; -} - -message Message { - oneof union { - SignedId signed_id = 3; - PublicKey public_key = 4; - TestDelay test_delay = 5; - VideoFrame video_frame = 6; - LoginRequest login_request = 7; - LoginResponse login_response = 8; - Hash hash = 9; - MouseEvent mouse_event = 10; - AudioFrame audio_frame = 11; - CursorData cursor_data = 12; - CursorPosition cursor_position = 13; - uint64 cursor_id = 14; - KeyEvent key_event = 15; - Clipboard clipboard = 16; - FileAction file_action = 17; - FileResponse file_response = 18; - Misc misc = 19; - Cliprdr cliprdr = 20; - MessageBox message_box = 21; - SwitchSidesResponse switch_sides_response = 22; - VoiceCallRequest voice_call_request = 23; - VoiceCallResponse voice_call_response = 24; - PeerInfo peer_info = 25; - PointerDeviceEvent pointer_device_event = 26; - Auth2FA auth_2fa = 27; - MultiClipboards multi_clipboards = 28; - } -} diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto deleted file mode 100644 index 2fc0d9040e6..00000000000 --- a/libs/hbb_common/protos/rendezvous.proto +++ /dev/null @@ -1,196 +0,0 @@ -syntax = "proto3"; -package hbb; - -message RegisterPeer { - string id = 1; - int32 serial = 2; -} - -enum ConnType { - DEFAULT_CONN = 0; - FILE_TRANSFER = 1; - PORT_FORWARD = 2; - RDP = 3; -} - -message RegisterPeerResponse { bool request_pk = 2; } - -message PunchHoleRequest { - string id = 1; - NatType nat_type = 2; - string licence_key = 3; - ConnType conn_type = 4; - string token = 5; - string version = 6; -} - -message PunchHole { - bytes socket_addr = 1; - string relay_server = 2; - NatType nat_type = 3; -} - -message TestNatRequest { - int32 serial = 1; -} - -// per my test, uint/int has no difference in encoding, int not good for negative, use sint for negative -message TestNatResponse { - int32 port = 1; - ConfigUpdate cu = 2; // for mobile -} - -enum NatType { - UNKNOWN_NAT = 0; - ASYMMETRIC = 1; - SYMMETRIC = 2; -} - -message PunchHoleSent { - bytes socket_addr = 1; - string id = 2; - string relay_server = 3; - NatType nat_type = 4; - string version = 5; -} - -message RegisterPk { - string id = 1; - bytes uuid = 2; - bytes pk = 3; - string old_id = 4; -} - -message RegisterPkResponse { - enum Result { - OK = 0; - UUID_MISMATCH = 2; - ID_EXISTS = 3; - TOO_FREQUENT = 4; - INVALID_ID_FORMAT = 5; - NOT_SUPPORT = 6; - SERVER_ERROR = 7; - } - Result result = 1; - int32 keep_alive = 2; -} - -message PunchHoleResponse { - bytes socket_addr = 1; - bytes pk = 2; - enum Failure { - ID_NOT_EXIST = 0; - OFFLINE = 2; - LICENSE_MISMATCH = 3; - LICENSE_OVERUSE = 4; - } - Failure failure = 3; - string relay_server = 4; - oneof union { - NatType nat_type = 5; - bool is_local = 6; - } - string other_failure = 7; - int32 feedback = 8; -} - -message ConfigUpdate { - int32 serial = 1; - repeated string rendezvous_servers = 2; -} - -message RequestRelay { - string id = 1; - string uuid = 2; - bytes socket_addr = 3; - string relay_server = 4; - bool secure = 5; - string licence_key = 6; - ConnType conn_type = 7; - string token = 8; -} - -message RelayResponse { - bytes socket_addr = 1; - string uuid = 2; - string relay_server = 3; - oneof union { - string id = 4; - bytes pk = 5; - } - string refuse_reason = 6; - string version = 7; - int32 feedback = 9; -} - -message SoftwareUpdate { string url = 1; } - -// if in same intranet, punch hole won't work both for udp and tcp, -// even some router has below connection error if we connect itself, -// { kind: Other, error: "could not resolve to any address" }, -// so we request local address to connect. -message FetchLocalAddr { - bytes socket_addr = 1; - string relay_server = 2; -} - -message LocalAddr { - bytes socket_addr = 1; - bytes local_addr = 2; - string relay_server = 3; - string id = 4; - string version = 5; -} - -message PeerDiscovery { - string cmd = 1; - string mac = 2; - string id = 3; - string username = 4; - string hostname = 5; - string platform = 6; - string misc = 7; -} - -message OnlineRequest { - string id = 1; - repeated string peers = 2; -} - -message OnlineResponse { - bytes states = 1; -} - -message KeyExchange { - repeated bytes keys = 1; -} - -message HealthCheck { - string token = 1; -} - -message RendezvousMessage { - oneof union { - RegisterPeer register_peer = 6; - RegisterPeerResponse register_peer_response = 7; - PunchHoleRequest punch_hole_request = 8; - PunchHole punch_hole = 9; - PunchHoleSent punch_hole_sent = 10; - PunchHoleResponse punch_hole_response = 11; - FetchLocalAddr fetch_local_addr = 12; - LocalAddr local_addr = 13; - ConfigUpdate configure_update = 14; - RegisterPk register_pk = 15; - RegisterPkResponse register_pk_response = 16; - SoftwareUpdate software_update = 17; - RequestRelay request_relay = 18; - RelayResponse relay_response = 19; - TestNatRequest test_nat_request = 20; - TestNatResponse test_nat_response = 21; - PeerDiscovery peer_discovery = 22; - OnlineRequest online_request = 23; - OnlineResponse online_response = 24; - KeyExchange key_exchange = 25; - HealthCheck hc = 26; - } -} diff --git a/libs/hbb_common/src/bytes_codec.rs b/libs/hbb_common/src/bytes_codec.rs deleted file mode 100644 index bfc79871554..00000000000 --- a/libs/hbb_common/src/bytes_codec.rs +++ /dev/null @@ -1,280 +0,0 @@ -use bytes::{Buf, BufMut, Bytes, BytesMut}; -use std::io; -use tokio_util::codec::{Decoder, Encoder}; - -#[derive(Debug, Clone, Copy)] -pub struct BytesCodec { - state: DecodeState, - raw: bool, - max_packet_length: usize, -} - -#[derive(Debug, Clone, Copy)] -enum DecodeState { - Head, - Data(usize), -} - -impl Default for BytesCodec { - fn default() -> Self { - Self::new() - } -} - -impl BytesCodec { - pub fn new() -> Self { - Self { - state: DecodeState::Head, - raw: false, - max_packet_length: usize::MAX, - } - } - - pub fn set_raw(&mut self) { - self.raw = true; - } - - pub fn set_max_packet_length(&mut self, n: usize) { - self.max_packet_length = n; - } - - fn decode_head(&mut self, src: &mut BytesMut) -> io::Result> { - if src.is_empty() { - return Ok(None); - } - let head_len = ((src[0] & 0x3) + 1) as usize; - if src.len() < head_len { - return Ok(None); - } - let mut n = src[0] as usize; - if head_len > 1 { - n |= (src[1] as usize) << 8; - } - if head_len > 2 { - n |= (src[2] as usize) << 16; - } - if head_len > 3 { - n |= (src[3] as usize) << 24; - } - n >>= 2; - if n > self.max_packet_length { - return Err(io::Error::new(io::ErrorKind::InvalidData, "Too big packet")); - } - src.advance(head_len); - src.reserve(n); - Ok(Some(n)) - } - - fn decode_data(&self, n: usize, src: &mut BytesMut) -> io::Result> { - if src.len() < n { - return Ok(None); - } - Ok(Some(src.split_to(n))) - } -} - -impl Decoder for BytesCodec { - type Item = BytesMut; - type Error = io::Error; - - fn decode(&mut self, src: &mut BytesMut) -> Result, io::Error> { - if self.raw { - if !src.is_empty() { - let len = src.len(); - return Ok(Some(src.split_to(len))); - } else { - return Ok(None); - } - } - let n = match self.state { - DecodeState::Head => match self.decode_head(src)? { - Some(n) => { - self.state = DecodeState::Data(n); - n - } - None => return Ok(None), - }, - DecodeState::Data(n) => n, - }; - - match self.decode_data(n, src)? { - Some(data) => { - self.state = DecodeState::Head; - Ok(Some(data)) - } - None => Ok(None), - } - } -} - -impl Encoder for BytesCodec { - type Error = io::Error; - - fn encode(&mut self, data: Bytes, buf: &mut BytesMut) -> Result<(), io::Error> { - if self.raw { - buf.reserve(data.len()); - buf.put(data); - return Ok(()); - } - if data.len() <= 0x3F { - buf.put_u8((data.len() << 2) as u8); - } else if data.len() <= 0x3FFF { - buf.put_u16_le((data.len() << 2) as u16 | 0x1); - } else if data.len() <= 0x3FFFFF { - let h = (data.len() << 2) as u32 | 0x2; - buf.put_u16_le((h & 0xFFFF) as u16); - buf.put_u8((h >> 16) as u8); - } else if data.len() <= 0x3FFFFFFF { - buf.put_u32_le((data.len() << 2) as u32 | 0x3); - } else { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "Overflow")); - } - buf.extend(data); - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_codec1() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3F, 1); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - let buf_saved = buf.clone(); - assert_eq!(buf.len(), 0x3F + 1); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3F); - assert_eq!(res[0], 1); - } else { - panic!(); - } - let mut codec2 = BytesCodec::new(); - let mut buf2 = BytesMut::new(); - if let Ok(None) = codec2.decode(&mut buf2) { - } else { - panic!(); - } - buf2.extend(&buf_saved[0..1]); - if let Ok(None) = codec2.decode(&mut buf2) { - } else { - panic!(); - } - buf2.extend(&buf_saved[1..]); - if let Ok(Some(res)) = codec2.decode(&mut buf2) { - assert_eq!(res.len(), 0x3F); - assert_eq!(res[0], 1); - } else { - panic!(); - } - } - - #[test] - fn test_codec2() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - assert!(codec.encode("".into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 1); - bytes.resize(0x3F + 1, 2); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 0x3F + 2 + 2); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0); - } else { - panic!(); - } - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3F + 1); - assert_eq!(res[0], 2); - } else { - panic!(); - } - } - - #[test] - fn test_codec3() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3F - 1, 3); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 0x3F + 1 - 1); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3F - 1); - assert_eq!(res[0], 3); - } else { - panic!(); - } - } - #[test] - fn test_codec4() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3FFF, 4); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 0x3FFF + 2); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3FFF); - assert_eq!(res[0], 4); - } else { - panic!(); - } - } - - #[test] - fn test_codec5() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3FFFFF, 5); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - assert_eq!(buf.len(), 0x3FFFFF + 3); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3FFFFF); - assert_eq!(res[0], 5); - } else { - panic!(); - } - } - - #[test] - fn test_codec6() { - let mut codec = BytesCodec::new(); - let mut buf = BytesMut::new(); - let mut bytes: Vec = Vec::new(); - bytes.resize(0x3FFFFF + 1, 6); - assert!(codec.encode(bytes.into(), &mut buf).is_ok()); - let buf_saved = buf.clone(); - assert_eq!(buf.len(), 0x3FFFFF + 4 + 1); - if let Ok(Some(res)) = codec.decode(&mut buf) { - assert_eq!(res.len(), 0x3FFFFF + 1); - assert_eq!(res[0], 6); - } else { - panic!(); - } - let mut codec2 = BytesCodec::new(); - let mut buf2 = BytesMut::new(); - buf2.extend(&buf_saved[0..1]); - if let Ok(None) = codec2.decode(&mut buf2) { - } else { - panic!(); - } - buf2.extend(&buf_saved[1..6]); - if let Ok(None) = codec2.decode(&mut buf2) { - } else { - panic!(); - } - buf2.extend(&buf_saved[6..]); - if let Ok(Some(res)) = codec2.decode(&mut buf2) { - assert_eq!(res.len(), 0x3FFFFF + 1); - assert_eq!(res[0], 6); - } else { - panic!(); - } - } -} diff --git a/libs/hbb_common/src/compress.rs b/libs/hbb_common/src/compress.rs deleted file mode 100644 index 761d916e4f8..00000000000 --- a/libs/hbb_common/src/compress.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::{cell::RefCell, io}; -use zstd::bulk::Compressor; - -// The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), -// which is currently 22. Levels >= 20 -// Default level is ZSTD_CLEVEL_DEFAULT==3. -// value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT -thread_local! { - static COMPRESSOR: RefCell>> = RefCell::new(Compressor::new(crate::config::COMPRESS_LEVEL)); -} - -pub fn compress(data: &[u8]) -> Vec { - let mut out = Vec::new(); - COMPRESSOR.with(|c| { - if let Ok(mut c) = c.try_borrow_mut() { - match &mut *c { - Ok(c) => match c.compress(data) { - Ok(res) => out = res, - Err(err) => { - crate::log::debug!("Failed to compress: {}", err); - } - }, - Err(err) => { - crate::log::debug!("Failed to get compressor: {}", err); - } - } - } - }); - out -} - -pub fn decompress(data: &[u8]) -> Vec { - zstd::decode_all(data).unwrap_or_default() -} diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs deleted file mode 100644 index dd4abaf9d9b..00000000000 --- a/libs/hbb_common/src/config.rs +++ /dev/null @@ -1,2692 +0,0 @@ -use std::{ - collections::{HashMap, HashSet}, - fs, - io::{Read, Write}, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - ops::{Deref, DerefMut}, - path::{Path, PathBuf}, - sync::{Mutex, RwLock}, - time::{Duration, Instant, SystemTime}, -}; - -use anyhow::Result; -use bytes::Bytes; -use rand::Rng; -use regex::Regex; -use serde as de; -use serde_derive::{Deserialize, Serialize}; -use serde_json; -use sodiumoxide::base64; -use sodiumoxide::crypto::sign; - -use crate::{ - compress::{compress, decompress}, - log, - password_security::{ - decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original, - encrypt_vec_or_original, symmetric_crypt, - }, -}; - -pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; -pub const CONNECT_TIMEOUT: u64 = 18_000; -pub const READ_TIMEOUT: u64 = 18_000; -// https://github.com/quic-go/quic-go/issues/525#issuecomment-294531351 -// https://datatracker.ietf.org/doc/html/draft-hamilton-early-deployment-quic-00#section-6.10 -// 15 seconds is recommended by quic, though oneSIP recommend 25 seconds, -// https://www.onsip.com/voip-resources/voip-fundamentals/what-is-nat-keepalive -pub const REG_INTERVAL: i64 = 15_000; -pub const COMPRESS_LEVEL: i32 = 3; -const SERIAL: i32 = 3; -const PASSWORD_ENC_VERSION: &str = "00"; -pub const ENCRYPT_MAX_LEN: usize = 128; // used for password, pin, etc, not for all - -#[cfg(target_os = "macos")] -lazy_static::lazy_static! { - pub static ref ORG: RwLock = RwLock::new("com.carriez".to_owned()); -} - -type Size = (i32, i32, i32, i32); -type KeyPair = (Vec, Vec); - -lazy_static::lazy_static! { - static ref CONFIG: RwLock = RwLock::new(Config::load()); - static ref CONFIG2: RwLock = RwLock::new(Config2::load()); - static ref LOCAL_CONFIG: RwLock = RwLock::new(LocalConfig::load()); - static ref TRUSTED_DEVICES: RwLock<(Vec, bool)> = Default::default(); - static ref ONLINE: Mutex> = Default::default(); - pub static ref PROD_RENDEZVOUS_SERVER: RwLock = RwLock::new(match option_env!("RENDEZVOUS_SERVER") { - Some(key) if !key.is_empty() => key, - _ => "", - }.to_owned()); - pub static ref EXE_RENDEZVOUS_SERVER: RwLock = Default::default(); - pub static ref APP_NAME: RwLock = RwLock::new("RustDesk".to_owned()); - static ref KEY_PAIR: Mutex> = Default::default(); - static ref USER_DEFAULT_CONFIG: RwLock<(UserDefaultConfig, Instant)> = RwLock::new((UserDefaultConfig::load(), Instant::now())); - pub static ref NEW_STORED_PEER_CONFIG: Mutex> = Default::default(); - pub static ref DEFAULT_SETTINGS: RwLock> = Default::default(); - pub static ref OVERWRITE_SETTINGS: RwLock> = Default::default(); - pub static ref DEFAULT_DISPLAY_SETTINGS: RwLock> = Default::default(); - pub static ref OVERWRITE_DISPLAY_SETTINGS: RwLock> = Default::default(); - pub static ref DEFAULT_LOCAL_SETTINGS: RwLock> = Default::default(); - pub static ref OVERWRITE_LOCAL_SETTINGS: RwLock> = Default::default(); - pub static ref HARD_SETTINGS: RwLock> = Default::default(); - pub static ref BUILTIN_SETTINGS: RwLock> = Default::default(); -} - -lazy_static::lazy_static! { - pub static ref APP_DIR: RwLock = Default::default(); -} - -#[cfg(any(target_os = "android", target_os = "ios"))] -lazy_static::lazy_static! { - pub static ref APP_HOME_DIR: RwLock = Default::default(); -} - -pub const LINK_DOCS_HOME: &str = "https://rustdesk.com/docs/en/"; -pub const LINK_DOCS_X11_REQUIRED: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; -pub const LINK_HEADLESS_LINUX_SUPPORT: &str = - "https://github.com/rustdesk/rustdesk/wiki/Headless-Linux-Support"; -lazy_static::lazy_static! { - pub static ref HELPER_URL: HashMap<&'static str, &'static str> = HashMap::from([ - ("rustdesk docs home", LINK_DOCS_HOME), - ("rustdesk docs x11-required", LINK_DOCS_X11_REQUIRED), - ("rustdesk x11 headless", LINK_HEADLESS_LINUX_SUPPORT), - ]); -} - -const CHARS: &[char] = &[ - '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', - 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', -]; - -pub const RENDEZVOUS_SERVERS: &[&str] = &["rs-ny.rustdesk.com"]; -pub const PUBLIC_RS_PUB_KEY: &str = "OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw="; - -pub const RS_PUB_KEY: &str = match option_env!("RS_PUB_KEY") { - Some(key) if !key.is_empty() => key, - _ => PUBLIC_RS_PUB_KEY, -}; - -pub const RENDEZVOUS_PORT: i32 = 21116; -pub const RELAY_PORT: i32 = 21117; - -macro_rules! serde_field_string { - ($default_func:ident, $de_func:ident, $default_expr:expr) => { - fn $default_func() -> String { - $default_expr - } - - fn $de_func<'de, D>(deserializer: D) -> Result - where - D: de::Deserializer<'de>, - { - let s: String = - de::Deserialize::deserialize(deserializer).unwrap_or(Self::$default_func()); - if s.is_empty() { - return Ok(Self::$default_func()); - } - Ok(s) - } - }; -} - -macro_rules! serde_field_bool { - ($struct_name: ident, $field_name: literal, $func: ident, $default: literal) => { - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] - pub struct $struct_name { - #[serde(default = $default, rename = $field_name, deserialize_with = "deserialize_bool")] - pub v: bool, - } - impl Default for $struct_name { - fn default() -> Self { - Self { v: Self::$func() } - } - } - impl $struct_name { - pub fn $func() -> bool { - UserDefaultConfig::read($field_name) == "Y" - } - } - impl Deref for $struct_name { - type Target = bool; - - fn deref(&self) -> &Self::Target { - &self.v - } - } - impl DerefMut for $struct_name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.v - } - } - }; -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum NetworkType { - Direct, - ProxySocks, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] -pub struct Config { - #[serde( - default, - skip_serializing_if = "String::is_empty", - deserialize_with = "deserialize_string" - )] - pub id: String, // use - #[serde(default, deserialize_with = "deserialize_string")] - enc_id: String, // store - #[serde(default, deserialize_with = "deserialize_string")] - password: String, - #[serde(default, deserialize_with = "deserialize_string")] - salt: String, - #[serde(default, deserialize_with = "deserialize_keypair")] - key_pair: KeyPair, // sk, pk - #[serde(default, deserialize_with = "deserialize_bool")] - key_confirmed: bool, - #[serde(default, deserialize_with = "deserialize_hashmap_string_bool")] - keys_confirmed: HashMap, -} - -#[derive(Debug, Default, PartialEq, Serialize, Deserialize, Clone)] -pub struct Socks5Server { - #[serde(default, deserialize_with = "deserialize_string")] - pub proxy: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub username: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub password: String, -} - -// more variable configs -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] -pub struct Config2 { - #[serde(default, deserialize_with = "deserialize_string")] - rendezvous_server: String, - #[serde(default, deserialize_with = "deserialize_i32")] - nat_type: i32, - #[serde(default, deserialize_with = "deserialize_i32")] - serial: i32, - #[serde(default, deserialize_with = "deserialize_string")] - unlock_pin: String, - #[serde(default, deserialize_with = "deserialize_string")] - trusted_devices: String, - - #[serde(default)] - socks: Option, - - // the other scalar value must before this - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - pub options: HashMap, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] -pub struct Resolution { - pub w: i32, - pub h: i32, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] -pub struct PeerConfig { - #[serde(default, deserialize_with = "deserialize_vec_u8")] - pub password: Vec, - #[serde(default, deserialize_with = "deserialize_size")] - pub size: Size, - #[serde(default, deserialize_with = "deserialize_size")] - pub size_ft: Size, - #[serde(default, deserialize_with = "deserialize_size")] - pub size_pf: Size, - #[serde( - default = "PeerConfig::default_view_style", - deserialize_with = "PeerConfig::deserialize_view_style", - skip_serializing_if = "String::is_empty" - )] - pub view_style: String, - // Image scroll style, scrollbar or scroll auto - #[serde( - default = "PeerConfig::default_scroll_style", - deserialize_with = "PeerConfig::deserialize_scroll_style", - skip_serializing_if = "String::is_empty" - )] - pub scroll_style: String, - #[serde( - default = "PeerConfig::default_image_quality", - deserialize_with = "PeerConfig::deserialize_image_quality", - skip_serializing_if = "String::is_empty" - )] - pub image_quality: String, - #[serde( - default = "PeerConfig::default_custom_image_quality", - deserialize_with = "PeerConfig::deserialize_custom_image_quality", - skip_serializing_if = "Vec::is_empty" - )] - pub custom_image_quality: Vec, - #[serde(flatten)] - pub show_remote_cursor: ShowRemoteCursor, - #[serde(flatten)] - pub lock_after_session_end: LockAfterSessionEnd, - #[serde(flatten)] - pub privacy_mode: PrivacyMode, - #[serde(flatten)] - pub allow_swap_key: AllowSwapKey, - #[serde(default, deserialize_with = "deserialize_vec_i32_string_i32")] - pub port_forwards: Vec<(i32, String, i32)>, - #[serde(default, deserialize_with = "deserialize_i32")] - pub direct_failures: i32, - #[serde(flatten)] - pub disable_audio: DisableAudio, - #[serde(flatten)] - pub disable_clipboard: DisableClipboard, - #[serde(flatten)] - pub enable_file_copy_paste: EnableFileCopyPaste, - #[serde(flatten)] - pub show_quality_monitor: ShowQualityMonitor, - #[serde(flatten)] - pub follow_remote_cursor: FollowRemoteCursor, - #[serde(flatten)] - pub follow_remote_window: FollowRemoteWindow, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub keyboard_mode: String, - #[serde(flatten)] - pub view_only: ViewOnly, - #[serde(flatten)] - pub sync_init_clipboard: SyncInitClipboard, - // Mouse wheel or touchpad scroll mode - #[serde( - default = "PeerConfig::default_reverse_mouse_wheel", - deserialize_with = "PeerConfig::deserialize_reverse_mouse_wheel", - skip_serializing_if = "String::is_empty" - )] - pub reverse_mouse_wheel: String, - #[serde( - default = "PeerConfig::default_displays_as_individual_windows", - deserialize_with = "PeerConfig::deserialize_displays_as_individual_windows", - skip_serializing_if = "String::is_empty" - )] - pub displays_as_individual_windows: String, - #[serde( - default = "PeerConfig::default_use_all_my_displays_for_the_remote_session", - deserialize_with = "PeerConfig::deserialize_use_all_my_displays_for_the_remote_session", - skip_serializing_if = "String::is_empty" - )] - pub use_all_my_displays_for_the_remote_session: String, - - #[serde( - default, - deserialize_with = "deserialize_hashmap_resolutions", - skip_serializing_if = "HashMap::is_empty" - )] - pub custom_resolutions: HashMap, - - // The other scalar value must before this - #[serde( - default, - deserialize_with = "deserialize_hashmap_string_string", - skip_serializing_if = "HashMap::is_empty" - )] - pub options: HashMap, // not use delete to represent default values - // Various data for flutter ui - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - pub ui_flutter: HashMap, - #[serde(default)] - pub info: PeerInfoSerde, - #[serde(default)] - pub transfer: TransferSerde, -} - -impl Default for PeerConfig { - fn default() -> Self { - Self { - password: Default::default(), - size: Default::default(), - size_ft: Default::default(), - size_pf: Default::default(), - view_style: Self::default_view_style(), - scroll_style: Self::default_scroll_style(), - image_quality: Self::default_image_quality(), - custom_image_quality: Self::default_custom_image_quality(), - show_remote_cursor: Default::default(), - lock_after_session_end: Default::default(), - privacy_mode: Default::default(), - allow_swap_key: Default::default(), - port_forwards: Default::default(), - direct_failures: Default::default(), - disable_audio: Default::default(), - disable_clipboard: Default::default(), - enable_file_copy_paste: Default::default(), - show_quality_monitor: Default::default(), - follow_remote_cursor: Default::default(), - follow_remote_window: Default::default(), - keyboard_mode: Default::default(), - view_only: Default::default(), - reverse_mouse_wheel: Self::default_reverse_mouse_wheel(), - displays_as_individual_windows: Self::default_displays_as_individual_windows(), - use_all_my_displays_for_the_remote_session: - Self::default_use_all_my_displays_for_the_remote_session(), - custom_resolutions: Default::default(), - options: Self::default_options(), - ui_flutter: Default::default(), - info: Default::default(), - transfer: Default::default(), - sync_init_clipboard: Default::default(), - } - } -} - -#[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] -pub struct PeerInfoSerde { - #[serde(default, deserialize_with = "deserialize_string")] - pub username: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub hostname: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub platform: String, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)] -pub struct TransferSerde { - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub write_jobs: Vec, - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub read_jobs: Vec, -} - -#[inline] -pub fn get_online_state() -> i64 { - *ONLINE.lock().unwrap().values().max().unwrap_or(&0) -} - -#[cfg(not(any(target_os = "android", target_os = "ios")))] -fn patch(path: PathBuf) -> PathBuf { - if let Some(_tmp) = path.to_str() { - #[cfg(windows)] - return _tmp - .replace( - "system32\\config\\systemprofile", - "ServiceProfiles\\LocalService", - ) - .into(); - #[cfg(target_os = "macos")] - return _tmp.replace("Application Support", "Preferences").into(); - #[cfg(target_os = "linux")] - { - if _tmp == "/root" { - if let Ok(user) = crate::platform::linux::run_cmds_trim_newline("whoami") { - if user != "root" { - let cmd = format!("getent passwd '{}' | awk -F':' '{{print $6}}'", user); - if let Ok(output) = crate::platform::linux::run_cmds_trim_newline(&cmd) { - return output.into(); - } - return format!("/home/{user}").into(); - } - } - } - } - } - path -} - -impl Config2 { - fn load() -> Config2 { - let mut config = Config::load_::("2"); - let mut store = false; - if let Some(mut socks) = config.socks { - let (password, _, store2) = - decrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION); - socks.password = password; - config.socks = Some(socks); - store |= store2; - } - let (unlock_pin, _, store2) = - decrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION); - config.unlock_pin = unlock_pin; - store |= store2; - if store { - config.store(); - } - config - } - - pub fn file() -> PathBuf { - Config::file_("2") - } - - fn store(&self) { - let mut config = self.clone(); - if let Some(mut socks) = config.socks { - socks.password = - encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - config.socks = Some(socks); - } - config.unlock_pin = - encrypt_str_or_original(&config.unlock_pin, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - Config::store_(&config, "2"); - } - - pub fn get() -> Config2 { - return CONFIG2.read().unwrap().clone(); - } - - pub fn set(cfg: Config2) -> bool { - let mut lock = CONFIG2.write().unwrap(); - if *lock == cfg { - return false; - } - *lock = cfg; - lock.store(); - true - } -} - -pub fn load_path( - file: PathBuf, -) -> T { - let cfg = match confy::load_path(&file) { - Ok(config) => config, - Err(err) => { - if let confy::ConfyError::GeneralLoadError(err) = &err { - if err.kind() == std::io::ErrorKind::NotFound { - return T::default(); - } - } - log::error!("Failed to load config '{}': {}", file.display(), err); - T::default() - } - }; - cfg -} - -#[inline] -pub fn store_path(path: PathBuf, cfg: T) -> crate::ResultType<()> { - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - Ok(confy::store_path_perms( - path, - cfg, - fs::Permissions::from_mode(0o600), - )?) - } - #[cfg(windows)] - { - Ok(confy::store_path(path, cfg)?) - } -} - -impl Config { - fn load_( - suffix: &str, - ) -> T { - let file = Self::file_(suffix); - let cfg = load_path(file); - if suffix.is_empty() { - log::trace!("{:?}", cfg); - } - cfg - } - - fn store_(config: &T, suffix: &str) { - let file = Self::file_(suffix); - if let Err(err) = store_path(file, config) { - log::error!("Failed to store {suffix} config: {err}"); - } - } - - fn load() -> Config { - let mut config = Config::load_::(""); - let mut store = false; - let (password, _, store1) = decrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION); - config.password = password; - store |= store1; - let mut id_valid = false; - let (id, encrypted, store2) = decrypt_str_or_original(&config.enc_id, PASSWORD_ENC_VERSION); - if encrypted { - config.id = id; - id_valid = true; - store |= store2; - } else if - // Comment out for forward compatible - // crate::get_modified_time(&Self::file_("")) - // .checked_sub(std::time::Duration::from_secs(30)) // allow modification during installation - // .unwrap_or_else(crate::get_exe_time) - // < crate::get_exe_time() - // && - !config.id.is_empty() - && config.enc_id.is_empty() - && !decrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION).1 - { - id_valid = true; - store = true; - } - if !id_valid { - for _ in 0..3 { - if let Some(id) = Config::get_auto_id() { - config.id = id; - store = true; - break; - } else { - log::error!("Failed to generate new id"); - } - } - } - if store { - config.store(); - } - config - } - - fn store(&self) { - let mut config = self.clone(); - config.password = - encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - config.enc_id = encrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - config.id = "".to_owned(); - Config::store_(&config, ""); - } - - pub fn file() -> PathBuf { - Self::file_("") - } - - fn file_(suffix: &str) -> PathBuf { - let name = format!("{}{}", *APP_NAME.read().unwrap(), suffix); - Config::with_extension(Self::path(name)) - } - - pub fn is_empty(&self) -> bool { - (self.id.is_empty() && self.enc_id.is_empty()) || self.key_pair.0.is_empty() - } - - pub fn get_home() -> PathBuf { - #[cfg(any(target_os = "android", target_os = "ios"))] - return PathBuf::from(APP_HOME_DIR.read().unwrap().as_str()); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - if let Some(path) = dirs_next::home_dir() { - patch(path) - } else if let Ok(path) = std::env::current_dir() { - path - } else { - std::env::temp_dir() - } - } - } - - pub fn path>(p: P) -> PathBuf { - #[cfg(any(target_os = "android", target_os = "ios"))] - { - let mut path: PathBuf = APP_DIR.read().unwrap().clone().into(); - path.push(p); - return path; - } - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - #[cfg(not(target_os = "macos"))] - let org = "".to_owned(); - #[cfg(target_os = "macos")] - let org = ORG.read().unwrap().clone(); - // /var/root for root - if let Some(project) = - directories_next::ProjectDirs::from("", &org, &APP_NAME.read().unwrap()) - { - let mut path = patch(project.config_dir().to_path_buf()); - path.push(p); - return path; - } - "".into() - } - } - - #[allow(unreachable_code)] - pub fn log_path() -> PathBuf { - #[cfg(target_os = "macos")] - { - if let Some(path) = dirs_next::home_dir().as_mut() { - path.push(format!("Library/Logs/{}", *APP_NAME.read().unwrap())); - return path.clone(); - } - } - #[cfg(target_os = "linux")] - { - let mut path = Self::get_home(); - path.push(format!(".local/share/logs/{}", *APP_NAME.read().unwrap())); - std::fs::create_dir_all(&path).ok(); - return path; - } - #[cfg(target_os = "android")] - { - let mut path = Self::get_home(); - path.push(format!("{}/Logs", *APP_NAME.read().unwrap())); - std::fs::create_dir_all(&path).ok(); - return path; - } - if let Some(path) = Self::path("").parent() { - let mut path: PathBuf = path.into(); - path.push("log"); - return path; - } - "".into() - } - - pub fn ipc_path(postfix: &str) -> String { - #[cfg(windows)] - { - // \\ServerName\pipe\PipeName - // where ServerName is either the name of a remote computer or a period, to specify the local computer. - // https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names - format!( - "\\\\.\\pipe\\{}\\query{}", - *APP_NAME.read().unwrap(), - postfix - ) - } - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - #[cfg(target_os = "android")] - let mut path: PathBuf = - format!("{}/{}", *APP_DIR.read().unwrap(), *APP_NAME.read().unwrap()).into(); - #[cfg(not(target_os = "android"))] - let mut path: PathBuf = format!("/tmp/{}", *APP_NAME.read().unwrap()).into(); - fs::create_dir(&path).ok(); - fs::set_permissions(&path, fs::Permissions::from_mode(0o0777)).ok(); - path.push(format!("ipc{postfix}")); - path.to_str().unwrap_or("").to_owned() - } - } - - pub fn icon_path() -> PathBuf { - let mut path = Self::path("icons"); - if fs::create_dir_all(&path).is_err() { - path = std::env::temp_dir(); - } - path - } - - #[inline] - 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 { - let mut rendezvous_server = EXE_RENDEZVOUS_SERVER.read().unwrap().clone(); - if rendezvous_server.is_empty() { - rendezvous_server = Self::get_option("custom-rendezvous-server"); - } - if rendezvous_server.is_empty() { - rendezvous_server = PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); - } - if rendezvous_server.is_empty() { - rendezvous_server = CONFIG2.read().unwrap().rendezvous_server.clone(); - } - if rendezvous_server.is_empty() { - rendezvous_server = Self::get_rendezvous_servers() - .drain(..) - .next() - .unwrap_or_default(); - } - if !rendezvous_server.contains(':') { - rendezvous_server = format!("{rendezvous_server}:{RENDEZVOUS_PORT}"); - } - rendezvous_server - } - - pub fn get_rendezvous_servers() -> Vec { - let s = EXE_RENDEZVOUS_SERVER.read().unwrap().clone(); - if !s.is_empty() { - return vec![s]; - } - let s = Self::get_option("custom-rendezvous-server"); - if !s.is_empty() { - return vec![s]; - } - let s = PROD_RENDEZVOUS_SERVER.read().unwrap().clone(); - if !s.is_empty() { - return vec![s]; - } - let serial_obsolute = CONFIG2.read().unwrap().serial > SERIAL; - if serial_obsolute { - let ss: Vec = Self::get_option("rendezvous-servers") - .split(',') - .filter(|x| x.contains('.')) - .map(|x| x.to_owned()) - .collect(); - if !ss.is_empty() { - return ss; - } - } - return RENDEZVOUS_SERVERS.iter().map(|x| x.to_string()).collect(); - } - - pub fn reset_online() { - *ONLINE.lock().unwrap() = Default::default(); - } - - pub fn update_latency(host: &str, latency: i64) { - ONLINE.lock().unwrap().insert(host.to_owned(), latency); - let mut host = "".to_owned(); - let mut delay = i64::MAX; - for (tmp_host, tmp_delay) in ONLINE.lock().unwrap().iter() { - if tmp_delay > &0 && tmp_delay < &delay { - delay = *tmp_delay; - host = tmp_host.to_string(); - } - } - if !host.is_empty() { - let mut config = CONFIG2.write().unwrap(); - if host != config.rendezvous_server { - log::debug!("Update rendezvous_server in config to {}", host); - log::debug!("{:?}", *ONLINE.lock().unwrap()); - config.rendezvous_server = host; - config.store(); - } - } - } - - pub fn set_id(id: &str) { - let mut config = CONFIG.write().unwrap(); - if id == config.id { - return; - } - config.id = id.into(); - config.store(); - } - - pub fn set_nat_type(nat_type: i32) { - let mut config = CONFIG2.write().unwrap(); - if nat_type == config.nat_type { - return; - } - config.nat_type = nat_type; - config.store(); - } - - pub fn get_nat_type() -> i32 { - CONFIG2.read().unwrap().nat_type - } - - pub fn set_serial(serial: i32) { - let mut config = CONFIG2.write().unwrap(); - if serial == config.serial { - return; - } - config.serial = serial; - config.store(); - } - - pub fn get_serial() -> i32 { - std::cmp::max(CONFIG2.read().unwrap().serial, SERIAL) - } - - fn get_auto_id() -> Option { - #[cfg(any(target_os = "android", target_os = "ios"))] - { - return Some( - rand::thread_rng() - .gen_range(1_000_000_000..2_000_000_000) - .to_string(), - ); - } - - #[cfg(not(any(target_os = "android", target_os = "ios")))] - { - let mut id = 0u32; - if let Ok(Some(ma)) = mac_address::get_mac_address() { - for x in &ma.bytes()[2..] { - id = (id << 8) | (*x as u32); - } - id &= 0x1FFFFFFF; - Some(id.to_string()) - } else { - None - } - } - } - - pub fn get_auto_password(length: usize) -> String { - let mut rng = rand::thread_rng(); - (0..length) - .map(|_| CHARS[rng.gen::() % CHARS.len()]) - .collect() - } - - pub fn get_key_confirmed() -> bool { - CONFIG.read().unwrap().key_confirmed - } - - pub fn set_key_confirmed(v: bool) { - let mut config = CONFIG.write().unwrap(); - if config.key_confirmed == v { - return; - } - config.key_confirmed = v; - if !v { - config.keys_confirmed = Default::default(); - } - config.store(); - } - - pub fn get_host_key_confirmed(host: &str) -> bool { - matches!(CONFIG.read().unwrap().keys_confirmed.get(host), Some(true)) - } - - pub fn set_host_key_confirmed(host: &str, v: bool) { - if Self::get_host_key_confirmed(host) == v { - return; - } - let mut config = CONFIG.write().unwrap(); - config.keys_confirmed.insert(host.to_owned(), v); - config.store(); - } - - pub fn get_key_pair() -> KeyPair { - // lock here to make sure no gen_keypair more than once - // no use of CONFIG directly here to ensure no recursive calling in Config::load because of password dec which calling this function - let mut lock = KEY_PAIR.lock().unwrap(); - if let Some(p) = lock.as_ref() { - return p.clone(); - } - let mut config = Config::load_::(""); - if config.key_pair.0.is_empty() { - log::info!("Generated new keypair for id: {}", config.id); - let (pk, sk) = sign::gen_keypair(); - let key_pair = (sk.0.to_vec(), pk.0.into()); - config.key_pair = key_pair.clone(); - std::thread::spawn(|| { - let mut config = CONFIG.write().unwrap(); - config.key_pair = key_pair; - config.store(); - }); - } - *lock = Some(config.key_pair.clone()); - config.key_pair - } - - pub fn get_id() -> String { - let mut id = CONFIG.read().unwrap().id.clone(); - if id.is_empty() { - if let Some(tmp) = Config::get_auto_id() { - id = tmp; - Config::set_id(&id); - } - } - id - } - - pub fn get_id_or(b: String) -> String { - let a = CONFIG.read().unwrap().id.clone(); - if a.is_empty() { - b - } else { - a - } - } - - pub fn get_options() -> HashMap { - let mut res = DEFAULT_SETTINGS.read().unwrap().clone(); - res.extend(CONFIG2.read().unwrap().options.clone()); - res.extend(OVERWRITE_SETTINGS.read().unwrap().clone()); - res - } - - #[inline] - fn purify_options(v: &mut HashMap) { - v.retain(|k, v| is_option_can_save(&OVERWRITE_SETTINGS, k, &DEFAULT_SETTINGS, v)); - } - - pub fn set_options(mut v: HashMap) { - Self::purify_options(&mut v); - let mut config = CONFIG2.write().unwrap(); - if config.options == v { - return; - } - config.options = v; - config.store(); - } - - pub fn get_option(k: &str) -> String { - get_or( - &OVERWRITE_SETTINGS, - &CONFIG2.read().unwrap().options, - &DEFAULT_SETTINGS, - k, - ) - .unwrap_or_default() - } - - pub fn get_bool_option(k: &str) -> bool { - option2bool(k, &Self::get_option(k)) - } - - pub fn set_option(k: String, v: String) { - if !is_option_can_save(&OVERWRITE_SETTINGS, &k, &DEFAULT_SETTINGS, &v) { - return; - } - let mut config = CONFIG2.write().unwrap(); - let v2 = if v.is_empty() { None } else { Some(&v) }; - if v2 != config.options.get(&k) { - if v2.is_none() { - config.options.remove(&k); - } else { - config.options.insert(k, v); - } - config.store(); - } - } - - pub fn update_id() { - // to-do: how about if one ip register a lot of ids? - let id = Self::get_id(); - let mut rng = rand::thread_rng(); - let new_id = rng.gen_range(1_000_000_000..2_000_000_000).to_string(); - Config::set_id(&new_id); - log::info!("id updated from {} to {}", id, new_id); - } - - pub fn set_permanent_password(password: &str) { - if HARD_SETTINGS - .read() - .unwrap() - .get("password") - .map_or(false, |v| v == password) - { - return; - } - let mut config = CONFIG.write().unwrap(); - if password == config.password { - return; - } - config.password = password.into(); - config.store(); - Self::clear_trusted_devices(); - } - - pub fn get_permanent_password() -> String { - let mut password = CONFIG.read().unwrap().password.clone(); - if password.is_empty() { - if let Some(v) = HARD_SETTINGS.read().unwrap().get("password") { - password = v.to_owned(); - } - } - password - } - - pub fn set_salt(salt: &str) { - let mut config = CONFIG.write().unwrap(); - if salt == config.salt { - return; - } - config.salt = salt.into(); - config.store(); - } - - pub fn get_salt() -> String { - let mut salt = CONFIG.read().unwrap().salt.clone(); - if salt.is_empty() { - salt = Config::get_auto_password(6); - Config::set_salt(&salt); - } - salt - } - - pub fn set_socks(socks: Option) { - let mut config = CONFIG2.write().unwrap(); - if config.socks == socks { - return; - } - config.socks = socks; - config.store(); - } - - #[inline] - fn get_socks_from_custom_client_advanced_settings( - settings: &HashMap, - ) -> Option { - let url = settings.get(keys::OPTION_PROXY_URL)?; - Some(Socks5Server { - proxy: url.to_owned(), - username: settings - .get(keys::OPTION_PROXY_USERNAME) - .map(|x| x.to_string()) - .unwrap_or_default(), - password: settings - .get(keys::OPTION_PROXY_PASSWORD) - .map(|x| x.to_string()) - .unwrap_or_default(), - }) - } - - pub fn get_socks() -> Option { - Self::get_socks_from_custom_client_advanced_settings(&OVERWRITE_SETTINGS.read().unwrap()) - .or(CONFIG2.read().unwrap().socks.clone()) - .or(Self::get_socks_from_custom_client_advanced_settings( - &DEFAULT_SETTINGS.read().unwrap(), - )) - } - - #[inline] - pub fn is_proxy() -> bool { - Self::get_network_type() != NetworkType::Direct - } - - pub fn get_network_type() -> NetworkType { - if OVERWRITE_SETTINGS - .read() - .unwrap() - .get(keys::OPTION_PROXY_URL) - .is_some() - { - return NetworkType::ProxySocks; - } - if CONFIG2.read().unwrap().socks.is_some() { - return NetworkType::ProxySocks; - } - if DEFAULT_SETTINGS - .read() - .unwrap() - .get(keys::OPTION_PROXY_URL) - .is_some() - { - return NetworkType::ProxySocks; - } - NetworkType::Direct - } - - pub fn get_unlock_pin() -> String { - CONFIG2.read().unwrap().unlock_pin.clone() - } - - pub fn set_unlock_pin(pin: &str) { - let mut config = CONFIG2.write().unwrap(); - if pin == config.unlock_pin { - return; - } - config.unlock_pin = pin.to_string(); - config.store(); - } - - pub fn get_trusted_devices_json() -> String { - serde_json::to_string(&Self::get_trusted_devices()).unwrap_or_default() - } - - pub fn get_trusted_devices() -> Vec { - let (devices, synced) = TRUSTED_DEVICES.read().unwrap().clone(); - if synced { - return devices; - } - let devices = CONFIG2.read().unwrap().trusted_devices.clone(); - let (devices, succ, store) = decrypt_str_or_original(&devices, PASSWORD_ENC_VERSION); - if succ { - let mut devices: Vec = - serde_json::from_str(&devices).unwrap_or_default(); - let len = devices.len(); - devices.retain(|d| !d.outdate()); - if store || devices.len() != len { - Self::set_trusted_devices(devices.clone()); - } - *TRUSTED_DEVICES.write().unwrap() = (devices.clone(), true); - devices - } else { - Default::default() - } - } - - fn set_trusted_devices(mut trusted_devices: Vec) { - trusted_devices.retain(|d| !d.outdate()); - let devices = serde_json::to_string(&trusted_devices).unwrap_or_default(); - let max_len = 1024 * 1024; - if devices.bytes().len() > max_len { - log::error!("Trusted devices too large: {}", devices.bytes().len()); - return; - } - let devices = encrypt_str_or_original(&devices, PASSWORD_ENC_VERSION, max_len); - let mut config = CONFIG2.write().unwrap(); - config.trusted_devices = devices; - config.store(); - *TRUSTED_DEVICES.write().unwrap() = (trusted_devices, true); - } - - pub fn add_trusted_device(device: TrustedDevice) { - let mut devices = Self::get_trusted_devices(); - devices.retain(|d| d.hwid != device.hwid); - devices.push(device); - Self::set_trusted_devices(devices); - } - - pub fn remove_trusted_devices(hwids: &Vec) { - let mut devices = Self::get_trusted_devices(); - devices.retain(|d| !hwids.contains(&d.hwid)); - Self::set_trusted_devices(devices); - } - - pub fn clear_trusted_devices() { - Self::set_trusted_devices(Default::default()); - } - - pub fn get() -> Config { - return CONFIG.read().unwrap().clone(); - } - - pub fn set(cfg: Config) -> bool { - let mut lock = CONFIG.write().unwrap(); - if *lock == cfg { - return false; - } - *lock = cfg; - lock.store(); - true - } - - fn with_extension(path: PathBuf) -> PathBuf { - let ext = path.extension(); - if let Some(ext) = ext { - let ext = format!("{}.toml", ext.to_string_lossy()); - path.with_extension(ext) - } else { - path.with_extension("toml") - } - } -} - -const PEERS: &str = "peers"; - -impl PeerConfig { - pub fn load(id: &str) -> PeerConfig { - let _lock = CONFIG.read().unwrap(); - match confy::load_path(Self::path(id)) { - Ok(config) => { - let mut config: PeerConfig = config; - let mut store = false; - let (password, _, store2) = - decrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION); - config.password = password; - store = store || store2; - for opt in ["rdp_password", "os-username", "os-password"] { - if let Some(v) = config.options.get_mut(opt) { - let (encrypted, _, store2) = - decrypt_str_or_original(v, PASSWORD_ENC_VERSION); - *v = encrypted; - store = store || store2; - } - } - if store { - config.store(id); - } - config - } - Err(err) => { - if let confy::ConfyError::GeneralLoadError(err) = &err { - if err.kind() == std::io::ErrorKind::NotFound { - return Default::default(); - } - } - log::error!("Failed to load peer config '{}': {}", id, err); - Default::default() - } - } - } - - pub fn store(&self, id: &str) { - let _lock = CONFIG.read().unwrap(); - let mut config = self.clone(); - config.password = - encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN); - for opt in ["rdp_password", "os-username", "os-password"] { - if let Some(v) = config.options.get_mut(opt) { - *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN) - } - } - if let Err(err) = store_path(Self::path(id), config) { - log::error!("Failed to store config: {}", err); - } - NEW_STORED_PEER_CONFIG.lock().unwrap().insert(id.to_owned()); - } - - pub fn remove(id: &str) { - fs::remove_file(Self::path(id)).ok(); - } - - fn path(id: &str) -> PathBuf { - //If the id contains invalid chars, encode it - let forbidden_paths = Regex::new(r".*[<>:/\\|\?\*].*"); - let path: PathBuf; - if let Ok(forbidden_paths) = forbidden_paths { - let id_encoded = if forbidden_paths.is_match(id) { - "base64_".to_string() + base64::encode(id, base64::Variant::Original).as_str() - } else { - id.to_string() - }; - path = [PEERS, id_encoded.as_str()].iter().collect(); - } else { - log::warn!("Regex create failed: {:?}", forbidden_paths.err()); - // fallback for failing to create this regex. - path = [PEERS, id.replace(":", "_").as_str()].iter().collect(); - } - Config::with_extension(Config::path(path)) - } - - pub fn peers(id_filters: Option>) -> Vec<(String, SystemTime, PeerConfig)> { - if let Ok(peers) = Config::path(PEERS).read_dir() { - if let Ok(peers) = peers - .map(|res| res.map(|e| e.path())) - .collect::, _>>() - { - let mut peers: Vec<_> = peers - .iter() - .filter(|p| { - p.is_file() - && p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml") - }) - .map(|p| { - let id = p - .file_stem() - .map(|p| p.to_str().unwrap_or("")) - .unwrap_or("") - .to_owned(); - - let id_decoded_string = if id.starts_with("base64_") && id.len() != 7 { - let id_decoded = base64::decode(&id[7..], base64::Variant::Original) - .unwrap_or_default(); - String::from_utf8_lossy(&id_decoded).as_ref().to_owned() - } else { - id - }; - (id_decoded_string, p) - }) - .filter(|(id, _)| { - let Some(filters) = &id_filters else { - return true; - }; - filters.contains(id) - }) - .map(|(id, p)| { - let t = crate::get_modified_time(p); - let c = PeerConfig::load(&id); - if c.info.platform.is_empty() { - fs::remove_file(p).ok(); - } - (id, t, c) - }) - .filter(|p| !p.2.info.platform.is_empty()) - .collect(); - peers.sort_unstable_by(|a, b| b.1.cmp(&a.1)); - return peers; - } - } - Default::default() - } - - pub fn exists(id: &str) -> bool { - Self::path(id).exists() - } - - serde_field_string!( - default_view_style, - deserialize_view_style, - UserDefaultConfig::read(keys::OPTION_VIEW_STYLE) - ); - serde_field_string!( - default_scroll_style, - deserialize_scroll_style, - UserDefaultConfig::read(keys::OPTION_SCROLL_STYLE) - ); - serde_field_string!( - default_image_quality, - deserialize_image_quality, - UserDefaultConfig::read(keys::OPTION_IMAGE_QUALITY) - ); - serde_field_string!( - default_reverse_mouse_wheel, - deserialize_reverse_mouse_wheel, - UserDefaultConfig::read(keys::OPTION_REVERSE_MOUSE_WHEEL) - ); - serde_field_string!( - default_displays_as_individual_windows, - deserialize_displays_as_individual_windows, - UserDefaultConfig::read(keys::OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS) - ); - serde_field_string!( - default_use_all_my_displays_for_the_remote_session, - deserialize_use_all_my_displays_for_the_remote_session, - UserDefaultConfig::read(keys::OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION) - ); - - fn default_custom_image_quality() -> Vec { - let f: f64 = UserDefaultConfig::read(keys::OPTION_CUSTOM_IMAGE_QUALITY) - .parse() - .unwrap_or(50.0); - vec![f as _] - } - - fn deserialize_custom_image_quality<'de, D>(deserializer: D) -> Result, D::Error> - where - D: de::Deserializer<'de>, - { - let v: Vec = de::Deserialize::deserialize(deserializer)?; - if v.len() == 1 && v[0] >= 10 && v[0] <= 0xFFF { - Ok(v) - } else { - Ok(Self::default_custom_image_quality()) - } - } - - fn default_options() -> HashMap { - let mut mp: HashMap = Default::default(); - [ - keys::OPTION_CODEC_PREFERENCE, - keys::OPTION_CUSTOM_FPS, - keys::OPTION_ZOOM_CURSOR, - keys::OPTION_TOUCH_MODE, - keys::OPTION_I444, - keys::OPTION_SWAP_LEFT_RIGHT_MOUSE, - keys::OPTION_COLLAPSE_TOOLBAR, - ] - .map(|key| { - mp.insert(key.to_owned(), UserDefaultConfig::read(key)); - }); - mp - } -} - -serde_field_bool!( - ShowRemoteCursor, - "show_remote_cursor", - default_show_remote_cursor, - "ShowRemoteCursor::default_show_remote_cursor" -); -serde_field_bool!( - FollowRemoteCursor, - "follow_remote_cursor", - default_follow_remote_cursor, - "FollowRemoteCursor::default_follow_remote_cursor" -); - -serde_field_bool!( - FollowRemoteWindow, - "follow_remote_window", - default_follow_remote_window, - "FollowRemoteWindow::default_follow_remote_window" -); -serde_field_bool!( - ShowQualityMonitor, - "show_quality_monitor", - default_show_quality_monitor, - "ShowQualityMonitor::default_show_quality_monitor" -); -serde_field_bool!( - DisableAudio, - "disable_audio", - default_disable_audio, - "DisableAudio::default_disable_audio" -); -serde_field_bool!( - EnableFileCopyPaste, - "enable-file-copy-paste", - default_enable_file_copy_paste, - "EnableFileCopyPaste::default_enable_file_copy_paste" -); -serde_field_bool!( - DisableClipboard, - "disable_clipboard", - default_disable_clipboard, - "DisableClipboard::default_disable_clipboard" -); -serde_field_bool!( - LockAfterSessionEnd, - "lock_after_session_end", - default_lock_after_session_end, - "LockAfterSessionEnd::default_lock_after_session_end" -); -serde_field_bool!( - PrivacyMode, - "privacy_mode", - default_privacy_mode, - "PrivacyMode::default_privacy_mode" -); - -serde_field_bool!( - AllowSwapKey, - "allow_swap_key", - default_allow_swap_key, - "AllowSwapKey::default_allow_swap_key" -); - -serde_field_bool!( - ViewOnly, - "view_only", - default_view_only, - "ViewOnly::default_view_only" -); - -serde_field_bool!( - SyncInitClipboard, - "sync-init-clipboard", - default_sync_init_clipboard, - "SyncInitClipboard::default_sync_init_clipboard" -); - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct LocalConfig { - #[serde(default, deserialize_with = "deserialize_string")] - remote_id: String, // latest used one - #[serde(default, deserialize_with = "deserialize_string")] - kb_layout_type: String, - #[serde(default, deserialize_with = "deserialize_size")] - size: Size, - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub fav: Vec, - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - options: HashMap, - // Various data for flutter ui - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - ui_flutter: HashMap, -} - -impl LocalConfig { - fn load() -> LocalConfig { - Config::load_::("_local") - } - - fn store(&self) { - 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 - } - - pub fn set_size(x: i32, y: i32, w: i32, h: i32) { - let mut config = LOCAL_CONFIG.write().unwrap(); - let size = (x, y, w, h); - if size == config.size || size.2 < 300 || size.3 < 300 { - return; - } - config.size = size; - config.store(); - } - - pub fn set_remote_id(remote_id: &str) { - let mut config = LOCAL_CONFIG.write().unwrap(); - if remote_id == config.remote_id { - return; - } - config.remote_id = remote_id.into(); - config.store(); - } - - pub fn get_remote_id() -> String { - LOCAL_CONFIG.read().unwrap().remote_id.clone() - } - - pub fn set_fav(fav: Vec) { - let mut lock = LOCAL_CONFIG.write().unwrap(); - if lock.fav == fav { - return; - } - lock.fav = fav; - lock.store(); - } - - pub fn get_fav() -> Vec { - LOCAL_CONFIG.read().unwrap().fav.clone() - } - - pub fn get_option(k: &str) -> String { - get_or( - &OVERWRITE_LOCAL_SETTINGS, - &LOCAL_CONFIG.read().unwrap().options, - &DEFAULT_LOCAL_SETTINGS, - k, - ) - .unwrap_or_default() - } - - // Usually get_option should be used. - pub fn get_option_from_file(k: &str) -> String { - get_or( - &OVERWRITE_LOCAL_SETTINGS, - &Self::load().options, - &DEFAULT_LOCAL_SETTINGS, - k, - ) - .unwrap_or_default() - } - - pub fn get_bool_option(k: &str) -> bool { - option2bool(k, &Self::get_option(k)) - } - - pub fn set_option(k: String, v: String) { - if !is_option_can_save(&OVERWRITE_LOCAL_SETTINGS, &k, &DEFAULT_LOCAL_SETTINGS, &v) { - return; - } - let mut config = LOCAL_CONFIG.write().unwrap(); - // The custom client will explictly set "default" as the default language. - let is_custom_client_default_lang = k == keys::OPTION_LANGUAGE && v == "default"; - if is_custom_client_default_lang { - config.options.insert(k, "".to_owned()); - config.store(); - return; - } - let v2 = if v.is_empty() { None } else { Some(&v) }; - if v2 != config.options.get(&k) { - if v2.is_none() { - config.options.remove(&k); - } else { - config.options.insert(k, v); - } - config.store(); - } - } - - pub fn get_flutter_option(k: &str) -> String { - get_or( - &OVERWRITE_LOCAL_SETTINGS, - &LOCAL_CONFIG.read().unwrap().ui_flutter, - &DEFAULT_LOCAL_SETTINGS, - k, - ) - .unwrap_or_default() - } - - pub fn set_flutter_option(k: String, v: String) { - let mut config = LOCAL_CONFIG.write().unwrap(); - let v2 = if v.is_empty() { None } else { Some(&v) }; - if v2 != config.ui_flutter.get(&k) { - if v2.is_none() { - config.ui_flutter.remove(&k); - } else { - config.ui_flutter.insert(k, v); - } - config.store(); - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct DiscoveryPeer { - #[serde(default, deserialize_with = "deserialize_string")] - pub id: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub username: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub hostname: String, - #[serde(default, deserialize_with = "deserialize_string")] - pub platform: String, - #[serde(default, deserialize_with = "deserialize_bool")] - pub online: bool, - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - pub ip_mac: HashMap, -} - -impl DiscoveryPeer { - pub fn is_same_peer(&self, other: &DiscoveryPeer) -> bool { - self.id == other.id && self.username == other.username - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct LanPeers { - #[serde(default, deserialize_with = "deserialize_vec_discoverypeer")] - pub peers: Vec, -} - -impl LanPeers { - pub fn load() -> LanPeers { - let _lock = CONFIG.read().unwrap(); - match confy::load_path(Config::file_("_lan_peers")) { - Ok(peers) => peers, - Err(err) => { - log::error!("Failed to load lan peers: {}", err); - Default::default() - } - } - } - - pub fn store(peers: &[DiscoveryPeer]) { - let f = LanPeers { - peers: peers.to_owned(), - }; - if let Err(err) = store_path(Config::file_("_lan_peers"), f) { - log::error!("Failed to store lan peers: {}", err); - } - } - - pub fn modify_time() -> crate::ResultType { - let p = Config::file_("_lan_peers"); - Ok(fs::metadata(p)? - .modified()? - .duration_since(SystemTime::UNIX_EPOCH)? - .as_millis() as _) - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct UserDefaultConfig { - #[serde(default, deserialize_with = "deserialize_hashmap_string_string")] - options: HashMap, -} - -impl UserDefaultConfig { - fn read(key: &str) -> String { - let mut cfg = USER_DEFAULT_CONFIG.write().unwrap(); - // we do so, because default config may changed in another process, but we don't sync it - // but no need to read every time, give a small interval to avoid too many redundant read waste - if cfg.1.elapsed() > Duration::from_secs(1) { - *cfg = (Self::load(), Instant::now()); - } - cfg.0.get(key) - } - - pub fn load() -> UserDefaultConfig { - Config::load_::("_default") - } - - #[inline] - fn store(&self) { - Config::store_(self, "_default"); - } - - pub fn get(&self, key: &str) -> String { - match key { - #[cfg(any(target_os = "android", target_os = "ios"))] - keys::OPTION_VIEW_STYLE => self.get_string(key, "adaptive", vec!["original"]), - #[cfg(not(any(target_os = "android", target_os = "ios")))] - keys::OPTION_VIEW_STYLE => self.get_string(key, "original", vec!["adaptive"]), - keys::OPTION_SCROLL_STYLE => self.get_string(key, "scrollauto", vec!["scrollbar"]), - keys::OPTION_IMAGE_QUALITY => { - self.get_string(key, "balanced", vec!["best", "low", "custom"]) - } - keys::OPTION_CODEC_PREFERENCE => { - self.get_string(key, "auto", vec!["vp8", "vp9", "av1", "h264", "h265"]) - } - keys::OPTION_CUSTOM_IMAGE_QUALITY => { - self.get_double_string(key, 50.0, 10.0, 0xFFF as f64) - } - keys::OPTION_CUSTOM_FPS => self.get_double_string(key, 30.0, 5.0, 120.0), - keys::OPTION_ENABLE_FILE_COPY_PASTE => self.get_string(key, "Y", vec!["", "N"]), - _ => self - .get_after(key) - .map(|v| v.to_string()) - .unwrap_or_default(), - } - } - - pub fn set(&mut self, key: String, value: String) { - if !is_option_can_save( - &OVERWRITE_DISPLAY_SETTINGS, - &key, - &DEFAULT_DISPLAY_SETTINGS, - &value, - ) { - return; - } - if value.is_empty() { - self.options.remove(&key); - } else { - self.options.insert(key, value); - } - self.store(); - } - - #[inline] - fn get_string(&self, key: &str, default: &str, others: Vec<&str>) -> String { - match self.get_after(key) { - Some(option) => { - if others.contains(&option.as_str()) { - option.to_owned() - } else { - default.to_owned() - } - } - None => default.to_owned(), - } - } - - #[inline] - fn get_double_string(&self, key: &str, default: f64, min: f64, max: f64) -> String { - match self.get_after(key) { - Some(option) => { - let v: f64 = option.parse().unwrap_or(default); - if v >= min && v <= max { - v.to_string() - } else { - default.to_string() - } - } - None => default.to_string(), - } - } - - fn get_after(&self, k: &str) -> Option { - get_or( - &OVERWRITE_DISPLAY_SETTINGS, - &self.options, - &DEFAULT_DISPLAY_SETTINGS, - k, - ) - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct AbPeer { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub id: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub hash: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub username: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub hostname: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub platform: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub alias: String, - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub tags: Vec, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct AbEntry { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub guid: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub name: String, - #[serde(default, deserialize_with = "deserialize_vec_abpeer")] - pub peers: Vec, - #[serde(default, deserialize_with = "deserialize_vec_string")] - pub tags: Vec, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub tag_colors: String, -} - -impl AbEntry { - pub fn personal(&self) -> bool { - self.name == "My address book" || self.name == "Legacy address book" - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct Ab { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub access_token: String, - #[serde(default, deserialize_with = "deserialize_vec_abentry")] - pub ab_entries: Vec, -} - -impl Ab { - fn path() -> PathBuf { - let filename = format!("{}_ab", APP_NAME.read().unwrap().clone()); - Config::path(filename) - } - - pub fn store(json: String) { - if let Ok(mut file) = std::fs::File::create(Self::path()) { - let data = compress(json.as_bytes()); - let max_len = 64 * 1024 * 1024; - if data.len() > max_len { - // maxlen of function decompress - log::error!("ab data too large, {} > {}", data.len(), max_len); - return; - } - if let Ok(data) = symmetric_crypt(&data, true) { - file.write_all(&data).ok(); - } - }; - } - - pub fn load() -> Ab { - if let Ok(mut file) = std::fs::File::open(Self::path()) { - let mut data = vec![]; - if file.read_to_end(&mut data).is_ok() { - if let Ok(data) = symmetric_crypt(&data, false) { - let data = decompress(&data); - if let Ok(ab) = serde_json::from_str::(&String::from_utf8_lossy(&data)) { - return ab; - } - } - } - }; - Self::remove(); - Ab::default() - } - - pub fn remove() { - std::fs::remove_file(Self::path()).ok(); - } -} - -// use default value when field type is wrong -macro_rules! deserialize_default { - ($func_name:ident, $return_type:ty) => { - fn $func_name<'de, D>(deserializer: D) -> Result<$return_type, D::Error> - where - D: de::Deserializer<'de>, - { - Ok(de::Deserialize::deserialize(deserializer).unwrap_or_default()) - } - }; -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct GroupPeer { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub id: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub username: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub hostname: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub platform: String, - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub login_name: String, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct GroupUser { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub name: String, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct Group { - #[serde( - default, - deserialize_with = "deserialize_string", - skip_serializing_if = "String::is_empty" - )] - pub access_token: String, - #[serde(default, deserialize_with = "deserialize_vec_groupuser")] - pub users: Vec, - #[serde(default, deserialize_with = "deserialize_vec_grouppeer")] - pub peers: Vec, -} - -impl Group { - fn path() -> PathBuf { - let filename = format!("{}_group", APP_NAME.read().unwrap().clone()); - Config::path(filename) - } - - pub fn store(json: String) { - if let Ok(mut file) = std::fs::File::create(Self::path()) { - let data = compress(json.as_bytes()); - let max_len = 64 * 1024 * 1024; - if data.len() > max_len { - // maxlen of function decompress - return; - } - if let Ok(data) = symmetric_crypt(&data, true) { - file.write_all(&data).ok(); - } - }; - } - - pub fn load() -> Self { - if let Ok(mut file) = std::fs::File::open(Self::path()) { - let mut data = vec![]; - if file.read_to_end(&mut data).is_ok() { - if let Ok(data) = symmetric_crypt(&data, false) { - let data = decompress(&data); - if let Ok(group) = serde_json::from_str::(&String::from_utf8_lossy(&data)) - { - return group; - } - } - } - }; - Self::remove(); - Self::default() - } - - pub fn remove() { - std::fs::remove_file(Self::path()).ok(); - } -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct TrustedDevice { - pub hwid: Bytes, - pub time: i64, - pub id: String, - pub name: String, - pub platform: String, -} - -impl TrustedDevice { - pub fn outdate(&self) -> bool { - const DAYS_90: i64 = 90 * 24 * 60 * 60 * 1000; - self.time + DAYS_90 < crate::get_time() - } -} - -deserialize_default!(deserialize_string, String); -deserialize_default!(deserialize_bool, bool); -deserialize_default!(deserialize_i32, i32); -deserialize_default!(deserialize_vec_u8, Vec); -deserialize_default!(deserialize_vec_string, Vec); -deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>); -deserialize_default!(deserialize_vec_discoverypeer, Vec); -deserialize_default!(deserialize_vec_abpeer, Vec); -deserialize_default!(deserialize_vec_abentry, Vec); -deserialize_default!(deserialize_vec_groupuser, Vec); -deserialize_default!(deserialize_vec_grouppeer, Vec); -deserialize_default!(deserialize_keypair, KeyPair); -deserialize_default!(deserialize_size, Size); -deserialize_default!(deserialize_hashmap_string_string, HashMap); -deserialize_default!(deserialize_hashmap_string_bool, HashMap); -deserialize_default!(deserialize_hashmap_resolutions, HashMap); - -#[inline] -fn get_or( - a: &RwLock>, - b: &HashMap, - c: &RwLock>, - k: &str, -) -> Option { - a.read() - .unwrap() - .get(k) - .or(b.get(k)) - .or(c.read().unwrap().get(k)) - .cloned() -} - -#[inline] -fn is_option_can_save( - overwrite: &RwLock>, - k: &str, - defaults: &RwLock>, - v: &str, -) -> bool { - if overwrite.read().unwrap().contains_key(k) - || defaults.read().unwrap().get(k).map_or(false, |x| x == v) - { - return false; - } - true -} - -#[inline] -pub fn is_incoming_only() -> bool { - HARD_SETTINGS - .read() - .unwrap() - .get("conn-type") - .map_or(false, |x| x == ("incoming")) -} - -#[inline] -pub fn is_outgoing_only() -> bool { - HARD_SETTINGS - .read() - .unwrap() - .get("conn-type") - .map_or(false, |x| x == ("outgoing")) -} - -#[inline] -fn is_some_hard_opton(name: &str) -> bool { - HARD_SETTINGS - .read() - .unwrap() - .get(name) - .map_or(false, |x| x == ("Y")) -} - -#[inline] -pub fn is_disable_tcp_listen() -> bool { - is_some_hard_opton("disable-tcp-listen") -} - -#[inline] -pub fn is_disable_settings() -> bool { - is_some_hard_opton("disable-settings") -} - -#[inline] -pub fn is_disable_ab() -> bool { - is_some_hard_opton("disable-ab") -} - -#[inline] -pub fn is_disable_account() -> bool { - is_some_hard_opton("disable-account") -} - -#[inline] -pub fn is_disable_installation() -> bool { - is_some_hard_opton("disable-installation") -} - -// This function must be kept the same as the one in flutter and sciter code. -// flutter: flutter/lib/common.dart -> option2bool() -// sciter: Does not have the function, but it should be kept the same. -pub fn option2bool(option: &str, value: &str) -> bool { - if option.starts_with("enable-") { - value != "N" - } else if option.starts_with("allow-") - || option == "stop-service" - || option == keys::OPTION_DIRECT_SERVER - || option == "force-always-relay" - { - value == "Y" - } else { - value != "N" - } -} - -pub mod keys { - pub const OPTION_VIEW_ONLY: &str = "view_only"; - pub const OPTION_SHOW_MONITORS_TOOLBAR: &str = "show_monitors_toolbar"; - pub const OPTION_COLLAPSE_TOOLBAR: &str = "collapse_toolbar"; - pub const OPTION_SHOW_REMOTE_CURSOR: &str = "show_remote_cursor"; - pub const OPTION_FOLLOW_REMOTE_CURSOR: &str = "follow_remote_cursor"; - pub const OPTION_FOLLOW_REMOTE_WINDOW: &str = "follow_remote_window"; - pub const OPTION_ZOOM_CURSOR: &str = "zoom-cursor"; - pub const OPTION_SHOW_QUALITY_MONITOR: &str = "show_quality_monitor"; - pub const OPTION_DISABLE_AUDIO: &str = "disable_audio"; - pub const OPTION_ENABLE_FILE_COPY_PASTE: &str = "enable-file-copy-paste"; - pub const OPTION_DISABLE_CLIPBOARD: &str = "disable_clipboard"; - pub const OPTION_LOCK_AFTER_SESSION_END: &str = "lock_after_session_end"; - pub const OPTION_PRIVACY_MODE: &str = "privacy_mode"; - pub const OPTION_TOUCH_MODE: &str = "touch-mode"; - pub const OPTION_I444: &str = "i444"; - pub const OPTION_REVERSE_MOUSE_WHEEL: &str = "reverse_mouse_wheel"; - pub const OPTION_SWAP_LEFT_RIGHT_MOUSE: &str = "swap-left-right-mouse"; - pub const OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS: &str = "displays_as_individual_windows"; - pub const OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION: &str = - "use_all_my_displays_for_the_remote_session"; - pub const OPTION_VIEW_STYLE: &str = "view_style"; - pub const OPTION_SCROLL_STYLE: &str = "scroll_style"; - pub const OPTION_IMAGE_QUALITY: &str = "image_quality"; - pub const OPTION_CUSTOM_IMAGE_QUALITY: &str = "custom_image_quality"; - pub const OPTION_CUSTOM_FPS: &str = "custom-fps"; - pub const OPTION_CODEC_PREFERENCE: &str = "codec-preference"; - pub const OPTION_SYNC_INIT_CLIPBOARD: &str = "sync-init-clipboard"; - pub const OPTION_THEME: &str = "theme"; - pub const OPTION_LANGUAGE: &str = "lang"; - pub const OPTION_REMOTE_MENUBAR_DRAG_LEFT: &str = "remote-menubar-drag-left"; - pub const OPTION_REMOTE_MENUBAR_DRAG_RIGHT: &str = "remote-menubar-drag-right"; - pub const OPTION_HIDE_AB_TAGS_PANEL: &str = "hideAbTagsPanel"; - pub const OPTION_ENABLE_CONFIRM_CLOSING_TABS: &str = "enable-confirm-closing-tabs"; - pub const OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS: &str = - "enable-open-new-connections-in-tabs"; - pub const OPTION_TEXTURE_RENDER: &str = "use-texture-render"; - pub const OPTION_ENABLE_CHECK_UPDATE: &str = "enable-check-update"; - pub const OPTION_SYNC_AB_WITH_RECENT_SESSIONS: &str = "sync-ab-with-recent-sessions"; - pub const OPTION_SYNC_AB_TAGS: &str = "sync-ab-tags"; - pub const OPTION_FILTER_AB_BY_INTERSECTION: &str = "filter-ab-by-intersection"; - pub const OPTION_ACCESS_MODE: &str = "access-mode"; - pub const OPTION_ENABLE_KEYBOARD: &str = "enable-keyboard"; - pub const OPTION_ENABLE_CLIPBOARD: &str = "enable-clipboard"; - pub const OPTION_ENABLE_FILE_TRANSFER: &str = "enable-file-transfer"; - pub const OPTION_ENABLE_AUDIO: &str = "enable-audio"; - pub const OPTION_ENABLE_TUNNEL: &str = "enable-tunnel"; - pub const OPTION_ENABLE_REMOTE_RESTART: &str = "enable-remote-restart"; - pub const OPTION_ENABLE_RECORD_SESSION: &str = "enable-record-session"; - pub const OPTION_ENABLE_BLOCK_INPUT: &str = "enable-block-input"; - pub const OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION: &str = "allow-remote-config-modification"; - pub const OPTION_ENABLE_LAN_DISCOVERY: &str = "enable-lan-discovery"; - pub const OPTION_DIRECT_SERVER: &str = "direct-server"; - pub const OPTION_DIRECT_ACCESS_PORT: &str = "direct-access-port"; - pub const OPTION_WHITELIST: &str = "whitelist"; - pub const OPTION_ALLOW_AUTO_DISCONNECT: &str = "allow-auto-disconnect"; - pub const OPTION_AUTO_DISCONNECT_TIMEOUT: &str = "auto-disconnect-timeout"; - pub const OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN: &str = "allow-only-conn-window-open"; - pub const OPTION_ALLOW_AUTO_RECORD_INCOMING: &str = "allow-auto-record-incoming"; - pub const OPTION_ALLOW_AUTO_RECORD_OUTGOING: &str = "allow-auto-record-outgoing"; - pub const OPTION_VIDEO_SAVE_DIRECTORY: &str = "video-save-directory"; - pub const OPTION_ENABLE_ABR: &str = "enable-abr"; - pub const OPTION_ALLOW_REMOVE_WALLPAPER: &str = "allow-remove-wallpaper"; - pub const OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER: &str = "allow-always-software-render"; - pub const OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless"; - pub const OPTION_ENABLE_HWCODEC: &str = "enable-hwcodec"; - pub const OPTION_APPROVE_MODE: &str = "approve-mode"; - pub const OPTION_VERIFICATION_METHOD: &str = "verification-method"; - pub const OPTION_CUSTOM_RENDEZVOUS_SERVER: &str = "custom-rendezvous-server"; - pub const OPTION_API_SERVER: &str = "api-server"; - pub const OPTION_KEY: &str = "key"; - pub const OPTION_PRESET_ADDRESS_BOOK_NAME: &str = "preset-address-book-name"; - pub const OPTION_PRESET_ADDRESS_BOOK_TAG: &str = "preset-address-book-tag"; - pub const OPTION_ENABLE_DIRECTX_CAPTURE: &str = "enable-directx-capture"; - pub const OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE: &str = - "enable-android-software-encoding-half-scale"; - pub const OPTION_ENABLE_TRUSTED_DEVICES: &str = "enable-trusted-devices"; - pub const OPTION_AV1_TEST: &str = "av1-test"; - - // buildin options - pub const OPTION_DISPLAY_NAME: &str = "display-name"; - pub const OPTION_DISABLE_UDP: &str = "disable-udp"; - pub const OPTION_PRESET_USERNAME: &str = "preset-user-name"; - pub const OPTION_PRESET_STRATEGY_NAME: &str = "preset-strategy-name"; - pub const OPTION_REMOVE_PRESET_PASSWORD_WARNING: &str = "remove-preset-password-warning"; - pub const OPTION_HIDE_SECURITY_SETTINGS: &str = "hide-security-settings"; - pub const OPTION_HIDE_NETWORK_SETTINGS: &str = "hide-network-settings"; - pub const OPTION_HIDE_SERVER_SETTINGS: &str = "hide-server-settings"; - pub const OPTION_HIDE_PROXY_SETTINGS: &str = "hide-proxy-settings"; - pub const OPTION_HIDE_USERNAME_ON_CARD: &str = "hide-username-on-card"; - pub const OPTION_HIDE_HELP_CARDS: &str = "hide-help-cards"; - pub const OPTION_DEFAULT_CONNECT_PASSWORD: &str = "default-connect-password"; - pub const OPTION_HIDE_TRAY: &str = "hide-tray"; - pub const OPTION_ONE_WAY_CLIPBOARD_REDIRECTION: &str = "one-way-clipboard-redirection"; - pub const OPTION_ALLOW_LOGON_SCREEN_PASSWORD: &str = "allow-logon-screen-password"; - pub const OPTION_ONE_WAY_FILE_TRANSFER: &str = "one-way-file-transfer"; - - // flutter local options - pub const OPTION_FLUTTER_REMOTE_MENUBAR_STATE: &str = "remoteMenubarState"; - pub const OPTION_FLUTTER_PEER_SORTING: &str = "peer-sorting"; - pub const OPTION_FLUTTER_PEER_TAB_INDEX: &str = "peer-tab-index"; - pub const OPTION_FLUTTER_PEER_TAB_ORDER: &str = "peer-tab-order"; - pub const OPTION_FLUTTER_PEER_TAB_VISIBLE: &str = "peer-tab-visible"; - pub const OPTION_FLUTTER_PEER_CARD_UI_TYLE: &str = "peer-card-ui-type"; - pub const OPTION_FLUTTER_CURRENT_AB_NAME: &str = "current-ab-name"; - pub const OPTION_ALLOW_REMOTE_CM_MODIFICATION: &str = "allow-remote-cm-modification"; - - // android floating window options - pub const OPTION_DISABLE_FLOATING_WINDOW: &str = "disable-floating-window"; - pub const OPTION_FLOATING_WINDOW_SIZE: &str = "floating-window-size"; - pub const OPTION_FLOATING_WINDOW_UNTOUCHABLE: &str = "floating-window-untouchable"; - pub const OPTION_FLOATING_WINDOW_TRANSPARENCY: &str = "floating-window-transparency"; - pub const OPTION_FLOATING_WINDOW_SVG: &str = "floating-window-svg"; - - // android keep screen on - pub const OPTION_KEEP_SCREEN_ON: &str = "keep-screen-on"; - - pub const OPTION_DISABLE_GROUP_PANEL: &str = "disable-group-panel"; - pub const OPTION_PRE_ELEVATE_SERVICE: &str = "pre-elevate-service"; - - // proxy settings - // The following options are not real keys, they are just used for custom client advanced settings. - // The real keys are in Config2::socks. - pub const OPTION_PROXY_URL: &str = "proxy-url"; - pub const OPTION_PROXY_USERNAME: &str = "proxy-username"; - pub const OPTION_PROXY_PASSWORD: &str = "proxy-password"; - - // DEFAULT_DISPLAY_SETTINGS, OVERWRITE_DISPLAY_SETTINGS - pub const KEYS_DISPLAY_SETTINGS: &[&str] = &[ - OPTION_VIEW_ONLY, - OPTION_SHOW_MONITORS_TOOLBAR, - OPTION_COLLAPSE_TOOLBAR, - OPTION_SHOW_REMOTE_CURSOR, - OPTION_FOLLOW_REMOTE_CURSOR, - OPTION_FOLLOW_REMOTE_WINDOW, - OPTION_ZOOM_CURSOR, - OPTION_SHOW_QUALITY_MONITOR, - OPTION_DISABLE_AUDIO, - OPTION_ENABLE_FILE_COPY_PASTE, - OPTION_DISABLE_CLIPBOARD, - OPTION_LOCK_AFTER_SESSION_END, - OPTION_PRIVACY_MODE, - OPTION_TOUCH_MODE, - OPTION_I444, - OPTION_REVERSE_MOUSE_WHEEL, - OPTION_SWAP_LEFT_RIGHT_MOUSE, - OPTION_DISPLAYS_AS_INDIVIDUAL_WINDOWS, - OPTION_USE_ALL_MY_DISPLAYS_FOR_THE_REMOTE_SESSION, - OPTION_VIEW_STYLE, - OPTION_SCROLL_STYLE, - OPTION_IMAGE_QUALITY, - OPTION_CUSTOM_IMAGE_QUALITY, - OPTION_CUSTOM_FPS, - OPTION_CODEC_PREFERENCE, - OPTION_SYNC_INIT_CLIPBOARD, - ]; - // DEFAULT_LOCAL_SETTINGS, OVERWRITE_LOCAL_SETTINGS - pub const KEYS_LOCAL_SETTINGS: &[&str] = &[ - OPTION_THEME, - OPTION_LANGUAGE, - OPTION_ENABLE_CONFIRM_CLOSING_TABS, - OPTION_ENABLE_OPEN_NEW_CONNECTIONS_IN_TABS, - OPTION_TEXTURE_RENDER, - OPTION_SYNC_AB_WITH_RECENT_SESSIONS, - OPTION_SYNC_AB_TAGS, - OPTION_FILTER_AB_BY_INTERSECTION, - OPTION_REMOTE_MENUBAR_DRAG_LEFT, - OPTION_REMOTE_MENUBAR_DRAG_RIGHT, - OPTION_HIDE_AB_TAGS_PANEL, - OPTION_FLUTTER_REMOTE_MENUBAR_STATE, - OPTION_FLUTTER_PEER_SORTING, - OPTION_FLUTTER_PEER_TAB_INDEX, - OPTION_FLUTTER_PEER_TAB_ORDER, - OPTION_FLUTTER_PEER_TAB_VISIBLE, - OPTION_FLUTTER_PEER_CARD_UI_TYLE, - OPTION_FLUTTER_CURRENT_AB_NAME, - OPTION_DISABLE_FLOATING_WINDOW, - OPTION_FLOATING_WINDOW_SIZE, - OPTION_FLOATING_WINDOW_UNTOUCHABLE, - OPTION_FLOATING_WINDOW_TRANSPARENCY, - OPTION_FLOATING_WINDOW_SVG, - OPTION_KEEP_SCREEN_ON, - OPTION_DISABLE_GROUP_PANEL, - OPTION_PRE_ELEVATE_SERVICE, - OPTION_ALLOW_REMOTE_CM_MODIFICATION, - OPTION_ALLOW_AUTO_RECORD_OUTGOING, - OPTION_VIDEO_SAVE_DIRECTORY, - ]; - // DEFAULT_SETTINGS, OVERWRITE_SETTINGS - pub const KEYS_SETTINGS: &[&str] = &[ - OPTION_ACCESS_MODE, - OPTION_ENABLE_KEYBOARD, - OPTION_ENABLE_CLIPBOARD, - OPTION_ENABLE_FILE_TRANSFER, - OPTION_ENABLE_AUDIO, - OPTION_ENABLE_TUNNEL, - OPTION_ENABLE_REMOTE_RESTART, - OPTION_ENABLE_RECORD_SESSION, - OPTION_ENABLE_BLOCK_INPUT, - OPTION_ALLOW_REMOTE_CONFIG_MODIFICATION, - OPTION_ENABLE_LAN_DISCOVERY, - OPTION_DIRECT_SERVER, - OPTION_DIRECT_ACCESS_PORT, - OPTION_WHITELIST, - OPTION_ALLOW_AUTO_DISCONNECT, - OPTION_AUTO_DISCONNECT_TIMEOUT, - OPTION_ALLOW_ONLY_CONN_WINDOW_OPEN, - OPTION_ALLOW_AUTO_RECORD_INCOMING, - OPTION_ENABLE_ABR, - OPTION_ALLOW_REMOVE_WALLPAPER, - OPTION_ALLOW_ALWAYS_SOFTWARE_RENDER, - OPTION_ALLOW_LINUX_HEADLESS, - OPTION_ENABLE_HWCODEC, - OPTION_APPROVE_MODE, - OPTION_VERIFICATION_METHOD, - OPTION_PROXY_URL, - OPTION_PROXY_USERNAME, - OPTION_PROXY_PASSWORD, - OPTION_CUSTOM_RENDEZVOUS_SERVER, - OPTION_API_SERVER, - OPTION_KEY, - OPTION_PRESET_ADDRESS_BOOK_NAME, - OPTION_PRESET_ADDRESS_BOOK_TAG, - OPTION_ENABLE_DIRECTX_CAPTURE, - OPTION_ENABLE_ANDROID_SOFTWARE_ENCODING_HALF_SCALE, - OPTION_ENABLE_TRUSTED_DEVICES, - ]; - - // BUILDIN_SETTINGS - pub const KEYS_BUILDIN_SETTINGS: &[&str] = &[ - OPTION_DISPLAY_NAME, - OPTION_DISABLE_UDP, - OPTION_PRESET_USERNAME, - OPTION_PRESET_STRATEGY_NAME, - OPTION_REMOVE_PRESET_PASSWORD_WARNING, - OPTION_HIDE_SECURITY_SETTINGS, - OPTION_HIDE_NETWORK_SETTINGS, - OPTION_HIDE_SERVER_SETTINGS, - OPTION_HIDE_PROXY_SETTINGS, - OPTION_HIDE_USERNAME_ON_CARD, - OPTION_HIDE_HELP_CARDS, - OPTION_DEFAULT_CONNECT_PASSWORD, - OPTION_HIDE_TRAY, - OPTION_ONE_WAY_CLIPBOARD_REDIRECTION, - OPTION_ALLOW_LOGON_SCREEN_PASSWORD, - OPTION_ONE_WAY_FILE_TRANSFER, - ]; -} - -pub fn common_load< - T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug, ->( - suffix: &str, -) -> T { - Config::load_::(suffix) -} - -pub fn common_store(config: &T, suffix: &str) { - Config::store_(config, suffix); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_serialize() { - let cfg: Config = Default::default(); - let res = toml::to_string_pretty(&cfg); - assert!(res.is_ok()); - let cfg: PeerConfig = Default::default(); - let res = toml::to_string_pretty(&cfg); - assert!(res.is_ok()); - } - - #[test] - fn test_overwrite_settings() { - DEFAULT_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "a".to_string()); - DEFAULT_SETTINGS - .write() - .unwrap() - .insert("c".to_string(), "a".to_string()); - CONFIG2 - .write() - .unwrap() - .options - .insert("a".to_string(), "b".to_string()); - CONFIG2 - .write() - .unwrap() - .options - .insert("b".to_string(), "b".to_string()); - OVERWRITE_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "c".to_string()); - OVERWRITE_SETTINGS - .write() - .unwrap() - .insert("c".to_string(), "f".to_string()); - OVERWRITE_SETTINGS - .write() - .unwrap() - .insert("d".to_string(), "c".to_string()); - let mut res: HashMap = Default::default(); - res.insert("b".to_owned(), "c".to_string()); - res.insert("d".to_owned(), "c".to_string()); - res.insert("c".to_owned(), "a".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 0); - res.insert("b".to_owned(), "c".to_string()); - res.insert("d".to_owned(), "c".to_string()); - res.insert("c".to_owned(), "a".to_string()); - res.insert("f".to_owned(), "a".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 1); - res.insert("b".to_owned(), "c".to_string()); - res.insert("d".to_owned(), "c".to_string()); - res.insert("c".to_owned(), "a".to_string()); - res.insert("f".to_owned(), "a".to_string()); - res.insert("e".to_owned(), "d".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 2); - res.insert("b".to_owned(), "c".to_string()); - res.insert("d".to_owned(), "c".to_string()); - res.insert("c".to_owned(), "a".to_string()); - res.insert("f".to_owned(), "a".to_string()); - res.insert("c".to_owned(), "d".to_string()); - res.insert("d".to_owned(), "cc".to_string()); - Config::purify_options(&mut res); - DEFAULT_SETTINGS - .write() - .unwrap() - .insert("f".to_string(), "c".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 2); - DEFAULT_SETTINGS - .write() - .unwrap() - .insert("f".to_string(), "a".to_string()); - Config::purify_options(&mut res); - assert!(res.len() == 1); - let res = Config::get_options(); - assert!(res["a"] == "b"); - assert!(res["c"] == "f"); - assert!(res["b"] == "c"); - assert!(res["d"] == "c"); - assert!(Config::get_option("a") == "b"); - assert!(Config::get_option("c") == "f"); - assert!(Config::get_option("b") == "c"); - assert!(Config::get_option("d") == "c"); - DEFAULT_SETTINGS.write().unwrap().clear(); - OVERWRITE_SETTINGS.write().unwrap().clear(); - CONFIG2.write().unwrap().options.clear(); - - DEFAULT_LOCAL_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "a".to_string()); - DEFAULT_LOCAL_SETTINGS - .write() - .unwrap() - .insert("c".to_string(), "a".to_string()); - LOCAL_CONFIG - .write() - .unwrap() - .options - .insert("a".to_string(), "b".to_string()); - LOCAL_CONFIG - .write() - .unwrap() - .options - .insert("b".to_string(), "b".to_string()); - OVERWRITE_LOCAL_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "c".to_string()); - OVERWRITE_LOCAL_SETTINGS - .write() - .unwrap() - .insert("d".to_string(), "c".to_string()); - assert!(LocalConfig::get_option("a") == "b"); - assert!(LocalConfig::get_option("c") == "a"); - assert!(LocalConfig::get_option("b") == "c"); - assert!(LocalConfig::get_option("d") == "c"); - DEFAULT_LOCAL_SETTINGS.write().unwrap().clear(); - OVERWRITE_LOCAL_SETTINGS.write().unwrap().clear(); - LOCAL_CONFIG.write().unwrap().options.clear(); - - DEFAULT_DISPLAY_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "a".to_string()); - DEFAULT_DISPLAY_SETTINGS - .write() - .unwrap() - .insert("c".to_string(), "a".to_string()); - USER_DEFAULT_CONFIG - .write() - .unwrap() - .0 - .options - .insert("a".to_string(), "b".to_string()); - USER_DEFAULT_CONFIG - .write() - .unwrap() - .0 - .options - .insert("b".to_string(), "b".to_string()); - OVERWRITE_DISPLAY_SETTINGS - .write() - .unwrap() - .insert("b".to_string(), "c".to_string()); - OVERWRITE_DISPLAY_SETTINGS - .write() - .unwrap() - .insert("d".to_string(), "c".to_string()); - assert!(UserDefaultConfig::read("a") == "b"); - assert!(UserDefaultConfig::read("c") == "a"); - assert!(UserDefaultConfig::read("b") == "c"); - assert!(UserDefaultConfig::read("d") == "c"); - DEFAULT_DISPLAY_SETTINGS.write().unwrap().clear(); - OVERWRITE_DISPLAY_SETTINGS.write().unwrap().clear(); - LOCAL_CONFIG.write().unwrap().options.clear(); - } - - #[test] - fn test_config_deserialize() { - let wrong_type_str = r#" - id = true - enc_id = [] - password = 1 - salt = "123456" - key_pair = {} - key_confirmed = "1" - keys_confirmed = 1 - "#; - let cfg = toml::from_str::(wrong_type_str); - assert_eq!( - cfg, - Ok(Config { - salt: "123456".to_string(), - ..Default::default() - }) - ); - - let wrong_field_str = r#" - hello = "world" - key_confirmed = true - "#; - let cfg = toml::from_str::(wrong_field_str); - assert_eq!( - cfg, - Ok(Config { - key_confirmed: true, - ..Default::default() - }) - ); - } - - #[test] - fn test_peer_config_deserialize() { - let default_peer_config = toml::from_str::("").unwrap(); - // test custom_resolution - { - let wrong_type_str = r#" - view_style = "adaptive" - scroll_style = "scrollbar" - custom_resolutions = true - "#; - let mut cfg_to_compare = default_peer_config.clone(); - cfg_to_compare.view_style = "adaptive".to_string(); - cfg_to_compare.scroll_style = "scrollbar".to_string(); - let cfg = toml::from_str::(wrong_type_str); - assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_type_str"); - - let wrong_type_str = r#" - view_style = "adaptive" - scroll_style = "scrollbar" - [custom_resolutions.0] - w = "1920" - h = 1080 - "#; - let mut cfg_to_compare = default_peer_config.clone(); - cfg_to_compare.view_style = "adaptive".to_string(); - cfg_to_compare.scroll_style = "scrollbar".to_string(); - let cfg = toml::from_str::(wrong_type_str); - assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_type_str"); - - let wrong_field_str = r#" - [custom_resolutions.0] - w = 1920 - h = 1080 - hello = "world" - [ui_flutter] - "#; - let mut cfg_to_compare = default_peer_config.clone(); - cfg_to_compare.custom_resolutions = - HashMap::from([("0".to_string(), Resolution { w: 1920, h: 1080 })]); - let cfg = toml::from_str::(wrong_field_str); - assert_eq!(cfg, Ok(cfg_to_compare), "Failed to test wrong_field_str"); - } - } - - #[test] - fn test_store_load() { - let peerconfig_id = "123456789"; - let cfg: PeerConfig = Default::default(); - cfg.store(&peerconfig_id); - assert_eq!(PeerConfig::load(&peerconfig_id), cfg); - - #[cfg(not(windows))] - { - use std::os::unix::fs::PermissionsExt; - assert_eq!( - // ignore file type information by masking with 0o777 (see https://stackoverflow.com/a/50045872) - fs::metadata(PeerConfig::path(&peerconfig_id)) - .expect("reading metadata failed") - .permissions() - .mode() - & 0o777, - 0o600 - ); - } - } -} diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs deleted file mode 100644 index 8031516972e..00000000000 --- a/libs/hbb_common/src/fs.rs +++ /dev/null @@ -1,960 +0,0 @@ -#[cfg(windows)] -use std::os::windows::prelude::*; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use serde_derive::{Deserialize, Serialize}; -use serde_json::json; -use tokio::{fs::File, io::*}; - -use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream}; -// https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html -use crate::{ - compress::{compress, decompress}, - config::Config, -}; - -pub fn read_dir(path: &Path, include_hidden: bool) -> ResultType { - let mut dir = FileDirectory { - path: get_string(path), - ..Default::default() - }; - #[cfg(windows)] - if "/" == &get_string(path) { - let drives = unsafe { winapi::um::fileapi::GetLogicalDrives() }; - for i in 0..32 { - if drives & (1 << i) != 0 { - let name = format!( - "{}:", - std::char::from_u32('A' as u32 + i as u32).unwrap_or('A') - ); - dir.entries.push(FileEntry { - name, - entry_type: FileType::DirDrive.into(), - ..Default::default() - }); - } - } - return Ok(dir); - } - for entry in path.read_dir()?.flatten() { - let p = entry.path(); - let name = p - .file_name() - .map(|p| p.to_str().unwrap_or("")) - .unwrap_or("") - .to_owned(); - if name.is_empty() { - continue; - } - let mut is_hidden = false; - let meta; - if let Ok(tmp) = std::fs::symlink_metadata(&p) { - meta = tmp; - } else { - continue; - } - // docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants - #[cfg(windows)] - if meta.file_attributes() & 0x2 != 0 { - is_hidden = true; - } - #[cfg(not(windows))] - if name.find('.').unwrap_or(usize::MAX) == 0 { - is_hidden = true; - } - if is_hidden && !include_hidden { - continue; - } - let (entry_type, size) = { - if p.is_dir() { - if meta.file_type().is_symlink() { - (FileType::DirLink.into(), 0) - } else { - (FileType::Dir.into(), 0) - } - } else if meta.file_type().is_symlink() { - (FileType::FileLink.into(), 0) - } else { - (FileType::File.into(), meta.len()) - } - }; - let modified_time = meta - .modified() - .map(|x| { - x.duration_since(std::time::SystemTime::UNIX_EPOCH) - .map(|x| x.as_secs()) - .unwrap_or(0) - }) - .unwrap_or(0); - dir.entries.push(FileEntry { - name: get_file_name(&p), - entry_type, - is_hidden, - size, - modified_time, - ..Default::default() - }); - } - Ok(dir) -} - -#[inline] -pub fn get_file_name(p: &Path) -> String { - p.file_name() - .map(|p| p.to_str().unwrap_or("")) - .unwrap_or("") - .to_owned() -} - -#[inline] -pub fn get_string(path: &Path) -> String { - path.to_str().unwrap_or("").to_owned() -} - -#[inline] -pub fn get_path(path: &str) -> PathBuf { - Path::new(path).to_path_buf() -} - -#[inline] -pub fn get_home_as_string() -> String { - get_string(&Config::get_home()) -} - -fn read_dir_recursive( - path: &PathBuf, - prefix: &Path, - include_hidden: bool, -) -> ResultType> { - let mut files = Vec::new(); - if path.is_dir() { - // to-do: symbol link handling, cp the link rather than the content - // to-do: file mode, for unix - let fd = read_dir(path, include_hidden)?; - for entry in fd.entries.iter() { - match entry.entry_type.enum_value() { - Ok(FileType::File) => { - let mut entry = entry.clone(); - entry.name = get_string(&prefix.join(entry.name)); - files.push(entry); - } - Ok(FileType::Dir) => { - if let Ok(mut tmp) = read_dir_recursive( - &path.join(&entry.name), - &prefix.join(&entry.name), - include_hidden, - ) { - for entry in tmp.drain(0..) { - files.push(entry); - } - } - } - _ => {} - } - } - Ok(files) - } else if path.is_file() { - let (size, modified_time) = if let Ok(meta) = std::fs::metadata(path) { - ( - meta.len(), - meta.modified() - .map(|x| { - x.duration_since(std::time::SystemTime::UNIX_EPOCH) - .map(|x| x.as_secs()) - .unwrap_or(0) - }) - .unwrap_or(0), - ) - } else { - (0, 0) - }; - files.push(FileEntry { - entry_type: FileType::File.into(), - size, - modified_time, - ..Default::default() - }); - Ok(files) - } else { - bail!("Not exists"); - } -} - -pub fn get_recursive_files(path: &str, include_hidden: bool) -> ResultType> { - read_dir_recursive(&get_path(path), &get_path(""), include_hidden) -} - -fn read_empty_dirs_recursive( - path: &PathBuf, - prefix: &Path, - include_hidden: bool, -) -> ResultType> { - let mut dirs = Vec::new(); - if path.is_dir() { - // to-do: symbol link handling, cp the link rather than the content - // to-do: file mode, for unix - let fd = read_dir(path, include_hidden)?; - if fd.entries.is_empty() { - dirs.push(fd); - } else { - for entry in fd.entries.iter() { - match entry.entry_type.enum_value() { - Ok(FileType::Dir) => { - if let Ok(mut tmp) = read_empty_dirs_recursive( - &path.join(&entry.name), - &prefix.join(&entry.name), - include_hidden, - ) { - for entry in tmp.drain(0..) { - dirs.push(entry); - } - } - } - _ => {} - } - } - } - Ok(dirs) - } else if path.is_file() { - Ok(dirs) - } else { - bail!("Not exists"); - } -} - -pub fn get_empty_dirs_recursive( - path: &str, - include_hidden: bool, -) -> ResultType> { - read_empty_dirs_recursive(&get_path(path), &get_path(""), include_hidden) -} - -#[inline] -pub fn is_file_exists(file_path: &str) -> bool { - return Path::new(file_path).exists(); -} - -#[inline] -pub fn can_enable_overwrite_detection(version: i64) -> bool { - version >= get_version_number("1.1.10") -} - -#[derive(Default, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct TransferJob { - pub id: i32, - pub remote: String, - pub path: PathBuf, - pub show_hidden: bool, - pub is_remote: bool, - pub is_last_job: bool, - pub file_num: i32, - #[serde(skip_serializing)] - pub files: Vec, - pub conn_id: i32, // server only - - #[serde(skip_serializing)] - file: Option, - pub total_size: u64, - finished_size: u64, - 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, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct TransferJobMeta { - #[serde(default)] - pub id: i32, - #[serde(default)] - pub remote: String, - #[serde(default)] - pub to: String, - #[serde(default)] - pub show_hidden: bool, - #[serde(default)] - pub file_num: i32, - #[serde(default)] - pub is_remote: bool, -} - -#[derive(Debug, Default, Serialize, Deserialize, Clone)] -pub struct RemoveJobMeta { - #[serde(default)] - pub path: String, - #[serde(default)] - pub is_remote: bool, - #[serde(default)] - pub no_confirm: bool, -} - -#[inline] -fn get_ext(name: &str) -> &str { - if let Some(i) = name.rfind('.') { - return &name[i + 1..]; - } - "" -} - -#[inline] -fn is_compressed_file(name: &str) -> bool { - let ext = get_ext(name); - ext == "xz" - || ext == "gz" - || ext == "zip" - || ext == "7z" - || ext == "rar" - || ext == "bz2" - || ext == "tgz" - || ext == "png" - || ext == "jpg" -} - -impl TransferJob { - #[allow(clippy::too_many_arguments)] - pub fn new_write( - id: i32, - remote: String, - path: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, - files: Vec, - enable_overwrite_detection: bool, - ) -> Self { - log::info!("new write {}", path); - let total_size = files.iter().map(|x| x.size).sum(); - Self { - id, - remote, - path: get_path(&path), - file_num, - show_hidden, - is_remote, - files, - total_size, - enable_overwrite_detection, - ..Default::default() - } - } - - pub fn new_read( - id: i32, - remote: String, - path: String, - file_num: i32, - show_hidden: bool, - is_remote: bool, - enable_overwrite_detection: bool, - ) -> ResultType { - log::info!("new read {}", path); - let files = get_recursive_files(&path, show_hidden)?; - let total_size = files.iter().map(|x| x.size).sum(); - Ok(Self { - id, - remote, - path: get_path(&path), - file_num, - show_hidden, - is_remote, - files, - total_size, - enable_overwrite_detection, - ..Default::default() - }) - } - - #[inline] - pub fn files(&self) -> &Vec { - &self.files - } - - #[inline] - pub fn set_files(&mut self, files: Vec) { - self.files = files; - } - - #[inline] - pub fn id(&self) -> i32 { - self.id - } - - #[inline] - pub fn total_size(&self) -> u64 { - self.total_size - } - - #[inline] - pub fn finished_size(&self) -> u64 { - self.finished_size - } - - #[inline] - pub fn transferred(&self) -> u64 { - self.transferred - } - - #[inline] - pub fn file_num(&self) -> i32 { - self.file_num - } - - pub fn modify_time(&self) { - let file_num = self.file_num as usize; - if file_num < self.files.len() { - let entry = &self.files[file_num]; - let path = self.join(&entry.name); - let download_path = format!("{}.download", get_string(&path)); - std::fs::rename(download_path, &path).ok(); - filetime::set_file_mtime( - &path, - filetime::FileTime::from_unix_time(entry.modified_time as _, 0), - ) - .ok(); - } - } - - pub fn remove_download_file(&self) { - let file_num = self.file_num as usize; - if file_num < self.files.len() { - let entry = &self.files[file_num]; - let path = self.join(&entry.name); - let download_path = format!("{}.download", get_string(&path)); - std::fs::remove_file(download_path).ok(); - } - } - - pub async fn write(&mut self, block: FileTransferBlock) -> ResultType<()> { - if block.id != self.id { - bail!("Wrong id"); - } - let file_num = block.file_num as usize; - if file_num >= self.files.len() { - bail!("Wrong file number"); - } - if file_num != self.file_num as usize || self.file.is_none() { - self.modify_time(); - if let Some(file) = self.file.as_mut() { - file.sync_all().await?; - } - self.file_num = block.file_num; - let entry = &self.files[file_num]; - let path = self.join(&entry.name); - if let Some(p) = path.parent() { - std::fs::create_dir_all(p).ok(); - } - let path = format!("{}.download", get_string(&path)); - self.file = Some(File::create(&path).await?); - } - if block.compressed { - let tmp = decompress(&block.data); - self.file - .as_mut() - .ok_or(anyhow!("file is None"))? - .write_all(&tmp) - .await?; - self.finished_size += tmp.len() as u64; - } else { - self.file - .as_mut() - .ok_or(anyhow!("file is None"))? - .write_all(&block.data) - .await?; - self.finished_size += block.data.len() as u64; - } - self.transferred += block.data.len() as u64; - Ok(()) - } - - #[inline] - pub fn join(&self, name: &str) -> PathBuf { - if name.is_empty() { - self.path.clone() - } else { - self.path.join(name) - } - } - - pub async fn read(&mut self, stream: &mut Stream) -> ResultType> { - let file_num = self.file_num as usize; - if file_num >= self.files.len() { - self.file.take(); - return Ok(None); - } - let name = &self.files[file_num].name; - if self.file.is_none() { - match File::open(self.join(name)).await { - Ok(file) => { - self.file = Some(file); - self.file_confirmed = false; - self.file_is_waiting = false; - } - Err(err) => { - self.file_num += 1; - self.file_confirmed = false; - self.file_is_waiting = false; - return Err(err.into()); - } - } - } - if self.enable_overwrite_detection && !self.file_confirmed() { - if !self.file_is_waiting() { - self.send_current_digest(stream).await?; - self.set_file_is_waiting(true); - } - return Ok(None); - } - const BUF_SIZE: usize = 128 * 1024; - let mut buf: Vec = vec![0; BUF_SIZE]; - let mut compressed = false; - let mut offset: usize = 0; - loop { - match self - .file - .as_mut() - .ok_or(anyhow!("file is None"))? - .read(&mut buf[offset..]) - .await - { - Err(err) => { - self.file_num += 1; - self.file = None; - self.file_confirmed = false; - self.file_is_waiting = false; - return Err(err.into()); - } - Ok(n) => { - offset += n; - if n == 0 || offset == BUF_SIZE { - break; - } - } - } - } - unsafe { buf.set_len(offset) }; - if offset == 0 { - self.file_num += 1; - self.file = None; - self.file_confirmed = false; - self.file_is_waiting = false; - } else { - self.finished_size += offset as u64; - if !is_compressed_file(name) { - let tmp = compress(&buf); - if tmp.len() < buf.len() { - buf = tmp; - compressed = true; - } - } - self.transferred += buf.len() as u64; - } - Ok(Some(FileTransferBlock { - id: self.id, - file_num: file_num as _, - data: buf.into(), - compressed, - ..Default::default() - })) - } - - async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> { - let mut msg = Message::new(); - let mut resp = FileResponse::new(); - let meta = self - .file - .as_ref() - .ok_or(anyhow!("file is None"))? - .metadata() - .await?; - let last_modified = meta - .modified()? - .duration_since(SystemTime::UNIX_EPOCH)? - .as_secs(); - resp.set_digest(FileTransferDigest { - id: self.id, - file_num: self.file_num, - last_modified, - file_size: meta.len(), - ..Default::default() - }); - msg.set_file_response(resp); - stream.send(&msg).await?; - log::info!( - "id: {}, file_num: {}, digest message is sent. waiting for confirm. msg: {:?}", - self.id, - self.file_num, - msg - ); - Ok(()) - } - - pub fn set_overwrite_strategy(&mut self, overwrite_strategy: Option) { - self.default_overwrite_strategy = overwrite_strategy; - } - - pub fn default_overwrite_strategy(&self) -> Option { - self.default_overwrite_strategy - } - - 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 - } - - /// 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 - !self.enable_overwrite_detection || (!self.file_confirmed && !self.file_is_waiting) - } - - /// 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 - } - - pub fn confirm(&mut self, r: &FileTransferSendConfirmRequest) -> bool { - if self.file_num() != r.file_num { - log::info!("file num truncated, ignoring"); - } else { - match r.union { - Some(file_transfer_send_confirm_request::Union::Skip(s)) => { - if s { - self.set_file_skipped(); - } else { - self.set_file_confirmed(true); - } - } - Some(file_transfer_send_confirm_request::Union::OffsetBlk(_offset)) => { - self.set_file_confirmed(true); - } - _ => {} - } - } - true - } - - #[inline] - pub fn gen_meta(&self) -> TransferJobMeta { - TransferJobMeta { - id: self.id, - remote: self.remote.to_string(), - to: self.path.to_string_lossy().to_string(), - file_num: self.file_num, - show_hidden: self.show_hidden, - is_remote: self.is_remote, - } - } -} - -#[inline] -pub fn new_error(id: i32, err: T, file_num: i32) -> Message { - let mut resp = FileResponse::new(); - resp.set_error(FileTransferError { - id, - error: err.to_string(), - file_num, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn new_dir(id: i32, path: String, files: Vec) -> Message { - let mut resp = FileResponse::new(); - resp.set_dir(FileDirectory { - id, - path, - entries: files, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn new_block(block: FileTransferBlock) -> Message { - let mut resp = FileResponse::new(); - resp.set_block(block); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message { - let mut msg_out = Message::new(); - let mut action = FileAction::new(); - action.set_send_confirm(r); - msg_out.set_file_action(action); - msg_out -} - -#[inline] -pub fn new_receive( - id: i32, - path: String, - file_num: i32, - files: Vec, - total_size: u64, -) -> Message { - let mut action = FileAction::new(); - action.set_receive(FileTransferReceiveRequest { - id, - path, - files, - file_num, - total_size, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_action(action); - msg_out -} - -#[inline] -pub fn new_send(id: i32, path: String, file_num: i32, include_hidden: bool) -> Message { - log::info!("new send: {}, id: {}", path, id); - let mut action = FileAction::new(); - action.set_send(FileTransferSendRequest { - id, - path, - include_hidden, - file_num, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_action(action); - msg_out -} - -#[inline] -pub fn new_done(id: i32, file_num: i32) -> Message { - let mut resp = FileResponse::new(); - resp.set_done(FileTransferDone { - id, - file_num, - ..Default::default() - }); - let mut msg_out = Message::new(); - msg_out.set_file_response(resp); - msg_out -} - -#[inline] -pub fn remove_job(id: i32, jobs: &mut Vec) { - *jobs = jobs.drain(0..).filter(|x| x.id() != id).collect(); -} - -#[inline] -pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> { - jobs.iter_mut().find(|x| x.id() == id) -} - -#[inline] -pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> { - jobs.iter().find(|x| x.id() == id) -} - -pub async fn handle_read_jobs( - jobs: &mut Vec, - stream: &mut crate::Stream, -) -> ResultType { - let mut job_log = Default::default(); - let mut finished = Vec::new(); - for job in jobs.iter_mut() { - if job.is_last_job { - continue; - } - match job.read(stream).await { - Err(err) => { - stream - .send(&new_error(job.id(), err, job.file_num())) - .await?; - } - Ok(Some(block)) => { - stream.send(&new_block(block)).await?; - } - Ok(None) => { - if job.job_completed() { - job_log = serialize_transfer_job(job, true, false, ""); - finished.push(job.id()); - match job.job_error() { - Some(err) => { - job_log = serialize_transfer_job(job, false, false, &err); - stream - .send(&new_error(job.id(), err, job.file_num())) - .await? - } - None => stream.send(&new_done(job.id(), job.file_num())).await?, - } - } else { - // waiting confirmation. - } - } - } - } - for id in finished { - remove_job(id, jobs); - } - Ok(job_log) -} - -pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> { - let fd = read_dir(path, true)?; - for entry in fd.entries.iter() { - match entry.entry_type.enum_value() { - Ok(FileType::Dir) => { - remove_all_empty_dir(&path.join(&entry.name)).ok(); - } - Ok(FileType::DirLink) | Ok(FileType::FileLink) => { - std::fs::remove_file(path.join(&entry.name)).ok(); - } - _ => {} - } - } - std::fs::remove_dir(path).ok(); - Ok(()) -} - -#[inline] -pub fn remove_file(file: &str) -> ResultType<()> { - std::fs::remove_file(get_path(file))?; - Ok(()) -} - -#[inline] -pub fn create_dir(dir: &str) -> ResultType<()> { - std::fs::create_dir_all(get_path(dir))?; - Ok(()) -} - -#[inline] -pub fn rename_file(path: &str, new_name: &str) -> ResultType<()> { - let path = std::path::Path::new(&path); - if path.exists() { - let dir = path - .parent() - .ok_or(anyhow!("Parent directoy of {path:?} not exists"))?; - let new_path = dir.join(&new_name); - std::fs::rename(&path, &new_path)?; - Ok(()) - } else { - bail!("{path:?} not exists"); - } -} - -#[inline] -pub fn transform_windows_path(entries: &mut Vec) { - for entry in entries { - entry.name = entry.name.replace('\\', "/"); - } -} - -pub enum DigestCheckResult { - IsSame, - NeedConfirm(FileTransferDigest), - NoSuchFile, -} - -#[inline] -pub fn is_write_need_confirmation( - file_path: &str, - digest: &FileTransferDigest, -) -> ResultType { - let path = Path::new(file_path); - if path.exists() && path.is_file() { - let metadata = std::fs::metadata(path)?; - let modified_time = metadata.modified()?; - let remote_mt = Duration::from_secs(digest.last_modified); - let local_mt = modified_time.duration_since(UNIX_EPOCH)?; - // [Note] - // We decide to give the decision whether to override the existing file to users, - // which obey the behavior of the file manager in our system. - let mut is_identical = false; - if remote_mt == local_mt && digest.file_size == metadata.len() { - is_identical = true; - } - Ok(DigestCheckResult::NeedConfirm(FileTransferDigest { - id: digest.id, - file_num: digest.file_num, - last_modified: local_mt.as_secs(), - file_size: metadata.len(), - is_identical, - ..Default::default() - })) - } else { - Ok(DigestCheckResult::NoSuchFile) - } -} - -pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String { - let mut v = vec![]; - for job in jobs { - let value = serde_json::to_value(job).unwrap_or_default(); - v.push(value); - } - serde_json::to_string(&v).unwrap_or_default() -} - -pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String { - let mut value = serde_json::to_value(job).unwrap_or_default(); - value["done"] = json!(done); - value["cancel"] = json!(cancel); - value["error"] = json!(error); - serde_json::to_string(&value).unwrap_or_default() -} diff --git a/libs/hbb_common/src/keyboard.rs b/libs/hbb_common/src/keyboard.rs deleted file mode 100644 index 10979f520e9..00000000000 --- a/libs/hbb_common/src/keyboard.rs +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 36a68550fa5..00000000000 --- a/libs/hbb_common/src/lib.rs +++ /dev/null @@ -1,500 +0,0 @@ -pub mod compress; -pub mod platform; -pub mod protos; -pub use bytes; -use config::Config; -pub use futures; -pub use protobuf; -pub use protos::message as message_proto; -pub use protos::rendezvous as rendezvous_proto; -use std::{ - fs::File, - io::{self, BufRead}, - net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}, - path::Path, - time::{self, SystemTime, UNIX_EPOCH}, -}; -pub use tokio; -pub use tokio_util; -pub mod proxy; -pub mod socket_client; -pub mod tcp; -pub mod udp; -pub use env_logger; -pub use log; -pub mod bytes_codec; -pub use anyhow::{self, bail}; -pub use futures_util; -pub mod config; -pub mod fs; -pub mod mem; -pub use lazy_static; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use mac_address; -pub use rand; -pub use regex; -pub use sodiumoxide; -pub use tokio_socks; -pub use tokio_socks::IntoTargetAddr; -pub use tokio_socks::TargetAddr; -pub mod password_security; -pub use chrono; -pub use directories_next; -pub use libc; -pub mod keyboard; -pub use base64; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use dlopen; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -pub use machine_uid; -pub use serde_derive; -pub use serde_json; -pub use sysinfo; -pub use thiserror; -pub use toml; -pub use uuid; - -pub type Stream = tcp::FramedStream; -pub type SessionID = uuid::Uuid; - -#[inline] -pub async fn sleep(sec: f32) { - tokio::time::sleep(time::Duration::from_secs_f32(sec)).await; -} - -#[macro_export] -macro_rules! allow_err { - ($e:expr) => { - if let Err(err) = $e { - log::debug!( - "{:?}, {}:{}:{}:{}", - err, - module_path!(), - file!(), - line!(), - column!() - ); - } else { - } - }; - - ($e:expr, $($arg:tt)*) => { - if let Err(err) = $e { - log::debug!( - "{:?}, {}, {}:{}:{}:{}", - err, - format_args!($($arg)*), - module_path!(), - file!(), - line!(), - column!() - ); - } else { - } - }; -} - -#[inline] -pub fn timeout(ms: u64, future: T) -> tokio::time::Timeout { - tokio::time::timeout(std::time::Duration::from_millis(ms), future) -} - -pub type ResultType = anyhow::Result; - -/// Certain router and firewalls scan the packet and if they -/// find an IP address belonging to their pool that they use to do the NAT mapping/translation, so here we mangle the ip address - -pub struct AddrMangle(); - -#[inline] -pub fn try_into_v4(addr: SocketAddr) -> SocketAddr { - match addr { - SocketAddr::V6(v6) if !addr.ip().is_loopback() => { - if let Some(v4) = v6.ip().to_ipv4() { - SocketAddr::new(IpAddr::V4(v4), addr.port()) - } else { - addr - } - } - _ => addr, - } -} - -impl AddrMangle { - pub fn encode(addr: SocketAddr) -> Vec { - // not work with [:1]: - let addr = try_into_v4(addr); - match addr { - SocketAddr::V4(addr_v4) => { - let tm = (SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(std::time::Duration::ZERO) - .as_micros() as u32) as u128; - let ip = u32::from_le_bytes(addr_v4.ip().octets()) as u128; - let port = addr.port() as u128; - let v = ((ip + tm) << 49) | (tm << 17) | (port + (tm & 0xFFFF)); - let bytes = v.to_le_bytes(); - let mut n_padding = 0; - for i in bytes.iter().rev() { - if i == &0u8 { - n_padding += 1; - } else { - break; - } - } - bytes[..(16 - n_padding)].to_vec() - } - 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 { - use std::convert::TryInto; - - if bytes.len() > 16 { - if bytes.len() != 18 { - return Config::get_any_listen_addr(false); - } - let tmp: [u8; 2] = bytes[16..].try_into().unwrap_or_default(); - let port = u16::from_le_bytes(tmp); - let tmp: [u8; 16] = bytes[..16].try_into().unwrap_or_default(); - 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); - let tm = (number >> 17) & (u32::max_value() as u128); - let ip = (((number >> 49) - tm) as u32).to_le_bytes(); - let port = (number & 0xFFFFFF) - (tm & 0xFFFF); - SocketAddr::V4(SocketAddrV4::new( - Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]), - port as u16, - )) - } -} - -pub fn get_version_from_url(url: &str) -> String { - let n = url.chars().count(); - let a = url.chars().rev().position(|x| x == '-'); - if let Some(a) = a { - let b = url.chars().rev().position(|x| x == '.'); - if let Some(b) = b { - if a > b { - if url - .chars() - .skip(n - b) - .collect::() - .parse::() - .is_ok() - { - return url.chars().skip(n - a).collect(); - } else { - return url.chars().skip(n - a).take(a - b - 1).collect(); - } - } else { - return url.chars().skip(n - a).collect(); - } - } - } - "".to_owned() -} - -pub fn gen_version() { - println!("cargo:rerun-if-changed=Cargo.toml"); - use std::io::prelude::*; - let mut file = File::create("./src/version.rs").unwrap(); - for line in read_lines("Cargo.toml").unwrap().flatten() { - let ab: Vec<&str> = line.split('=').map(|x| x.trim()).collect(); - if ab.len() == 2 && ab[0] == "version" { - file.write_all(format!("pub const VERSION: &str = {};\n", ab[1]).as_bytes()) - .ok(); - break; - } - } - // generate build date - let build_date = format!("{}", chrono::Local::now().format("%Y-%m-%d %H:%M")); - file.write_all( - format!("#[allow(dead_code)]\npub const BUILD_DATE: &str = \"{build_date}\";\n").as_bytes(), - ) - .ok(); - file.sync_all().ok(); -} - -fn read_lines

(filename: P) -> io::Result>> -where - P: AsRef, -{ - let file = File::open(filename)?; - Ok(io::BufReader::new(file).lines()) -} - -pub fn is_valid_custom_id(id: &str) -> bool { - regex::Regex::new(r"^[a-zA-Z]\w{5,15}$") - .unwrap() - .is_match(id) -} - -// Support 1.1.10-1, the number after - is a patch version. -pub fn get_version_number(v: &str) -> i64 { - let mut versions = v.split('-'); - - let mut n = 0; - - // The first part is the version number. - // 1.1.10 -> 1001100, 1.2.3 -> 1001030, multiple the last number by 10 - // to leave space for patch version. - if let Some(v) = versions.next() { - let mut last = 0; - for x in v.split('.') { - last = x.parse::().unwrap_or(0); - n = n * 1000 + last; - } - n -= last; - n += last * 10; - } - - if let Some(v) = versions.next() { - n += v.parse::().unwrap_or(0); - } - - // Ignore the rest - - n -} - -pub fn get_modified_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(path) - .map(|m| m.modified().unwrap_or(UNIX_EPOCH)) - .unwrap_or(UNIX_EPOCH) -} - -pub fn get_created_time(path: &std::path::Path) -> SystemTime { - std::fs::metadata(path) - .map(|m| m.created().unwrap_or(UNIX_EPOCH)) - .unwrap_or(UNIX_EPOCH) -} - -pub fn get_exe_time() -> SystemTime { - std::env::current_exe().map_or(UNIX_EPOCH, |path| { - let m = get_modified_time(&path); - let c = get_created_time(&path); - if m > c { - m - } else { - c - } - }) -} - -pub fn get_uuid() -> Vec { - #[cfg(not(any(target_os = "android", target_os = "ios")))] - if let Ok(id) = machine_uid::get() { - return id.into(); - } - Config::get_key_pair().1 -} - -#[inline] -pub fn get_time() -> i64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis()) - .unwrap_or(0) as _ -} - -#[inline] -pub fn is_ipv4_str(id: &str) -> bool { - if let Ok(reg) = regex::Regex::new( - r"^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\d+)?$", - ) { - reg.is_match(id) - } else { - false - } -} - -#[inline] -pub fn is_ipv6_str(id: &str) -> bool { - if let Ok(reg) = 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+))$", - ) { - reg.is_match(id) - } else { - false - } -} - -#[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. - if let Ok(reg) = 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}$", - ) { - reg.is_match(id) - } else { - false - } -} - -pub fn init_log(_is_async: bool, _name: &str) -> Option { - static INIT: std::sync::Once = std::sync::Once::new(); - #[allow(unused_mut)] - let mut logger_holder: Option = None; - INIT.call_once(|| { - #[cfg(debug_assertions)] - { - use env_logger::*; - init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); - } - #[cfg(not(debug_assertions))] - { - // https://docs.rs/flexi_logger/latest/flexi_logger/error_info/index.html#write - // though async logger more efficient, but it also causes more problems, disable it for now - let mut path = config::Config::log_path(); - #[cfg(target_os = "android")] - if !config::Config::get_home().exists() { - return; - } - if !_name.is_empty() { - path.push(_name); - } - use flexi_logger::*; - if let Ok(x) = Logger::try_with_env_or_str("debug") { - logger_holder = x - .log_to_file(FileSpec::default().directory(path)) - .write_mode(if _is_async { - WriteMode::Async - } else { - WriteMode::Direct - }) - .format(opt_format) - .rotate( - Criterion::Age(Age::Day), - Naming::Timestamps, - Cleanup::KeepLogFiles(31), - ) - .start() - .ok(); - } - } - }); - logger_holder -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - 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" - ); - } - - #[test] - fn test_ipv6() { - assert!(is_ipv6_str("1:2:3")); - assert!(is_ipv6_str("[ab:2:3]:12")); - assert!(is_ipv6_str("[ABEF:2a:3]:12")); - assert!(!is_ipv6_str("[ABEG:2a:3]:12")); - assert!(!is_ipv6_str("1[ab:2:3]:12")); - assert!(!is_ipv6_str("1.1.1.1")); - assert!(is_ip_str("1.1.1.1")); - assert!(!is_ipv6_str("1:2:")); - assert!(is_ipv6_str("1:2::0")); - assert!(is_ipv6_str("[1:2::0]:1")); - assert!(!is_ipv6_str("[1:2::0]:")); - assert!(!is_ipv6_str("1:2::0]:1")); - } - - #[test] - fn test_ipv4() { - assert!(is_ipv4_str("1.2.3.4")); - assert!(is_ipv4_str("1.2.3.4:90")); - assert!(is_ipv4_str("192.168.0.1")); - assert!(is_ipv4_str("0.0.0.0")); - assert!(is_ipv4_str("255.255.255.255")); - assert!(!is_ipv4_str("256.0.0.0")); - assert!(!is_ipv4_str("256.256.256.256")); - assert!(!is_ipv4_str("1:2:")); - assert!(!is_ipv4_str("192.168.0.256")); - assert!(!is_ipv4_str("192.168.0.1/24")); - assert!(!is_ipv4_str("192.168.0.")); - assert!(!is_ipv4_str("192.168..1")); - } - - #[test] - fn test_hostname_port() { - assert!(!is_domain_port_str("a:12")); - assert!(!is_domain_port_str("a.b.c:12")); - assert!(is_domain_port_str("test.com:12")); - assert!(is_domain_port_str("test-UPPER.com:12")); - assert!(is_domain_port_str("some-other.domain.com:12")); - assert!(!is_domain_port_str("under_score:12")); - assert!(!is_domain_port_str("a@bc:12")); - assert!(!is_domain_port_str("1.1.1.1:12")); - assert!(!is_domain_port_str("1.2.3:12")); - assert!(!is_domain_port_str("1.2.3.45:12")); - assert!(!is_domain_port_str("a.b.c:123456")); - assert!(!is_domain_port_str("---:12")); - assert!(!is_domain_port_str(".:12")); - // todo: should we also check for these edge cases? - // out-of-range port - assert!(is_domain_port_str("test.com:0")); - assert!(is_domain_port_str("test.com:98989")); - } - - #[test] - fn test_mangle2() { - let addr = "[::ffff:127.0.0.1]:8080".parse().unwrap(); - let addr_v4 = "127.0.0.1:8080".parse().unwrap(); - assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr)), addr_v4); - assert_eq!( - AddrMangle::decode(&AddrMangle::encode("[::127.0.0.1]:8080".parse().unwrap())), - addr_v4 - ); - assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v4)), addr_v4); - let addr_v6 = "[ef::fe]:8080".parse().unwrap(); - assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); - let addr_v6 = "[::1]:8080".parse().unwrap(); - assert_eq!(AddrMangle::decode(&AddrMangle::encode(addr_v6)), addr_v6); - } - - #[test] - fn test_get_version_number() { - assert_eq!(get_version_number("1.1.10"), 1001100); - assert_eq!(get_version_number("1.1.10-1"), 1001101); - assert_eq!(get_version_number("1.1.11-1"), 1001111); - assert_eq!(get_version_number("1.2.3"), 1002030); - } -} diff --git a/libs/hbb_common/src/mem.rs b/libs/hbb_common/src/mem.rs deleted file mode 100644 index 90a5d6d402e..00000000000 --- a/libs/hbb_common/src/mem.rs +++ /dev/null @@ -1,14 +0,0 @@ -/// SAFETY: the returned Vec must not be resized or reserverd -pub unsafe fn aligned_u8_vec(cap: usize, align: usize) -> Vec { - use std::alloc::*; - - let layout = - Layout::from_size_align(cap, align).expect("invalid aligned value, must be power of 2"); - unsafe { - let ptr = alloc(layout); - if ptr.is_null() { - panic!("failed to allocate {} bytes", cap); - } - Vec::from_raw_parts(ptr, 0, cap) - } -} diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs deleted file mode 100644 index 5c04cc97b92..00000000000 --- a/libs/hbb_common/src/password_security.rs +++ /dev/null @@ -1,295 +0,0 @@ -use crate::config::Config; -use sodiumoxide::base64; -use std::sync::{Arc, RwLock}; - -lazy_static::lazy_static! { - pub static ref TEMPORARY_PASSWORD:Arc> = Arc::new(RwLock::new(Config::get_auto_password(temporary_password_length()))); -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum VerificationMethod { - OnlyUseTemporaryPassword, - OnlyUsePermanentPassword, - UseBothPasswords, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ApproveMode { - Both, - Password, - Click, -} - -// Should only be called in server -pub fn update_temporary_password() { - *TEMPORARY_PASSWORD.write().unwrap() = Config::get_auto_password(temporary_password_length()); -} - -// Should only be called in server -pub fn temporary_password() -> String { - TEMPORARY_PASSWORD.read().unwrap().clone() -} - -fn verification_method() -> VerificationMethod { - let method = Config::get_option("verification-method"); - if method == "use-temporary-password" { - VerificationMethod::OnlyUseTemporaryPassword - } else if method == "use-permanent-password" { - VerificationMethod::OnlyUsePermanentPassword - } else { - VerificationMethod::UseBothPasswords // default - } -} - -pub fn temporary_password_length() -> usize { - let length = Config::get_option("temporary-password-length"); - if length == "8" { - 8 - } else if length == "10" { - 10 - } else { - 6 // default - } -} - -pub fn temporary_enabled() -> bool { - verification_method() != VerificationMethod::OnlyUsePermanentPassword -} - -pub fn permanent_enabled() -> bool { - verification_method() != VerificationMethod::OnlyUseTemporaryPassword -} - -pub fn has_valid_password() -> bool { - temporary_enabled() && !temporary_password().is_empty() - || permanent_enabled() && !Config::get_permanent_password().is_empty() -} - -pub fn approve_mode() -> ApproveMode { - let mode = Config::get_option("approve-mode"); - if mode == "password" { - ApproveMode::Password - } else if mode == "click" { - ApproveMode::Click - } else { - ApproveMode::Both - } -} - -pub fn hide_cm() -> bool { - approve_mode() == ApproveMode::Password - && verification_method() == VerificationMethod::OnlyUsePermanentPassword - && crate::config::option2bool("allow-hide-cm", &Config::get_option("allow-hide-cm")) -} - -const VERSION_LEN: usize = 2; - -pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String { - if decrypt_str_or_original(s, version).1 { - log::error!("Duplicate encryption!"); - return s.to_owned(); - } - if s.chars().count() > max_len { - return String::default(); - } - if version == "00" { - if let Ok(s) = encrypt(s.as_bytes()) { - return version.to_owned() + &s; - } - } - s.to_owned() -} - -// String: password -// bool: whether decryption is successful -// bool: whether should store to re-encrypt when load -// note: s.len() return length in bytes, s.chars().count() return char count -// &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars -pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) { - if s.len() > VERSION_LEN { - if s.starts_with("00") { - if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) { - return ( - String::from_utf8_lossy(&v).to_string(), - true, - "00" != current_version, - ); - } - } - } - - (s.to_owned(), false, !s.is_empty()) -} - -pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec { - if decrypt_vec_or_original(v, version).1 { - log::error!("Duplicate encryption!"); - return v.to_owned(); - } - if v.len() > max_len { - return vec![]; - } - if version == "00" { - if let Ok(s) = encrypt(v) { - let mut version = version.to_owned().into_bytes(); - version.append(&mut s.into_bytes()); - return version; - } - } - v.to_owned() -} - -// Vec: password -// bool: whether decryption is successful -// bool: whether should store to re-encrypt when load -pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec, bool, bool) { - if v.len() > VERSION_LEN { - let version = String::from_utf8_lossy(&v[..VERSION_LEN]); - if version == "00" { - if let Ok(v) = decrypt(&v[VERSION_LEN..]) { - return (v, true, version != current_version); - } - } - } - - (v.to_owned(), false, !v.is_empty()) -} - -fn encrypt(v: &[u8]) -> Result { - if !v.is_empty() { - symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original)) - } else { - Err(()) - } -} - -fn decrypt(v: &[u8]) -> Result, ()> { - if !v.is_empty() { - base64::decode(v, base64::Variant::Original).and_then(|v| symmetric_crypt(&v, false)) - } else { - Err(()) - } -} - -pub fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result, ()> { - use sodiumoxide::crypto::secretbox; - use std::convert::TryInto; - - let mut keybuf = crate::get_uuid(); - keybuf.resize(secretbox::KEYBYTES, 0); - let key = secretbox::Key(keybuf.try_into().map_err(|_| ())?); - let nonce = secretbox::Nonce([0; secretbox::NONCEBYTES]); - - if encrypt { - Ok(secretbox::seal(data, &nonce, &key)) - } else { - secretbox::open(data, &nonce, &key) - } -} - -mod test { - - #[test] - fn test() { - use super::*; - use rand::{thread_rng, Rng}; - use std::time::Instant; - - let version = "00"; - let max_len = 128; - - println!("test str"); - let data = "1ü1111"; - let encrypted = encrypt_str_or_original(data, version, max_len); - let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version); - println!("data: {data}"); - println!("encrypted: {encrypted}"); - println!("decrypted: {decrypted}"); - assert_eq!(data, decrypted); - assert_eq!(version, &encrypted[..2]); - assert!(succ); - assert!(!store); - let (_, _, store) = decrypt_str_or_original(&encrypted, "99"); - assert!(store); - assert!(!decrypt_str_or_original(&decrypted, version).1); - assert_eq!( - encrypt_str_or_original(&encrypted, version, max_len), - encrypted - ); - - println!("test vec"); - let data: Vec = "1ü1111".as_bytes().to_vec(); - let encrypted = encrypt_vec_or_original(&data, version, max_len); - let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version); - println!("data: {data:?}"); - println!("encrypted: {encrypted:?}"); - println!("decrypted: {decrypted:?}"); - assert_eq!(data, decrypted); - assert_eq!(version.as_bytes(), &encrypted[..2]); - assert!(!store); - assert!(succ); - let (_, _, store) = decrypt_vec_or_original(&encrypted, "99"); - assert!(store); - assert!(!decrypt_vec_or_original(&decrypted, version).1); - assert_eq!( - encrypt_vec_or_original(&encrypted, version, max_len), - encrypted - ); - - println!("test original"); - let data = version.to_string() + "Hello World"; - let (decrypted, succ, store) = decrypt_str_or_original(&data, version); - assert_eq!(data, decrypted); - assert!(store); - assert!(!succ); - let verbytes = version.as_bytes(); - let data: Vec = vec![verbytes[0], verbytes[1], 1, 2, 3, 4, 5, 6]; - let (decrypted, succ, store) = decrypt_vec_or_original(&data, version); - assert_eq!(data, decrypted); - assert!(store); - assert!(!succ); - let (_, succ, store) = decrypt_str_or_original("", version); - assert!(!store); - assert!(!succ); - let (_, succ, store) = decrypt_vec_or_original(&[], version); - assert!(!store); - assert!(!succ); - let data = "1ü1111"; - assert_eq!(decrypt_str_or_original(data, version).0, data); - let data: Vec = "1ü1111".as_bytes().to_vec(); - assert_eq!(decrypt_vec_or_original(&data, version).0, data); - - println!("test speed"); - let test_speed = |len: usize, name: &str| { - let mut data: Vec = vec![]; - let mut rng = thread_rng(); - for _ in 0..len { - data.push(rng.gen_range(0..255)); - } - let start: Instant = Instant::now(); - let encrypted = encrypt_vec_or_original(&data, version, len); - assert_ne!(data, decrypted); - let t1 = start.elapsed(); - let start = Instant::now(); - let (decrypted, _, _) = decrypt_vec_or_original(&encrypted, version); - let t2 = start.elapsed(); - assert_eq!(data, decrypted); - println!("{name}"); - println!("encrypt:{:?}, decrypt:{:?}", t1, t2); - - let start: Instant = Instant::now(); - let encrypted = base64::encode(&data, base64::Variant::Original); - let t1 = start.elapsed(); - let start = Instant::now(); - let decrypted = base64::decode(&encrypted, base64::Variant::Original).unwrap(); - let t2 = start.elapsed(); - assert_eq!(data, decrypted); - println!("base64, encrypt:{:?}, decrypt:{:?}", t1, t2,); - }; - test_speed(128, "128"); - test_speed(1024, "1k"); - test_speed(1024 * 1024, "1M"); - test_speed(10 * 1024 * 1024, "10M"); - test_speed(100 * 1024 * 1024, "100M"); - } -} diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs deleted file mode 100644 index 60c8714d821..00000000000 --- a/libs/hbb_common/src/platform/linux.rs +++ /dev/null @@ -1,300 +0,0 @@ -use crate::ResultType; -use std::{collections::HashMap, process::Command}; - -lazy_static::lazy_static! { - pub static ref DISTRO: Distro = Distro::new(); -} - -pub const DISPLAY_SERVER_WAYLAND: &str = "wayland"; -pub const DISPLAY_SERVER_X11: &str = "x11"; -pub const DISPLAY_DESKTOP_KDE: &str = "KDE"; - -pub const XDG_CURRENT_DESKTOP: &str = "XDG_CURRENT_DESKTOP"; - -pub struct Distro { - pub name: String, - pub version_id: String, -} - -impl Distro { - fn new() -> Self { - let name = run_cmds("awk -F'=' '/^NAME=/ {print $2}' /etc/os-release") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); - let version_id = run_cmds("awk -F'=' '/^VERSION_ID=/ {print $2}' /etc/os-release") - .unwrap_or_default() - .trim() - .trim_matches('"') - .to_string(); - Self { name, version_id } - } -} - -#[inline] -pub fn is_kde() -> bool { - if let Ok(env) = std::env::var(XDG_CURRENT_DESKTOP) { - env == DISPLAY_DESKTOP_KDE - } else { - false - } -} - -#[inline] -pub fn is_gdm_user(username: &str) -> bool { - username == "gdm" - // || username == "lightgdm" -} - -#[inline] -pub fn is_desktop_wayland() -> bool { - get_display_server() == DISPLAY_SERVER_WAYLAND -} - -#[inline] -pub fn is_x11_or_headless() -> bool { - !is_desktop_wayland() -} - -// -1 -const INVALID_SESSION: &str = "4294967295"; - -pub fn get_display_server() -> String { - // Check for forced display server environment variable first - if let Ok(forced_display) = std::env::var("RUSTDESK_FORCED_DISPLAY_SERVER") { - return forced_display; - } - - // Check if `loginctl` can be called successfully - if run_loginctl(None).is_err() { - return DISPLAY_SERVER_X11.to_owned(); - } - - let mut session = get_values_of_seat0(&[0])[0].clone(); - if session.is_empty() { - // loginctl has not given the expected output. try something else. - if let Ok(sid) = std::env::var("XDG_SESSION_ID") { - // could also execute "cat /proc/self/sessionid" - session = sid; - } - if session.is_empty() { - session = run_cmds("cat /proc/self/sessionid").unwrap_or_default(); - if session == INVALID_SESSION { - session = "".to_owned(); - } - } - } - if session.is_empty() { - std::env::var("XDG_SESSION_TYPE").unwrap_or("x11".to_owned()) - } else { - get_display_server_of_session(&session) - } -} - -pub fn get_display_server_of_session(session: &str) -> String { - let mut display_server = if let Ok(output) = - run_loginctl(Some(vec!["show-session", "-p", "Type", session])) - // Check session type of the session - { - String::from_utf8_lossy(&output.stdout) - .replace("Type=", "") - .trim_end() - .into() - } else { - "".to_owned() - }; - if display_server.is_empty() || display_server == "tty" { - if let Ok(sestype) = std::env::var("XDG_SESSION_TYPE") { - if !sestype.is_empty() { - return sestype.to_lowercase(); - } - } - display_server = "x11".to_owned(); - } - display_server.to_lowercase() -} - -#[inline] -fn line_values(indices: &[usize], line: &str) -> Vec { - indices - .into_iter() - .map(|idx| line.split_whitespace().nth(*idx).unwrap_or("").to_owned()) - .collect::>() -} - -#[inline] -pub fn get_values_of_seat0(indices: &[usize]) -> Vec { - _get_values_of_seat0(indices, true) -} - -#[inline] -pub fn get_values_of_seat0_with_gdm_wayland(indices: &[usize]) -> Vec { - _get_values_of_seat0(indices, false) -} - -// Ignore "3 sessions listed." -fn ignore_loginctl_line(line: &str) -> bool { - line.contains("sessions") || line.split(" ").count() < 4 -} - -fn _get_values_of_seat0(indices: &[usize], ignore_gdm_wayland: bool) -> Vec { - if let Ok(output) = run_loginctl(None) { - for line in String::from_utf8_lossy(&output.stdout).lines() { - if ignore_loginctl_line(line) { - continue; - } - if line.contains("seat0") { - if let Some(sid) = line.split_whitespace().next() { - if is_active(sid) { - if ignore_gdm_wayland { - if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) - && get_display_server_of_session(sid) == DISPLAY_SERVER_WAYLAND - { - continue; - } - } - return line_values(indices, line); - } - } - } - } - - // some case, there is no seat0 https://github.com/rustdesk/rustdesk/issues/73 - for line in String::from_utf8_lossy(&output.stdout).lines() { - if ignore_loginctl_line(line) { - continue; - } - if let Some(sid) = line.split_whitespace().next() { - if is_active(sid) { - let d = get_display_server_of_session(sid); - if ignore_gdm_wayland { - if is_gdm_user(line.split_whitespace().nth(2).unwrap_or("")) - && d == DISPLAY_SERVER_WAYLAND - { - continue; - } - } - if d == "tty" { - continue; - } - return line_values(indices, line); - } - } - } - } - - line_values(indices, "") -} - -pub fn is_active(sid: &str) -> bool { - if let Ok(output) = run_loginctl(Some(vec!["show-session", "-p", "State", sid])) { - String::from_utf8_lossy(&output.stdout).contains("active") - } else { - false - } -} - -pub fn is_active_and_seat0(sid: &str) -> bool { - if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) { - String::from_utf8_lossy(&output.stdout).contains("State=active") - && String::from_utf8_lossy(&output.stdout).contains("Seat=seat0") - } else { - false - } -} - -// **Note** that the return value here, the last character is '\n'. -// Use `run_cmds_trim_newline()` if you want to remove '\n' at the end. -pub fn run_cmds(cmds: &str) -> ResultType { - let output = std::process::Command::new("sh") - .args(vec!["-c", cmds]) - .output()?; - Ok(String::from_utf8_lossy(&output.stdout).to_string()) -} - -pub fn run_cmds_trim_newline(cmds: &str) -> ResultType { - let output = std::process::Command::new("sh") - .args(vec!["-c", cmds]) - .output()?; - let out = String::from_utf8_lossy(&output.stdout); - Ok(if out.ends_with('\n') { - out[..out.len() - 1].to_string() - } else { - out.to_string() - }) -} - -fn run_loginctl(args: Option>) -> std::io::Result { - if std::env::var("FLATPAK_ID").is_ok() { - let mut l_args = String::from("loginctl"); - if let Some(a) = args.as_ref() { - l_args = format!("{} {}", l_args, a.join(" ")); - } - let res = std::process::Command::new("flatpak-spawn") - .args(vec![String::from("--host"), l_args]) - .output(); - if res.is_ok() { - return res; - } - } - let mut cmd = std::process::Command::new("loginctl"); - if let Some(a) = args { - return cmd.args(a).output(); - } - cmd.output() -} - -/// forever: may not work -#[cfg(target_os = "linux")] -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 Command::new(k).args(v).spawn().is_ok() { - return Ok(()); - } - } - crate::bail!("failed to post system message"); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_run_cmds_trim_newline() { - assert_eq!(run_cmds_trim_newline("echo -n 123").unwrap(), "123"); - assert_eq!(run_cmds_trim_newline("echo 123").unwrap(), "123"); - assert_eq!( - run_cmds_trim_newline("whoami").unwrap() + "\n", - run_cmds("whoami").unwrap() - ); - } -} diff --git a/libs/hbb_common/src/platform/macos.rs b/libs/hbb_common/src/platform/macos.rs deleted file mode 100644 index dd83a87385b..00000000000 --- a/libs/hbb_common/src/platform/macos.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::ResultType; -use osascript; -use serde_derive::{Deserialize, Serialize}; - -#[derive(Serialize)] -struct AlertParams { - title: String, - message: String, - alert_type: String, - buttons: Vec, -} - -#[derive(Deserialize)] -struct AlertResult { - #[serde(rename = "buttonReturned")] - button: String, -} - -/// Firstly run the specified app, then alert a dialog. Return the clicked button value. -/// -/// # Arguments -/// -/// * `app` - The app to execute the script. -/// * `alert_type` - Alert type. . informational, warning, critical -/// * `title` - The alert title. -/// * `message` - The alert message. -/// * `buttons` - The buttons to show. -pub fn alert( - app: String, - alert_type: String, - title: String, - message: String, - buttons: Vec, -) -> ResultType { - let script = osascript::JavaScript::new(&format!( - " - var App = Application('{}'); - App.includeStandardAdditions = true; - return App.displayAlert($params.title, {{ - message: $params.message, - 'as': $params.alert_type, - buttons: $params.buttons, - }}); - ", - app - )); - - let result: AlertResult = script.execute_with_params(AlertParams { - title, - message, - alert_type, - buttons, - })?; - Ok(result.button) -} diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs deleted file mode 100644 index 5dc004a81b7..00000000000 --- a/libs/hbb_common/src/platform/mod.rs +++ /dev/null @@ -1,81 +0,0 @@ -#[cfg(target_os = "linux")] -pub mod linux; - -#[cfg(target_os = "macos")] -pub mod macos; - -#[cfg(target_os = "windows")] -pub mod windows; - -#[cfg(not(debug_assertions))] -use crate::{config::Config, log}; -#[cfg(not(debug_assertions))] -use std::process::exit; - -#[cfg(not(debug_assertions))] -static mut GLOBAL_CALLBACK: Option> = None; - -#[cfg(not(debug_assertions))] -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") - || s.contains("glGetString") - }) { - 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); - } - if stack.iter().any(|s| { - s.to_lowercase().contains("nvidia") - || s.to_lowercase().contains("amf") - || s.to_lowercase().contains("mfx") - || s.contains("cuProfilerStop") - }) { - Config::set_option("enable-hwcodec".to_string(), "N".to_string()); - info = "Perhaps hwcodec causing the crash, disable it first".to_string(); - log::info!("{}", info); - } - log::error!( - "Got signal {} and exit. stack:\n{}", - sig, - stack.join("\n").to_string() - ); - if !info.is_empty() { - #[cfg(target_os = "linux")] - linux::system_message( - "RustDesk", - &format!("Got signal {} and exit.{}", sig, info), - true, - ) - .ok(); - } - unsafe { - if let Some(callback) = &GLOBAL_CALLBACK { - callback() - } - } - exit(0); -} - -#[cfg(not(debug_assertions))] -pub fn register_breakdown_handler(callback: T) -where - T: Fn() + 'static, -{ - unsafe { - GLOBAL_CALLBACK = Some(Box::new(callback)); - libc::signal(libc::SIGSEGV, breakdown_signal_handler as _); - } -} diff --git a/libs/hbb_common/src/platform/windows.rs b/libs/hbb_common/src/platform/windows.rs deleted file mode 100644 index 7481631ace1..00000000000 --- a/libs/hbb_common/src/platform/windows.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::{ - collections::VecDeque, - sync::{Arc, Mutex}, - time::Instant, -}; -use winapi::{ - shared::minwindef::{DWORD, FALSE, TRUE}, - um::{ - handleapi::CloseHandle, - pdh::{ - PdhAddEnglishCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx, - PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE, - PDH_HCOUNTER, PDH_HQUERY, - }, - synchapi::{CreateEventA, WaitForSingleObject}, - sysinfoapi::VerSetConditionMask, - winbase::{VerifyVersionInfoW, INFINITE, WAIT_OBJECT_0}, - winnt::{ - HANDLE, OSVERSIONINFOEXW, VER_BUILDNUMBER, VER_GREATER_EQUAL, VER_MAJORVERSION, - VER_MINORVERSION, VER_SERVICEPACKMAJOR, VER_SERVICEPACKMINOR, - }, - }, -}; - -lazy_static::lazy_static! { - static ref CPU_USAGE_ONE_MINUTE: Arc>> = Arc::new(Mutex::new(None)); -} - -// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs -#[repr(transparent)] -pub struct RAIIHandle(pub HANDLE); - -impl Drop for RAIIHandle { - fn drop(&mut self) { - // This never gives problem except when running under a debugger. - unsafe { CloseHandle(self.0) }; - } -} - -#[repr(transparent)] -pub(self) struct RAIIPDHQuery(pub PDH_HQUERY); - -impl Drop for RAIIPDHQuery { - fn drop(&mut self) { - unsafe { PdhCloseQuery(self.0) }; - } -} - -pub fn start_cpu_performance_monitor() { - // Code from: - // https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data - // https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex - // Why value lower than taskManager: - // https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43 - // Therefore we should compare with Precess Explorer rather than taskManager - - let f = || unsafe { - // load avg or cpu usage, test with prime95. - // Prefer cpu usage because we can get accurate value from Precess Explorer. - // const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0"; - const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0"; - const SAMPLE_INTERVAL: DWORD = 2; // 2 second - - let mut ret; - let mut query: PDH_HQUERY = std::mem::zeroed(); - ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query); - if ret != 0 { - log::error!("PdhOpenQueryA failed: 0x{:X}", ret); - return; - } - let _query = RAIIPDHQuery(query); - let mut counter: PDH_HCOUNTER = std::mem::zeroed(); - ret = PdhAddEnglishCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter); - if ret != 0 { - log::error!("PdhAddEnglishCounterA failed: 0x{:X}", ret); - return; - } - ret = PdhCollectQueryData(query); - if ret != 0 { - log::error!("PdhCollectQueryData failed: 0x{:X}", ret); - return; - } - let mut _counter_type: DWORD = 0; - let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed(); - let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _); - if event.is_null() { - log::error!("CreateEventA failed"); - return; - } - let _event: RAIIHandle = RAIIHandle(event); - ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event); - if ret != 0 { - log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret); - return; - } - - let mut queue: VecDeque = VecDeque::new(); - let mut recent_valid: VecDeque = VecDeque::new(); - loop { - // latest one minute - if queue.len() == 31 { - queue.pop_front(); - } - if recent_valid.len() == 31 { - recent_valid.pop_front(); - } - // allow get value within one minute - if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 { - let sum: f64 = queue.iter().map(|f| f.to_owned()).sum(); - let avg = sum / (queue.len() as f64); - *CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now())); - } else { - *CPU_USAGE_ONE_MINUTE.lock().unwrap() = None; - } - if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) { - recent_valid.push_back(false); - continue; - } - if PdhGetFormattedCounterValue( - counter, - PDH_FMT_DOUBLE, - &mut _counter_type, - &mut counter_value, - ) != 0 - || counter_value.CStatus != 0 - { - recent_valid.push_back(false); - continue; - } - queue.push_back(counter_value.u.doubleValue().clone()); - recent_valid.push_back(true); - } - }; - use std::sync::Once; - static ONCE: Once = Once::new(); - ONCE.call_once(|| { - std::thread::spawn(f); - }); -} - -pub fn cpu_uage_one_minute() -> Option { - let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone(); - if let Some((v, instant)) = v { - if instant.elapsed().as_secs() < 30 { - return Some(v); - } - } - None -} - -pub fn sync_cpu_usage(cpu_usage: Option) { - let v = match cpu_usage { - Some(cpu_usage) => Some((cpu_usage, Instant::now())), - None => None, - }; - *CPU_USAGE_ONE_MINUTE.lock().unwrap() = v; - log::info!("cpu usage synced: {:?}", cpu_usage); -} - -// https://learn.microsoft.com/en-us/windows/win32/sysinfo/targeting-your-application-at-windows-8-1 -// https://github.com/nodejs/node-convergence-archive/blob/e11fe0c2777561827cdb7207d46b0917ef3c42a7/deps/uv/src/win/util.c#L780 -pub fn is_windows_version_or_greater( - os_major: u32, - os_minor: u32, - build_number: u32, - service_pack_major: u32, - service_pack_minor: u32, -) -> bool { - let mut osvi: OSVERSIONINFOEXW = unsafe { std::mem::zeroed() }; - osvi.dwOSVersionInfoSize = std::mem::size_of::() as DWORD; - osvi.dwMajorVersion = os_major as _; - osvi.dwMinorVersion = os_minor as _; - osvi.dwBuildNumber = build_number as _; - osvi.wServicePackMajor = service_pack_major as _; - osvi.wServicePackMinor = service_pack_minor as _; - - let result = unsafe { - let mut condition_mask = 0; - let op = VER_GREATER_EQUAL; - condition_mask = VerSetConditionMask(condition_mask, VER_MAJORVERSION, op); - condition_mask = VerSetConditionMask(condition_mask, VER_MINORVERSION, op); - condition_mask = VerSetConditionMask(condition_mask, VER_BUILDNUMBER, op); - condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMAJOR, op); - condition_mask = VerSetConditionMask(condition_mask, VER_SERVICEPACKMINOR, op); - - VerifyVersionInfoW( - &mut osvi as *mut OSVERSIONINFOEXW, - VER_MAJORVERSION - | VER_MINORVERSION - | VER_BUILDNUMBER - | VER_SERVICEPACKMAJOR - | VER_SERVICEPACKMINOR, - condition_mask, - ) - }; - - result == TRUE -} diff --git a/libs/hbb_common/src/protos/mod.rs b/libs/hbb_common/src/protos/mod.rs deleted file mode 100644 index 57d9b68fe34..00000000000 --- a/libs/hbb_common/src/protos/mod.rs +++ /dev/null @@ -1 +0,0 @@ -include!(concat!(env!("OUT_DIR"), "/protos/mod.rs")); diff --git a/libs/hbb_common/src/proxy.rs b/libs/hbb_common/src/proxy.rs deleted file mode 100644 index 34d2c5109f5..00000000000 --- a/libs/hbb_common/src/proxy.rs +++ /dev/null @@ -1,561 +0,0 @@ -use std::{ - io::Error as IoError, - net::{SocketAddr, ToSocketAddrs}, -}; - -use base64::{engine::general_purpose, Engine}; -use httparse::{Error as HttpParseError, Response, EMPTY_HEADER}; -use log::info; -use thiserror::Error as ThisError; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, BufStream}; -#[cfg(any(target_os = "windows", target_os = "macos"))] -use tokio_native_tls::{native_tls, TlsConnector, TlsStream}; -#[cfg(not(any(target_os = "windows", target_os = "macos")))] -use tokio_rustls::{client::TlsStream, TlsConnector}; -use tokio_socks::{tcp::Socks5Stream, IntoTargetAddr}; -use tokio_util::codec::Framed; -use url::Url; - -use crate::{ - bytes_codec::BytesCodec, - config::Socks5Server, - tcp::{DynTcpStream, FramedStream}, - ResultType, -}; - -#[derive(Debug, ThisError)] -pub enum ProxyError { - #[error("IO Error: {0}")] - IoError(#[from] IoError), - #[error("Target parse error: {0}")] - TargetParseError(String), - #[error("HTTP parse error: {0}")] - HttpParseError(#[from] HttpParseError), - #[error("The maximum response header length is exceeded: {0}")] - MaximumResponseHeaderLengthExceeded(usize), - #[error("The end of file is reached")] - EndOfFile, - #[error("The url is error: {0}")] - UrlBadScheme(String), - #[error("The url parse error: {0}")] - UrlParseScheme(#[from] url::ParseError), - #[error("No HTTP code was found in the response")] - NoHttpCode, - #[error("The HTTP code is not equal 200: {0}")] - HttpCode200(u16), - #[error("The proxy address resolution failed: {0}")] - AddressResolutionFailed(String), - #[cfg(any(target_os = "windows", target_os = "macos"))] - #[error("The native tls error: {0}")] - NativeTlsError(#[from] tokio_native_tls::native_tls::Error), -} - -const MAXIMUM_RESPONSE_HEADER_LENGTH: usize = 4096; -/// The maximum HTTP Headers, which can be parsed. -const MAXIMUM_RESPONSE_HEADERS: usize = 16; -const DEFINE_TIME_OUT: u64 = 600; - -pub trait IntoUrl { - - // Besides parsing as a valid `Url`, the `Url` must be a valid - // `http::Uri`, in that it makes sense to use in a network request. - fn into_url(self) -> Result; - - fn as_str(&self) -> &str; -} - -impl IntoUrl for Url { - fn into_url(self) -> Result { - if self.has_host() { - Ok(self) - } else { - Err(ProxyError::UrlBadScheme(self.to_string())) - } - } - - fn as_str(&self) -> &str { - self.as_ref() - } -} - -impl<'a> IntoUrl for &'a str { - fn into_url(self) -> Result { - Url::parse(self) - .map_err(ProxyError::UrlParseScheme)? - .into_url() - } - - fn as_str(&self) -> &str { - self - } -} - -impl<'a> IntoUrl for &'a String { - fn into_url(self) -> Result { - (&**self).into_url() - } - - fn as_str(&self) -> &str { - self.as_ref() - } -} - -impl<'a> IntoUrl for String { - fn into_url(self) -> Result { - (&*self).into_url() - } - - fn as_str(&self) -> &str { - self.as_ref() - } -} - -#[derive(Clone)] -pub struct Auth { - user_name: String, - password: String, -} - -impl Auth { - fn get_proxy_authorization(&self) -> String { - format!( - "Proxy-Authorization: Basic {}\r\n", - self.get_basic_authorization() - ) - } - - pub fn get_basic_authorization(&self) -> String { - let authorization = format!("{}:{}", &self.user_name, &self.password); - general_purpose::STANDARD.encode(authorization.as_bytes()) - } -} - -#[derive(Clone)] -pub enum ProxyScheme { - Http { - auth: Option, - host: String, - }, - Https { - auth: Option, - host: String, - }, - Socks5 { - addr: SocketAddr, - auth: Option, - remote_dns: bool, - }, -} - -impl ProxyScheme { - pub fn maybe_auth(&self) -> Option<&Auth> { - match self { - ProxyScheme::Http { auth, .. } - | ProxyScheme::Https { auth, .. } - | ProxyScheme::Socks5 { auth, .. } => auth.as_ref(), - } - } - - fn socks5(addr: SocketAddr) -> Result { - Ok(ProxyScheme::Socks5 { - addr, - auth: None, - remote_dns: false, - }) - } - - fn http(host: &str) -> Result { - Ok(ProxyScheme::Http { - auth: None, - host: host.to_string(), - }) - } - fn https(host: &str) -> Result { - Ok(ProxyScheme::Https { - auth: None, - host: host.to_string(), - }) - } - - fn set_basic_auth, U: Into>(&mut self, username: T, password: U) { - let auth = Auth { - user_name: username.into(), - password: password.into(), - }; - match self { - ProxyScheme::Http { auth: a, .. } => *a = Some(auth), - ProxyScheme::Https { auth: a, .. } => *a = Some(auth), - ProxyScheme::Socks5 { auth: a, .. } => *a = Some(auth), - } - } - - fn parse(url: Url) -> Result { - use url::Position; - - // Resolve URL to a host and port - let to_addr = || { - let addrs = url.socket_addrs(|| match url.scheme() { - "socks5" => Some(1080), - _ => None, - })?; - addrs - .into_iter() - .next() - .ok_or_else(|| ProxyError::UrlParseScheme(url::ParseError::EmptyHost)) - }; - - let mut scheme: Self = match url.scheme() { - "http" => Self::http(&url[Position::BeforeHost..Position::AfterPort])?, - "https" => Self::https(&url[Position::BeforeHost..Position::AfterPort])?, - "socks5" => Self::socks5(to_addr()?)?, - e => return Err(ProxyError::UrlBadScheme(e.to_string())), - }; - - if let Some(pwd) = url.password() { - let username = url.username(); - scheme.set_basic_auth(username, pwd); - } - - Ok(scheme) - } - pub async fn socket_addrs(&self) -> Result { - info!("Resolving socket address"); - match self { - ProxyScheme::Http { host, .. } => self.resolve_host(host, 80).await, - ProxyScheme::Https { host, .. } => self.resolve_host(host, 443).await, - ProxyScheme::Socks5 { addr, .. } => Ok(addr.clone()), - } - } - - async fn resolve_host(&self, host: &str, default_port: u16) -> Result { - let (host_str, port) = match host.split_once(':') { - Some((h, p)) => (h, p.parse::().ok()), - None => (host, None), - }; - let addr = (host_str, port.unwrap_or(default_port)) - .to_socket_addrs()? - .next() - .ok_or_else(|| ProxyError::AddressResolutionFailed(host.to_string()))?; - Ok(addr) - } - - pub fn get_domain(&self) -> Result { - match self { - ProxyScheme::Http { host, .. } | ProxyScheme::Https { host, .. } => { - let domain = host - .split(':') - .next() - .ok_or_else(|| ProxyError::AddressResolutionFailed(host.clone()))?; - Ok(domain.to_string()) - } - ProxyScheme::Socks5 { addr, .. } => match addr { - SocketAddr::V4(addr_v4) => Ok(addr_v4.ip().to_string()), - SocketAddr::V6(addr_v6) => Ok(addr_v6.ip().to_string()), - }, - } - } - pub fn get_host_and_port(&self) -> Result { - match self { - ProxyScheme::Http { host, .. } => Ok(self.append_default_port(host, 80)), - ProxyScheme::Https { host, .. } => Ok(self.append_default_port(host, 443)), - ProxyScheme::Socks5 { addr, .. } => Ok(format!("{}", addr)), - } - } - fn append_default_port(&self, host: &str, default_port: u16) -> String { - if host.contains(':') { - host.to_string() - } else { - format!("{}:{}", host, default_port) - } - } -} - -pub trait IntoProxyScheme { - fn into_proxy_scheme(self) -> Result; -} - -impl IntoProxyScheme for S { - fn into_proxy_scheme(self) -> Result { - // validate the URL - let url = match self.as_str().into_url() { - Ok(ok) => ok, - Err(e) => { - match e { - // If the string does not contain protocol headers, try to parse it using the socks5 protocol - ProxyError::UrlParseScheme(_source) => { - let try_this = format!("socks5://{}", self.as_str()); - try_this.into_url()? - } - _ => { - return Err(e); - } - } - } - }; - ProxyScheme::parse(url) - } -} - -impl IntoProxyScheme for ProxyScheme { - fn into_proxy_scheme(self) -> Result { - Ok(self) - } -} - -#[derive(Clone)] -pub struct Proxy { - pub intercept: ProxyScheme, - ms_timeout: u64, -} - -impl Proxy { - pub fn new(proxy_scheme: U, ms_timeout: u64) -> Result { - Ok(Self { - intercept: proxy_scheme.into_proxy_scheme()?, - ms_timeout, - }) - } - - pub fn is_http_or_https(&self) -> bool { - return match self.intercept { - ProxyScheme::Socks5 { .. } => false, - _ => true, - }; - } - - pub fn from_conf(conf: &Socks5Server, ms_timeout: Option) -> Result { - let mut proxy; - match ms_timeout { - None => { - proxy = Self::new(&conf.proxy, DEFINE_TIME_OUT)?; - } - Some(time_out) => { - proxy = Self::new(&conf.proxy, time_out)?; - } - } - - if !conf.password.is_empty() && !conf.username.is_empty() { - proxy = proxy.basic_auth(&conf.username, &conf.password); - } - Ok(proxy) - } - - pub async fn proxy_addrs(&self) -> Result { - self.intercept.socket_addrs().await - } - - fn basic_auth(mut self, username: &str, password: &str) -> Proxy { - self.intercept.set_basic_auth(username, password); - self - } - - pub async fn connect<'t, T>( - self, - target: T, - local_addr: Option, - ) -> ResultType - where - T: IntoTargetAddr<'t>, - { - info!("Connect to proxy server"); - let proxy = self.proxy_addrs().await?; - - let local = if let Some(addr) = local_addr { - addr - } else { - crate::config::Config::get_any_listen_addr(proxy.is_ipv4()) - }; - - let stream = super::timeout( - self.ms_timeout, - crate::tcp::new_socket(local, true)?.connect(proxy), - ) - .await??; - stream.set_nodelay(true).ok(); - - let addr = stream.local_addr()?; - - return match self.intercept { - ProxyScheme::Http { .. } => { - info!("Connect to remote http proxy server: {}", proxy); - let stream = - super::timeout(self.ms_timeout, self.http_connect(stream, target)).await??; - Ok(FramedStream( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )) - } - ProxyScheme::Https { .. } => { - info!("Connect to remote https proxy server: {}", proxy); - let stream = - super::timeout(self.ms_timeout, self.https_connect(stream, target)).await??; - Ok(FramedStream( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )) - } - ProxyScheme::Socks5 { .. } => { - info!("Connect to remote socket5 proxy server: {}", proxy); - let stream = if let Some(auth) = self.intercept.maybe_auth() { - super::timeout( - self.ms_timeout, - Socks5Stream::connect_with_password_and_socket( - stream, - target, - &auth.user_name, - &auth.password, - ), - ) - .await?? - } else { - super::timeout( - self.ms_timeout, - Socks5Stream::connect_with_socket(stream, target), - ) - .await?? - }; - Ok(FramedStream( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - )) - } - }; - } - - #[cfg(any(target_os = "windows", target_os = "macos"))] - pub async fn https_connect<'a, Input, T>( - self, - io: Input, - target: T, - ) -> Result>, ProxyError> - where - Input: AsyncRead + AsyncWrite + Unpin, - T: IntoTargetAddr<'a>, - { - let tls_connector = TlsConnector::from(native_tls::TlsConnector::new()?); - let stream = tls_connector - .connect(&self.intercept.get_domain()?, io) - .await?; - self.http_connect(stream, target).await - } - - #[cfg(not(any(target_os = "windows", target_os = "macos")))] - pub async fn https_connect<'a, Input, T>( - self, - io: Input, - target: T, - ) -> Result>, ProxyError> - where - Input: AsyncRead + AsyncWrite + Unpin, - T: IntoTargetAddr<'a>, - { - use std::convert::TryFrom; - let verifier = rustls_platform_verifier::tls_config(); - let url_domain = self.intercept.get_domain()?; - - let domain = rustls_pki_types::ServerName::try_from(url_domain.as_str()) - .map_err(|e| ProxyError::AddressResolutionFailed(e.to_string()))? - .to_owned(); - - let tls_connector = TlsConnector::from(std::sync::Arc::new(verifier)); - let stream = tls_connector.connect(domain, io).await?; - self.http_connect(stream, target).await - } - - pub async fn http_connect<'a, Input, T>( - self, - io: Input, - target: T, - ) -> Result, ProxyError> - where - Input: AsyncRead + AsyncWrite + Unpin, - T: IntoTargetAddr<'a>, - { - let mut stream = BufStream::new(io); - let (domain, port) = get_domain_and_port(target)?; - - let request = self.make_request(&domain, port); - stream.write_all(request.as_bytes()).await?; - stream.flush().await?; - recv_and_check_response(&mut stream).await?; - Ok(stream) - } - - fn make_request(&self, host: &str, port: u16) -> String { - let mut request = format!( - "CONNECT {host}:{port} HTTP/1.1\r\nHost: {host}:{port}\r\n", - host = host, - port = port - ); - - if let Some(auth) = self.intercept.maybe_auth() { - request = format!("{}{}", request, auth.get_proxy_authorization()); - } - - request.push_str("\r\n"); - request - } -} - -fn get_domain_and_port<'a, T: IntoTargetAddr<'a>>(target: T) -> Result<(String, u16), ProxyError> { - let target_addr = target - .into_target_addr() - .map_err(|e| ProxyError::TargetParseError(e.to_string()))?; - match target_addr { - tokio_socks::TargetAddr::Ip(addr) => Ok((addr.ip().to_string(), addr.port())), - tokio_socks::TargetAddr::Domain(name, port) => Ok((name.to_string(), port)), - } -} - -async fn get_response(stream: &mut BufStream) -> Result -where - IO: AsyncRead + AsyncWrite + Unpin, -{ - use tokio::io::AsyncBufReadExt; - let mut response = String::new(); - - loop { - if stream.read_line(&mut response).await? == 0 { - return Err(ProxyError::EndOfFile); - } - - if MAXIMUM_RESPONSE_HEADER_LENGTH < response.len() { - return Err(ProxyError::MaximumResponseHeaderLengthExceeded( - response.len(), - )); - } - - if response.ends_with("\r\n\r\n") { - return Ok(response); - } - } -} - -async fn recv_and_check_response(stream: &mut BufStream) -> Result<(), ProxyError> -where - IO: AsyncRead + AsyncWrite + Unpin, -{ - let response_string = get_response(stream).await?; - - let mut response_headers = [EMPTY_HEADER; MAXIMUM_RESPONSE_HEADERS]; - let mut response = Response::new(&mut response_headers); - let response_bytes = response_string.into_bytes(); - response.parse(&response_bytes)?; - - return match response.code { - Some(code) => { - if code == 200 { - Ok(()) - } else { - Err(ProxyError::HttpCode200(code)) - } - } - None => Err(ProxyError::NoHttpCode), - }; -} diff --git a/libs/hbb_common/src/socket_client.rs b/libs/hbb_common/src/socket_client.rs deleted file mode 100644 index 4cb0bf204b5..00000000000 --- a/libs/hbb_common/src/socket_client.rs +++ /dev/null @@ -1,291 +0,0 @@ -use crate::{ - config::{Config, NetworkType}, - tcp::FramedStream, - udp::FramedSocket, - ResultType, -}; -use anyhow::Context; -use std::net::SocketAddr; -use tokio::net::ToSocketAddrs; -use tokio_socks::{IntoTargetAddr, TargetAddr}; - -#[inline] -pub fn check_port(host: T, port: i32) -> String { - let host = host.to_string(); - if crate::is_ipv6_str(&host) { - if host.starts_with('[') { - return host; - } - return format!("[{host}]:{port}"); - } - if !host.contains(':') { - return format!("{host}:{port}"); - } - host -} - -#[inline] -pub fn increase_port(host: T, offset: i32) -> String { - let host = host.to_string(); - if crate::is_ipv6_str(&host) { - if host.starts_with('[') { - let tmp: Vec<&str> = host.split("]:").collect(); - if tmp.len() == 2 { - let port: i32 = tmp[1].parse().unwrap_or(0); - if port > 0 { - return format!("{}]:{}", tmp[0], port + offset); - } - } - } - } else if host.contains(':') { - let tmp: Vec<&str> = host.split(':').collect(); - if tmp.len() == 2 { - let port: i32 = tmp[1].parse().unwrap_or(0); - if port > 0 { - return format!("{}:{}", tmp[0], port + offset); - } - } - } - host -} - -pub fn test_if_valid_server(host: &str, test_with_proxy: bool) -> String { - let host = check_port(host, 0); - use std::net::ToSocketAddrs; - - if test_with_proxy && NetworkType::ProxySocks == Config::get_network_type() { - test_if_valid_server_for_proxy_(&host) - } else { - match host.to_socket_addrs() { - Err(err) => err.to_string(), - Ok(_) => "".to_owned(), - } - } -} - -#[inline] -pub fn test_if_valid_server_for_proxy_(host: &str) -> String { - // `&host.into_target_addr()` is defined in `tokio-socs`, but is a common pattern for testing, - // it can be used for both `socks` and `http` proxy. - match &host.into_target_addr() { - Err(err) => err.to_string(), - Ok(_) => "".to_owned(), - } -} - -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, - ms_timeout: u64, -) -> ResultType { - 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() { - return FramedStream::connect(target, local, &conf, ms_timeout).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 FramedStream::new(target, Some(local), ms_timeout).await; - } - } - } - FramedStream::new(target, local, ms_timeout).await -} - -#[inline] -pub fn is_ipv4(target: &TargetAddr<'_>) -> bool { - match target { - TargetAddr::Ip(addr) => addr.is_ipv4(), - _ => true, - } -} - -#[inline] -pub async fn query_nip_io(addr: &SocketAddr) -> ResultType { - tokio::net::lookup_host(format!("{}.nip.io:{}", addr.ip(), addr.port())) - .await? - .find(|x| x.is_ipv6()) - .context("Failed to get ipv6 from nip.io") -} - -#[inline] -pub fn ipv4_to_ipv6(addr: String, ipv4: bool) -> String { - if !ipv4 && crate::is_ipv4_str(&addr) { - if let Some(ip) = addr.split(':').next() { - return addr.replace(ip, &format!("{ip}.nip.io")); - } - } - 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) => { - let socket = FramedSocket::new_proxy( - conf.proxy.as_str(), - local, - conf.username.as_str(), - conf.password.as_str(), - ms_timeout, - ) - .await?; - Ok(socket) - } - } -} - -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", false).is_empty()); - // on Linux, "1" is resolved to "0.0.0.1" - assert!(test_if_valid_server("1.1.1.1", false).is_empty()); - assert!(test_if_valid_server("1.1.1.1:1", false).is_empty()); - assert!(test_if_valid_server("microsoft.com", false).is_empty()); - assert!(test_if_valid_server("microsoft.com:1", false).is_empty()); - - // with proxy - // `:0` indicates `let host = check_port(host, 0);` is called. - assert!(test_if_valid_server_for_proxy_("a:0").is_empty()); - assert!(test_if_valid_server_for_proxy_("1.1.1.1:0").is_empty()); - assert!(test_if_valid_server_for_proxy_("1.1.1.1:1").is_empty()); - assert!(test_if_valid_server_for_proxy_("abc.com:0").is_empty()); - assert!(test_if_valid_server_for_proxy_("abcd.com:1").is_empty()); - } - - #[test] - 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 deleted file mode 100644 index 17f360ff90c..00000000000 --- a/libs/hbb_common/src/tcp.rs +++ /dev/null @@ -1,341 +0,0 @@ -use crate::{bail, bytes_codec::BytesCodec, ResultType, config::Socks5Server, proxy::Proxy}; -use anyhow::Context as AnyhowCtx; -use bytes::{BufMut, Bytes, BytesMut}; -use futures::{SinkExt, StreamExt}; -use protobuf::Message; -use sodiumoxide::crypto::{ - box_, - secretbox::{self, Key, Nonce}, -}; -use std::{ - io::{self, Error, ErrorKind}, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - ops::{Deref, DerefMut}, - pin::Pin, - task::{Context, Poll}, -}; -use tokio::{ - io::{AsyncRead, AsyncWrite, ReadBuf}, - net::{lookup_host, TcpListener, TcpSocket, ToSocketAddrs}, -}; -use tokio_socks::IntoTargetAddr; -use tokio_util::codec::Framed; - -pub trait TcpStreamTrait: AsyncRead + AsyncWrite + Unpin {} -pub struct DynTcpStream(pub(crate) Box); - -#[derive(Clone)] -pub struct Encrypt(Key, u64, u64); - -pub struct FramedStream( - pub(crate) Framed, - pub(crate) SocketAddr, - pub(crate) Option, - pub(crate) u64, -); - -impl Deref for FramedStream { - type Target = Framed; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for FramedStream { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Deref for DynTcpStream { - type Target = Box; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for DynTcpStream { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -pub(crate) fn new_socket(addr: std::net::SocketAddr, reuse: bool) -> Result { - let socket = match addr { - std::net::SocketAddr::V4(..) => TcpSocket::new_v4()?, - std::net::SocketAddr::V6(..) => TcpSocket::new_v6()?, - }; - if reuse { - // windows has no reuse_port, but it's reuse_address - // almost equals to unix's reuse_port + reuse_address, - // though may introduce nondeterministic behavior - #[cfg(unix)] - socket.set_reuseport(true).ok(); - socket.set_reuseaddr(true).ok(); - } - socket.bind(addr)?; - Ok(socket) -} - -impl FramedStream { - pub async fn new( - remote_addr: T, - local_addr: Option, - ms_timeout: u64, - ) -> ResultType { - 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!(format!("Failed to connect to {remote_addr}")); - } - - pub async fn connect<'t, T>( - target: T, - local_addr: Option, - proxy_conf: &Socks5Server, - ms_timeout: u64, - ) -> ResultType - where - T: IntoTargetAddr<'t>, - { - let proxy = Proxy::from_conf(proxy_conf, Some(ms_timeout))?; - proxy.connect::(target, local_addr).await - } - - pub fn local_addr(&self) -> SocketAddr { - self.1 - } - - pub fn set_send_timeout(&mut self, ms: u64) { - self.3 = ms; - } - - pub fn from(stream: impl TcpStreamTrait + Send + Sync + 'static, addr: SocketAddr) -> Self { - Self( - Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), - addr, - None, - 0, - ) - } - - pub fn set_raw(&mut self) { - self.0.codec_mut().set_raw(); - self.2 = None; - } - - pub fn is_secured(&self) -> bool { - self.2.is_some() - } - - #[inline] - pub async fn send(&mut self, msg: &impl Message) -> ResultType<()> { - self.send_raw(msg.write_to_bytes()?).await - } - - #[inline] - pub async fn send_raw(&mut self, msg: Vec) -> ResultType<()> { - let mut msg = msg; - if let Some(key) = self.2.as_mut() { - msg = key.enc(&msg); - } - self.send_bytes(bytes::Bytes::from(msg)).await?; - Ok(()) - } - - #[inline] - pub async fn send_bytes(&mut self, bytes: Bytes) -> ResultType<()> { - if self.3 > 0 { - super::timeout(self.3, self.0.send(bytes)).await??; - } else { - self.0.send(bytes).await?; - } - Ok(()) - } - - #[inline] - pub async fn next(&mut self) -> Option> { - let mut res = self.0.next().await; - if let Some(Ok(bytes)) = res.as_mut() { - if let Some(key) = self.2.as_mut() { - if let Err(err) = key.dec(bytes) { - return Some(Err(err)); - } - } - } - res - } - - #[inline] - pub async fn next_timeout(&mut self, ms: u64) -> Option> { - if let Ok(res) = super::timeout(ms, self.next()).await { - res - } else { - None - } - } - - pub fn set_key(&mut self, key: Key) { - self.2 = Some(Encrypt::new(key)); - } - - fn get_nonce(seqnum: u64) -> Nonce { - let mut nonce = Nonce([0u8; secretbox::NONCEBYTES]); - nonce.0[..std::mem::size_of_val(&seqnum)].copy_from_slice(&seqnum.to_le_bytes()); - nonce - } -} - -const DEFAULT_BACKLOG: u32 = 128; - -pub async fn new_listener(addr: T, reuse: bool) -> ResultType { - if !reuse { - Ok(TcpListener::bind(addr).await?) - } else { - let addr = lookup_host(&addr) - .await? - .next() - .context("could not resolve to any address")?; - new_socket(addr, true)? - .listen(DEFAULT_BACKLOG) - .map_err(anyhow::Error::msg) - } -} - -pub async fn listen_any(port: u16) -> ResultType { - if let Ok(mut socket) = TcpSocket::new_v6() { - #[cfg(unix)] - { - socket.set_reuseport(true).ok(); - socket.set_reuseaddr(true).ok(); - 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); - } - } - } - Ok(new_socket( - SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port), - true, - )? - .listen(DEFAULT_BACKLOG)?) -} - -impl Unpin for DynTcpStream {} - -impl AsyncRead for DynTcpStream { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - AsyncRead::poll_read(Pin::new(&mut self.0), cx, buf) - } -} - -impl AsyncWrite for DynTcpStream { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - AsyncWrite::poll_write(Pin::new(&mut self.0), cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - AsyncWrite::poll_flush(Pin::new(&mut self.0), cx) - } - - fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - AsyncWrite::poll_shutdown(Pin::new(&mut self.0), cx) - } -} - -impl TcpStreamTrait for R {} - -impl Encrypt { - pub fn new(key: Key) -> Self { - Self(key, 0, 0) - } - - pub fn dec(&mut self, bytes: &mut BytesMut) -> Result<(), Error> { - if bytes.len() <= 1 { - return Ok(()); - } - self.2 += 1; - let nonce = FramedStream::get_nonce(self.2); - match secretbox::open(bytes, &nonce, &self.0) { - Ok(res) => { - bytes.clear(); - bytes.put_slice(&res); - Ok(()) - } - Err(()) => Err(Error::new(ErrorKind::Other, "decryption error")), - } - } - - pub fn enc(&mut self, data: &[u8]) -> Vec { - self.1 += 1; - let nonce = FramedStream::get_nonce(self.1); - secretbox::seal(&data, &nonce, &self.0) - } - - pub fn decode( - symmetric_data: &[u8], - their_pk_b: &[u8], - our_sk_b: &box_::SecretKey, - ) -> ResultType { - if their_pk_b.len() != box_::PUBLICKEYBYTES { - anyhow::bail!("Handshake failed: pk length {}", their_pk_b.len()); - } - let nonce = box_::Nonce([0u8; box_::NONCEBYTES]); - let mut pk_ = [0u8; box_::PUBLICKEYBYTES]; - pk_[..].copy_from_slice(their_pk_b); - let their_pk_b = box_::PublicKey(pk_); - let symmetric_key = box_::open(symmetric_data, &nonce, &their_pk_b, &our_sk_b) - .map_err(|_| anyhow::anyhow!("Handshake failed: box decryption failure"))?; - if symmetric_key.len() != secretbox::KEYBYTES { - anyhow::bail!("Handshake failed: invalid secret key length from peer"); - } - let mut key = [0u8; secretbox::KEYBYTES]; - key[..].copy_from_slice(&symmetric_key); - Ok(Key(key)) - } -} diff --git a/libs/hbb_common/src/udp.rs b/libs/hbb_common/src/udp.rs deleted file mode 100644 index 68abd42df9a..00000000000 --- a/libs/hbb_common/src/udp.rs +++ /dev/null @@ -1,170 +0,0 @@ -use crate::ResultType; -use anyhow::{anyhow, Context}; -use bytes::{Bytes, BytesMut}; -use futures::{SinkExt, StreamExt}; -use protobuf::Message; -use socket2::{Domain, Socket, Type}; -use std::net::SocketAddr; -use tokio::net::{lookup_host, ToSocketAddrs, UdpSocket}; -use tokio_socks::{udp::Socks5UdpFramed, IntoTargetAddr, TargetAddr, ToProxyAddrs}; -use tokio_util::{codec::BytesCodec, udp::UdpFramed}; - -pub enum FramedSocket { - Direct(UdpFramed), - ProxySocks(Socks5UdpFramed), -} - -fn new_socket(addr: SocketAddr, reuse: bool, buf_size: usize) -> Result { - let socket = match addr { - SocketAddr::V4(..) => Socket::new(Domain::ipv4(), Type::dgram(), None), - SocketAddr::V6(..) => Socket::new(Domain::ipv6(), Type::dgram(), None), - }?; - if reuse { - // windows has no reuse_port, but it's reuse_address - // almost equals to unix's reuse_port + reuse_address, - // though may introduce nondeterministic behavior - #[cfg(unix)] - socket.set_reuse_port(true).ok(); - socket.set_reuse_address(true).ok(); - } - // only nonblocking work with tokio, https://stackoverflow.com/questions/64649405/receiver-on-tokiompscchannel-only-receives-messages-when-buffer-is-full - socket.set_nonblocking(true)?; - if buf_size > 0 { - socket.set_recv_buffer_size(buf_size).ok(); - } - log::debug!( - "Receive buf size of udp {}: {:?}", - addr, - socket.recv_buffer_size() - ); - if addr.is_ipv6() && addr.ip().is_unspecified() && addr.port() > 0 { - socket.set_only_v6(false).ok(); - } - socket.bind(&addr.into())?; - Ok(socket) -} - -impl FramedSocket { - pub async fn new(addr: T) -> ResultType { - Self::new_reuse(addr, false, 0).await - } - - pub async fn new_reuse( - addr: T, - reuse: bool, - buf_size: usize, - ) -> ResultType { - let addr = lookup_host(&addr) - .await? - .next() - .context("could not resolve to any address")?; - Ok(Self::Direct(UdpFramed::new( - UdpSocket::from_std(new_socket(addr, reuse, buf_size)?.into_udp_socket())?, - BytesCodec::new(), - ))) - } - - pub async fn new_proxy<'a, 't, P: ToProxyAddrs, T: ToSocketAddrs>( - proxy: P, - local: T, - username: &'a str, - password: &'a str, - ms_timeout: u64, - ) -> ResultType { - let framed = if username.trim().is_empty() { - super::timeout(ms_timeout, Socks5UdpFramed::connect(proxy, Some(local))).await?? - } else { - super::timeout( - ms_timeout, - Socks5UdpFramed::connect_with_password(proxy, Some(local), username, password), - ) - .await?? - }; - log::trace!( - "Socks5 udp connected, local addr: {:?}, target addr: {}", - framed.local_addr(), - framed.socks_addr() - ); - Ok(Self::ProxySocks(framed)) - } - - #[inline] - pub async fn send( - &mut self, - msg: &impl Message, - addr: impl IntoTargetAddr<'_>, - ) -> ResultType<()> { - let addr = addr.into_target_addr()?.to_owned(); - let send_data = Bytes::from(msg.write_to_bytes()?); - match self { - Self::Direct(f) => { - if let TargetAddr::Ip(addr) = addr { - f.send((send_data, addr)).await? - } - } - Self::ProxySocks(f) => f.send((send_data, addr)).await?, - }; - Ok(()) - } - - // https://stackoverflow.com/a/68733302/1926020 - #[inline] - pub async fn send_raw( - &mut self, - msg: &'static [u8], - addr: impl IntoTargetAddr<'static>, - ) -> ResultType<()> { - let addr = addr.into_target_addr()?.to_owned(); - - match self { - Self::Direct(f) => { - if let TargetAddr::Ip(addr) = addr { - f.send((Bytes::from(msg), addr)).await? - } - } - Self::ProxySocks(f) => f.send((Bytes::from(msg), addr)).await?, - }; - Ok(()) - } - - #[inline] - pub async fn next(&mut self) -> Option)>> { - match self { - Self::Direct(f) => match f.next().await { - Some(Ok((data, addr))) => { - Some(Ok((data, addr.into_target_addr().ok()?.to_owned()))) - } - Some(Err(e)) => Some(Err(anyhow!(e))), - None => None, - }, - Self::ProxySocks(f) => match f.next().await { - Some(Ok((data, _))) => Some(Ok((data.data, data.dst_addr))), - Some(Err(e)) => Some(Err(anyhow!(e))), - None => None, - }, - } - } - - #[inline] - pub async fn next_timeout( - &mut self, - ms: u64, - ) -> Option)>> { - if let Ok(res) = - tokio::time::timeout(std::time::Duration::from_millis(ms), self.next()).await - { - res - } else { - None - } - } - - pub fn local_addr(&self) -> Option { - if let FramedSocket::Direct(x) = self { - if let Ok(v) = x.get_ref().local_addr() { - return Some(v); - } - } - None - } -} diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index b9c4447a213..8802ab306da 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rustdesk-portable-packer" -version = "1.3.5" +version = "1.4.1" edition = "2021" description = "RustDesk Remote Desktop" @@ -18,7 +18,7 @@ winapi = { version = "0.3", features = ["winbase"] } native-windows-gui = {version = "1.0", default-features = false, features = ["animation-timer", "image-decoder"]} [package.metadata.winres] -LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved." +LegalCopyright = "Copyright © 2025 Purslane Ltd. All rights reserved." ProductName = "RustDesk" OriginalFilename = "rustdesk.exe" FileDescription = "RustDesk Remote Desktop" diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs index ced5baf327f..9effbc5893b 100644 --- a/libs/portable/src/bin_reader.rs +++ b/libs/portable/src/bin_reader.rs @@ -1,7 +1,7 @@ use std::{ fs::{self}, io::{Cursor, Read}, - path::PathBuf, + path::Path, }; #[cfg(windows)] @@ -42,7 +42,7 @@ impl BinaryData { buf } - pub fn write_to_file(&self, prefix: &PathBuf) { + pub fn write_to_file(&self, prefix: &Path) { let p = prefix.join(&self.path); if let Some(parent) = p.parent() { if !parent.exists() { @@ -122,7 +122,7 @@ impl BinaryReader { } #[cfg(linux)] - pub fn configure_permission(&self, prefix: &PathBuf) { + pub fn configure_permission(&self, prefix: &Path) { use std::os::unix::prelude::PermissionsExt; let exe_path = prefix.join(&self.exe); diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index 7b68d821c81..87d4897c2d5 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -1,7 +1,7 @@ #![windows_subsystem = "windows"] use std::{ - path::PathBuf, + path::{Path, PathBuf}, process::{Command, Stdio}, }; @@ -22,7 +22,7 @@ const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME"; #[cfg(windows)] const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW"; -fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool { +fn is_timestamp_matches(dir: &Path, ts: &mut u64) -> bool { let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else { return true; }; @@ -50,7 +50,7 @@ fn is_timestamp_matches(dir: &PathBuf, ts: &mut u64) -> bool { false } -fn write_meta(dir: &PathBuf, ts: u64) { +fn write_meta(dir: &Path, ts: u64) { let meta_file = dir.join(APP_METADATA_CONFIG); if ts != 0 { let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts); @@ -169,13 +169,13 @@ fn main() { #[cfg(windows)] mod windows { - use std::{fs, os::windows::process::CommandExt, path::PathBuf, process::Command}; + use std::{fs, os::windows::process::CommandExt, path::Path, process::Command}; // Used for privacy mode(magnifier impl). pub const RUNTIME_BROKER_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe"; pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe"; - pub(super) fn copy_runtime_broker(dir: &PathBuf) { + pub(super) fn copy_runtime_broker(dir: &Path) { let src = RUNTIME_BROKER_EXE; let tgt = WIN_TOPMOST_INJECTED_PROCESS_EXE; let target_file = dir.join(tgt); diff --git a/libs/remote_printer/Cargo.toml b/libs/remote_printer/Cargo.toml new file mode 100644 index 00000000000..30f8aff33cd --- /dev/null +++ b/libs/remote_printer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "remote_printer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[target.'cfg(target_os = "windows")'.dependencies] +hbb_common = { version = "0.1.0", path = "../hbb_common" } +winapi = { version = "0.3" } +windows-strings = "0.3.1" diff --git a/libs/remote_printer/src/lib.rs b/libs/remote_printer/src/lib.rs new file mode 100644 index 00000000000..51ee3721a04 --- /dev/null +++ b/libs/remote_printer/src/lib.rs @@ -0,0 +1,34 @@ +#[cfg(target_os = "windows")] +mod setup; +#[cfg(target_os = "windows")] +pub use setup::{ + is_rd_printer_installed, + setup::{install_update_printer, uninstall_printer}, +}; + +#[cfg(target_os = "windows")] +const RD_DRIVER_INF_PATH: &str = "drivers/RustDeskPrinterDriver/RustDeskPrinterDriver.inf"; + +#[cfg(target_os = "windows")] +fn get_printer_name(app_name: &str) -> Vec { + format!("{} Printer", app_name) + .encode_utf16() + .chain(Some(0)) + .collect() +} + +#[cfg(target_os = "windows")] +fn get_driver_name() -> Vec { + "RustDesk v4 Printer Driver" + .encode_utf16() + .chain(Some(0)) + .collect() +} + +#[cfg(target_os = "windows")] +fn get_port_name(app_name: &str) -> Vec { + format!("{} Printer", app_name) + .encode_utf16() + .chain(Some(0)) + .collect() +} diff --git a/libs/remote_printer/src/setup/driver.rs b/libs/remote_printer/src/setup/driver.rs new file mode 100644 index 00000000000..81226c5cc17 --- /dev/null +++ b/libs/remote_printer/src/setup/driver.rs @@ -0,0 +1,202 @@ +use super::{common_enum, get_wstr_bytes, is_name_equal}; +use hbb_common::{bail, log, ResultType}; +use std::{io, ptr::null_mut, time::Duration}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD, MAX_PATH}, + ntdef::{DWORDLONG, LPCWSTR}, + winerror::{ERROR_UNKNOWN_PRINTER_DRIVER, S_OK}, + }, + um::{ + winspool::{ + DeletePrinterDriverExW, DeletePrinterDriverPackageW, EnumPrinterDriversW, + InstallPrinterDriverFromPackageW, UploadPrinterDriverPackageW, DPD_DELETE_ALL_FILES, + DRIVER_INFO_6W, DRIVER_INFO_8W, IPDFP_COPY_ALL_FILES, UPDP_SILENT_UPLOAD, + UPDP_UPLOAD_ALWAYS, + }, + winuser::GetForegroundWindow, + }, +}; +use windows_strings::PCWSTR; + +const HRESULT_ERR_ELEMENT_NOT_FOUND: u32 = 0x80070490; + +fn enum_printer_driver( + level: DWORD, + p_driver_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPrinterDriversW( + null_mut(), + null_mut(), + level, + p_driver_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +pub fn get_installed_driver_version(name: &PCWSTR) -> ResultType> { + common_enum( + "EnumPrinterDriversW", + enum_printer_driver, + 6, + |info: &DRIVER_INFO_6W| { + if is_name_equal(name, info.pName) { + Some(info.dwlDriverVersion) + } else { + None + } + }, + || None, + ) +} + +fn find_inf(name: &PCWSTR) -> ResultType> { + let r = common_enum( + "EnumPrinterDriversW", + enum_printer_driver, + 8, + |info: &DRIVER_INFO_8W| { + if is_name_equal(name, info.pName) { + Some(get_wstr_bytes(info.pszInfPath)) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(vec![])) +} + +fn delete_printer_driver(name: &PCWSTR) -> ResultType<()> { + unsafe { + // If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer. + // `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9). + // We can only ignore this error for now. + // Though restarting the spooler service is a solution, it's not a good idea to restart the service. + // + // Deleting the printer driver after deleting the printer is a common practice. + // No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once. + // https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422 + // AnyDesk printer driver and the simplest printer driver also have the same issue. + if FALSE + == DeletePrinterDriverExW( + null_mut(), + null_mut(), + name.as_ptr() as _, + DPD_DELETE_ALL_FILES, + 0, + ) + { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_UNKNOWN_PRINTER_DRIVER as _) { + return Ok(()); + } else { + bail!("Failed to delete the printer driver, {}", err) + } + } + } + Ok(()) +} + +// https://github.com/dvalter/chromium-android-ext-dev/blob/dab74f7d5bc5a8adf303090ee25c611b4d54e2db/cloud_print/virtual_driver/win/install/setup.cc#L190 +fn delete_printer_driver_package(inf: Vec) -> ResultType<()> { + if inf.is_empty() { + return Ok(()); + } + let slen = if inf[inf.len() - 1] == 0 { + inf.len() - 1 + } else { + inf.len() + }; + let inf_path = String::from_utf16_lossy(&inf[..slen]); + if !std::path::Path::new(&inf_path).exists() { + return Ok(()); + } + + let mut retries = 3; + loop { + unsafe { + let res = DeletePrinterDriverPackageW(null_mut(), inf.as_ptr(), null_mut()); + if res == S_OK || res == HRESULT_ERR_ELEMENT_NOT_FOUND as i32 { + return Ok(()); + } + log::error!("Failed to delete the printer driver, result: {}", res); + } + retries -= 1; + if retries <= 0 { + bail!("Failed to delete the printer driver"); + } + std::thread::sleep(Duration::from_secs(2)); + } +} + +pub fn uninstall_driver(name: &PCWSTR) -> ResultType<()> { + // Note: inf must be found before `delete_printer_driver()`. + let inf = find_inf(name)?; + delete_printer_driver(name)?; + delete_printer_driver_package(inf) +} + +pub fn install_driver(name: &PCWSTR, inf: LPCWSTR) -> ResultType<()> { + let mut size = (MAX_PATH * 10) as u32; + let mut package_path = [0u16; MAX_PATH * 10]; + unsafe { + let mut res = UploadPrinterDriverPackageW( + null_mut(), + inf, + null_mut(), + UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS, + null_mut(), + package_path.as_mut_ptr(), + &mut size as _, + ); + if res != S_OK { + log::error!( + "Failed to upload the printer driver package to the driver cache silently, {}. Will try with user UI.", + res + ); + + res = UploadPrinterDriverPackageW( + null_mut(), + inf, + null_mut(), + UPDP_UPLOAD_ALWAYS, + GetForegroundWindow(), + package_path.as_mut_ptr(), + &mut size as _, + ); + if res != S_OK { + bail!( + "Failed to upload the printer driver package to the driver cache with UI, {}", + res + ); + } + } + + // https://learn.microsoft.com/en-us/windows/win32/printdocs/installprinterdriverfrompackage + res = InstallPrinterDriverFromPackageW( + null_mut(), + package_path.as_ptr(), + name.as_ptr(), + null_mut(), + IPDFP_COPY_ALL_FILES, + ); + if res != S_OK { + bail!("Failed to install the printer driver from package, {}", res); + } + } + + Ok(()) +} diff --git a/libs/remote_printer/src/setup/mod.rs b/libs/remote_printer/src/setup/mod.rs new file mode 100644 index 00000000000..ddf386de348 --- /dev/null +++ b/libs/remote_printer/src/setup/mod.rs @@ -0,0 +1,99 @@ +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + ntdef::{LPCWSTR, LPWSTR}, + }, + um::winbase::{lstrcmpiW, lstrlenW}, +}; +use windows_strings::PCWSTR; + +mod driver; +mod port; +pub(crate) mod printer; +pub(crate) mod setup; + +#[inline] +pub fn is_rd_printer_installed(app_name: &str) -> ResultType { + let printer_name = crate::get_printer_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + printer::is_printer_added(&rd_printer_name) +} + +fn get_wstr_bytes(p: LPWSTR) -> Vec { + let mut vec_bytes = vec![]; + unsafe { + let len: isize = lstrlenW(p) as _; + if len > 0 { + for i in 0..len + 1 { + vec_bytes.push(*p.offset(i)); + } + } + } + vec_bytes +} + +fn is_name_equal(name: &PCWSTR, name_from_api: LPCWSTR) -> bool { + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw + // For some locales, the lstrcmpi function may be insufficient. + // If this occurs, use `CompareStringEx` to ensure proper comparison. + // For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison. + // Note that specifying these values slows performance, so use them only when necessary. + // + // No need to consider `CompareStringEx` for now. + unsafe { lstrcmpiW(name.as_ptr(), name_from_api) == 0 } +} + +fn common_enum( + enum_name: &str, + enum_fn: fn( + Level: DWORD, + pDriverInfo: LPBYTE, + cbBuf: DWORD, + pcbNeeded: LPDWORD, + pcReturned: LPDWORD, + ) -> BOOL, + level: DWORD, + on_data: impl Fn(&T) -> Option, + on_no_data: impl Fn() -> Option, +) -> ResultType> { + let mut needed = 0; + let mut returned = 0; + enum_fn(level, null_mut(), 0, &mut needed, &mut returned); + if needed == 0 { + return Ok(on_no_data()); + } + + let mut buffer = vec![0u8; needed as usize]; + if FALSE + == enum_fn( + level, + buffer.as_mut_ptr(), + needed, + &mut needed, + &mut returned, + ) + { + bail!( + "Failed to call {}, error: {}", + enum_name, + io::Error::last_os_error() + ) + } + + // to-do: how to free the buffers in *const T? + + let p_enum_info = buffer.as_ptr() as *const T; + unsafe { + for i in 0..returned { + let enum_info = p_enum_info.offset(i as isize); + let r = on_data(&*enum_info); + if r.is_some() { + return Ok(r); + } + } + } + + Ok(on_no_data()) +} diff --git a/libs/remote_printer/src/setup/port.rs b/libs/remote_printer/src/setup/port.rs new file mode 100644 index 00000000000..d5ab0bace08 --- /dev/null +++ b/libs/remote_printer/src/setup/port.rs @@ -0,0 +1,128 @@ +use super::{common_enum, is_name_equal, printer::get_printer_installed_on_port}; +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + um::{ + winnt::HANDLE, + winspool::{ + ClosePrinter, EnumPortsW, OpenPrinterW, XcvDataW, PORT_INFO_2W, PRINTER_DEFAULTSW, + SERVER_WRITE, + }, + }, +}; +use windows_strings::{w, PCWSTR}; + +const XCV_MONITOR_LOCAL_PORT: PCWSTR = w!(",XcvMonitor Local Port"); + +fn enum_printer_port( + level: DWORD, + p_port_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPortsW( + null_mut(), + level, + p_port_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +fn is_port_exists(name: &PCWSTR) -> ResultType { + let r = common_enum( + "EnumPortsW", + enum_printer_port, + 2, + |info: &PORT_INFO_2W| { + if is_name_equal(name, info.pPortName) { + Some(true) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(false)) +} + +unsafe fn execute_on_local_port(port: &PCWSTR, command: &PCWSTR) -> ResultType<()> { + let mut dft = PRINTER_DEFAULTSW { + pDataType: null_mut(), + pDevMode: null_mut(), + DesiredAccess: SERVER_WRITE, + }; + let mut h_monitor: HANDLE = null_mut(); + if FALSE + == OpenPrinterW( + XCV_MONITOR_LOCAL_PORT.as_ptr() as _, + &mut h_monitor, + &mut dft as *mut PRINTER_DEFAULTSW as _, + ) + { + bail!(format!( + "Failed to open Local Port monitor. Error: {}", + io::Error::last_os_error() + )) + } + + let mut output_needed: u32 = 0; + let mut status: u32 = 0; + if FALSE + == XcvDataW( + h_monitor, + command.as_ptr(), + port.as_ptr() as *mut u8, + (port.len() + 1) as u32 * 2, + null_mut(), + 0, + &mut output_needed, + &mut status, + ) + { + ClosePrinter(h_monitor); + bail!(format!( + "Failed to execute the command on the printer port, Error: {}", + io::Error::last_os_error() + )) + } + + ClosePrinter(h_monitor); + + Ok(()) +} + +fn add_local_port(port: &PCWSTR) -> ResultType<()> { + unsafe { execute_on_local_port(port, &w!("AddPort")) } +} + +fn delete_local_port(port: &PCWSTR) -> ResultType<()> { + unsafe { execute_on_local_port(port, &w!("DeletePort")) } +} + +pub fn check_add_local_port(port: &PCWSTR) -> ResultType<()> { + if !is_port_exists(port)? { + return add_local_port(port); + } + Ok(()) +} + +pub fn check_delete_local_port(port: &PCWSTR) -> ResultType<()> { + if is_port_exists(port)? { + if get_printer_installed_on_port(port)?.is_some() { + bail!("The printer is installed on the port. Please remove the printer first."); + } + return delete_local_port(port); + } + Ok(()) +} diff --git a/libs/remote_printer/src/setup/printer.rs b/libs/remote_printer/src/setup/printer.rs new file mode 100644 index 00000000000..9882b8f38b9 --- /dev/null +++ b/libs/remote_printer/src/setup/printer.rs @@ -0,0 +1,161 @@ +use super::{common_enum, get_wstr_bytes, is_name_equal}; +use hbb_common::{bail, ResultType}; +use std::{io, ptr::null_mut}; +use winapi::{ + shared::{ + minwindef::{BOOL, DWORD, FALSE, LPBYTE, LPDWORD}, + ntdef::HANDLE, + winerror::ERROR_INVALID_PRINTER_NAME, + }, + um::winspool::{ + AddPrinterW, ClosePrinter, DeletePrinter, EnumPrintersW, OpenPrinterW, SetPrinterW, + PRINTER_ALL_ACCESS, PRINTER_ATTRIBUTE_LOCAL, PRINTER_CONTROL_PURGE, PRINTER_DEFAULTSW, + PRINTER_ENUM_LOCAL, PRINTER_INFO_1W, PRINTER_INFO_2W, + }, +}; +use windows_strings::{w, PCWSTR}; + +fn enum_local_printer( + level: DWORD, + p_printer_info: LPBYTE, + cb_buf: DWORD, + pcb_needed: LPDWORD, + pc_returned: LPDWORD, +) -> BOOL { + unsafe { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + EnumPrintersW( + PRINTER_ENUM_LOCAL, + null_mut(), + level, + p_printer_info, + cb_buf, + pcb_needed, + pc_returned, + ) + } +} + +#[inline] +pub fn is_printer_added(name: &PCWSTR) -> ResultType { + let r = common_enum( + "EnumPrintersW", + enum_local_printer, + 1, + |info: &PRINTER_INFO_1W| { + if is_name_equal(name, info.pName) { + Some(true) + } else { + None + } + }, + || None, + )?; + Ok(r.unwrap_or(false)) +} + +// Only return the first matched printer +pub fn get_printer_installed_on_port(port: &PCWSTR) -> ResultType>> { + common_enum( + "EnumPrintersW", + enum_local_printer, + 2, + |info: &PRINTER_INFO_2W| { + if is_name_equal(port, info.pPortName) { + Some(get_wstr_bytes(info.pPrinterName)) + } else { + None + } + }, + || None, + ) +} + +pub fn add_printer(name: &PCWSTR, driver: &PCWSTR, port: &PCWSTR) -> ResultType<()> { + let mut printer_info = PRINTER_INFO_2W { + pServerName: null_mut(), + pPrinterName: name.as_ptr() as _, + pShareName: null_mut(), + pPortName: port.as_ptr() as _, + pDriverName: driver.as_ptr() as _, + pComment: null_mut(), + pLocation: null_mut(), + pDevMode: null_mut(), + pSepFile: null_mut(), + pPrintProcessor: w!("WinPrint").as_ptr() as _, + pDatatype: w!("RAW").as_ptr() as _, + pParameters: null_mut(), + pSecurityDescriptor: null_mut(), + Attributes: PRINTER_ATTRIBUTE_LOCAL, + Priority: 0, + DefaultPriority: 0, + StartTime: 0, + UntilTime: 0, + Status: 0, + cJobs: 0, + AveragePPM: 0, + }; + unsafe { + let h_printer = AddPrinterW( + null_mut(), + 2, + &mut printer_info as *mut PRINTER_INFO_2W as _, + ); + if h_printer.is_null() { + bail!(format!( + "Failed to add printer. Error: {}", + io::Error::last_os_error() + )) + } + } + Ok(()) +} + +pub fn delete_printer(name: &PCWSTR) -> ResultType<()> { + let mut dft = PRINTER_DEFAULTSW { + pDataType: null_mut(), + pDevMode: null_mut(), + DesiredAccess: PRINTER_ALL_ACCESS, + }; + let mut h_printer: HANDLE = null_mut(); + unsafe { + if FALSE + == OpenPrinterW( + name.as_ptr() as _, + &mut h_printer, + &mut dft as *mut PRINTER_DEFAULTSW as _, + ) + { + let err = io::Error::last_os_error(); + if err.raw_os_error() == Some(ERROR_INVALID_PRINTER_NAME as _) { + return Ok(()); + } else { + bail!(format!("Failed to open printer. Error: {}", err)) + } + } + + if FALSE == SetPrinterW(h_printer, 0, null_mut(), PRINTER_CONTROL_PURGE) { + ClosePrinter(h_printer); + bail!(format!( + "Failed to purge printer queue. Error: {}", + io::Error::last_os_error() + )) + } + + if FALSE == DeletePrinter(h_printer) { + ClosePrinter(h_printer); + bail!(format!( + "Failed to delete printer. Error: {}", + io::Error::last_os_error() + )) + } + + ClosePrinter(h_printer); + } + + Ok(()) +} diff --git a/libs/remote_printer/src/setup/setup.rs b/libs/remote_printer/src/setup/setup.rs new file mode 100644 index 00000000000..f461ab75c55 --- /dev/null +++ b/libs/remote_printer/src/setup/setup.rs @@ -0,0 +1,94 @@ +use super::{ + driver::{get_installed_driver_version, install_driver, uninstall_driver}, + port::{check_add_local_port, check_delete_local_port}, + printer::{add_printer, delete_printer}, +}; +use hbb_common::{allow_err, bail, lazy_static, log, ResultType}; +use std::{path::PathBuf, sync::Mutex}; +use windows_strings::PCWSTR; + +lazy_static::lazy_static!( + static ref SETUP_MTX: Mutex<()> = Mutex::new(()); +); + +fn get_driver_inf_abs_path() -> ResultType { + use crate::RD_DRIVER_INF_PATH; + + let exe_file = std::env::current_exe()?; + let abs_path = match exe_file.parent() { + Some(parent) => parent.join(RD_DRIVER_INF_PATH), + None => bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ), + }; + if !abs_path.exists() { + bail!( + "The driver inf file \"{}\" does not exists", + RD_DRIVER_INF_PATH + ) + } + Ok(abs_path) +} + +// Note: This function must be called in a separate thread. +// Because many functions in this module are blocking or synchronous. +// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. +// Steps: +// 1. Add the local port. +// 2. Check if the driver is installed. +// Uninstall the existing driver if it is installed. +// We should not check the driver version because the driver is deployed with the application. +// It's better to uninstall the existing driver and install the driver from the application. +// 3. Add the printer. +pub fn install_update_printer(app_name: &str) -> ResultType<()> { + let printer_name = crate::get_printer_name(app_name); + let driver_name = crate::get_driver_name(); + let port = crate::get_port_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr()); + let rd_printer_port = PCWSTR::from_raw(port.as_ptr()); + + let inf_file = get_driver_inf_abs_path()?; + let inf_file: Vec = inf_file + .to_string_lossy() + .as_ref() + .encode_utf16() + .chain(Some(0).into_iter()) + .collect(); + let _lock = SETUP_MTX.lock().unwrap(); + + check_add_local_port(&rd_printer_port)?; + + let should_install_driver = match get_installed_driver_version(&rd_printer_driver_name)? { + Some(_version) => { + delete_printer(&rd_printer_name)?; + allow_err!(uninstall_driver(&rd_printer_driver_name)); + true + } + None => true, + }; + + if should_install_driver { + allow_err!(install_driver(&rd_printer_driver_name, inf_file.as_ptr())); + } + + add_printer(&rd_printer_name, &rd_printer_driver_name, &rd_printer_port)?; + + Ok(()) +} + +pub fn uninstall_printer(app_name: &str) { + let printer_name = crate::get_printer_name(app_name); + let driver_name = crate::get_driver_name(); + let port = crate::get_port_name(app_name); + let rd_printer_name = PCWSTR::from_raw(printer_name.as_ptr()); + let rd_printer_driver_name = PCWSTR::from_raw(driver_name.as_ptr()); + let rd_printer_port = PCWSTR::from_raw(port.as_ptr()); + + let _lock = SETUP_MTX.lock().unwrap(); + + allow_err!(delete_printer(&rd_printer_name)); + allow_err!(uninstall_driver(&rd_printer_driver_name)); + allow_err!(check_delete_local_port(&rd_printer_port)); +} diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index 529010f1607..16196d11f25 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -62,3 +62,6 @@ gstreamer-video = { version = "0.16", optional = true } git = "https://github.com/rustdesk-org/hwcodec" optional = true +[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies] +nokhwa = { git = "https://github.com/rustdesk-org/nokhwa.git", branch = "fix_from_raw_parts", features = ["input-native"] } + diff --git a/libs/scrap/examples/benchmark.rs b/libs/scrap/examples/benchmark.rs index 803a4343ce2..a867a2d3fcc 100644 --- a/libs/scrap/examples/benchmark.rs +++ b/libs/scrap/examples/benchmark.rs @@ -5,7 +5,7 @@ use hbb_common::{ }; use scrap::{ aom::{AomDecoder, AomEncoder, AomEncoderConfig}, - codec::{EncoderApi, EncoderCfg, Quality as Q}, + codec::{EncoderApi, EncoderCfg}, Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId::{self, *}, STRIDE_ALIGN, @@ -27,25 +27,17 @@ Usage: Options: -h --help Show this screen. --count=COUNT Capture frame count [default: 100]. - --quality=QUALITY Video quality [default: Balanced]. - Valid values: Best, Balanced, Low. + --quality=QUALITY Video quality [default: 1.0]. --i444 I444. "; #[derive(Debug, serde::Deserialize, Clone, Copy)] struct Args { flag_count: usize, - flag_quality: Quality, + flag_quality: f32, flag_i444: bool, } -#[derive(Debug, serde::Deserialize, Clone, Copy)] -enum Quality { - Best, - Balanced, - Low, -} - fn main() { init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); let args: Args = Docopt::new(USAGE) @@ -70,11 +62,6 @@ fn main() { "benchmark {}x{} quality:{:?}, i444:{:?}", width, height, quality, args.flag_i444 ); - let quality = match quality { - Quality::Best => Q::Best, - Quality::Balanced => Q::Balanced, - Quality::Low => Q::Low, - }; [VP8, VP9].map(|codec| { test_vpx( &mut c, @@ -98,7 +85,7 @@ fn test_vpx( codec_id: VpxVideoCodecId, width: usize, height: usize, - quality: Q, + quality: f32, yuv_count: usize, i444: bool, ) { @@ -177,7 +164,7 @@ fn test_av1( c: &mut Capturer, width: usize, height: usize, - quality: Q, + quality: f32, yuv_count: usize, i444: bool, ) { @@ -247,7 +234,7 @@ mod hw { use super::*; - pub fn test(c: &mut Capturer, width: usize, height: usize, quality: Q, yuv_count: usize) { + pub fn test(c: &mut Capturer, width: usize, height: usize, quality: f32, yuv_count: usize) { let mut h264s = Vec::new(); let mut h265s = Vec::new(); if let Some(info) = HwRamEncoder::try_get(CodecFormat::H264) { @@ -263,7 +250,7 @@ mod hw { fn test_encoder( width: usize, height: usize, - quality: Q, + quality: f32, info: CodecInfo, c: &mut Capturer, yuv_count: usize, diff --git a/libs/scrap/examples/record-screen.rs b/libs/scrap/examples/record-screen.rs index 6d68a7352f9..ca620608adb 100644 --- a/libs/scrap/examples/record-screen.rs +++ b/libs/scrap/examples/record-screen.rs @@ -13,7 +13,7 @@ use std::time::{Duration, Instant}; use std::{io, thread}; use docopt::Docopt; -use scrap::codec::{EncoderApi, EncoderCfg, Quality as Q}; +use scrap::codec::{EncoderApi, EncoderCfg}; use webm::mux; use webm::mux::Track; @@ -31,8 +31,7 @@ Options: -h --help Show this screen. --time= Recording duration in seconds. --fps= Frames per second [default: 30]. - --quality= Video quality [default: Balanced]. - Valid values: Best, Balanced, Low. + --quality= Video quality [default: 1.0]. --ba= Audio bitrate in kilobits per second [default: 96]. --codec CODEC Configure the codec used. [default: vp9] Valid values: vp8, vp9. @@ -44,14 +43,7 @@ struct Args { flag_codec: Codec, flag_time: Option, flag_fps: u64, - flag_quality: Quality, -} - -#[derive(Debug, serde::Deserialize)] -enum Quality { - Best, - Balanced, - Low, + flag_quality: f32, } #[derive(Debug, serde::Deserialize)] @@ -105,11 +97,7 @@ fn main() -> io::Result<()> { let mut vt = webm.add_video_track(width, height, None, mux_codec); // Setup the encoder. - let quality = match args.flag_quality { - Quality::Best => Q::Best, - Quality::Balanced => Q::Balanced, - Quality::Low => Q::Low, - }; + let quality = args.flag_quality; let mut vpx = vpx_encode::VpxEncoder::new( EncoderCfg::VPX(vpx_encode::VpxEncoderConfig { width, diff --git a/libs/scrap/src/common/aom.rs b/libs/scrap/src/common/aom.rs index d2bb2feb77f..4bf17a2fee3 100644 --- a/libs/scrap/src/common/aom.rs +++ b/libs/scrap/src/common/aom.rs @@ -6,7 +6,7 @@ include!(concat!(env!("OUT_DIR"), "/aom_ffi.rs")); -use crate::codec::{base_bitrate, codec_thread_num, Quality}; +use crate::codec::{base_bitrate, codec_thread_num}; use crate::{codec::EncoderApi, EncodeFrame, STRIDE_ALIGN}; use crate::{common::GoogleImage, generate_call_macro, generate_call_ptr_macro, Error, Result}; use crate::{EncodeInput, EncodeYuvFormat, Pixfmt}; @@ -45,7 +45,7 @@ impl Default for aom_image_t { pub struct AomEncoderConfig { pub width: u32, pub height: u32, - pub quality: Quality, + pub quality: f32, pub keyframe_interval: Option, } @@ -62,15 +62,9 @@ mod webrtc { use super::*; const kUsageProfile: u32 = AOM_USAGE_REALTIME; - const kMinQindex: u32 = 145; // Min qindex threshold for QP scaling. - const kMaxQindex: u32 = 205; // Max qindex threshold for QP scaling. const kBitDepth: u32 = 8; const kLagInFrames: u32 = 0; // No look ahead. pub(super) const kTimeBaseDen: i64 = 1000; - const kMinimumFrameRate: f64 = 1.0; - - pub const DEFAULT_Q_MAX: u32 = 56; // no more than 63 - pub const DEFAULT_Q_MIN: u32 = 12; // no more than 63, litter than q_max // Only positive speeds, range for real-time coding currently is: 6 - 8. // Lower means slower/better quality, higher means fastest/lower quality. @@ -116,21 +110,10 @@ mod webrtc { } else { c.kf_mode = aom_kf_mode::AOM_KF_DISABLED; } - let (q_min, q_max, b) = AomEncoder::convert_quality(cfg.quality); - if q_min > 0 && q_min < q_max && q_max < 64 { - c.rc_min_quantizer = q_min; - c.rc_max_quantizer = q_max; - } else { - c.rc_min_quantizer = DEFAULT_Q_MIN; - c.rc_max_quantizer = DEFAULT_Q_MAX; - } - let base_bitrate = base_bitrate(cfg.width as _, cfg.height as _); - let bitrate = base_bitrate * b / 100; - if bitrate > 0 { - c.rc_target_bitrate = bitrate; - } else { - c.rc_target_bitrate = base_bitrate; - } + let (q_min, q_max) = AomEncoder::calc_q_values(cfg.quality); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = AomEncoder::bitrate(cfg.width as _, cfg.height as _, cfg.quality); c.rc_undershoot_pct = 50; c.rc_overshoot_pct = 50; c.rc_buf_initial_sz = 600; @@ -273,17 +256,12 @@ impl EncoderApi for AomEncoder { false } - fn set_quality(&mut self, quality: Quality) -> ResultType<()> { + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { let mut c = unsafe { *self.ctx.config.enc.to_owned() }; - let (q_min, q_max, b) = Self::convert_quality(quality); - if q_min > 0 && q_min < q_max && q_max < 64 { - c.rc_min_quantizer = q_min; - c.rc_max_quantizer = q_max; - } - let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100; - if bitrate > 0 { - c.rc_target_bitrate = bitrate; - } + let (q_min, q_max) = Self::calc_q_values(ratio); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio); call_aom!(aom_codec_enc_config_set(&mut self.ctx, &c)); Ok(()) } @@ -293,10 +271,6 @@ impl EncoderApi for AomEncoder { c.rc_target_bitrate } - fn support_abr(&self) -> bool { - true - } - fn support_changing_quality(&self) -> bool { true } @@ -370,31 +344,27 @@ impl AomEncoder { } } - pub fn convert_quality(quality: Quality) -> (u32, u32, u32) { - // we can use lower bitrate for av1 - match quality { - Quality::Best => (12, 25, 100), - Quality::Balanced => (12, 35, 100 * 2 / 3), - Quality::Low => (18, 45, 50), - Quality::Custom(b) => { - let (q_min, q_max) = Self::calc_q_values(b); - (q_min, q_max, b) - } - } + fn bitrate(width: u32, height: u32, ratio: f32) -> u32 { + let bitrate = base_bitrate(width, height) as f32; + (bitrate * ratio) as u32 } #[inline] - fn calc_q_values(b: u32) -> (u32, u32) { + fn calc_q_values(ratio: f32) -> (u32, u32) { + let b = (ratio * 100.0) as u32; let b = std::cmp::min(b, 200); - let q_min1: i32 = 24; + let q_min1 = 24; let q_min2 = 5; let q_max1 = 45; let q_max2 = 25; let t = b as f32 / 200.0; - let q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; - let q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; + let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + + q_min = q_min.clamp(q_min2, q_min1); + q_max = q_max.clamp(q_max2, q_max1); (q_min, q_max) } diff --git a/libs/scrap/src/common/camera.rs b/libs/scrap/src/common/camera.rs new file mode 100644 index 00000000000..da5d62613d6 --- /dev/null +++ b/libs/scrap/src/common/camera.rs @@ -0,0 +1,283 @@ +use std::{ + io, + sync::{Arc, Mutex}, +}; + +#[cfg(any(target_os = "windows", target_os = "linux"))] +use nokhwa::{ + pixel_format::RgbAFormat, + query, + utils::{ApiBackend, CameraIndex, RequestedFormat, RequestedFormatType}, + Camera, +}; + +use hbb_common::message_proto::{DisplayInfo, Resolution}; + +#[cfg(feature = "vram")] +use crate::AdapterDevice; + +use crate::common::{bail, ResultType}; +use crate::{Frame, PixelBuffer, Pixfmt, TraitCapturer}; + +pub const PRIMARY_CAMERA_IDX: usize = 0; +lazy_static::lazy_static! { + static ref SYNC_CAMERA_DISPLAYS: Arc>> = Arc::new(Mutex::new(Vec::new())); +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +const CAMERA_NOT_SUPPORTED: &str = "This platform doesn't support camera yet"; + +pub struct Cameras; + +// pre-condition +pub fn primary_camera_exists() -> bool { + Cameras::exists(PRIMARY_CAMERA_IDX) +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +impl Cameras { + pub fn all_info() -> ResultType> { + match query(ApiBackend::Auto) { + Ok(cameras) => { + let mut camera_displays = SYNC_CAMERA_DISPLAYS.lock().unwrap(); + camera_displays.clear(); + // FIXME: nokhwa returns duplicate info for one physical camera on linux for now. + // issue: https://github.com/l1npengtul/nokhwa/issues/171 + // Use only one camera as a temporary hack. + cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + let Some(info) = cameras.first() else { + bail!("No camera found") + }; + // Use index (0) camera as main camera, fallback to the first camera if index (0) is not available. + // But maybe we also need to check index (1) or the lowest index camera. + // + // https://askubuntu.com/questions/234362/how-to-fix-this-problem-where-sometimes-dev-video0-becomes-automatically-dev + // https://github.com/rustdesk/rustdesk/pull/12010#issue-3125329069 + let mut camera_index = info.index().clone(); + if !matches!(camera_index, CameraIndex::Index(0)) { + if cameras.iter().any(|cam| matches!(cam.index(), CameraIndex::Index(0))) { + camera_index = CameraIndex::Index(0); + } + } + let camera = Self::create_camera(&camera_index)?; + let resolution = camera.resolution(); + let (width, height) = (resolution.width() as i32, resolution.height() as i32); + camera_displays.push(DisplayInfo { + x: 0, + y: 0, + name: info.human_name().clone(), + width, + height, + online: true, + cursor_embedded: false, + scale:1.0, + original_resolution: Some(Resolution { + width, + height, + ..Default::default() + }).into(), + ..Default::default() + }); + } else { + let mut x = 0; + for info in &cameras { + let camera = Self::create_camera(info.index())?; + let resolution = camera.resolution(); + let (width, height) = (resolution.width() as i32, resolution.height() as i32); + camera_displays.push(DisplayInfo { + x, + y: 0, + name: info.human_name().clone(), + width, + height, + online: true, + cursor_embedded: false, + scale:1.0, + original_resolution: Some(Resolution { + width, + height, + ..Default::default() + }).into(), + ..Default::default() + }); + x += width; + } + } + } + Ok(camera_displays.clone()) + } + Err(e) => { + bail!("Query cameras error: {}", e) + } + } + } + + pub fn exists(index: usize) -> bool { + match query(ApiBackend::Auto) { + Ok(cameras) => index < cameras.len(), + _ => return false, + } + } + + fn create_camera(index: &CameraIndex) -> ResultType { + let format_type = if cfg!(target_os = "linux") { + RequestedFormatType::None + } else { + RequestedFormatType::AbsoluteHighestResolution + }; + let result = Camera::new( + index.clone(), + RequestedFormat::new::(format_type), + ); + match result { + Ok(camera) => Ok(camera), + Err(e) => bail!("create camera{} error: {}", index, e), + } + } + + pub fn get_camera_resolution(index: usize) -> ResultType { + let index = CameraIndex::Index(index as u32); + let camera = Self::create_camera(&index)?; + let resolution = camera.resolution(); + Ok(Resolution { + width: resolution.width() as i32, + height: resolution.height() as i32, + ..Default::default() + }) + } + + pub fn get_sync_cameras() -> Vec { + SYNC_CAMERA_DISPLAYS.lock().unwrap().clone() + } + + pub fn get_capturer(current: usize) -> ResultType> { + Ok(Box::new(CameraCapturer::new(current)?)) + } +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +impl Cameras { + pub fn all_info() -> ResultType> { + return Ok(Vec::new()); + } + + pub fn exists(index: usize) -> bool { + false + } + + pub fn get_camera_resolution(index: usize) -> ResultType { + bail!(CAMERA_NOT_SUPPORTED); + } + + pub fn get_sync_cameras() -> Vec { + vec![] + } + + pub fn get_capturer(current: usize) -> ResultType> { + bail!(CAMERA_NOT_SUPPORTED); + } +} + +#[cfg(any(target_os = "windows", target_os = "linux"))] +pub struct CameraCapturer { + camera: Camera, + data: Vec, + last_data: Vec, // for faster compare and copy +} + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] +pub struct CameraCapturer; + +impl CameraCapturer { + #[cfg(any(target_os = "windows", target_os = "linux"))] + fn new(current: usize) -> ResultType { + let index = CameraIndex::Index(current as u32); + let camera = Cameras::create_camera(&index)?; + Ok(CameraCapturer { + camera, + data: Vec::new(), + last_data: Vec::new(), + }) + } + + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + fn new(_current: usize) -> ResultType { + bail!(CAMERA_NOT_SUPPORTED); + } +} + +impl TraitCapturer for CameraCapturer { + #[cfg(any(target_os = "windows", target_os = "linux"))] + fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result> { + // TODO: move this check outside `frame`. + if !self.camera.is_stream_open() { + if let Err(e) = self.camera.open_stream() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera open stream error: {}", e), + )); + } + } + match self.camera.frame() { + Ok(buffer) => { + match buffer.decode_image::() { + Ok(decoded) => { + self.data = decoded.as_raw().to_vec(); + crate::would_block_if_equal(&mut self.last_data, &self.data)?; + // FIXME: macos's PixelBuffer cannot be directly created from bytes slice. + cfg_if::cfg_if! { + if #[cfg(any(target_os = "linux", target_os = "windows"))] { + Ok(Frame::PixelBuffer(PixelBuffer::new( + &self.data, + Pixfmt::RGBA, + decoded.width() as usize, + decoded.height() as usize, + ))) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera is not supported on this platform yet"), + )) + } + } + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera frame decode error: {}", e), + )), + } + } + Err(e) => Err(io::Error::new( + io::ErrorKind::Other, + format!("Camera frame error: {}", e), + )), + } + } + + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + fn frame<'a>(&'a mut self, _timeout: std::time::Duration) -> std::io::Result> { + Err(io::Error::new( + io::ErrorKind::Other, + CAMERA_NOT_SUPPORTED.to_string(), + )) + } + + #[cfg(windows)] + fn is_gdi(&self) -> bool { + false + } + + #[cfg(windows)] + fn set_gdi(&mut self) -> bool { + false + } + + #[cfg(feature = "vram")] + fn device(&self) -> AdapterDevice { + AdapterDevice::default() + } + + #[cfg(feature = "vram")] + fn set_output_texture(&mut self, _texture: bool) {} +} diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs index 648de19b069..8eb0c158978 100644 --- a/libs/scrap/src/common/codec.rs +++ b/libs/scrap/src/common/codec.rs @@ -62,12 +62,10 @@ pub trait EncoderApi { #[cfg(feature = "vram")] fn input_texture(&self) -> bool; - fn set_quality(&mut self, quality: Quality) -> ResultType<()>; + fn set_quality(&mut self, ratio: f32) -> ResultType<()>; fn bitrate(&self) -> u32; - fn support_abr(&self) -> bool; - fn support_changing_quality(&self) -> bool; fn latency_free(&self) -> bool; @@ -866,7 +864,7 @@ pub fn enable_vram_option(encode: bool) -> bool { if encode { enable && enable_directx_capture() } else { - enable + enable && allow_d3d_render() } } else { false @@ -876,18 +874,25 @@ pub fn enable_vram_option(encode: bool) -> bool { #[cfg(windows)] pub fn enable_directx_capture() -> bool { use hbb_common::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE as OPTION; - option2bool( - OPTION, - &Config::get_option(hbb_common::config::keys::OPTION_ENABLE_DIRECTX_CAPTURE), - ) + option2bool(OPTION, &Config::get_option(OPTION)) } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(windows)] +pub fn allow_d3d_render() -> bool { + use hbb_common::config::keys::OPTION_ALLOW_D3D_RENDER as OPTION; + option2bool(OPTION, &hbb_common::config::LocalConfig::get_option(OPTION)) +} + +pub const BR_BEST: f32 = 1.5; +pub const BR_BALANCED: f32 = 0.67; +pub const BR_SPEED: f32 = 0.5; + +#[derive(Debug, Clone, Copy, PartialEq)] pub enum Quality { Best, Balanced, Low, - Custom(u32), + Custom(f32), } impl Default for Quality { @@ -903,22 +908,59 @@ impl Quality { _ => false, } } + + pub fn ratio(&self) -> f32 { + match self { + Quality::Best => BR_BEST, + Quality::Balanced => BR_BALANCED, + Quality::Low => BR_SPEED, + Quality::Custom(v) => *v, + } + } } pub fn base_bitrate(width: u32, height: u32) -> u32 { - #[allow(unused_mut)] - let mut base_bitrate = ((width * height) / 1000) as u32; // same as 1.1.9 - if base_bitrate == 0 { - base_bitrate = 1920 * 1080 / 1000; - } + const RESOLUTION_PRESETS: &[(u32, u32, u32)] = &[ + (640, 480, 400), // VGA, 307k pixels + (800, 600, 500), // SVGA, 480k pixels + (1024, 768, 800), // XGA, 786k pixels + (1280, 720, 1000), // 720p, 921k pixels + (1366, 768, 1100), // HD, 1049k pixels + (1440, 900, 1300), // WXGA+, 1296k pixels + (1600, 900, 1500), // HD+, 1440k pixels + (1920, 1080, 2073), // 1080p, 2073k pixels + (2048, 1080, 2200), // 2K DCI, 2211k pixels + (2560, 1440, 3000), // 2K QHD, 3686k pixels + (3440, 1440, 4000), // UWQHD, 4953k pixels + (3840, 2160, 5000), // 4K UHD, 8294k pixels + (7680, 4320, 12000), // 8K UHD, 33177k pixels + ]; + let pixels = width * height; + + let (preset_pixels, preset_bitrate) = RESOLUTION_PRESETS + .iter() + .map(|(w, h, bitrate)| (w * h, bitrate)) + .min_by_key(|(preset_pixels, _)| { + if *preset_pixels >= pixels { + preset_pixels - pixels + } else { + pixels - preset_pixels + } + }) + .unwrap_or(((1920 * 1080) as u32, &2073)); // default 1080p + + let bitrate = (*preset_bitrate as f32 * (pixels as f32 / preset_pixels as f32)).round() as u32; + #[cfg(target_os = "android")] { - // fix when android screen shrinks let fix = crate::Display::fix_quality() as u32; log::debug!("Android screen, fix quality:{}", fix); - base_bitrate = base_bitrate * fix; + bitrate * fix + } + #[cfg(not(target_os = "android"))] + { + bitrate } - base_bitrate } pub fn codec_thread_num(limit: usize) -> usize { @@ -1001,8 +1043,7 @@ pub fn test_av1() { static ONCE: Once = Once::new(); ONCE.call_once(|| { let f = || { - let (width, height, quality, keyframe_interval, i444) = - (1920, 1080, Quality::Balanced, None, false); + let (width, height, quality, keyframe_interval, i444) = (1920, 1080, 1.0, None, false); let frame_count = 10; let block_size = 300; let move_step = 50; diff --git a/libs/scrap/src/common/convert.rs b/libs/scrap/src/common/convert.rs index d3883192891..40c17e5bad2 100644 --- a/libs/scrap/src/common/convert.rs +++ b/libs/scrap/src/common/convert.rs @@ -197,3 +197,40 @@ pub fn convert_to_yuv( } Ok(()) } + +#[cfg(not(target_os = "ios"))] +pub fn convert(captured: &PixelBuffer, pixfmt: crate::Pixfmt, dst: &mut Vec) -> ResultType<()> { + if captured.pixfmt() == pixfmt { + dst.extend_from_slice(captured.data()); + return Ok(()); + } + + let src = captured.data(); + let src_stride = captured.stride(); + let src_pixfmt = captured.pixfmt(); + let src_width = captured.width(); + let src_height = captured.height(); + + let unsupported = format!( + "unsupported pixfmt conversion: {src_pixfmt:?} -> {:?}", + pixfmt + ); + + match (src_pixfmt, pixfmt) { + (crate::Pixfmt::BGRA, crate::Pixfmt::RGBA) | (crate::Pixfmt::RGBA, crate::Pixfmt::BGRA) => { + dst.resize(src.len(), 0); + call_yuv!(ABGRToARGB( + src.as_ptr(), + src_stride[0] as _, + dst.as_mut_ptr(), + src_stride[0] as _, + src_width as _, + src_height as _, + )); + } + _ => { + bail!(unsupported); + } + } + Ok(()) +} diff --git a/libs/scrap/src/common/dxgi.rs b/libs/scrap/src/common/dxgi.rs index ae2f1130feb..f7bf167d2d2 100644 --- a/libs/scrap/src/common/dxgi.rs +++ b/libs/scrap/src/common/dxgi.rs @@ -70,23 +70,30 @@ impl TraitCapturer for Capturer { pub struct PixelBuffer<'a> { data: &'a [u8], + pixfmt: Pixfmt, width: usize, height: usize, stride: Vec, } impl<'a> PixelBuffer<'a> { - pub fn new(data: &'a [u8], width: usize, height: usize) -> Self { + pub fn new(data: &'a [u8], pixfmt: Pixfmt, width: usize, height: usize) -> Self { let stride0 = data.len() / height; let mut stride = Vec::new(); stride.push(stride0); PixelBuffer { data, + pixfmt, width, height, stride, } } + + #[allow(non_snake_case)] + pub fn with_BGRA(data: &'a [u8], width: usize, height: usize) -> Self { + Self::new(data, Pixfmt::BGRA, width, height) + } } impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { @@ -107,7 +114,7 @@ impl<'a> crate::TraitPixelBuffer for PixelBuffer<'a> { } fn pixfmt(&self) -> Pixfmt { - Pixfmt::BGRA + self.pixfmt } } @@ -232,7 +239,7 @@ impl CapturerMag { impl TraitCapturer for CapturerMag { fn frame<'a>(&'a mut self, _timeout_ms: Duration) -> io::Result> { self.inner.frame(&mut self.data)?; - Ok(Frame::PixelBuffer(PixelBuffer::new( + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( &self.data, self.inner.get_rect().1, self.inner.get_rect().2, diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs index 40eefa72c41..8f3cd6d0c71 100644 --- a/libs/scrap/src/common/hwcodec.rs +++ b/libs/scrap/src/common/hwcodec.rs @@ -1,7 +1,5 @@ use crate::{ - codec::{ - base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg, Quality as Q, - }, + codec::{base_bitrate, codec_thread_num, enable_hwcodec_option, EncoderApi, EncoderCfg}, convert::*, CodecFormat, EncodeInput, ImageFormat, ImageRgb, Pixfmt, HW_STRIDE_ALIGN, }; @@ -47,7 +45,7 @@ pub struct HwRamEncoderConfig { pub mc_name: Option, pub width: usize, pub height: usize, - pub quality: Q, + pub quality: f32, pub keyframe_interval: Option, } @@ -67,12 +65,8 @@ impl EncoderApi for HwRamEncoder { match cfg { EncoderCfg::HWRAM(config) => { let rc = Self::rate_control(&config); - let b = Self::convert_quality(&config.name, config.quality); - let base_bitrate = base_bitrate(config.width as _, config.height as _); - let mut bitrate = base_bitrate * b / 100; - if bitrate <= 0 { - bitrate = base_bitrate; - } + let mut bitrate = + Self::bitrate(&config.name, config.width, config.height, config.quality); bitrate = Self::check_bitrate_range(&config, bitrate); let gop = config.keyframe_interval.unwrap_or(DEFAULT_GOP as _) as i32; let ctx = EncodeContext { @@ -176,15 +170,19 @@ impl EncoderApi for HwRamEncoder { false } - fn set_quality(&mut self, quality: crate::codec::Quality) -> ResultType<()> { - let b = Self::convert_quality(&self.config.name, quality); - let mut bitrate = base_bitrate(self.config.width as _, self.config.height as _) * b / 100; + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { + let mut bitrate = Self::bitrate( + &self.config.name, + self.config.width, + self.config.height, + ratio, + ); if bitrate > 0 { bitrate = Self::check_bitrate_range(&self.config, bitrate); self.encoder.set_bitrate(bitrate as _).ok(); self.bitrate = bitrate; } - self.config.quality = quality; + self.config.quality = ratio; Ok(()) } @@ -192,12 +190,6 @@ impl EncoderApi for HwRamEncoder { self.bitrate } - fn support_abr(&self) -> bool { - ["qsv", "vaapi"] - .iter() - .all(|&x| !self.config.name.contains(x)) - } - fn support_changing_quality(&self) -> bool { ["vaapi"].iter().all(|&x| !self.config.name.contains(x)) } @@ -256,21 +248,35 @@ impl HwRamEncoder { RC_CBR } - pub fn convert_quality(name: &str, quality: crate::codec::Quality) -> u32 { - use crate::codec::Quality; - let quality = match quality { - Quality::Best => 150, - Quality::Balanced => 100, - Quality::Low => 50, - Quality::Custom(b) => b, - }; - let factor = if name.contains("mediacodec") { + pub fn bitrate(name: &str, width: usize, height: usize, ratio: f32) -> u32 { + Self::calc_bitrate(width, height, ratio, name.contains("h264")) + } + + pub fn calc_bitrate(width: usize, height: usize, ratio: f32, h264: bool) -> u32 { + let base = base_bitrate(width as _, height as _) as f32 * ratio; + let threshold = 2000.0; + let decay_rate = 0.001; // 1000 * 0.001 = 1 + let factor: f32 = if cfg!(target_os = "android") { // https://stackoverflow.com/questions/26110337/what-are-valid-bit-rates-to-set-for-mediacodec?rq=3 - 5 + if base > threshold { + 1.0 + 4.0 / (1.0 + (base - threshold) * decay_rate) + } else { + 5.0 + } + } else if h264 { + if base > threshold { + 1.0 + 1.0 / (1.0 + (base - threshold) * decay_rate) + } else { + 2.0 + } } else { - 1 + if base > threshold { + 1.0 + 0.5 / (1.0 + (base - threshold) * decay_rate) + } else { + 1.5 + } }; - quality * factor + (base * factor) as u32 } pub fn check_bitrate_range(_config: &HwRamEncoderConfig, bitrate: u32) -> u32 { @@ -672,6 +678,8 @@ impl HwCodecConfig { } pub fn check_available_hwcodec() -> String { + #[cfg(any(target_os = "linux", target_os = "macos"))] + hwcodec::common::setup_parent_death_signal(); let ctx = EncodeContext { name: String::from(""), mc_name: None, @@ -718,6 +726,8 @@ pub fn start_check_process() { if let Some(_) = exe.file_name().to_owned() { let arg = "--check-hwcodec-config"; if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() { + #[cfg(windows)] + hwcodec::common::child_exit_when_parent_exit(child.id()); // wait up to 30 seconds, it maybe slow on windows startup for poorly performing machines for _ in 0..30 { std::thread::sleep(std::time::Duration::from_secs(1)); diff --git a/libs/scrap/src/common/mod.rs b/libs/scrap/src/common/mod.rs index ee96f57c851..2d74caa0dd5 100644 --- a/libs/scrap/src/common/mod.rs +++ b/libs/scrap/src/common/mod.rs @@ -13,12 +13,12 @@ cfg_if! { } else if #[cfg(x11)] { cfg_if! { if #[cfg(feature="wayland")] { - mod linux; - mod wayland; - mod x11; - pub use self::linux::*; - pub use self::wayland::set_map_err; - pub use self::x11::PixelBuffer; + mod linux; + mod wayland; + mod x11; + pub use self::linux::*; + pub use self::wayland::set_map_err; + pub use self::x11::PixelBuffer; } else { mod x11; pub use self::x11::*; @@ -49,6 +49,8 @@ pub const STRIDE_ALIGN: usize = 64; // commonly used in libvpx vpx_img_alloc cal pub const HW_STRIDE_ALIGN: usize = 0; // recommended by av_frame_get_buffer pub mod aom; +#[cfg(not(any(target_os = "ios")))] +pub mod camera; pub mod record; mod vpx; @@ -61,6 +63,7 @@ pub enum ImageFormat { } #[repr(C)] +#[derive(Clone)] pub struct ImageRgb { pub raw: Vec, pub w: usize, @@ -189,7 +192,7 @@ impl Frame<'_> { yuvfmt: EncodeYuvFormat, yuv: &'a mut Vec, mid_data: &mut Vec, - ) -> ResultType { + ) -> ResultType> { match self { Frame::PixelBuffer(pixelbuffer) => { convert_to_yuv(&pixelbuffer, yuvfmt, yuv, mid_data)?; diff --git a/libs/scrap/src/common/record.rs b/libs/scrap/src/common/record.rs index 6a1a6d60fcb..d121984f1be 100644 --- a/libs/scrap/src/common/record.rs +++ b/libs/scrap/src/common/record.rs @@ -25,7 +25,8 @@ pub struct RecorderContext { pub server: bool, pub id: String, pub dir: String, - pub display: usize, + pub display_idx: usize, + pub camera: bool, pub tx: Option>, } @@ -46,7 +47,11 @@ impl RecorderContext2 { + "_" + &ctx.id.clone() + &chrono::Local::now().format("_%Y%m%d%H%M%S%3f_").to_string() - + &format!("display{}_", ctx.display) + + &format!( + "{}{}_", + if ctx.camera { "camera" } else { "display" }, + ctx.display_idx + ) + &self.format.to_string().to_lowercase() + if self.format == CodecFormat::VP9 || self.format == CodecFormat::VP8 diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs index 11b497fb3bc..244f38ed57b 100644 --- a/libs/scrap/src/common/vpxcodec.rs +++ b/libs/scrap/src/common/vpxcodec.rs @@ -1,13 +1,14 @@ // https://github.com/astraw/vpx-encode // https://github.com/astraw/env-libvpx-sys // https://github.com/rust-av/vpx-rs/blob/master/src/decoder.rs +// https://github.com/chromium/chromium/blob/e7b24573bc2e06fed4749dd6b6abfce67f29052f/media/video/vpx_video_encoder.cc#L522 use hbb_common::anyhow::{anyhow, Context}; use hbb_common::log; use hbb_common::message_proto::{Chroma, EncodedVideoFrame, EncodedVideoFrames, VideoFrame}; use hbb_common::ResultType; -use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, Quality}; +use crate::codec::{base_bitrate, codec_thread_num, EncoderApi}; use crate::{EncodeInput, EncodeYuvFormat, GoogleImage, Pixfmt, STRIDE_ALIGN}; use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *}; @@ -19,9 +20,6 @@ use std::{ptr, slice}; generate_call_macro!(call_vpx, false); generate_call_ptr_macro!(call_vpx_ptr); -const DEFAULT_QP_MAX: u32 = 56; // no more than 63 -const DEFAULT_QP_MIN: u32 = 12; // no more than 63 - #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub enum VpxVideoCodecId { VP8, @@ -85,21 +83,11 @@ impl EncoderApi for VpxEncoder { c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot } - let (q_min, q_max, b) = Self::convert_quality(config.quality); - if q_min > 0 && q_min < q_max && q_max < 64 { - c.rc_min_quantizer = q_min; - c.rc_max_quantizer = q_max; - } else { - c.rc_min_quantizer = DEFAULT_QP_MIN; - c.rc_max_quantizer = DEFAULT_QP_MAX; - } - let base_bitrate = base_bitrate(config.width as _, config.height as _); - let bitrate = base_bitrate * b / 100; - if bitrate > 0 { - c.rc_target_bitrate = bitrate; - } else { - c.rc_target_bitrate = base_bitrate; - } + let (q_min, q_max) = Self::calc_q_values(config.quality); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = + Self::bitrate(config.width as _, config.height as _, config.quality); // https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp9/common/vp9_enums.h#29 // https://chromium.googlesource.com/webm/libvpx/+/refs/heads/main/vp8/vp8_cx_iface.c#282 c.g_profile = if i444 && config.codec == VpxVideoCodecId::VP9 { @@ -212,17 +200,12 @@ impl EncoderApi for VpxEncoder { false } - fn set_quality(&mut self, quality: Quality) -> ResultType<()> { + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { let mut c = unsafe { *self.ctx.config.enc.to_owned() }; - let (q_min, q_max, b) = Self::convert_quality(quality); - if q_min > 0 && q_min < q_max && q_max < 64 { - c.rc_min_quantizer = q_min; - c.rc_max_quantizer = q_max; - } - let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100; - if bitrate > 0 { - c.rc_target_bitrate = bitrate; - } + let (q_min, q_max) = Self::calc_q_values(ratio); + c.rc_min_quantizer = q_min; + c.rc_max_quantizer = q_max; + c.rc_target_bitrate = Self::bitrate(self.width as _, self.height as _, ratio); call_vpx!(vpx_codec_enc_config_set(&mut self.ctx, &c)); Ok(()) } @@ -232,9 +215,6 @@ impl EncoderApi for VpxEncoder { c.rc_target_bitrate } - fn support_abr(&self) -> bool { - true - } fn support_changing_quality(&self) -> bool { true } @@ -331,30 +311,27 @@ impl VpxEncoder { } } - fn convert_quality(quality: Quality) -> (u32, u32, u32) { - match quality { - Quality::Best => (6, 45, 150), - Quality::Balanced => (12, 56, 100 * 2 / 3), - Quality::Low => (18, 56, 50), - Quality::Custom(b) => { - let (q_min, q_max) = Self::calc_q_values(b); - (q_min, q_max, b) - } - } + fn bitrate(width: u32, height: u32, ratio: f32) -> u32 { + let bitrate = base_bitrate(width, height) as f32; + (bitrate * ratio) as u32 } #[inline] - fn calc_q_values(b: u32) -> (u32, u32) { + fn calc_q_values(ratio: f32) -> (u32, u32) { + let b = (ratio * 100.0) as u32; let b = std::cmp::min(b, 200); - let q_min1: i32 = 36; + let q_min1 = 36; let q_min2 = 0; let q_max1 = 56; let q_max2 = 37; let t = b as f32 / 200.0; - let q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; - let q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + let mut q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32; + let mut q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32; + + q_min = q_min.clamp(q_min2, q_min1); + q_max = q_max.clamp(q_max2, q_max1); (q_min, q_max) } @@ -415,8 +392,8 @@ pub struct VpxEncoderConfig { pub width: c_uint, /// The height (in pixels). pub height: c_uint, - /// The image quality - pub quality: Quality, + /// The bitrate ratio + pub quality: f32, /// The codec pub codec: VpxVideoCodecId, /// keyframe interval diff --git a/libs/scrap/src/common/vram.rs b/libs/scrap/src/common/vram.rs index eb3b8e1ce39..c003fa698e8 100644 --- a/libs/scrap/src/common/vram.rs +++ b/libs/scrap/src/common/vram.rs @@ -5,7 +5,7 @@ use std::{ }; use crate::{ - codec::{base_bitrate, enable_vram_option, EncoderApi, EncoderCfg, Quality}, + codec::{enable_vram_option, EncoderApi, EncoderCfg}, hwcodec::HwCodecConfig, AdapterDevice, CodecFormat, EncodeInput, EncodeYuvFormat, Pixfmt, }; @@ -17,7 +17,7 @@ use hbb_common::{ ResultType, }; use hwcodec::{ - common::{AdapterVendor::*, DataFormat, Driver, MAX_GOP}, + common::{DataFormat, Driver, MAX_GOP}, vram::{ decode::{self, DecodeFrame, Decoder}, encode::{self, EncodeFrame, Encoder}, @@ -30,8 +30,8 @@ use hwcodec::{ // https://cybersided.com/two-monitors-two-gpus/ // https://learn.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-getadapterluid#remarks lazy_static::lazy_static! { - static ref ENOCDE_NOT_USE: Arc>> = Default::default(); - static ref FALLBACK_GDI_DISPLAYS: Arc>> = Default::default(); + static ref ENOCDE_NOT_USE: Arc>> = Default::default(); + static ref FALLBACK_GDI_DISPLAYS: Arc>> = Default::default(); } #[derive(Debug, Clone)] @@ -39,7 +39,7 @@ pub struct VRamEncoderConfig { pub device: AdapterDevice, pub width: usize, pub height: usize, - pub quality: Quality, + pub quality: f32, pub feature: FeatureContext, pub keyframe_interval: Option, } @@ -51,7 +51,6 @@ pub struct VRamEncoder { bitrate: u32, last_frame_len: usize, same_bad_len_counter: usize, - config: VRamEncoderConfig, } impl EncoderApi for VRamEncoder { @@ -61,12 +60,12 @@ impl EncoderApi for VRamEncoder { { match cfg { EncoderCfg::VRAM(config) => { - let b = Self::convert_quality(config.quality, &config.feature); - let base_bitrate = base_bitrate(config.width as _, config.height as _); - let mut bitrate = base_bitrate * b / 100; - if bitrate <= 0 { - bitrate = base_bitrate; - } + let bitrate = Self::bitrate( + config.feature.data_format, + config.width, + config.height, + config.quality, + ); let gop = config.keyframe_interval.unwrap_or(MAX_GOP as _) as i32; let ctx = EncodeContext { f: config.feature.clone(), @@ -87,7 +86,6 @@ impl EncoderApi for VRamEncoder { bitrate, last_frame_len: 0, same_bad_len_counter: 0, - config, }), Err(_) => Err(anyhow!(format!("Failed to create encoder"))), } @@ -172,9 +170,13 @@ impl EncoderApi for VRamEncoder { true } - fn set_quality(&mut self, quality: Quality) -> ResultType<()> { - let b = Self::convert_quality(quality, &self.ctx.f); - let bitrate = base_bitrate(self.ctx.d.width as _, self.ctx.d.height as _) * b / 100; + fn set_quality(&mut self, ratio: f32) -> ResultType<()> { + let bitrate = Self::bitrate( + self.ctx.f.data_format, + self.ctx.d.width as _, + self.ctx.d.height as _, + ratio, + ); if bitrate > 0 { if self.encoder.set_bitrate((bitrate) as _).is_ok() { self.bitrate = bitrate; @@ -187,10 +189,6 @@ impl EncoderApi for VRamEncoder { self.bitrate } - fn support_abr(&self) -> bool { - self.config.device.vendor_id != ADAPTER_VENDOR_INTEL as u32 - } - fn support_changing_quality(&self) -> bool { true } @@ -285,43 +283,29 @@ impl VRamEncoder { } } - pub fn convert_quality(quality: Quality, f: &FeatureContext) -> u32 { - match quality { - Quality::Best => { - if f.driver == Driver::MFX && f.data_format == DataFormat::H264 { - 200 - } else { - 150 - } - } - Quality::Balanced => { - if f.driver == Driver::MFX && f.data_format == DataFormat::H264 { - 150 - } else { - 100 - } - } - Quality::Low => { - if f.driver == Driver::MFX && f.data_format == DataFormat::H264 { - 75 - } else { - 50 - } - } - Quality::Custom(b) => b, - } + pub fn bitrate(fmt: DataFormat, width: usize, height: usize, ratio: f32) -> u32 { + crate::hwcodec::HwRamEncoder::calc_bitrate(width, height, ratio, fmt == DataFormat::H264) } - pub fn set_not_use(display: usize, not_use: bool) { - log::info!("set display#{display} not use vram encode to {not_use}"); - ENOCDE_NOT_USE.lock().unwrap().insert(display, not_use); + pub fn set_not_use(video_service_name: String, not_use: bool) { + log::info!("set {video_service_name} not use vram encode to {not_use}"); + ENOCDE_NOT_USE + .lock() + .unwrap() + .insert(video_service_name, not_use); } - pub fn set_fallback_gdi(display: usize, fallback: bool) { + pub fn set_fallback_gdi(video_service_name: String, fallback: bool) { if fallback { - FALLBACK_GDI_DISPLAYS.lock().unwrap().insert(display); + FALLBACK_GDI_DISPLAYS + .lock() + .unwrap() + .insert(video_service_name); } else { - FALLBACK_GDI_DISPLAYS.lock().unwrap().remove(&display); + FALLBACK_GDI_DISPLAYS + .lock() + .unwrap() + .remove(&video_service_name); } } } diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs index 923606a8277..cc36b3c2308 100644 --- a/libs/scrap/src/dxgi/mag.rs +++ b/libs/scrap/src/dxgi/mag.rs @@ -133,7 +133,7 @@ impl MagInterface { s.lib_handle = LoadLibraryExA( lib_file_name_c.as_ptr() as _, NULL, - LOAD_WITH_ALTERED_SEARCH_PATH, + LOAD_LIBRARY_SEARCH_SYSTEM32, ); if s.lib_handle.is_null() { return Err(Error::new( diff --git a/libs/scrap/src/dxgi/mod.rs b/libs/scrap/src/dxgi/mod.rs index 9220fa60593..1f5296954d0 100644 --- a/libs/scrap/src/dxgi/mod.rs +++ b/libs/scrap/src/dxgi/mod.rs @@ -7,7 +7,6 @@ use winapi::{ shared::{ dxgi::*, dxgi1_2::*, - dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM, dxgitype::*, minwindef::{DWORD, FALSE, TRUE, UINT}, ntdef::LONG, @@ -118,6 +117,7 @@ impl Capturer { } else { hres } + // NVFBC(NVIDIA Capture SDK) which xpra used already deprecated, https://developer.nvidia.com/capture-sdk // also try high version DXGI for better performance, e.g. @@ -129,6 +129,8 @@ impl Capturer { // can help us update screen incrementally /* // not supported on my PC, try in the future + use winapi::shared::dxgiformat::DXGI_FORMAT_B8G8R8A8_UNORM; + let format : Vec = vec![DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT_420_OPAQUE]; (*display.inner).DuplicateOutput1( device as *mut _, @@ -394,7 +396,7 @@ impl Capturer { } else { let width = self.width; let height = self.height; - Ok(Frame::PixelBuffer(PixelBuffer::new( + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( self.get_pixelbuffer(timeout)?, width, height, diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs index 2f1e2a85267..9b2c5a6e051 100644 --- a/libs/scrap/src/wayland/pipewire.rs +++ b/libs/scrap/src/wayland/pipewire.rs @@ -24,6 +24,7 @@ use super::capturable::{Capturable, Recorder}; use super::remote_desktop_portal::OrgFreedesktopPortalRemoteDesktop as remote_desktop_portal; use super::request_portal::OrgFreedesktopPortalRequestResponse; use super::screencast_portal::OrgFreedesktopPortalScreenCast as screencast_portal; +use hbb_common::platform::linux::CMD_SH; use lazy_static::lazy_static; lazy_static! { @@ -880,7 +881,7 @@ pub fn get_capturables() -> Result, Box> { // `remote_desktop_portal` does not support restore_token and persist_mode. fn is_server_running() -> bool { let app_name = config::APP_NAME.read().unwrap().clone().to_lowercase(); - let output = match Command::new("sh") + let output = match Command::new(CMD_SH.as_str()) .arg("-c") .arg(&format!("ps aux | grep {}", app_name)) .output() diff --git a/libs/virtual_display/src/lib.rs b/libs/virtual_display/src/lib.rs index c9243d8223a..6d602aa2e51 100644 --- a/libs/virtual_display/src/lib.rs +++ b/libs/virtual_display/src/lib.rs @@ -48,7 +48,6 @@ macro_rules! make_lib_wrapper { $(let $field = if let Some(lib) = &lib { match unsafe { lib.symbol::<$tp>(stringify!($field)) } { Ok(m) => { - log::info!("method found {}", stringify!($field)); Some(*m) }, Err(e) => { diff --git a/res/.devcontainer/Dockerfile b/res/.devcontainer/Dockerfile deleted file mode 100644 index 93fd92ecbce..00000000000 --- a/res/.devcontainer/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -FROM mcr.microsoft.com/devcontainers/base:ubuntu-22.04 -ENV HOME=/home/vscode -ENV WORKDIR=$HOME/rustdesk - -WORKDIR $HOME -RUN sudo apt update -y && sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev -WORKDIR / - -RUN git clone https://github.com/microsoft/vcpkg -WORKDIR vcpkg -RUN git checkout 2023.04.15 -RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics -ENV VCPKG_ROOT=/vcpkg -RUN $VCPKG_ROOT/vcpkg --disable-metrics install libvpx libyuv opus aom - -WORKDIR / -RUN wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/dep.tar.gz && tar xzf dep.tar.gz - - -USER vscode -WORKDIR $HOME -RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so -RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh -RUN chmod +x rustup.sh -RUN $HOME/rustup.sh -y -RUN $HOME/.cargo/bin/rustup target add aarch64-linux-android -RUN $HOME/.cargo/bin/cargo install cargo-ndk - -# Install Flutter -RUN wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.10.1-stable.tar.xz -RUN tar xf flutter_linux_3.10.1-stable.tar.xz && rm flutter_linux_3.10.1-stable.tar.xz -ENV PATH="$PATH:$HOME/flutter/bin" -RUN dart pub global activate ffigen 5.0.1 - - -# Install packages -RUN sudo apt-get install -y libclang-dev -RUN sudo apt install -y gcc-multilib - -WORKDIR $WORKDIR -ENV ANDROID_NDK_HOME=/opt/android/ndk/22.1.7171670 - -# Somehow try to automate flutter pub get -# https://rustdesk.com/docs/en/dev/build/android/ -# Put below steps in entrypoint.sh -# cd flutter -# wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz -# tar xzf so.tar.gz - -# own /opt/android diff --git a/res/.devcontainer/build.sh b/res/.devcontainer/build.sh deleted file mode 100755 index df87aace74e..00000000000 --- a/res/.devcontainer/build.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/bash - -set -e - -MODE=${1:---debug} -TYPE=${2:-linux} -MODE=${MODE/*-/} - - -build(){ - pwd - $WORKDIR/entrypoint $1 -} - -build_arm64(){ - CWD=$(pwd) - cd $WORKDIR/flutter - flutter pub get - cd $WORKDIR - $WORKDIR/flutter/ndk_arm64.sh - cp $WORKDIR/target/aarch64-linux-android/release/liblibrustdesk.so $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so - cd $CWD -} - -build_apk(){ - cd $WORKDIR/flutter - MODE=$1 $WORKDIR/flutter/build_android.sh - cd $WORKDIR -} - -key_gen(){ - if [ ! -f $WORKDIR/flutter/android/key.properties ] - then - if [ ! -f $HOME/upload-keystore.jks ] - then - $WORKDIR/.devcontainer/setup.sh key - fi - read -r -p "enter the password used to generate $HOME/upload-keystore.jks\n" password - echo -e "storePassword=${password}\nkeyPassword=${password}\nkeyAlias=upload\nstoreFile=$HOME/upload-keystore.jks" > $WORKDIR/flutter/android/key.properties - else - echo "Believing storeFile is created ref: $WORKDIR/flutter/android/key.properties" - fi -} - -android_build(){ - if [ ! -d $WORKDIR/flutter/android/app/src/main/jniLibs/arm64-v8a ] - then - $WORKDIR/.devcontainer/setup.sh android - fi - build_arm64 - case $1 in - debug) - build_apk debug - ;; - release) - key_gen - build_apk release - ;; - esac -} - -case "$MODE:$TYPE" in - "debug:linux") - build - ;; - "release:linux") - build --release - ;; - "debug:android") - android_build debug - ;; - "release:android") - android_build release - ;; -esac diff --git a/res/.devcontainer/devcontainer.json b/res/.devcontainer/devcontainer.json deleted file mode 100644 index 953196eb319..00000000000 --- a/res/.devcontainer/devcontainer.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "rustdesk", - "build": { - "dockerfile": "./Dockerfile", - "context": "." - }, - "workspaceMount": "source=${localWorkspaceFolder},target=/home/vscode/rustdesk,type=bind,consistency=cache", - "workspaceFolder": "/home/vscode/rustdesk", - "postStartCommand": ".devcontainer/build.sh", - "features": { - "ghcr.io/devcontainers/features/java:1": {}, - "ghcr.io/akhildevelops/devcontainer-features/android-cli:latest": { - "PACKAGES": "platform-tools,ndk;23.2.8568313" - } - }, - "customizations": { - "vscode": { - "extensions": [ - "vadimcn.vscode-lldb", - "mutantdino.resourcemonitor", - "rust-lang.rust-analyzer", - "tamasfe.even-better-toml", - "serayuzgur.crates", - "mhutchie.git-graph", - "eamodio.gitlens" - ], - "settings": { - "files.watcherExclude": { - "**/target/**": true - } - } - } - } -} diff --git a/res/.devcontainer/setup.sh b/res/.devcontainer/setup.sh deleted file mode 100755 index c972f47b24a..00000000000 --- a/res/.devcontainer/setup.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -e -case $1 in - android) - # install deps - cd $WORKDIR/flutter - flutter pub get - wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/so.tar.gz - tar xzf so.tar.gz - rm so.tar.gz - sudo chown -R $(whoami) $ANDROID_HOME - echo "Setup is Done." - ;; - linux) - echo "Linux Setup" - ;; - key) - echo -e "\n$HOME/upload-keystore.jks is not created.\nLet's create it.\nRemember the password you enter in keytool!" - keytool -genkey -v -keystore $HOME/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload - ;; -esac - - \ No newline at end of file diff --git a/res/DEBIAN/postinst b/res/DEBIAN/postinst index eeeccaaec8b..51e11eefa3b 100755 --- a/res/DEBIAN/postinst +++ b/res/DEBIAN/postinst @@ -5,8 +5,8 @@ set -e if [ "$1" = configure ]; then INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') - ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk - + ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk + if [ "systemd" == "$INITSYS" ]; then if [ -e /etc/systemd/system/rustdesk.service ]; then @@ -23,7 +23,9 @@ if [ "$1" = configure ]; then sed -i "s|pkill|/usr/bin/pkill|g" /usr/lib/systemd/system/rustdesk.service fi systemctl daemon-reload - systemctl enable rustdesk - systemctl start rustdesk + + # ensure rustdesk service is NOT running on startup + systemctl disable rustdesk + systemctl stop rustdesk fi fi diff --git a/res/DEBIAN/prerm b/res/DEBIAN/prerm index baef2e2e202..133ff11debd 100755 --- a/res/DEBIAN/prerm +++ b/res/DEBIAN/prerm @@ -5,7 +5,7 @@ set -e case $1 in remove|upgrade) INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}') - rm /usr/bin/rustdesk + rm -f /usr/bin/rustdesk if [ "systemd" == "${INITSYS}" ]; then diff --git a/res/PKGBUILD b/res/PKGBUILD index 79061a41b05..269ded858a6 100644 --- a/res/PKGBUILD +++ b/res/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.3.5 +pkgver=1.4.1 pkgrel=0 epoch= pkgdesc="" @@ -23,10 +23,10 @@ md5sums=() #generate with 'makepkg -g' package() { if [[ ${FLUTTER} ]]; then - mkdir -p "${pkgdir}/usr/lib/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/lib/rustdesk" + mkdir -p "${pkgdir}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/share/rustdesk" fi mkdir -p "${pkgdir}/usr/bin" - pushd ${pkgdir} && ln -s /usr/lib/rustdesk/rustdesk usr/bin/rustdesk && popd + pushd ${pkgdir} && ln -s /usr/share/rustdesk/rustdesk usr/bin/rustdesk && popd install -Dm 644 $HBB/res/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${pkgdir}/usr/share/rustdesk/files" diff --git a/res/devices.py b/res/devices.py index f9bf2735292..215bd6dffdd 100755 --- a/res/devices.py +++ b/res/devices.py @@ -12,6 +12,7 @@ def view( device_name=None, user_name=None, group_name=None, + device_group_name=None, offline_days=None, ): headers = {"Authorization": f"Bearer {token}"} @@ -21,6 +22,7 @@ def view( "device_name": device_name, "user_name": user_name, "group_name": group_name, + "device_group_name": device_group_name, } params = { @@ -118,7 +120,8 @@ def main(): parser.add_argument("--id", help="Device ID") parser.add_argument("--device_name", help="Device name") parser.add_argument("--user_name", help="User name") - parser.add_argument("--group_name", help="Group name") + parser.add_argument("--group_name", help="User group name") + parser.add_argument("--device_group_name", help="Device group name") parser.add_argument( "--assign_to", help="=, e.g. user_name=mike, strategy_name=test, ab=ab1, ab=ab1,tag1", @@ -138,6 +141,7 @@ def main(): args.device_name, args.user_name, args.group_name, + args.device_group_name, args.offline_days, ) diff --git a/res/inline-sciter.py b/res/inline-sciter.py index 26cf1a75471..4c81bd621ae 100644 --- a/res/inline-sciter.py +++ b/res/inline-sciter.py @@ -23,7 +23,8 @@ def strip(s): return re.sub(r'\s+\n', '\n', re.sub(r'\n\s+', '\n', s)) .replace('include "grid.tis";', open('src/ui/grid.tis').read()) \ .replace('include "header.tis";', open('src/ui/header.tis').read()) \ .replace('include "file_transfer.tis";', open('src/ui/file_transfer.tis').read()) \ - .replace('include "port_forward.tis";', open('src/ui/port_forward.tis').read()) + .replace('include "port_forward.tis";', open('src/ui/port_forward.tis').read()) \ + .replace('include "printer.tis";', open('src/ui/printer.tis').read()) chatbox = open('src/ui/chatbox.html').read() install = open('src/ui/install.html').read().replace('include "install.tis";', open('src/ui/install.tis').read()) diff --git a/res/msi/CustomActions/Common.h b/res/msi/CustomActions/Common.h index 5dcd529c065..08302d98c36 100644 --- a/res/msi/CustomActions/Common.h +++ b/res/msi/CustomActions/Common.h @@ -15,3 +15,9 @@ bool MyStopServiceW(LPCWSTR serviceName); std::wstring ReadConfig(const std::wstring& filename, const std::wstring& key); void UninstallDriver(LPCWSTR hardwareId, BOOL &rebootRequired); + +namespace RemotePrinter +{ + VOID installUpdatePrinter(const std::wstring& installFolder); + VOID uninstallPrinter(); +} diff --git a/res/msi/CustomActions/CustomActions.cpp b/res/msi/CustomActions/CustomActions.cpp index afe06fdbb0b..fafbab6b5f6 100644 --- a/res/msi/CustomActions/CustomActions.cpp +++ b/res/msi/CustomActions/CustomActions.cpp @@ -878,3 +878,55 @@ void TryStopDeleteServiceByShell(LPWSTR svcName) WcaLog(LOGMSG_STANDARD, "Failed to delete service: \"%ls\" with shell, current status: %d.", svcName, svcStatus.dwCurrentState); } } + +UINT __stdcall InstallPrinter( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + int nResult = 0; + LPWSTR installFolder = NULL; + LPWSTR pwz = NULL; + LPWSTR pwzData = NULL; + + hr = WcaInitialize(hInstall, "InstallPrinter"); + ExitOnFailure(hr, "Failed to initialize"); + + hr = WcaGetProperty(L"CustomActionData", &pwzData); + ExitOnFailure(hr, "failed to get CustomActionData"); + + pwz = pwzData; + hr = WcaReadStringFromCaData(&pwz, &installFolder); + ExitOnFailure(hr, "failed to read database key from custom action data: %ls", pwz); + + WcaLog(LOGMSG_STANDARD, "Try to install RD printer in : %ls", installFolder); + RemotePrinter::installUpdatePrinter(installFolder); + WcaLog(LOGMSG_STANDARD, "Install RD printer done"); + +LExit: + if (pwzData) { + ReleaseStr(pwzData); + } + + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} + +UINT __stdcall UninstallPrinter( + __in MSIHANDLE hInstall) +{ + HRESULT hr = S_OK; + DWORD er = ERROR_SUCCESS; + + hr = WcaInitialize(hInstall, "UninstallPrinter"); + ExitOnFailure(hr, "Failed to initialize"); + + WcaLog(LOGMSG_STANDARD, "Try to uninstall RD printer"); + RemotePrinter::uninstallPrinter(); + WcaLog(LOGMSG_STANDARD, "Uninstall RD printer done"); + +LExit: + er = SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE; + return WcaFinalize(er); +} diff --git a/res/msi/CustomActions/CustomActions.def b/res/msi/CustomActions/CustomActions.def index 557bfaf1871..01b03490c57 100644 --- a/res/msi/CustomActions/CustomActions.def +++ b/res/msi/CustomActions/CustomActions.def @@ -12,3 +12,5 @@ EXPORTS SetPropertyFromConfig AddRegSoftwareSASGeneration RemoveAmyuniIdd + InstallPrinter + UninstallPrinter diff --git a/res/msi/CustomActions/CustomActions.vcxproj b/res/msi/CustomActions/CustomActions.vcxproj index 1bff7b15402..2e704fbb5e8 100644 --- a/res/msi/CustomActions/CustomActions.vcxproj +++ b/res/msi/CustomActions/CustomActions.vcxproj @@ -67,6 +67,7 @@ Create + diff --git a/res/msi/CustomActions/RemotePrinter.cpp b/res/msi/CustomActions/RemotePrinter.cpp new file mode 100644 index 00000000000..767c8c82cef --- /dev/null +++ b/res/msi/CustomActions/RemotePrinter.cpp @@ -0,0 +1,517 @@ +#include "pch.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Common.h" + +#pragma comment(lib, "setupapi.lib") +#pragma comment(lib, "winspool.lib") + +namespace RemotePrinter +{ +#define HRESULT_ERR_ELEMENT_NOT_FOUND 0x80070490 + + LPCWCH RD_DRIVER_INF_PATH = L"drivers\\RustDeskPrinterDriver\\RustDeskPrinterDriver.inf"; + LPCWCH RD_PRINTER_PORT = L"RustDesk Printer"; + LPCWCH RD_PRINTER_NAME = L"RustDesk Printer"; + LPCWCH RD_PRINTER_DRIVER_NAME = L"RustDesk v4 Printer Driver"; + LPCWCH XCV_MONITOR_LOCAL_PORT = L",XcvMonitor Local Port"; + + using FuncEnum = std::function; + template + using FuncOnData = std::function(const T &)>; + template + using FuncOnNoData = std::function()>; + + template + std::shared_ptr commonEnum(std::wstring funcName, FuncEnum func, DWORD level, FuncOnData onData, FuncOnNoData onNoData) + { + DWORD needed = 0; + DWORD returned = 0; + func(level, NULL, 0, &needed, &returned); + if (needed == 0) + { + return onNoData(); + } + + std::vector buffer(needed); + if (!func(level, buffer.data(), needed, &needed, &returned)) + { + return nullptr; + } + + T *pPortInfo = reinterpret_cast(buffer.data()); + for (DWORD i = 0; i < returned; i++) + { + auto r = onData(pPortInfo[i]); + if (r) + { + return r; + } + } + return onNoData(); + } + + BOOL isNameEqual(LPCWSTR lhs, LPCWSTR rhs) + { + // https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw + // For some locales, the lstrcmpi function may be insufficient. + // If this occurs, use `CompareStringEx` to ensure proper comparison. + // For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison. + // Note that specifying these values slows performance, so use them only when necessary. + // + // No need to consider `CompareStringEx` for now. + return lstrcmpiW(lhs, rhs) == 0 ? TRUE : FALSE; + } + + BOOL enumPrinterPort( + DWORD level, + LPBYTE pPortInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPortsW(NULL, level, pPortInfo, cbBuf, pcbNeeded, pcReturned); + } + + BOOL isPortExists(LPCWSTR port) + { + auto onData = [port](const PORT_INFO_2 &info) + { + if (isNameEqual(info.pPortName, port) == TRUE) { + return std::shared_ptr(new BOOL(TRUE)); + } + else { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPortsW", enumPrinterPort, 2, onData, onNoData); + if (res == nullptr) + { + return false; + } + else + { + return *res; + } + } + + BOOL executeOnLocalPort(LPCWSTR port, LPCWSTR command) + { + PRINTER_DEFAULTSW dft = {0}; + dft.DesiredAccess = SERVER_WRITE; + HANDLE hMonitor = NULL; + if (OpenPrinterW(const_cast(XCV_MONITOR_LOCAL_PORT), &hMonitor, &dft) == FALSE) + { + return FALSE; + } + + DWORD outputNeeded = 0; + DWORD status = 0; + if (XcvDataW(hMonitor, command, (LPBYTE)port, (lstrlenW(port) + 1) * 2, NULL, 0, &outputNeeded, &status) == FALSE) + { + ClosePrinter(hMonitor); + return FALSE; + } + + ClosePrinter(hMonitor); + return TRUE; + } + + BOOL addLocalPort(LPCWSTR port) + { + return executeOnLocalPort(port, L"AddPort"); + } + + BOOL deleteLocalPort(LPCWSTR port) + { + return executeOnLocalPort(port, L"DeletePort"); + } + + BOOL checkAddLocalPort(LPCWSTR port) + { + if (!isPortExists(port)) + { + return addLocalPort(port); + } + return TRUE; + } + + std::wstring getPrinterInstalledOnPort(LPCWSTR port); + + BOOL checkDeleteLocalPort(LPCWSTR port) + { + if (isPortExists(port)) + { + if (getPrinterInstalledOnPort(port) != L"") + { + WcaLog(LOGMSG_STANDARD, "The printer is installed on the port. Please remove the printer first.\n"); + return FALSE; + } + return deleteLocalPort(port); + } + return TRUE; + } + + BOOL enumPrinterDriver( + DWORD level, + LPBYTE pDriverInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPrinterDriversW( + NULL, + NULL, + level, + pDriverInfo, + cbBuf, + pcbNeeded, + pcReturned); + } + + DWORDLONG getInstalledDriverVersion(LPCWSTR name) + { + auto onData = [name](const DRIVER_INFO_6W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new DWORDLONG(info.dwlDriverVersion)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrinterDriversW", enumPrinterDriver, 6, onData, onNoData); + if (res == nullptr) + { + return 0; + } + else + { + return *res; + } + } + + std::wstring findInf(LPCWSTR name) + { + auto onData = [name](const DRIVER_INFO_8W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new std::wstring(info.pszInfPath)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrinterDriversW", enumPrinterDriver, 8, onData, onNoData); + if (res == nullptr) + { + return L""; + } + else + { + return *res; + } + } + + BOOL deletePrinterDriver(LPCWSTR name) + { + // If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer. + // `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9). + // We can only ignore this error for now. + // Though restarting the spooler service is a solution, it's not a good idea to restart the service. + // + // Deleting the printer driver after deleting the printer is a common practice. + // No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once. + // https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422 + // AnyDesk printer driver and the simplest printer driver also have the same issue. + BOOL res = DeletePrinterDriverExW(NULL, NULL, const_cast(name), DPD_DELETE_ALL_FILES, 0); + if (res == FALSE) + { + DWORD error = GetLastError(); + if (error == ERROR_UNKNOWN_PRINTER_DRIVER) + { + return TRUE; + } + else + { + WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver. Error (%d)\n", error); + } + } + return res; + } + + BOOL deletePrinterDriverPackage(const std::wstring &inf) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/deleteprinterdriverpackage + // This function is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + int tries = 3; + HRESULT result = S_FALSE; + while ((result = DeletePrinterDriverPackage(NULL, inf.c_str(), NULL)) != S_OK) + { + if (result == HRESULT_ERR_ELEMENT_NOT_FOUND) + { + return TRUE; + } + + WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver package. HRESULT (%d)\n", result); + tries--; + if (tries <= 0) + { + return FALSE; + } + Sleep(2000); + } + return S_OK; + } + + BOOL uninstallDriver(LPCWSTR name) + { + auto infFile = findInf(name); + if (!deletePrinterDriver(name)) + { + return FALSE; + } + if (infFile != L"" && !deletePrinterDriverPackage(infFile)) + { + return FALSE; + } + return TRUE; + } + + BOOL installDriver(LPCWSTR name, LPCWSTR inf) + { + DWORD size = MAX_PATH * 10; + wchar_t package_path[MAX_PATH * 10] = {0}; + HRESULT result = UploadPrinterDriverPackage( + NULL, inf, NULL, + UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS, NULL, package_path, &size); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache silently, failed. Will retry with user UI. HRESULT (%d)\n", result); + result = UploadPrinterDriverPackage( + NULL, inf, NULL, UPDP_UPLOAD_ALWAYS, + GetForegroundWindow(), package_path, &size); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache failed with user UI. Aborting...\n"); + return FALSE; + } + } + + result = InstallPrinterDriverFromPackage( + NULL, package_path, name, NULL, IPDFP_COPY_ALL_FILES); + if (result != S_OK) + { + WcaLog(LOGMSG_STANDARD, "Installing the printer driver failed. HRESULT (%d)\n", result); + } + return result == S_OK; + } + + BOOL enumLocalPrinter( + DWORD level, + LPBYTE pPrinterInfo, + DWORD cbBuf, + LPDWORD pcbNeeded, + LPDWORD pcReturned) + { + // https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters + // This is a blocking or synchronous function and might not return immediately. + // How quickly this function returns depends on run-time factors + // such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application. + // Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive. + return EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, level, pPrinterInfo, cbBuf, pcbNeeded, pcReturned); + } + + BOOL isPrinterAdded(LPCWSTR name) + { + auto onData = [name](const PRINTER_INFO_1W &info) + { + if (isNameEqual(name, info.pName) == TRUE) + { + return std::shared_ptr(new BOOL(TRUE)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrintersW", enumLocalPrinter, 1, onData, onNoData); + if (res == nullptr) + { + return FALSE; + } + else + { + return *res; + } + } + + std::wstring getPrinterInstalledOnPort(LPCWSTR port) + { + auto onData = [port](const PRINTER_INFO_2W &info) + { + if (isNameEqual(port, info.pPortName) == TRUE) + { + return std::shared_ptr(new std::wstring(info.pPrinterName)); + } + else + { + return std::shared_ptr(nullptr); + } }; + auto onNoData = []() + { return nullptr; }; + auto res = commonEnum(L"EnumPrintersW", enumLocalPrinter, 2, onData, onNoData); + if (res == nullptr) + { + return L""; + } + else + { + return *res; + } + } + + BOOL addPrinter(LPCWSTR name, LPCWSTR driver, LPCWSTR port) + { + PRINTER_INFO_2W printerInfo = {0}; + printerInfo.pPrinterName = const_cast(name); + printerInfo.pPortName = const_cast(port); + printerInfo.pDriverName = const_cast(driver); + printerInfo.pPrintProcessor = const_cast(L"WinPrint"); + printerInfo.pDatatype = const_cast(L"RAW"); + printerInfo.Attributes = PRINTER_ATTRIBUTE_LOCAL; + HANDLE hPrinter = AddPrinterW(NULL, 2, (LPBYTE)&printerInfo); + return hPrinter == NULL ? FALSE : TRUE; + } + + VOID deletePrinter(LPCWSTR name) + { + PRINTER_DEFAULTSW dft = {0}; + dft.DesiredAccess = PRINTER_ALL_ACCESS; + HANDLE hPrinter = NULL; + if (OpenPrinterW(const_cast(name), &hPrinter, &dft) == FALSE) + { + DWORD error = GetLastError(); + if (error == ERROR_INVALID_PRINTER_NAME) + { + return; + } + WcaLog(LOGMSG_STANDARD, "Failed to open printer. error (%d)\n", error); + return; + } + + if (SetPrinterW(hPrinter, 0, NULL, PRINTER_CONTROL_PURGE) == FALSE) + { + ClosePrinter(hPrinter); + WcaLog(LOGMSG_STANDARD, "Failed to purge printer queue. error (%d)\n", GetLastError()); + return; + } + + if (DeletePrinter(hPrinter) == FALSE) + { + ClosePrinter(hPrinter); + WcaLog(LOGMSG_STANDARD, "Failed to delete printer. error (%d)\n", GetLastError()); + return; + } + + ClosePrinter(hPrinter); + } + + bool FileExists(const std::wstring &filePath) + { + DWORD fileAttributes = GetFileAttributes(filePath.c_str()); + return (fileAttributes != INVALID_FILE_ATTRIBUTES && !(fileAttributes & FILE_ATTRIBUTE_DIRECTORY)); + } + + // Steps: + // 1. Add the local port. + // 2. Check if the driver is installed. + // Uninstall the existing driver if it is installed. + // We should not check the driver version because the driver is deployed with the application. + // It's better to uninstall the existing driver and install the driver from the application. + // 3. Add the printer. + VOID installUpdatePrinter(const std::wstring &installFolder) + { + const std::wstring infFile = installFolder + L"\\" + RemotePrinter::RD_DRIVER_INF_PATH; + if (!FileExists(infFile)) + { + WcaLog(LOGMSG_STANDARD, "Printer driver INF file not found, aborting...\n"); + return; + } + + if (!checkAddLocalPort(RD_PRINTER_PORT)) + { + WcaLog(LOGMSG_STANDARD, "Failed to check add local port, error (%d)\n", GetLastError()); + return; + } + else + { + WcaLog(LOGMSG_STANDARD, "Local port added successfully\n"); + } + + if (getInstalledDriverVersion(RD_PRINTER_DRIVER_NAME) > 0) + { + deletePrinter(RD_PRINTER_NAME); + if (FALSE == uninstallDriver(RD_PRINTER_DRIVER_NAME)) + { + WcaLog(LOGMSG_STANDARD, "Failed to uninstall previous printer driver, error (%d)\n", GetLastError()); + } + } + + if (FALSE == installDriver(RD_PRINTER_DRIVER_NAME, infFile.c_str())) + { + WcaLog(LOGMSG_STANDARD, "Driver installation failed, still try to add the printer\n"); + } + else + { + WcaLog(LOGMSG_STANDARD, "Driver installed successfully\n"); + } + + if (FALSE == addPrinter(RD_PRINTER_NAME, RD_PRINTER_DRIVER_NAME, RD_PRINTER_PORT)) + { + WcaLog(LOGMSG_STANDARD, "Failed to add printer, error (%d)\n", GetLastError()); + } + else + { + WcaLog(LOGMSG_STANDARD, "Printer installed successfully\n"); + } + } + + VOID uninstallPrinter() + { + deletePrinter(RD_PRINTER_NAME); + WcaLog(LOGMSG_STANDARD, "Deleted the printer\n"); + uninstallDriver(RD_PRINTER_DRIVER_NAME); + WcaLog(LOGMSG_STANDARD, "Uninstalled the printer driver\n"); + checkDeleteLocalPort(RD_PRINTER_PORT); + WcaLog(LOGMSG_STANDARD, "Deleted the local port\n"); + } +} diff --git a/res/msi/Package/Components/RustDesk.wxs b/res/msi/Package/Components/RustDesk.wxs index c17d60edb79..337e84ec3c5 100644 --- a/res/msi/Package/Components/RustDesk.wxs +++ b/res/msi/Package/Components/RustDesk.wxs @@ -30,6 +30,7 @@ + @@ -53,8 +54,21 @@ - - + + + + + + + + + + @@ -71,6 +85,8 @@ + + diff --git a/res/msi/Package/Fragments/AddRemoveProperties.wxs b/res/msi/Package/Fragments/AddRemoveProperties.wxs index a7139fab2c9..ac1d85a86cc 100644 --- a/res/msi/Package/Fragments/AddRemoveProperties.wxs +++ b/res/msi/Package/Fragments/AddRemoveProperties.wxs @@ -6,9 +6,11 @@ - + + + @@ -23,6 +24,9 @@ + + + @@ -46,6 +50,16 @@ + + + + + + + + + + + + + diff --git a/res/msi/Package/Language/Package.en-us.wxl b/res/msi/Package/Language/Package.en-us.wxl index 1bd3986ddbb..c65a5126d4a 100644 --- a/res/msi/Package/Language/Package.en-us.wxl +++ b/res/msi/Package/Language/Package.en-us.wxl @@ -51,5 +51,6 @@ This file contains the declaration of all the localizable strings. + diff --git a/res/msi/Package/Package.wxs b/res/msi/Package/Package.wxs index bdd8471cfc0..e11756a6506 100644 --- a/res/msi/Package/Package.wxs +++ b/res/msi/Package/Package.wxs @@ -51,6 +51,8 @@ + + diff --git a/res/msi/Package/UI/MyInstallDirDlg.wxs b/res/msi/Package/UI/MyInstallDirDlg.wxs index 6e27e2b2826..e4bad91972b 100644 --- a/res/msi/Package/UI/MyInstallDirDlg.wxs +++ b/res/msi/Package/UI/MyInstallDirDlg.wxs @@ -25,6 +25,7 @@ + diff --git a/res/msi/preprocess.py b/res/msi/preprocess.py index 9a43e9da6a9..cb2140d21bb 100644 --- a/res/msi/preprocess.py +++ b/res/msi/preprocess.py @@ -8,7 +8,9 @@ import datetime import subprocess import re +import platform from pathlib import Path +from itertools import chain import shutil g_indent_unit = "\t" @@ -47,7 +49,7 @@ def make_parser(): "--dist-dir", type=str, default="../../rustdesk", - help="The dist direcotry to install.", + help="The dist directory to install.", ) parser.add_argument( "--arp", @@ -187,6 +189,17 @@ def replace_app_name_in_langs(app_name): with open(file_path, "w", encoding="utf-8") as f: f.writelines(lines) +def replace_app_name_in_custom_actions(app_name): + custion_actions_dir = Path(sys.argv[0]).parent.joinpath("CustomActions") + for file_path in chain(custion_actions_dir.glob("*.cpp"), custion_actions_dir.glob("*.h")): + with open(file_path, "r", encoding="utf-8") as f: + lines = f.readlines() + for i, line in enumerate(lines): + line = re.sub(r"\bRustDesk\b", app_name, line) + line = line.replace(f"{app_name} v4 Printer Driver", "RustDesk v4 Printer Driver") + lines[i] = line + with open(file_path, "w", encoding="utf-8") as f: + f.writelines(lines) def gen_upgrade_info(): def func(lines, index_start): @@ -542,3 +555,4 @@ def replace_component_guids_in_wxs(): sys.exit(-1) replace_app_name_in_langs(args.app_name) + replace_app_name_in_custom_actions(args.app_name) diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec index 5686eea7abf..dd6b42c16b1 100644 --- a/res/rpm-flutter-suse.spec +++ b/res/rpm-flutter-suse.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.5 +Version: 1.4.1 Release: 0 Summary: RPM package License: GPL-3.0 @@ -9,6 +9,8 @@ Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstre Recommends: libayatana-appindicator3-1 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + %description The best open-source remote desktop client software, written in Rust. @@ -22,7 +24,7 @@ The best open-source remote desktop client software, written in Rust. %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/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/share/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" @@ -31,7 +33,7 @@ install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/25 install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" %files -/usr/lib/rustdesk/* +/usr/share/rustdesk/* /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -41,7 +43,6 @@ install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scal %changelog # let's skip this for now -# https://www.cnblogs.com/xingmuxin/p/8990255.html %pre # can do something for centos7 case "$1" in @@ -58,7 +59,7 @@ esac 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 +ln -sf /usr/share/rustdesk/rustdesk /usr/bin/rustdesk systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -81,12 +82,17 @@ esac case "$1" in 0) # for uninstall + rm /usr/bin/rustdesk || true + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true + rmdir /usr/share/rustdesk || true 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 + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true ;; esac diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec index 8862614ea32..b461507da0b 100644 --- a/res/rpm-flutter.spec +++ b/res/rpm-flutter.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.5 +Version: 1.4.1 Release: 0 Summary: RPM package License: GPL-3.0 @@ -9,6 +9,8 @@ Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva pam gstreamer1-plugins-b Recommends: libayatana-appindicator-gtk3 Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit) +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + %description The best open-source remote desktop client software, written in Rust. @@ -22,7 +24,7 @@ The best open-source remote desktop client software, written in Rust. %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/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/share/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" @@ -31,7 +33,7 @@ install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/25 install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg" %files -/usr/lib/rustdesk/* +/usr/share/rustdesk/* /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -41,7 +43,6 @@ install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scal %changelog # let's skip this for now -# https://www.cnblogs.com/xingmuxin/p/8990255.html %pre # can do something for centos7 case "$1" in @@ -58,7 +59,7 @@ esac 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 +ln -sf /usr/share/rustdesk/rustdesk /usr/bin/rustdesk systemctl daemon-reload systemctl enable rustdesk systemctl start rustdesk @@ -81,12 +82,17 @@ esac case "$1" in 0) # for uninstall + rm /usr/bin/rustdesk || true + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true + rmdir /usr/share/rustdesk || true 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 + rmdir /usr/lib/rustdesk || true + rmdir /usr/local/rustdesk || true ;; esac diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec index 46710e3c9d1..79b26d6f07c 100644 --- a/res/rpm-suse.spec +++ b/res/rpm-suse.spec @@ -6,6 +6,8 @@ License: GPL-3.0 Requires: gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire Recommends: libayatana-appindicator3-1 +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + %description The best open-source remote desktop client software, written in Rust. @@ -19,12 +21,12 @@ The best open-source remote desktop client software, written in Rust. %install mkdir -p %{buildroot}/usr/bin/ -mkdir -p %{buildroot}/usr/lib/rustdesk/ +mkdir -p %{buildroot}/usr/share/rustdesk/ mkdir -p %{buildroot}/usr/share/rustdesk/files/ mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/ mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk -install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so +install $HBB/libsciter-gtk.so %{buildroot}/usr/share/rustdesk/libsciter-gtk.so install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -33,7 +35,7 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk -/usr/lib/rustdesk/libsciter-gtk.so +/usr/share/rustdesk/libsciter-gtk.so /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -43,7 +45,6 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %changelog # let's skip this for now -# https://www.cnblogs.com/xingmuxin/p/8990255.html %pre # can do something for centos7 case "$1" in diff --git a/res/rpm.spec b/res/rpm.spec index 8d204eef279..a51646631a5 100644 --- a/res/rpm.spec +++ b/res/rpm.spec @@ -1,5 +1,5 @@ Name: rustdesk -Version: 1.3.5 +Version: 1.4.1 Release: 0 Summary: RPM package License: GPL-3.0 @@ -8,6 +8,8 @@ Vendor: rustdesk Requires: gtk3 libxcb libxdo libXfixes alsa-lib libva2 pam gstreamer1-plugins-base Recommends: libayatana-appindicator-gtk3 +# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/ + %description The best open-source remote desktop client software, written in Rust. @@ -21,12 +23,12 @@ The best open-source remote desktop client software, written in Rust. %install mkdir -p %{buildroot}/usr/bin/ -mkdir -p %{buildroot}/usr/lib/rustdesk/ +mkdir -p %{buildroot}/usr/share/rustdesk/ mkdir -p %{buildroot}/usr/share/rustdesk/files/ mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/ mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk -install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so +install $HBB/libsciter-gtk.so %{buildroot}/usr/share/rustdesk/libsciter-gtk.so install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -35,7 +37,7 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %files /usr/bin/rustdesk -/usr/lib/rustdesk/libsciter-gtk.so +/usr/share/rustdesk/libsciter-gtk.so /usr/share/rustdesk/files/rustdesk.service /usr/share/icons/hicolor/256x256/apps/rustdesk.png /usr/share/icons/hicolor/scalable/apps/rustdesk.svg @@ -46,7 +48,6 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/ %changelog # let's skip this for now -# https://www.cnblogs.com/xingmuxin/p/8990255.html %pre # can do something for centos7 case "$1" in diff --git a/res/rustdesk-link.desktop b/res/rustdesk-link.desktop index c64781aeb96..c7a9bd5cb7a 100644 --- a/res/rustdesk-link.desktop +++ b/res/rustdesk-link.desktop @@ -1,5 +1,5 @@ [Desktop Entry] -Name=RustDeskURL Scheme Handler +Name=RustDesk NoDisplay=true MimeType=x-scheme-handler/rustdesk; TryExec=rustdesk @@ -8,3 +8,4 @@ Icon=rustdesk Terminal=false Type=Application StartupNotify=false +StartupWMClass=rustdesk diff --git a/res/rustdesk.desktop b/res/rustdesk.desktop index cc72ff449bc..eb8c3b9be52 100644 --- a/res/rustdesk.desktop +++ b/res/rustdesk.desktop @@ -10,6 +10,7 @@ StartupNotify=true Categories=Network;RemoteAccess;GTK; Keywords=internet; Actions=new-window; +StartupWMClass=rustdesk X-Desktop-File-Install-Version=0.23 diff --git a/res/setup.nsi b/res/setup.nsi deleted file mode 100644 index 21cee15c80f..00000000000 --- a/res/setup.nsi +++ /dev/null @@ -1,178 +0,0 @@ -Unicode true - -#################################################################### -# Includes - -!include nsDialogs.nsh -!include MUI2.nsh -!include x64.nsh -!include LogicLib.nsh - -#################################################################### -# File Info - -!define PRODUCT_NAME "RustDesk" -!define PRODUCT_DESCRIPTION "Installer for ${PRODUCT_NAME}" -!define COPYRIGHT "Copyright © 2021" -!define VERSION "1.1.6" - -VIProductVersion "${VERSION}.0" -VIAddVersionKey "ProductName" "${PRODUCT_NAME}" -VIAddVersionKey "ProductVersion" "${VERSION}" -VIAddVersionKey "FileDescription" "${PRODUCT_DESCRIPTION}" -VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" -VIAddVersionKey "FileVersion" "${VERSION}.0" - -#################################################################### -# Installer Attributes - -Name "${PRODUCT_NAME}" -Outfile "rustdesk-${VERSION}-setup.exe" -Caption "Setup - ${PRODUCT_NAME}" -BrandingText "${PRODUCT_NAME}" - -ShowInstDetails show -RequestExecutionLevel admin -SetOverwrite on - -InstallDir "$PROGRAMFILES64\${PRODUCT_NAME}" - -#################################################################### -# Pages - -!define MUI_ICON "icon.ico" -!define MUI_ABORTWARNING -!define MUI_LANGDLL_ALLLANGUAGES -!define MUI_FINISHPAGE_SHOWREADME "" -!define MUI_FINISHPAGE_SHOWREADME_NOTCHECKED -!define MUI_FINISHPAGE_SHOWREADME_TEXT "Create desktop shortcut" -!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateDesktopShortcut -!define MUI_FINISHPAGE_RUN "$INSTDIR\${PRODUCT_NAME}.exe" - -!insertmacro MUI_PAGE_DIRECTORY -!insertmacro MUI_PAGE_INSTFILES -!insertmacro MUI_PAGE_FINISH - -#################################################################### -# Language - -!insertmacro MUI_LANGUAGE "English" ; The first language is the default language -!insertmacro MUI_LANGUAGE "French" -!insertmacro MUI_LANGUAGE "German" -!insertmacro MUI_LANGUAGE "Spanish" -!insertmacro MUI_LANGUAGE "SpanishInternational" -!insertmacro MUI_LANGUAGE "SimpChinese" -!insertmacro MUI_LANGUAGE "TradChinese" -!insertmacro MUI_LANGUAGE "Japanese" -!insertmacro MUI_LANGUAGE "Korean" -!insertmacro MUI_LANGUAGE "Italian" -!insertmacro MUI_LANGUAGE "Dutch" -!insertmacro MUI_LANGUAGE "Danish" -!insertmacro MUI_LANGUAGE "Swedish" -!insertmacro MUI_LANGUAGE "Norwegian" -!insertmacro MUI_LANGUAGE "NorwegianNynorsk" -!insertmacro MUI_LANGUAGE "Finnish" -!insertmacro MUI_LANGUAGE "Greek" -!insertmacro MUI_LANGUAGE "Russian" -!insertmacro MUI_LANGUAGE "Portuguese" -!insertmacro MUI_LANGUAGE "PortugueseBR" -!insertmacro MUI_LANGUAGE "Polish" -!insertmacro MUI_LANGUAGE "Ukrainian" -!insertmacro MUI_LANGUAGE "Czech" -!insertmacro MUI_LANGUAGE "Slovak" -!insertmacro MUI_LANGUAGE "Croatian" -!insertmacro MUI_LANGUAGE "Bulgarian" -!insertmacro MUI_LANGUAGE "Hungarian" -!insertmacro MUI_LANGUAGE "Thai" -!insertmacro MUI_LANGUAGE "Romanian" -!insertmacro MUI_LANGUAGE "Latvian" -!insertmacro MUI_LANGUAGE "Macedonian" -!insertmacro MUI_LANGUAGE "Estonian" -!insertmacro MUI_LANGUAGE "Turkish" -!insertmacro MUI_LANGUAGE "Lithuanian" -!insertmacro MUI_LANGUAGE "Slovenian" -!insertmacro MUI_LANGUAGE "Serbian" -!insertmacro MUI_LANGUAGE "SerbianLatin" -!insertmacro MUI_LANGUAGE "Arabic" -!insertmacro MUI_LANGUAGE "Farsi" -!insertmacro MUI_LANGUAGE "Hebrew" -!insertmacro MUI_LANGUAGE "Indonesian" -!insertmacro MUI_LANGUAGE "Mongolian" -!insertmacro MUI_LANGUAGE "Luxembourgish" -!insertmacro MUI_LANGUAGE "Albanian" -!insertmacro MUI_LANGUAGE "Breton" -!insertmacro MUI_LANGUAGE "Belarusian" -!insertmacro MUI_LANGUAGE "Icelandic" -!insertmacro MUI_LANGUAGE "Malay" -!insertmacro MUI_LANGUAGE "Bosnian" -!insertmacro MUI_LANGUAGE "Kurdish" -!insertmacro MUI_LANGUAGE "Irish" -!insertmacro MUI_LANGUAGE "Uzbek" -!insertmacro MUI_LANGUAGE "Galician" -!insertmacro MUI_LANGUAGE "Afrikaans" -!insertmacro MUI_LANGUAGE "Catalan" -!insertmacro MUI_LANGUAGE "Esperanto" -!insertmacro MUI_LANGUAGE "Asturian" -!insertmacro MUI_LANGUAGE "Basque" -!insertmacro MUI_LANGUAGE "Pashto" -!insertmacro MUI_LANGUAGE "ScotsGaelic" -!insertmacro MUI_LANGUAGE "Georgian" -!insertmacro MUI_LANGUAGE "Vietnamese" -!insertmacro MUI_LANGUAGE "Welsh" -!insertmacro MUI_LANGUAGE "Armenian" -!insertmacro MUI_LANGUAGE "Corsican" -!insertmacro MUI_LANGUAGE "Tatar" -!insertmacro MUI_LANGUAGE "Hindi" - - -#################################################################### -# Sections - -Section "Install" - SetOutPath $INSTDIR - - # Regkeys - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayIcon" "$INSTDIR\${PRODUCT_NAME}.exe" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME} (x64)" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${VERSION}" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" '"$INSTDIR\${PRODUCT_NAME}.exe" --uninstall' - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "InstallLocation" "$INSTDIR" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "Purslane Ltd." - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "HelpLink" "https://www.rustdesk.com/" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "https://www.rustdesk.com/" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLUpdateInfo" "https://www.rustdesk.com/" - - nsExec::Exec "taskkill /F /IM ${PRODUCT_NAME}.exe" - Sleep 500 ; Give time for process to be completely killed - File "${PRODUCT_NAME}.exe" - - SetShellVarContext all - CreateShortCut "$INSTDIR\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" - CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" - CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" - CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall ${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--uninstall" "msiexec.exe" - CreateShortCut "$SMSTARTUP\${PRODUCT_NAME} Tray.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" "--tray" - - nsExec::Exec 'sc create ${PRODUCT_NAME} start=auto DisplayName="${PRODUCT_NAME} Service" binPath= "\"$INSTDIR\${PRODUCT_NAME}.exe\" --service"' - nsExec::Exec 'netsh advfirewall firewall add rule name="${PRODUCT_NAME} Service" dir=in action=allow program="$INSTDIR\${PRODUCT_NAME}.exe" enable=yes' - nsExec::Exec 'sc start ${PRODUCT_NAME}' -SectionEnd - -#################################################################### -# Functions - -Function .onInit - # RustDesk is 64-bit only - ${IfNot} ${RunningX64} - MessageBox MB_ICONSTOP "${PRODUCT_NAME} is 64-bit only!" - Quit - ${EndIf} - ${DisableX64FSRedirection} - SetRegView 64 - - !insertmacro MUI_LANGDLL_DISPLAY -FunctionEnd - -Function CreateDesktopShortcut - CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" -FunctionEnd diff --git a/res/vcpkg/aom/portfile.cmake b/res/vcpkg/aom/portfile.cmake index 2df452a640e..24b02517348 100644 --- a/res/vcpkg/aom/portfile.cmake +++ b/res/vcpkg/aom/portfile.cmake @@ -8,16 +8,28 @@ vcpkg_find_acquire_program(PERL) get_filename_component(PERL_PATH ${PERL} DIRECTORY) vcpkg_add_to_path(${PERL_PATH}) -vcpkg_from_git( - OUT_SOURCE_PATH SOURCE_PATH - URL "https://aomedia.googlesource.com/aom" - REF 8ad484f8a18ed1853c094e7d3a4e023b2a92df28 # 3.9.1 - PATCHES - aom-uninitialized-pointer.diff - aom-avx2.diff - # Can be dropped when https://bugs.chromium.org/p/aomedia/issues/detail?id=3029 is merged into the upstream - aom-install.diff -) +if(DEFINED ENV{USE_AOM_391}) + vcpkg_from_git( + OUT_SOURCE_PATH SOURCE_PATH + URL "https://aomedia.googlesource.com/aom" + REF 8ad484f8a18ed1853c094e7d3a4e023b2a92df28 # 3.9.1 + PATCHES + aom-uninitialized-pointer.diff + aom-avx2.diff + aom-install.diff + ) +else() + vcpkg_from_git( + OUT_SOURCE_PATH SOURCE_PATH + URL "https://aomedia.googlesource.com/aom" + REF d6f30ae474dd6c358f26de0a0fc26a0d7340a84c # 3.11.0 + PATCHES + aom-uninitialized-pointer.diff + # aom-avx2.diff + # Can be dropped when https://bugs.chromium.org/p/aomedia/issues/detail?id=3029 is merged into the upstream + aom-install.diff + ) +endif() set(aom_target_cpu "") if(VCPKG_TARGET_IS_UWP OR (VCPKG_TARGET_IS_WINDOWS AND VCPKG_TARGET_ARCHITECTURE MATCHES "^arm")) diff --git a/res/vcpkg/aom/vcpkg.json b/res/vcpkg/aom/vcpkg.json index 78ccc898909..9ff755f6be6 100644 --- a/res/vcpkg/aom/vcpkg.json +++ b/res/vcpkg/aom/vcpkg.json @@ -1,6 +1,6 @@ { "name": "aom", - "version-semver": "3.9.1", + "version-semver": "3.11.0", "port-version": 0, "description": "AV1 codec library", "homepage": "https://aomedia.googlesource.com/aom", diff --git a/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch b/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch new file mode 100644 index 00000000000..ced7ba86be2 --- /dev/null +++ b/res/vcpkg/ffmpeg/0001-create-lib-libraries.patch @@ -0,0 +1,27 @@ +diff --git a/configure b/configure +index 1f0b9497cb..3243e23021 100644 +--- a/configure ++++ b/configure +@@ -5697,17 +5697,19 @@ case $target_os in + ;; + win32|win64) + disable symver +- if enabled shared; then ++# if enabled shared; then + # Link to the import library instead of the normal static library + # for shared libs. + LD_LIB='%.lib' + # Cannot build both shared and static libs with MSVC or icl. +- disable static +- fi ++# disable static ++# fi + ! enabled small && test_cmd $windres --version && enable gnu_windres + enabled x86_32 && check_ldflags -LARGEADDRESSAWARE + add_cppflags -DWIN32_LEAN_AND_MEAN + shlibdir_default="$bindir_default" ++ LIBPREF="" ++ LIBSUF=".lib" + SLIBPREF="" + SLIBSUF=".dll" + SLIBNAME_WITH_VERSION='$(SLIBPREF)$(FULLNAME)-$(LIBVERSION)$(SLIBSUF)' diff --git a/res/vcpkg/ffmpeg/0004-dependencies.patch b/res/vcpkg/ffmpeg/0004-dependencies.patch new file mode 100644 index 00000000000..f1f6e72bee3 --- /dev/null +++ b/res/vcpkg/ffmpeg/0004-dependencies.patch @@ -0,0 +1,65 @@ +diff --git a/configure b/configure +index a8b74e0..c99f41c 100755 +--- a/configure ++++ b/configure +@@ -6633,7 +6633,7 @@ fi + + enabled zlib && { check_pkg_config zlib zlib "zlib.h" zlibVersion || + check_lib zlib zlib.h zlibVersion -lz; } +-enabled bzlib && check_lib bzlib bzlib.h BZ2_bzlibVersion -lbz2 ++enabled bzlib && require_pkg_config bzlib bzip2 bzlib.h BZ2_bzlibVersion + enabled lzma && check_lib lzma lzma.h lzma_version_number -llzma + + enabled zlib && test_exec $zlib_extralibs <= 3.98.3" lame/lame.h lame_set_VBR_quality -lmp3lame $libm_extralibs ++enabled libmp3lame && { check_lib libmp3lame lame/lame.h lame_set_VBR_quality -lmp3lame $libm_extralibs || ++ require libmp3lame lame/lame.h lame_set_VBR_quality -llibmp3lame-static -llibmpghip-static $libm_extralibs; } + enabled libmysofa && { check_pkg_config libmysofa libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine || + require libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine -lmysofa $zlib_extralibs; } + enabled libnpp && { check_lib libnpp npp.h nppGetLibVersion -lnppig -lnppicc -lnppc -lnppidei -lnppif || +@@ -6772,7 +6773,7 @@ require_pkg_config libopencv opencv opencv/cxcore.h cvCreateImageHeader; } + enabled libopenh264 && require_pkg_config libopenh264 "openh264 >= 1.3.0" wels/codec_api.h WelsGetCodecVersion + enabled libopenjpeg && { check_pkg_config libopenjpeg "libopenjp2 >= 2.1.0" openjpeg.h opj_version || + { require_pkg_config libopenjpeg "libopenjp2 >= 2.1.0" openjpeg.h opj_version -DOPJ_STATIC && add_cppflags -DOPJ_STATIC; } } +-enabled libopenmpt && require_pkg_config libopenmpt "libopenmpt >= 0.2.6557" libopenmpt/libopenmpt.h openmpt_module_create -lstdc++ && append libopenmpt_extralibs "-lstdc++" ++enabled libopenmpt && require_pkg_config libopenmpt "libopenmpt >= 0.2.6557" libopenmpt/libopenmpt.h openmpt_module_create + enabled libopenvino && { { check_pkg_config libopenvino openvino openvino/c/openvino.h ov_core_create && enable openvino2; } || + { check_pkg_config libopenvino openvino c_api/ie_c_api.h ie_c_api_version || + require libopenvino c_api/ie_c_api.h ie_c_api_version -linference_engine_c_api; } } +@@ -6796,8 +6797,8 @@ enabled libshaderc && require_pkg_config spirv_compiler "shaderc >= 2019. + enabled libshine && require_pkg_config libshine shine shine/layer3.h shine_encode_buffer + enabled libsmbclient && { check_pkg_config libsmbclient smbclient libsmbclient.h smbc_init || + require libsmbclient libsmbclient.h smbc_init -lsmbclient; } +-enabled libsnappy && require libsnappy snappy-c.h snappy_compress -lsnappy -lstdc++ +-enabled libsoxr && require libsoxr soxr.h soxr_create -lsoxr ++enabled libsnappy && require_pkg_config libsnappy snappy snappy-c.h snappy_compress ++enabled libsoxr && require libsoxr soxr.h soxr_create -lsoxr $libm_extralibs + enabled libssh && require_pkg_config libssh "libssh >= 0.6.0" libssh/sftp.h sftp_init + enabled libspeex && require_pkg_config libspeex speex speex/speex.h speex_decoder_init + enabled libsrt && require_pkg_config libsrt "srt >= 1.3.0" srt/srt.h srt_socket +@@ -6880,6 +6881,8 @@ enabled openal && { check_pkg_config openal "openal >= 1.1" "AL/al.h" + enabled opencl && { check_pkg_config opencl OpenCL CL/cl.h clEnqueueNDRangeKernel || + check_lib opencl OpenCL/cl.h clEnqueueNDRangeKernel "-framework OpenCL" || + check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL || ++ check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL -lAdvapi32 -lOle32 -lCfgmgr32|| ++ check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL -pthread -ldl || + die "ERROR: opencl not found"; } && + { test_cpp_condition "OpenCL/cl.h" "defined(CL_VERSION_1_2)" || + test_cpp_condition "CL/cl.h" "defined(CL_VERSION_1_2)" || +@@ -7204,10 +7207,10 @@ enabled amf && + "(AMF_VERSION_MAJOR << 48 | AMF_VERSION_MINOR << 32 | AMF_VERSION_RELEASE << 16 | AMF_VERSION_BUILD_NUM) >= 0x0001000400210000" + + # Funny iconv installations are not unusual, so check it after all flags have been set +-if enabled libc_iconv; then ++if enabled libc_iconv && disabled iconv; then + check_func_headers iconv.h iconv + elif enabled iconv; then +- check_func_headers iconv.h iconv || check_lib iconv iconv.h iconv -liconv ++ check_func_headers iconv.h iconv || check_lib iconv iconv.h iconv -liconv || check_lib iconv iconv.h iconv -liconv -lcharset + fi + + enabled debug && add_cflags -g"$debuglevel" && add_asflags -g"$debuglevel" diff --git a/res/vcpkg/ffmpeg/0005-fix-nasm.patch b/res/vcpkg/ffmpeg/0005-fix-nasm.patch index 9308e714a6b..68b7503b244 100644 --- a/res/vcpkg/ffmpeg/0005-fix-nasm.patch +++ b/res/vcpkg/ffmpeg/0005-fix-nasm.patch @@ -1,55 +1,78 @@ -diff --git a/libavcodec/x86/Makefile b/libavcodec/x86/Makefile ---- a/libavcodec/x86/Makefile -+++ b/libavcodec/x86/Makefile -@@ -158,6 +158,8 @@ X86ASM-OBJS-$(CONFIG_ALAC_DECODER) += x86/alacdsp.o - X86ASM-OBJS-$(CONFIG_APNG_DECODER) += x86/pngdsp.o - X86ASM-OBJS-$(CONFIG_CAVS_DECODER) += x86/cavsidct.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_CFHD_ENCODER) += x86/cfhdencdsp.o -+endif - X86ASM-OBJS-$(CONFIG_CFHD_DECODER) += x86/cfhddsp.o - X86ASM-OBJS-$(CONFIG_DCA_DECODER) += x86/dcadsp.o x86/synth_filter.o - X86ASM-OBJS-$(CONFIG_DIRAC_DECODER) += x86/diracdsp.o \ -@@ -175,15 +177,21 @@ x86/hevc_sao_10bit.o - X86ASM-OBJS-$(CONFIG_JPEG2000_DECODER) += x86/jpeg2000dsp.o - X86ASM-OBJS-$(CONFIG_LSCR_DECODER) += x86/pngdsp.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_MLP_DECODER) += x86/mlpdsp.o -+endif - X86ASM-OBJS-$(CONFIG_MPEG4_DECODER) += x86/xvididct.o - X86ASM-OBJS-$(CONFIG_PNG_DECODER) += x86/pngdsp.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_PRORES_DECODER) += x86/proresdsp.o - X86ASM-OBJS-$(CONFIG_PRORES_LGPL_DECODER) += x86/proresdsp.o -+endif - X86ASM-OBJS-$(CONFIG_RV40_DECODER) += x86/rv40dsp.o - X86ASM-OBJS-$(CONFIG_SBC_ENCODER) += x86/sbcdsp.o - X86ASM-OBJS-$(CONFIG_SVQ1_ENCODER) += x86/svq1enc.o - X86ASM-OBJS-$(CONFIG_TAK_DECODER) += x86/takdsp.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_TRUEHD_DECODER) += x86/mlpdsp.o -+endif - X86ASM-OBJS-$(CONFIG_TTA_DECODER) += x86/ttadsp.o - X86ASM-OBJS-$(CONFIG_TTA_ENCODER) += x86/ttaencdsp.o - X86ASM-OBJS-$(CONFIG_UTVIDEO_DECODER) += x86/utvideodsp.o -diff --git a/libavfilter/x86/Makefile b/libavfilter/x86/Makefile ---- a/libavfilter/x86/Makefile -+++ b/libavfilter/x86/Makefile -@@ -44,6 +44,8 @@ - X86ASM-OBJS-$(CONFIG_AFIR_FILTER) += x86/af_afir.o - X86ASM-OBJS-$(CONFIG_ANLMDN_FILTER) += x86/af_anlmdn.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_ATADENOISE_FILTER) += x86/vf_atadenoise.o -+endif - X86ASM-OBJS-$(CONFIG_BLEND_FILTER) += x86/vf_blend.o - X86ASM-OBJS-$(CONFIG_BWDIF_FILTER) += x86/vf_bwdif.o - X86ASM-OBJS-$(CONFIG_COLORSPACE_FILTER) += x86/colorspacedsp.o -@@ -62,6 +62,8 @@ X86ASM-OBJS-$(CONFIG_LUT3D_FILTER) += x86/vf_lut3d.o - X86ASM-OBJS-$(CONFIG_MASKEDCLAMP_FILTER) += x86/vf_maskedclamp.o - X86ASM-OBJS-$(CONFIG_MASKEDMERGE_FILTER) += x86/vf_maskedmerge.o -+ifdef ARCH_X86_64 - X86ASM-OBJS-$(CONFIG_NLMEANS_FILTER) += x86/vf_nlmeans.o -+endif - X86ASM-OBJS-$(CONFIG_OVERLAY_FILTER) += x86/vf_overlay.o - X86ASM-OBJS-$(CONFIG_PP7_FILTER) += x86/vf_pp7.o - X86ASM-OBJS-$(CONFIG_PSNR_FILTER) += x86/vf_psnr.o +diff --git a/libavcodec/x86/mlpdsp.asm b/libavcodec/x86/mlpdsp.asm +index 3dc641e..609b834 100644 +--- a/libavcodec/x86/mlpdsp.asm ++++ b/libavcodec/x86/mlpdsp.asm +@@ -23,7 +23,9 @@ + + SECTION .text + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++mlpdsp_placeholder: times 4 db 0 ++%else + + %macro SHLX 2 + %if cpuflag(bmi2) +diff --git a/libavcodec/x86/proresdsp.asm b/libavcodec/x86/proresdsp.asm +index 65c9fad..5ad73f3 100644 +--- a/libavcodec/x86/proresdsp.asm ++++ b/libavcodec/x86/proresdsp.asm +@@ -24,7 +24,10 @@ + + %include "libavutil/x86/x86util.asm" + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++SECTION .rdata ++proresdsp_placeholder: times 4 db 0 ++%else + + SECTION_RODATA + +diff --git a/libavcodec/x86/vvc/vvc_mc.asm b/libavcodec/x86/vvc/vvc_mc.asm +index 30aa97c..3975f98 100644 +--- a/libavcodec/x86/vvc/vvc_mc.asm ++++ b/libavcodec/x86/vvc/vvc_mc.asm +@@ -31,7 +31,9 @@ + + SECTION_RODATA 32 + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++vvc_mc_placeholder: times 4 db 0 ++%else + + %if HAVE_AVX2_EXTERNAL + +diff --git a/libavfilter/x86/vf_atadenoise.asm b/libavfilter/x86/vf_atadenoise.asm +index 4945ad3..748b65a 100644 +--- a/libavfilter/x86/vf_atadenoise.asm ++++ b/libavfilter/x86/vf_atadenoise.asm +@@ -20,7 +20,10 @@ + ;* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + ;****************************************************************************** + +-%if ARCH_X86_64 ++%ifn ARCH_X86_64 ++SECTION .rdata ++vf_atadenoise_placeholder: times 4 db 0 ++%else + + %include "libavutil/x86/x86util.asm" + +diff --git a/libavfilter/x86/vf_nlmeans.asm b/libavfilter/x86/vf_nlmeans.asm +index 8f57801..9aef3a4 100644 +--- a/libavfilter/x86/vf_nlmeans.asm ++++ b/libavfilter/x86/vf_nlmeans.asm +@@ -21,7 +21,10 @@ + + %include "libavutil/x86/x86util.asm" + +-%if HAVE_AVX2_EXTERNAL && ARCH_X86_64 ++%ifn HAVE_AVX2_EXTERNAL && ARCH_X86_64 ++SECTION .rdata ++vf_nlmeans_placeholder: times 4 db 0 ++%else + + SECTION_RODATA 32 + diff --git a/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch b/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch new file mode 100644 index 00000000000..c22f9c1999d --- /dev/null +++ b/res/vcpkg/ffmpeg/0007-fix-lib-naming.patch @@ -0,0 +1,12 @@ +diff --git a/configure b/configure +index d6c4388..75b96c3 100644 +--- a/configure ++++ b/configure +@@ -4781,6 +4781,7 @@ msvc_common_flags(){ + -mfp16-format=*) ;; + -lz) echo zlib.lib ;; + -lx264) echo libx264.lib ;; ++ -lmp3lame) echo libmp3lame.lib ;; + -lstdc++) ;; + -l*) echo ${flag#-l}.lib ;; + -LARGEADDRESSAWARE) echo $flag ;; diff --git a/res/vcpkg/ffmpeg/0012-Fix-ssl-110-detection.patch b/res/vcpkg/ffmpeg/0012-Fix-ssl-110-detection.patch deleted file mode 100644 index b2e5501a13a..00000000000 --- a/res/vcpkg/ffmpeg/0012-Fix-ssl-110-detection.patch +++ /dev/null @@ -1,14 +0,0 @@ -diff --git a/configure b/configure -index 2be953f7e7..e075949ffc 100755 ---- a/configure -+++ b/configure -@@ -6497,6 +6497,7 @@ enabled openssl && { { check_pkg_config openssl "openssl >= 3.0.0 - { enabled gplv3 || ! enabled gpl || enabled nonfree || die "ERROR: OpenSSL >=3.0.0 requires --enable-version3"; }; } || - { enabled gpl && ! enabled nonfree && die "ERROR: OpenSSL <3.0.0 is incompatible with the gpl"; } || - check_pkg_config openssl openssl openssl/ssl.h OPENSSL_init_ssl || - check_pkg_config openssl openssl openssl/ssl.h SSL_library_init || -+ check_lib openssl openssl/ssl.h OPENSSL_init_ssl -lssl -lcrypto $pthreads_extralibs -ldl || - check_lib openssl openssl/ssl.h OPENSSL_init_ssl -lssl -lcrypto || - check_lib openssl openssl/ssl.h SSL_library_init -lssl -lcrypto || - check_lib openssl openssl/ssl.h SSL_library_init -lssl32 -leay32 || - diff --git a/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch b/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch new file mode 100644 index 00000000000..f47e82ed8a2 --- /dev/null +++ b/res/vcpkg/ffmpeg/0020-fix-aarch64-libswscale.patch @@ -0,0 +1,28 @@ +diff --git a/libswscale/aarch64/yuv2rgb_neon.S b/libswscale/aarch64/yuv2rgb_neon.S +index 89d69e7f6c..4bc1607a7a 100644 +--- a/libswscale/aarch64/yuv2rgb_neon.S ++++ b/libswscale/aarch64/yuv2rgb_neon.S +@@ -169,19 +169,19 @@ function ff_\ifmt\()_to_\ofmt\()_neon, export=1 + sqdmulh v26.8h, v26.8h, v0.8h // ((Y1*(1<<3) - y_offset) * y_coeff) >> 15 + sqdmulh v27.8h, v27.8h, v0.8h // ((Y2*(1<<3) - y_offset) * y_coeff) >> 15 + +-.ifc \ofmt,argb // 1 2 3 0 ++.ifc \ofmt,argb + compute_rgba v5.8b,v6.8b,v7.8b,v4.8b, v17.8b,v18.8b,v19.8b,v16.8b + .endif + +-.ifc \ofmt,rgba // 0 1 2 3 ++.ifc \ofmt,rgba + compute_rgba v4.8b,v5.8b,v6.8b,v7.8b, v16.8b,v17.8b,v18.8b,v19.8b + .endif + +-.ifc \ofmt,abgr // 3 2 1 0 ++.ifc \ofmt,abgr + compute_rgba v7.8b,v6.8b,v5.8b,v4.8b, v19.8b,v18.8b,v17.8b,v16.8b + .endif + +-.ifc \ofmt,bgra // 2 1 0 3 ++.ifc \ofmt,bgra + compute_rgba v6.8b,v5.8b,v4.8b,v7.8b, v18.8b,v17.8b,v16.8b,v19.8b + .endif + diff --git a/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch b/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch new file mode 100644 index 00000000000..dbce2f53b8e --- /dev/null +++ b/res/vcpkg/ffmpeg/0024-fix-osx-host-c11.patch @@ -0,0 +1,15 @@ +diff --git a/configure b/configure +index 4f5353f84b..dd9147c677 100755 +--- a/configure ++++ b/configure +@@ -5607,8 +5607,8 @@ check_cppflags -D_FILE_OFFSET_BITS=64 + check_cppflags -D_LARGEFILE_SOURCE + + add_host_cppflags -D_ISOC11_SOURCE + check_host_cflags_cc -std=$stdc ctype.h "__STDC_VERSION__ >= 201112L" || +- check_host_cflags_cc -std=c11 ctype.h "__STDC_VERSION__ >= 201112L" || die "Host compiler lacks C11 support" ++ check_host_cflags_cc -std=c11 ctype.h "__STDC_VERSION__ >= 201112L" + + check_host_cflags -Wall + check_host_cflags $host_cflags_speed + diff --git a/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch b/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch new file mode 100644 index 00000000000..c2e1d8ff0d7 --- /dev/null +++ b/res/vcpkg/ffmpeg/0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch @@ -0,0 +1,35 @@ +diff --git a/libavformat/avformat.h b/libavformat/avformat.h +index cd7b0d941c..b4a6dce885 100644 +--- a/libavformat/avformat.h ++++ b/libavformat/avformat.h +@@ -1169,7 +1169,11 @@ typedef struct AVStreamGroup { + } AVStreamGroup; + + struct AVCodecParserContext *av_stream_get_parser(const AVStream *s); + ++// Chromium: We use the internal field first_dts vvv ++int64_t av_stream_get_first_dts(const AVStream *st); ++// Chromium: We use the internal field first_dts ^^^ ++ + #define AV_PROGRAM_RUNNING 1 + + /** +diff --git a/libavformat/mux_utils.c b/libavformat/mux_utils.c +index de7580c32d..0ef0fe530e 100644 +--- a/libavformat/mux_utils.c ++++ b/libavformat/mux_utils.c +@@ -29,7 +29,14 @@ #include "avformat.h" + #include "avio.h" + #include "internal.h" + #include "mux.h" + ++// Chromium: We use the internal field first_dts vvv ++int64_t av_stream_get_first_dts(const AVStream *st) ++{ ++ return cffstream(st)->first_dts; ++} ++// Chromium: We use the internal field first_dts ^^^ ++ + int avformat_query_codec(const AVOutputFormat *ofmt, enum AVCodecID codec_id, + int std_compliance) + { diff --git a/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch b/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch new file mode 100644 index 00000000000..b22b40d1f37 --- /dev/null +++ b/res/vcpkg/ffmpeg/0041-add-const-for-opengl-definition.patch @@ -0,0 +1,13 @@ +diff --git a/libavdevice/opengl_enc.c b/libavdevice/opengl_enc.c +index b2ac6eb..6351614 100644 +--- a/libavdevice/opengl_enc.c ++++ b/libavdevice/opengl_enc.c +@@ -116,7 +116,7 @@ typedef void (APIENTRY *FF_PFNGLATTACHSHADERPROC) (GLuint program, GLuint shad + typedef GLuint (APIENTRY *FF_PFNGLCREATESHADERPROC) (GLenum type); + typedef void (APIENTRY *FF_PFNGLDELETESHADERPROC) (GLuint shader); + typedef void (APIENTRY *FF_PFNGLCOMPILESHADERPROC) (GLuint shader); +-typedef void (APIENTRY *FF_PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* *string, const GLint *length); ++typedef void (APIENTRY *FF_PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* const *string, const GLint *length); + typedef void (APIENTRY *FF_PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params); + typedef void (APIENTRY *FF_PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, char *infoLog); + diff --git a/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch b/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch new file mode 100644 index 00000000000..6ff63c3718d --- /dev/null +++ b/res/vcpkg/ffmpeg/0042-fix-arm64-linux.patch @@ -0,0 +1,9 @@ +diff --git a/ffbuild/libversion.sh b/ffbuild/libversion.sh +index a94ab58..ecaa90c 100644 +--- a/ffbuild/libversion.sh ++++ b/ffbuild/libversion.sh +@@ -1,3 +1,4 @@ ++#!/bin/sh + toupper(){ + echo "$@" | tr abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ + } diff --git a/res/vcpkg/ffmpeg/0043-fix-miss-head.patch b/res/vcpkg/ffmpeg/0043-fix-miss-head.patch new file mode 100644 index 00000000000..bad42798c83 --- /dev/null +++ b/res/vcpkg/ffmpeg/0043-fix-miss-head.patch @@ -0,0 +1,12 @@ +diff --git a/libavfilter/textutils.c b/libavfilter/textutils.c +index ef658d0..c61b0ad 100644 +--- a/libavfilter/textutils.c ++++ b/libavfilter/textutils.c +@@ -31,6 +31,7 @@ + #include "libavutil/file.h" + #include "libavutil/mem.h" + #include "libavutil/time.h" ++#include "libavutil/time_internal.h" + + static int ff_expand_text_function_internal(FFExpandTextContext *expand_text, AVBPrint *bp, + char *name, unsigned argc, char **argv) diff --git a/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch b/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch index 5431b3edd05..4fbce0d4849 100644 --- a/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch +++ b/res/vcpkg/ffmpeg/patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch @@ -1,7 +1,7 @@ -From f6988e5424e041ff6f6e241f4d8fa69a04c05e64 Mon Sep 17 00:00:00 2001 +From da6921d5bcb50961193526f47aa2dbe71ee5fe81 Mon Sep 17 00:00:00 2001 From: 21pages -Date: Thu, 5 Sep 2024 16:26:20 +0800 -Subject: [PATCH 1/3] avcodec/amfenc: add query_timeout option for h264/hevc +Date: Tue, 10 Dec 2024 13:40:46 +0800 +Subject: [PATCH 1/5] avcodec/amfenc: add query_timeout option for h264/hevc Signed-off-by: 21pages --- @@ -11,10 +11,10 @@ Signed-off-by: 21pages 3 files changed, 9 insertions(+) diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 2dbd378ef8..d636673a9d 100644 +index d985d01bb1..320c66919e 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -89,6 +89,7 @@ typedef struct AmfContext { +@@ -91,6 +91,7 @@ typedef struct AmfContext { int quality; int b_frame_delta_qp; int ref_b_frame_delta_qp; @@ -23,40 +23,40 @@ index 2dbd378ef8..d636673a9d 100644 // Dynamic options, can be set after Init() call diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index c1d5f4054e..415828f005 100644 +index 8edd39c633..6ad4961b2f 100644 --- a/libavcodec/amfenc_h264.c +++ b/libavcodec/amfenc_h264.c -@@ -135,6 +135,7 @@ static const AVOption options[] = { - { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, +@@ -137,6 +137,7 @@ static const AVOption options[] = { + { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg) , AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE }, + { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, //Pre Analysis options { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, -@@ -222,6 +223,9 @@ FF_ENABLE_DEPRECATION_WARNINGS +@@ -228,6 +229,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate); + if (ctx->query_timeout >= 0) -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); ++ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout); + switch (avctx->profile) { case AV_PROFILE_H264_BASELINE: profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE; diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 33a167aa52..65259d7153 100644 +index 4898824f3a..22cb95c7ce 100644 --- a/libavcodec/amfenc_hevc.c +++ b/libavcodec/amfenc_hevc.c -@@ -98,6 +98,7 @@ static const AVOption options[] = { - { "aud", "Inserts AU Delimiter NAL unit", OFFSET(aud) ,AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, +@@ -104,6 +104,7 @@ static const AVOption options[] = { + { "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg), AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE }, + { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE }, //Pre Analysis options { "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE }, -@@ -183,6 +184,9 @@ FF_ENABLE_DEPRECATION_WARNINGS +@@ -194,6 +195,9 @@ FF_ENABLE_DEPRECATION_WARNINGS AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate); diff --git a/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch index 62b86d08bd6..f2ec5df321e 100644 --- a/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch +++ b/res/vcpkg/ffmpeg/patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch @@ -1,7 +1,7 @@ -From 6e76c57cf2c0e790228f19c88089eef110fd74aa Mon Sep 17 00:00:00 2001 +From 8d061adb7b00fc765b8001307c025437ef1cad88 Mon Sep 17 00:00:00 2001 From: 21pages Date: Thu, 5 Sep 2024 16:32:16 +0800 -Subject: [PATCH 2/3] libavcodec/amfenc: reconfig when bitrate change +Subject: [PATCH 2/5] libavcodec/amfenc: reconfig when bitrate change Signed-off-by: 21pages --- @@ -10,10 +10,10 @@ Signed-off-by: 21pages 2 files changed, 21 insertions(+) diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c -index 061859f85c..97587fe66b 100644 +index a47aea6108..f70f0109f6 100644 --- a/libavcodec/amfenc.c +++ b/libavcodec/amfenc.c -@@ -222,6 +222,7 @@ static int amf_init_context(AVCodecContext *avctx) +@@ -275,6 +275,7 @@ static int amf_init_context(AVCodecContext *avctx) ctx->hwsurfaces_in_queue = 0; ctx->hwsurfaces_in_queue_max = 16; @@ -21,7 +21,7 @@ index 061859f85c..97587fe66b 100644 // configure AMF logger // the return of these functions indicates old state and do not affect behaviour -@@ -583,6 +584,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe +@@ -640,6 +641,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe frame_ref_storage_buffer->pVtbl->Release(frame_ref_storage_buffer); } @@ -45,7 +45,7 @@ index 061859f85c..97587fe66b 100644 int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) { AmfContext *ctx = avctx->priv_data; -@@ -596,6 +614,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) +@@ -653,6 +671,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) int query_output_data_flag = 0; AMF_RESULT res_resubmit; @@ -55,10 +55,10 @@ index 061859f85c..97587fe66b 100644 return AVERROR(EINVAL); diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index d636673a9d..09506ee2e0 100644 +index 320c66919e..481e0fb75d 100644 --- a/libavcodec/amfenc.h +++ b/libavcodec/amfenc.h -@@ -113,6 +113,7 @@ typedef struct AmfContext { +@@ -115,6 +115,7 @@ typedef struct AmfContext { int max_b_frames; int qvbr_quality_level; int hw_high_motion_quality_boost; diff --git a/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch b/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch deleted file mode 100644 index 9bcb6e6926c..00000000000 --- a/res/vcpkg/ffmpeg/patch/0003-amf-colorspace.patch +++ /dev/null @@ -1,161 +0,0 @@ -From 14b77216106eaaff9cf701528039ae4264eaf420 Mon Sep 17 00:00:00 2001 -From: 21pages -Date: Thu, 5 Sep 2024 16:41:59 +0800 -Subject: [PATCH 3/3] amf colorspace - -Signed-off-by: 21pages ---- - libavcodec/amfenc.h | 1 + - libavcodec/amfenc_h264.c | 40 ++++++++++++++++++++++++++++++++++ - libavcodec/amfenc_hevc.c | 47 ++++++++++++++++++++++++++++++++++++++++ - 3 files changed, 88 insertions(+) - -diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h -index 09506ee2e0..7f458b14f7 100644 ---- a/libavcodec/amfenc.h -+++ b/libavcodec/amfenc.h -@@ -24,6 +24,7 @@ - #include - #include - #include -+#include - - #include "libavutil/fifo.h" - -diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c -index 415828f005..7da5a96c71 100644 ---- a/libavcodec/amfenc_h264.c -+++ b/libavcodec/amfenc_h264.c -@@ -200,6 +200,9 @@ static av_cold int amf_encode_init_h264(AVCodecContext *avctx) - AMFRate framerate; - AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); - int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; -+ amf_int64 color_depth; -+ amf_int64 color_profile; -+ enum AVPixelFormat pix_fmt; - - if (avctx->framerate.num > 0 && avctx->framerate.den > 0) { - framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); -@@ -266,10 +269,47 @@ FF_ENABLE_DEPRECATION_WARNINGS - AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_ASPECT_RATIO, ratio); - } - -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_UNKNOWN; - /// Color Range (Partial/TV/MPEG or Full/PC/JPEG) - if (avctx->color_range == AVCOL_RANGE_JPEG) { - AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_FULL_RANGE_COLOR, 1); -+ switch (avctx->colorspace) { -+ case AVCOL_SPC_SMPTE170M: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_601; -+ break; -+ case AVCOL_SPC_BT709: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_709; -+ break; -+ case AVCOL_SPC_BT2020_NCL: -+ case AVCOL_SPC_BT2020_CL: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_2020; -+ break; -+ } -+ } else { -+ AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_FULL_RANGE_COLOR, 0); -+ switch (avctx->colorspace) { -+ case AVCOL_SPC_SMPTE170M: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_601; -+ break; -+ case AVCOL_SPC_BT709: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_709; -+ break; -+ case AVCOL_SPC_BT2020_NCL: -+ case AVCOL_SPC_BT2020_CL: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020; -+ break; -+ } - } -+ pix_fmt = avctx->hw_frames_ctx ? ((AVHWFramesContext*)avctx->hw_frames_ctx->data)->sw_format : avctx->pix_fmt; -+ color_depth = AMF_COLOR_BIT_DEPTH_8; -+ if (pix_fmt == AV_PIX_FMT_P010) { -+ color_depth = AMF_COLOR_BIT_DEPTH_10; -+ } -+ -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_COLOR_BIT_DEPTH, color_depth); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PROFILE, color_profile); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_TRANSFER_CHARACTERISTIC, (amf_int64)avctx->color_trc); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_OUTPUT_COLOR_PRIMARIES, (amf_int64)avctx->color_primaries); - - // autodetect rate control method - if (ctx->rate_control_mode == AMF_VIDEO_ENCODER_RATE_CONTROL_METHOD_UNKNOWN) { -diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c -index 65259d7153..7c930d3ccc 100644 ---- a/libavcodec/amfenc_hevc.c -+++ b/libavcodec/amfenc_hevc.c -@@ -161,6 +161,9 @@ static av_cold int amf_encode_init_hevc(AVCodecContext *avctx) - AMFRate framerate; - AMFSize framesize = AMFConstructSize(avctx->width, avctx->height); - int deblocking_filter = (avctx->flags & AV_CODEC_FLAG_LOOP_FILTER) ? 1 : 0; -+ amf_int64 color_depth; -+ amf_int64 color_profile; -+ enum AVPixelFormat pix_fmt; - - if (avctx->framerate.num > 0 && avctx->framerate.den > 0) { - framerate = AMFConstructRate(avctx->framerate.num, avctx->framerate.den); -@@ -191,6 +194,9 @@ FF_ENABLE_DEPRECATION_WARNINGS - case AV_PROFILE_HEVC_MAIN: - profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN; - break; -+ case AV_PROFILE_HEVC_MAIN_10: -+ profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN_10; -+ break; - default: - break; - } -@@ -219,6 +225,47 @@ FF_ENABLE_DEPRECATION_WARNINGS - AMF_ASSIGN_PROPERTY_RATIO(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_ASPECT_RATIO, ratio); - } - -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_UNKNOWN; -+ if (avctx->color_range == AVCOL_RANGE_JPEG) { -+ AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE, 1); -+ switch (avctx->colorspace) { -+ case AVCOL_SPC_SMPTE170M: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_601; -+ break; -+ case AVCOL_SPC_BT709: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_709; -+ break; -+ case AVCOL_SPC_BT2020_NCL: -+ case AVCOL_SPC_BT2020_CL: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_FULL_2020; -+ break; -+ } -+ } else { -+ AMF_ASSIGN_PROPERTY_BOOL(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_NOMINAL_RANGE, 0); -+ switch (avctx->colorspace) { -+ case AVCOL_SPC_SMPTE170M: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_601; -+ break; -+ case AVCOL_SPC_BT709: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_709; -+ break; -+ case AVCOL_SPC_BT2020_NCL: -+ case AVCOL_SPC_BT2020_CL: -+ color_profile = AMF_VIDEO_CONVERTER_COLOR_PROFILE_2020; -+ break; -+ } -+ } -+ pix_fmt = avctx->hw_frames_ctx ? ((AVHWFramesContext*)avctx->hw_frames_ctx->data)->sw_format : avctx->pix_fmt; -+ color_depth = AMF_COLOR_BIT_DEPTH_8; -+ if (pix_fmt == AV_PIX_FMT_P010) { -+ color_depth = AMF_COLOR_BIT_DEPTH_10; -+ } -+ -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_COLOR_BIT_DEPTH, color_depth); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_OUTPUT_COLOR_PROFILE, color_profile); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_OUTPUT_TRANSFER_CHARACTERISTIC, (amf_int64)avctx->color_trc); -+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_OUTPUT_COLOR_PRIMARIES, (amf_int64)avctx->color_primaries); -+ - // Picture control properties - AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_NUM_GOPS_PER_IDR, ctx->gops_per_idr); - AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_GOP_SIZE, avctx->gop_size); --- -2.43.0.windows.1 - diff --git a/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch index a0b337c5bae..77b41a7ada8 100644 --- a/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch +++ b/res/vcpkg/ffmpeg/patch/0004-videotoolbox-changing-bitrate.patch @@ -1,18 +1,18 @@ -From 7f12898fe8fd12c1042c98b34825ab2eda89e54d Mon Sep 17 00:00:00 2001 +From d74de94b49efcf7a0b25673ace6016938d1b9272 Mon Sep 17 00:00:00 2001 From: 21pages -Date: Sun, 24 Nov 2024 12:58:39 +0800 -Subject: [PATCH 1/2] videotoolbox changing bitrate +Date: Tue, 10 Dec 2024 14:12:01 +0800 +Subject: [PATCH 3/5] videotoolbox changing bitrate Signed-off-by: 21pages --- - libavcodec/videotoolboxenc.c | 39 ++++++++++++++++++++++++++++++++++++ - 1 file changed, 39 insertions(+) + libavcodec/videotoolboxenc.c | 40 ++++++++++++++++++++++++++++++++++++ + 1 file changed, 40 insertions(+) diff --git a/libavcodec/videotoolboxenc.c b/libavcodec/videotoolboxenc.c -index 5ea9afee22..89c927cdcc 100644 +index da7b291b03..3c866177f5 100644 --- a/libavcodec/videotoolboxenc.c +++ b/libavcodec/videotoolboxenc.c -@@ -278,6 +278,8 @@ typedef struct VTEncContext { +@@ -279,6 +279,8 @@ typedef struct VTEncContext { int max_slice_bytes; int power_efficient; int max_ref_frames; @@ -20,8 +20,8 @@ index 5ea9afee22..89c927cdcc 100644 + int last_bit_rate; } VTEncContext; - static int vt_dump_encoder(AVCodecContext *avctx) -@@ -1174,6 +1176,7 @@ static int vtenc_create_encoder(AVCodecContext *avctx, + static void vtenc_free_buf_node(BufNode *info) +@@ -1180,6 +1182,7 @@ static int vtenc_create_encoder(AVCodecContext *avctx, int64_t one_second_value = 0; void *nums[2]; @@ -29,8 +29,8 @@ index 5ea9afee22..89c927cdcc 100644 int status = VTCompressionSessionCreate(kCFAllocatorDefault, avctx->width, avctx->height, -@@ -2618,6 +2621,41 @@ static int vtenc_send_frame(AVCodecContext *avctx, - return 0; +@@ -2638,6 +2641,42 @@ out: + return status; } +static void update_config(AVCodecContext *avctx) @@ -58,7 +58,7 @@ index 5ea9afee22..89c927cdcc 100644 + int status = VTSessionSetProperty(vtctx->session, + kVTCompressionPropertyKey_AverageBitRate, + bit_rate_num); -+ if (!status) { ++ if (status) { + av_log(avctx, AV_LOG_ERROR, "Error: cannot set average bit rate: %d\n", status); + } + } @@ -67,13 +67,14 @@ index 5ea9afee22..89c927cdcc 100644 + } + } +} ++ + static av_cold int vtenc_frame( AVCodecContext *avctx, AVPacket *pkt, -@@ -2630,6 +2668,7 @@ static av_cold int vtenc_frame( +@@ -2650,6 +2689,7 @@ static av_cold int vtenc_frame( CMSampleBufferRef buf = NULL; - ExtraSEI *sei = NULL; + ExtraSEI sei = {0}; + update_config(avctx); if (frame) { diff --git a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch index 1fb369b5cea..4a552dda0fc 100644 --- a/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch +++ b/res/vcpkg/ffmpeg/patch/0005-mediacodec-changing-bitrate.patch @@ -1,17 +1,17 @@ -From ed73f8f6494d74ae47218f9503c7e3de385d9253 Mon Sep 17 00:00:00 2001 +From 7323bd68c1b34e9298ea557ff7a3e1883b653957 Mon Sep 17 00:00:00 2001 From: 21pages -Date: Sun, 24 Nov 2024 14:17:39 +0800 -Subject: [PATCH 1/2] mediacodec changing bitrate +Date: Tue, 10 Dec 2024 14:28:16 +0800 +Subject: [PATCH 4/5] mediacodec changing bitrate Signed-off-by: 21pages --- - libavcodec/mediacodec_wrapper.c | 97 +++++++++++++++++++++++++++++++++ + libavcodec/mediacodec_wrapper.c | 98 +++++++++++++++++++++++++++++++++ libavcodec/mediacodec_wrapper.h | 7 +++ libavcodec/mediacodecenc.c | 18 ++++++ - 3 files changed, 122 insertions(+) + 3 files changed, 123 insertions(+) diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c -index 306359071e..7edb38a7d7 100644 +index 96c886666a..06b8504304 100644 --- a/libavcodec/mediacodec_wrapper.c +++ b/libavcodec/mediacodec_wrapper.c @@ -35,6 +35,8 @@ @@ -66,10 +66,11 @@ index 306359071e..7edb38a7d7 100644 #define JNI_GET_ENV_OR_RETURN(env, log_ctx, ret) do { \ (env) = ff_jni_get_env(log_ctx); \ if (!(env)) { \ -@@ -1761,6 +1785,69 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx) +@@ -1762,6 +1786,70 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx) return 0; } ++ +static int mediacodec_jni_setParameter(FFAMediaCodec *ctx, const char* name, int value) +{ + JNIEnv *env = NULL; @@ -136,7 +137,7 @@ index 306359071e..7edb38a7d7 100644 static const FFAMediaFormat media_format_jni = { .class = &amediaformat_class, -@@ -1820,6 +1907,8 @@ static const FFAMediaCodec media_codec_jni = { +@@ -1821,6 +1909,8 @@ static const FFAMediaCodec media_codec_jni = { .getConfigureFlagEncode = mediacodec_jni_getConfigureFlagEncode, .cleanOutputBuffers = mediacodec_jni_cleanOutputBuffers, .signalEndOfInputStream = mediacodec_jni_signalEndOfInputStream, @@ -145,7 +146,7 @@ index 306359071e..7edb38a7d7 100644 }; typedef struct FFAMediaFormatNdk { -@@ -2428,6 +2517,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) +@@ -2335,6 +2425,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx) return 0; } @@ -158,7 +159,7 @@ index 306359071e..7edb38a7d7 100644 static const FFAMediaFormat media_format_ndk = { .class = &amediaformat_ndk_class, -@@ -2489,6 +2584,8 @@ static const FFAMediaCodec media_codec_ndk = { +@@ -2396,6 +2492,8 @@ static const FFAMediaCodec media_codec_ndk = { .getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode, .cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers, .signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream, @@ -193,19 +194,19 @@ index 11a4260497..86c64556ad 100644 enum FFAMediaFormatColorRange { diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c -index d3bf27cb7f..621529d686 100644 +index 6ca3968a24..221f7360f4 100644 --- a/libavcodec/mediacodecenc.c +++ b/libavcodec/mediacodecenc.c -@@ -73,6 +73,8 @@ typedef struct MediaCodecEncContext { - int bitrate_mode; +@@ -76,6 +76,8 @@ typedef struct MediaCodecEncContext { int level; int pts_as_dts; + int extract_extradata; + + int last_bit_rate; } MediaCodecEncContext; enum { -@@ -155,6 +157,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) +@@ -193,6 +195,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx) int ret; int gop; @@ -214,7 +215,7 @@ index d3bf27cb7f..621529d686 100644 if (s->use_ndk_codec < 0) s->use_ndk_codec = !av_jni_get_java_vm(avctx); -@@ -515,12 +519,26 @@ static int mediacodec_send(AVCodecContext *avctx, +@@ -542,11 +546,25 @@ static int mediacodec_send(AVCodecContext *avctx, return 0; } @@ -235,12 +236,11 @@ index d3bf27cb7f..621529d686 100644 { MediaCodecEncContext *s = avctx->priv_data; int ret; - int got_packet = 0; + update_config(avctx); // Return on three case: // 1. Serious error // 2. Got a packet success -- -2.34.1 +2.43.0.windows.1 diff --git a/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch b/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch index e13a5de11e8..a62be5a8195 100644 --- a/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch +++ b/res/vcpkg/ffmpeg/patch/0006-dlopen-libva.patch @@ -1,25 +1,23 @@ -From 6553fc4eae5d03bc712c30ae1e7519753c37275c Mon Sep 17 00:00:00 2001 +From 95ebc0ad912447ba83cacb197f506b881f82179e Mon Sep 17 00:00:00 2001 From: 21pages -Date: Wed, 4 Dec 2024 12:53:23 +0800 -Subject: [PATCH] dlopen libva +Date: Tue, 10 Dec 2024 15:29:21 +0800 +Subject: [PATCH 1/2] dlopen libva Signed-off-by: 21pages --- - libavcodec/vaapi_decode.c | 99 +++++++----- - libavcodec/vaapi_encode.c | 176 +++++++++++--------- - libavcodec/vaapi_encode_av1.c | 13 +- + libavcodec/vaapi_decode.c | 96 ++++++----- + libavcodec/vaapi_encode.c | 173 ++++++++++--------- libavcodec/vaapi_encode_h264.c | 3 +- - libavcodec/vaapi_encode_h265.c | 5 +- - libavutil/hwcontext_vaapi.c | 288 +++++++++++++++++++++++++-------- - libavutil/hwcontext_vaapi.h | 97 +++++++++++ - libavutil/hwcontext_vulkan.c | 5 +- - 8 files changed, 494 insertions(+), 192 deletions(-) + libavcodec/vaapi_encode_h265.c | 6 +- + libavutil/hwcontext_vaapi.c | 292 ++++++++++++++++++++++++--------- + libavutil/hwcontext_vaapi.h | 96 +++++++++++ + 6 files changed, 477 insertions(+), 189 deletions(-) diff --git a/libavcodec/vaapi_decode.c b/libavcodec/vaapi_decode.c -index cca94b5336..776270588f 100644 +index a59194340f..e202b673f4 100644 --- a/libavcodec/vaapi_decode.c +++ b/libavcodec/vaapi_decode.c -@@ -37,17 +37,18 @@ int ff_vaapi_decode_make_param_buffer(AVCodecContext *avctx, +@@ -38,17 +38,18 @@ int ff_vaapi_decode_make_param_buffer(AVCodecContext *avctx, size_t size) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -40,7 +38,7 @@ index cca94b5336..776270588f 100644 return AVERROR(EIO); } -@@ -67,6 +68,7 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, +@@ -69,6 +70,7 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, size_t slice_size) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -48,14 +46,14 @@ index cca94b5336..776270588f 100644 VAStatus vas; int index; -@@ -85,13 +87,13 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, +@@ -88,13 +90,13 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, index = 2 * pic->nb_slices; - vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, VASliceParameterBufferType, - params_size, 1, (void*)params_data, + params_size, nb_params, (void*)params_data, &pic->slice_buffers[index]); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create slice " @@ -64,7 +62,7 @@ index cca94b5336..776270588f 100644 return AVERROR(EIO); } -@@ -99,15 +101,15 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, +@@ -102,15 +104,15 @@ int ff_vaapi_decode_make_slice_buffer(AVCodecContext *avctx, "is %#x.\n", pic->nb_slices, params_size, pic->slice_buffers[index]); @@ -83,7 +81,7 @@ index cca94b5336..776270588f 100644 pic->slice_buffers[index]); return AVERROR(EIO); } -@@ -124,26 +126,27 @@ static void ff_vaapi_decode_destroy_buffers(AVCodecContext *avctx, +@@ -127,26 +129,27 @@ static void ff_vaapi_decode_destroy_buffers(AVCodecContext *avctx, VAAPIDecodePicture *pic) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -115,7 +113,7 @@ index cca94b5336..776270588f 100644 } } } -@@ -152,43 +155,44 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, +@@ -155,6 +158,7 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, VAAPIDecodePicture *pic) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -123,6 +121,7 @@ index cca94b5336..776270588f 100644 VAStatus vas; int err; +@@ -166,37 +170,37 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, av_log(avctx, AV_LOG_DEBUG, "Decode to surface %#x.\n", pic->output_surface); @@ -168,7 +167,7 @@ index cca94b5336..776270588f 100644 err = AVERROR(EIO); if (CONFIG_VAAPI_1 || ctx->hwctx->driver_quirks & AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS) -@@ -205,10 +209,10 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, +@@ -213,10 +217,10 @@ int ff_vaapi_decode_issue(AVCodecContext *avctx, goto exit; fail_with_picture: @@ -181,7 +180,7 @@ index cca94b5336..776270588f 100644 } fail: ff_vaapi_decode_destroy_buffers(avctx, pic); -@@ -296,6 +300,7 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, +@@ -304,6 +308,7 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, AVHWFramesContext *frames) { AVVAAPIDeviceContext *hwctx = device->hwctx; @@ -189,7 +188,7 @@ index cca94b5336..776270588f 100644 VAStatus vas; VASurfaceAttrib *attr; enum AVPixelFormat source_format, best_format, format; -@@ -305,11 +310,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, +@@ -313,11 +318,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, source_format = avctx->sw_pix_fmt; av_assert0(source_format != AV_PIX_FMT_NONE); @@ -203,7 +202,7 @@ index cca94b5336..776270588f 100644 return AVERROR(ENOSYS); } -@@ -317,11 +322,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, +@@ -325,11 +330,11 @@ static int vaapi_decode_find_best_format(AVCodecContext *avctx, if (!attr) return AVERROR(ENOMEM); @@ -217,7 +216,7 @@ index cca94b5336..776270588f 100644 av_freep(&attr); return AVERROR(ENOSYS); } -@@ -463,6 +468,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, +@@ -471,6 +476,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, AVHWDeviceContext *device = (AVHWDeviceContext*)device_ref->data; AVVAAPIDeviceContext *hwctx = device->hwctx; @@ -225,7 +224,7 @@ index cca94b5336..776270588f 100644 codec_desc = avcodec_descriptor_get(avctx->codec_id); if (!codec_desc) { -@@ -470,7 +476,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, +@@ -478,7 +484,7 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, goto fail; } @@ -234,7 +233,7 @@ index cca94b5336..776270588f 100644 profile_list = av_malloc_array(profile_count, sizeof(VAProfile)); if (!profile_list) { -@@ -478,11 +484,11 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, +@@ -486,11 +492,11 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, goto fail; } @@ -248,7 +247,7 @@ index cca94b5336..776270588f 100644 err = AVERROR(ENOSYS); goto fail; } -@@ -542,12 +548,12 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, +@@ -550,12 +556,12 @@ static int vaapi_decode_make_config(AVCodecContext *avctx, } } @@ -263,7 +262,7 @@ index cca94b5336..776270588f 100644 err = AVERROR(EIO); goto fail; } -@@ -626,7 +632,7 @@ fail: +@@ -638,7 +644,7 @@ fail: av_hwframe_constraints_free(&constraints); av_freep(&hwconfig); if (*va_config != VA_INVALID_ID) { @@ -272,7 +271,7 @@ index cca94b5336..776270588f 100644 *va_config = VA_INVALID_ID; } av_freep(&profile_list); -@@ -639,20 +645,21 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, +@@ -651,12 +657,14 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, AVHWFramesContext *hw_frames = (AVHWFramesContext *)hw_frames_ctx->data; AVHWDeviceContext *device_ctx = hw_frames->device_ctx; AVVAAPIDeviceContext *hwctx; @@ -283,11 +282,11 @@ index cca94b5336..776270588f 100644 if (device_ctx->type != AV_HWDEVICE_TYPE_VAAPI) return AVERROR(EINVAL); hwctx = device_ctx->hwctx; -- + vaf = hwctx->funcs; + err = vaapi_decode_make_config(avctx, hw_frames->device_ref, &va_config, hw_frames_ctx); - if (err) +@@ -664,7 +672,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, return err; if (va_config != VA_INVALID_ID) @@ -296,7 +295,7 @@ index cca94b5336..776270588f 100644 return 0; } -@@ -660,6 +667,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, +@@ -672,6 +680,7 @@ int ff_vaapi_common_frame_params(AVCodecContext *avctx, int ff_vaapi_decode_init(AVCodecContext *avctx) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -304,16 +303,16 @@ index cca94b5336..776270588f 100644 VAStatus vas; int err; -@@ -674,13 +682,17 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) +@@ -686,13 +695,18 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) ctx->hwfc = ctx->frames->hwctx; ctx->device = ctx->frames->device_ctx; ctx->hwctx = ctx->device->hwctx; -- + if (!ctx->hwctx || !ctx->hwctx->funcs) { + err = AVERROR(EINVAL); + goto fail; + } + vaf = ctx->hwctx->funcs; + err = vaapi_decode_make_config(avctx, ctx->frames->device_ref, &ctx->va_config, NULL); if (err) @@ -324,7 +323,7 @@ index cca94b5336..776270588f 100644 avctx->coded_width, avctx->coded_height, VA_PROGRESSIVE, ctx->hwfc->surface_ids, -@@ -688,7 +700,7 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) +@@ -700,7 +714,7 @@ int ff_vaapi_decode_init(AVCodecContext *avctx) &ctx->va_context); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create decode " @@ -333,7 +332,7 @@ index cca94b5336..776270588f 100644 err = AVERROR(EIO); goto fail; } -@@ -706,22 +718,29 @@ fail: +@@ -718,22 +732,28 @@ fail: int ff_vaapi_decode_uninit(AVCodecContext *avctx) { VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data; @@ -342,7 +341,6 @@ index cca94b5336..776270588f 100644 + if (ctx->hwctx && ctx->hwctx->funcs) + vaf = ctx->hwctx->funcs; -+ + if (!vaf) + return 0; + @@ -368,10 +366,10 @@ index cca94b5336..776270588f 100644 } diff --git a/libavcodec/vaapi_encode.c b/libavcodec/vaapi_encode.c -index b8765a19c7..65eb8740a8 100644 +index 16a9a364f0..ccf6fa59d6 100644 --- a/libavcodec/vaapi_encode.c +++ b/libavcodec/vaapi_encode.c -@@ -44,6 +44,7 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, +@@ -43,6 +43,7 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, int type, char *data, size_t bit_len) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -379,7 +377,7 @@ index b8765a19c7..65eb8740a8 100644 VAStatus vas; VABufferID param_buffer, data_buffer; VABufferID *tmp; -@@ -58,24 +59,24 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, +@@ -57,24 +58,24 @@ static int vaapi_encode_make_packed_header(AVCodecContext *avctx, return AVERROR(ENOMEM); pic->param_buffers = tmp; @@ -408,7 +406,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR(EIO); } pic->param_buffers[pic->nb_param_buffers++] = data_buffer; -@@ -90,6 +91,7 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, +@@ -89,6 +90,7 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, int type, char *data, size_t len) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -416,14 +414,13 @@ index b8765a19c7..65eb8740a8 100644 VAStatus vas; VABufferID *tmp; VABufferID buffer; -@@ -99,11 +101,11 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, +@@ -98,11 +100,11 @@ static int vaapi_encode_make_param_buffer(AVCodecContext *avctx, return AVERROR(ENOMEM); pic->param_buffers = tmp; - vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, -- type, len, 1, data, &buffer); + vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, -+ type, len, 1, data, &buffer); + type, len, 1, data, &buffer); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create parameter buffer " - "(type %d): %d (%s).\n", type, vas, vaErrorStr(vas)); @@ -431,21 +428,21 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR(EIO); } pic->param_buffers[pic->nb_param_buffers++] = buffer; -@@ -140,6 +142,7 @@ static int vaapi_encode_wait(AVCodecContext *avctx, - VAAPIEncodePicture *pic) - { +@@ -141,6 +143,7 @@ static int vaapi_encode_wait(AVCodecContext *avctx, FFHWBaseEncodePicture *base_ + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + #endif VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodePicture *pic = base_pic->priv; VAStatus vas; - av_assert0(pic->encode_issued); -@@ -154,22 +157,22 @@ static int vaapi_encode_wait(AVCodecContext *avctx, - pic->encode_order, pic->input_surface); +@@ -156,22 +159,22 @@ static int vaapi_encode_wait(AVCodecContext *avctx, FFHWBaseEncodePicture *base_ + base_pic->encode_order, pic->input_surface); #if VA_CHECK_VERSION(1, 9, 0) -- if (ctx->has_sync_buffer_func) { +- if (base_ctx->async_encode) { - vas = vaSyncBuffer(ctx->hwctx->display, -+ if (ctx->has_sync_buffer_func && vaf->vaSyncBuffer) { ++ if (base_ctx->async_encode && vaf->vaSyncBuffer) { + vas = vaf->vaSyncBuffer(ctx->hwctx->display, pic->output_buffer, VA_TIMEOUT_INFINITE); @@ -467,15 +464,15 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR(EIO); } } -@@ -267,6 +270,7 @@ static int vaapi_encode_issue(AVCodecContext *avctx, - VAAPIEncodePicture *pic) +@@ -270,6 +273,7 @@ static int vaapi_encode_issue(AVCodecContext *avctx, { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; + VAAPIEncodePicture *pic = base_pic->priv; VAAPIEncodeSlice *slice; VAStatus vas; - int err, i; -@@ -594,28 +598,28 @@ static int vaapi_encode_issue(AVCodecContext *avctx, +@@ -587,28 +591,28 @@ static int vaapi_encode_issue(AVCodecContext *avctx, } #endif @@ -506,11 +503,11 @@ index b8765a19c7..65eb8740a8 100644 if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to end picture encode issue: " - "%d (%s).\n", vas, vaErrorStr(vas)); -+ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); ++ "%d (%s).\n", vas, vaf->vaErrorStr(vas)); err = AVERROR(EIO); // vaRenderPicture() has been called here, so we should not destroy // the parameter buffers unless separate destruction is required. -@@ -629,12 +633,12 @@ static int vaapi_encode_issue(AVCodecContext *avctx, +@@ -622,12 +626,12 @@ static int vaapi_encode_issue(AVCodecContext *avctx, if (CONFIG_VAAPI_1 || ctx->hwctx->driver_quirks & AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS) { for (i = 0; i < pic->nb_param_buffers; i++) { @@ -525,7 +522,7 @@ index b8765a19c7..65eb8740a8 100644 // And ignore. } } -@@ -645,10 +649,10 @@ static int vaapi_encode_issue(AVCodecContext *avctx, +@@ -636,10 +640,10 @@ static int vaapi_encode_issue(AVCodecContext *avctx, return 0; fail_with_picture: @@ -538,7 +535,7 @@ index b8765a19c7..65eb8740a8 100644 if (pic->slices) { for (i = 0; i < pic->nb_slices; i++) av_freep(&pic->slices[i].codec_slice_params); -@@ -707,16 +711,17 @@ static int vaapi_encode_set_output_property(AVCodecContext *avctx, +@@ -657,16 +661,17 @@ fail_at_end: static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID buf_id) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -558,7 +555,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); return err; } -@@ -724,10 +729,10 @@ static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID +@@ -674,10 +679,10 @@ static int vaapi_encode_get_coded_buffer_size(AVCodecContext *avctx, VABufferID for (buf = buf_list; buf; buf = buf->next) size += buf->size; @@ -571,7 +568,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); return err; } -@@ -739,15 +744,16 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, +@@ -689,15 +694,16 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, VABufferID buf_id, uint8_t **dst) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -590,7 +587,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); return err; } -@@ -760,10 +766,10 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, +@@ -710,10 +716,10 @@ static int vaapi_encode_get_coded_buffer_data(AVCodecContext *avctx, *dst += buf->size; } @@ -603,15 +600,15 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); return err; } -@@ -1552,6 +1558,7 @@ static const VAEntrypoint vaapi_encode_entrypoints_low_power[] = { - static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -936,6 +942,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; - VAProfile *va_profiles = NULL; - VAEntrypoint *va_entrypoints = NULL; + VAProfile *va_profiles = NULL; + VAEntrypoint *va_entrypoints = NULL; VAStatus vas; -@@ -1593,16 +1600,16 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -977,16 +984,16 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) av_log(avctx, AV_LOG_VERBOSE, "Input surface format is %s.\n", desc->name); @@ -631,7 +628,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR_EXTERNAL; goto fail; } -@@ -1623,7 +1630,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -1007,7 +1014,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) continue; #if VA_CHECK_VERSION(1, 0, 0) @@ -640,7 +637,7 @@ index b8765a19c7..65eb8740a8 100644 #else profile_string = "(no profile names)"; #endif -@@ -1653,18 +1660,18 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -1037,18 +1044,18 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) av_log(avctx, AV_LOG_VERBOSE, "Using VAAPI profile %s (%d).\n", profile_string, ctx->va_profile); @@ -662,7 +659,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR_EXTERNAL; goto fail; } -@@ -1686,7 +1693,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -1070,7 +1077,7 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) ctx->va_entrypoint = va_entrypoints[i]; #if VA_CHECK_VERSION(1, 0, 0) @@ -671,7 +668,7 @@ index b8765a19c7..65eb8740a8 100644 #else entrypoint_string = "(no entrypoint names)"; #endif -@@ -1711,12 +1718,12 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) +@@ -1095,12 +1102,12 @@ static av_cold int vaapi_encode_profile_entrypoint(AVCodecContext *avctx) } rt_format_attr = (VAConfigAttrib) { VAConfigAttribRTFormat }; @@ -686,7 +683,7 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR_EXTERNAL; goto fail; } -@@ -1773,6 +1780,7 @@ static const VAAPIEncodeRCMode vaapi_encode_rc_modes[] = { +@@ -1157,6 +1164,7 @@ static const VAAPIEncodeRCMode vaapi_encode_rc_modes[] = { static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -694,7 +691,7 @@ index b8765a19c7..65eb8740a8 100644 uint32_t supported_va_rc_modes; const VAAPIEncodeRCMode *rc_mode; int64_t rc_bits_per_second; -@@ -1786,12 +1794,12 @@ static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) +@@ -1170,12 +1178,12 @@ static av_cold int vaapi_encode_init_rate_control(AVCodecContext *avctx) VAStatus vas; char supported_rc_modes_string[64]; @@ -709,7 +706,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } if (rc_attr.value == VA_ATTRIB_NOT_SUPPORTED) { -@@ -2132,6 +2140,7 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) +@@ -1516,6 +1524,7 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) { #if VA_CHECK_VERSION(1, 5, 0) VAAPIEncodeContext *ctx = avctx->priv_data; @@ -717,7 +714,7 @@ index b8765a19c7..65eb8740a8 100644 VAConfigAttrib attr = { VAConfigAttribMaxFrameSize }; VAStatus vas; -@@ -2142,14 +2151,14 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) +@@ -1526,14 +1535,14 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) return AVERROR(EINVAL); } @@ -734,15 +731,15 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2188,18 +2197,19 @@ static av_cold int vaapi_encode_init_max_frame_size(AVCodecContext *avctx) - static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) +@@ -1573,18 +1582,19 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; VAStatus vas; VAConfigAttrib attr = { VAConfigAttribEncMaxRefFrames }; uint32_t ref_l0, ref_l1; - int prediction_pre_only; + int prediction_pre_only, err; - vas = vaGetConfigAttributes(ctx->hwctx->display, + vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, @@ -756,8 +753,8 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2217,13 +2227,13 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) - if (!(ctx->codec->flags & FLAG_INTRA_ONLY || +@@ -1602,13 +1612,13 @@ static av_cold int vaapi_encode_init_gop_structure(AVCodecContext *avctx) + if (!(ctx->codec->flags & FF_HW_FLAG_INTRA_ONLY || avctx->gop_size <= 1)) { attr = (VAConfigAttrib) { VAConfigAttribPredictionDirection }; - vas = vaGetConfigAttributes(ctx->hwctx->display, @@ -772,22 +769,15 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { av_log(avctx, AV_LOG_VERBOSE, "Driver does not report any additional " -@@ -2409,12 +2419,14 @@ static av_cold int vaapi_encode_init_tile_slice_structure(AVCodecContext *avctx, - av_log(avctx, AV_LOG_VERBOSE, "Encoding pictures with %d x %d tile.\n", - ctx->tile_rows, ctx->tile_cols); - -+ - return 0; - } - - static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) +@@ -1758,6 +1768,7 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) { + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; VAConfigAttrib attr[3] = { { VAConfigAttribEncMaxSlices }, { VAConfigAttribEncSliceStructure }, #if VA_CHECK_VERSION(1, 1, 0) -@@ -2446,13 +2458,13 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) +@@ -1789,13 +1800,13 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) return 0; } @@ -803,7 +793,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } max_slices = attr[0].value; -@@ -2506,16 +2518,17 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) +@@ -1849,16 +1860,17 @@ static av_cold int vaapi_encode_init_slice_structure(AVCodecContext *avctx) static av_cold int vaapi_encode_init_packed_headers(AVCodecContext *avctx) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -823,7 +813,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2567,17 +2580,18 @@ static av_cold int vaapi_encode_init_quality(AVCodecContext *avctx) +@@ -1910,17 +1922,18 @@ static av_cold int vaapi_encode_init_quality(AVCodecContext *avctx) { #if VA_CHECK_VERSION(0, 36, 0) VAAPIEncodeContext *ctx = avctx->priv_data; @@ -844,10 +834,10 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2614,16 +2628,17 @@ static av_cold int vaapi_encode_init_roi(AVCodecContext *avctx) - { +@@ -1958,16 +1971,17 @@ static av_cold int vaapi_encode_init_roi(AVCodecContext *avctx) #if VA_CHECK_VERSION(1, 0, 0) - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; VAStatus vas; VAConfigAttrib attr = { VAConfigAttribEncROI }; @@ -864,7 +854,7 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR_EXTERNAL; } -@@ -2648,10 +2663,11 @@ static void vaapi_encode_free_output_buffer(FFRefStructOpaque opaque, +@@ -1992,10 +2006,11 @@ static void vaapi_encode_free_output_buffer(FFRefStructOpaque opaque, { AVCodecContext *avctx = opaque.nc; VAAPIEncodeContext *ctx = avctx->priv_data; @@ -877,22 +867,22 @@ index b8765a19c7..65eb8740a8 100644 av_log(avctx, AV_LOG_DEBUG, "Freed output buffer %#x\n", buffer_id); } -@@ -2660,6 +2676,7 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) - { +@@ -2005,6 +2020,7 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) AVCodecContext *avctx = opaque.nc; - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; VABufferID *buffer_id = obj; VAStatus vas; -@@ -2667,13 +2684,13 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) +@@ -2012,13 +2028,13 @@ static int vaapi_encode_alloc_output_buffer(FFRefStructOpaque opaque, void *obj) // to hold the largest possible compressed frame. We assume here // that the uncompressed frame plus some header data is an upper // bound on that. - vas = vaCreateBuffer(ctx->hwctx->display, ctx->va_context, + vas = vaf->vaCreateBuffer(ctx->hwctx->display, ctx->va_context, VAEncCodedBufferType, - 3 * ctx->surface_width * ctx->surface_height + + 3 * base_ctx->surface_width * base_ctx->surface_height + (1 << 16), 1, 0, buffer_id); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create bitstream " @@ -901,17 +891,17 @@ index b8765a19c7..65eb8740a8 100644 return AVERROR(ENOMEM); } -@@ -2773,6 +2790,7 @@ static av_cold int vaapi_encode_create_recon_frames(AVCodecContext *avctx) - av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) +@@ -2092,6 +2108,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; + VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = NULL; AVVAAPIFramesContext *recon_hwctx = NULL; VAStatus vas; int err; -@@ -2808,6 +2826,12 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) - ctx->device = (AVHWDeviceContext*)ctx->device_ref->data; - ctx->hwctx = ctx->device->hwctx; +@@ -2107,6 +2124,12 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + + ctx->hwctx = base_ctx->device->hwctx; + if (!ctx->hwctx || !ctx->hwctx->funcs) { + err = AVERROR(EINVAL); @@ -919,10 +909,10 @@ index b8765a19c7..65eb8740a8 100644 + } + vaf = ctx->hwctx->funcs; + - ctx->tail_pkt = av_packet_alloc(); - if (!ctx->tail_pkt) { - err = AVERROR(ENOMEM); -@@ -2864,13 +2888,13 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) + err = vaapi_encode_profile_entrypoint(avctx); + if (err < 0) + goto fail; +@@ -2157,13 +2180,13 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) goto fail; } @@ -938,16 +928,16 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); goto fail; } -@@ -2880,7 +2904,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) +@@ -2173,7 +2196,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) goto fail; - recon_hwctx = ctx->recon_frames->hwctx; + recon_hwctx = base_ctx->recon_frames->hwctx; - vas = vaCreateContext(ctx->hwctx->display, ctx->va_config, + vas = vaf->vaCreateContext(ctx->hwctx->display, ctx->va_config, - ctx->surface_width, ctx->surface_height, + base_ctx->surface_width, base_ctx->surface_height, VA_PROGRESSIVE, recon_hwctx->surface_ids, -@@ -2888,7 +2912,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) +@@ -2181,7 +2204,7 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) &ctx->va_context); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to create encode pipeline " @@ -956,32 +946,32 @@ index b8765a19c7..65eb8740a8 100644 err = AVERROR(EIO); goto fail; } -@@ -2962,14 +2986,16 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) +@@ -2255,14 +2278,16 @@ av_cold int ff_vaapi_encode_init(AVCodecContext *avctx) #if VA_CHECK_VERSION(1, 9, 0) // check vaSyncBuffer function - vas = vaSyncBuffer(ctx->hwctx->display, VA_INVALID_ID, 0); - if (vas != VA_STATUS_ERROR_UNIMPLEMENTED) { -- ctx->has_sync_buffer_func = 1; -- ctx->encode_fifo = av_fifo_alloc2(ctx->async_depth, -- sizeof(VAAPIEncodePicture *), -- 0); -- if (!ctx->encode_fifo) +- base_ctx->async_encode = 1; +- base_ctx->encode_fifo = av_fifo_alloc2(base_ctx->async_depth, +- sizeof(VAAPIEncodePicture*), +- 0); +- if (!base_ctx->encode_fifo) - return AVERROR(ENOMEM); + if (vaf->vaSyncBuffer) { + vas = vaf->vaSyncBuffer(ctx->hwctx->display, VA_INVALID_ID, 0); + if (vas != VA_STATUS_ERROR_UNIMPLEMENTED) { -+ ctx->has_sync_buffer_func = 1; -+ ctx->encode_fifo = av_fifo_alloc2(ctx->async_depth, -+ sizeof(VAAPIEncodePicture *), -+ 0); -+ if (!ctx->encode_fifo) ++ base_ctx->async_encode = 1; ++ base_ctx->encode_fifo = av_fifo_alloc2(base_ctx->async_depth, ++ sizeof(VAAPIEncodePicture*), ++ 0); ++ if (!base_ctx->encode_fifo) + return AVERROR(ENOMEM); + } } #endif -@@ -2997,14 +3023,14 @@ av_cold int ff_vaapi_encode_close(AVCodecContext *avctx) +@@ -2291,14 +2316,14 @@ av_cold int ff_vaapi_encode_close(AVCodecContext *avctx) ff_refstruct_pool_uninit(&ctx->output_buffer_pool); if (ctx->va_context != VA_INVALID_ID) { @@ -1000,71 +990,11 @@ index b8765a19c7..65eb8740a8 100644 ctx->va_config = VA_INVALID_ID; } -diff --git a/libavcodec/vaapi_encode_av1.c b/libavcodec/vaapi_encode_av1.c -index a46b882ab9..2e64611ab3 100644 ---- a/libavcodec/vaapi_encode_av1.c -+++ b/libavcodec/vaapi_encode_av1.c -@@ -766,6 +766,7 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) - { - VAAPIEncodeContext *ctx = avctx->priv_data; - VAAPIEncodeAV1Context *priv = avctx->priv_data; -+ VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; - VAConfigAttrib attr; - VAStatus vas; - int ret; -@@ -791,13 +792,13 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) - return ret; - - attr.type = VAConfigAttribEncAV1; -- vas = vaGetConfigAttributes(ctx->hwctx->display, -+ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, - ctx->va_profile, - ctx->va_entrypoint, - &attr, 1); - if (vas != VA_STATUS_SUCCESS) { - av_log(avctx, AV_LOG_ERROR, "Failed to query " -- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); -+ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); - return AVERROR_EXTERNAL; - } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { - priv->attr.value = 0; -@@ -808,13 +809,13 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) - } - - attr.type = VAConfigAttribEncAV1Ext1; -- vas = vaGetConfigAttributes(ctx->hwctx->display, -+ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, - ctx->va_profile, - ctx->va_entrypoint, - &attr, 1); - if (vas != VA_STATUS_SUCCESS) { - av_log(avctx, AV_LOG_ERROR, "Failed to query " -- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); -+ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); - return AVERROR_EXTERNAL; - } else if (attr.value == VA_ATTRIB_NOT_SUPPORTED) { - priv->attr_ext1.value = 0; -@@ -826,13 +827,13 @@ static av_cold int vaapi_encode_av1_init(AVCodecContext *avctx) - - /** This attr provides essential indicators, return error if not support. */ - attr.type = VAConfigAttribEncAV1Ext2; -- vas = vaGetConfigAttributes(ctx->hwctx->display, -+ vas = vaf->vaGetConfigAttributes(ctx->hwctx->display, - ctx->va_profile, - ctx->va_entrypoint, - &attr, 1); - if (vas != VA_STATUS_SUCCESS || attr.value == VA_ATTRIB_NOT_SUPPORTED) { - av_log(avctx, AV_LOG_ERROR, "Failed to query " -- "config attribute: %d (%s).\n", vas, vaErrorStr(vas)); -+ "config attribute: %d (%s).\n", vas, vaf->vaErrorStr(vas)); - return AVERROR_EXTERNAL; - } else { - priv->attr_ext2.value = attr.value; diff --git a/libavcodec/vaapi_encode_h264.c b/libavcodec/vaapi_encode_h264.c -index 37df9103ae..b83e45d333 100644 +index fb87b68bec..6d4ce630ce 100644 --- a/libavcodec/vaapi_encode_h264.c +++ b/libavcodec/vaapi_encode_h264.c -@@ -1083,6 +1083,7 @@ static int vaapi_encode_h264_init_slice_params(AVCodecContext *avctx, +@@ -868,6 +868,7 @@ static int vaapi_encode_h264_init_slice_params(AVCodecContext *avctx, static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) { VAAPIEncodeContext *ctx = avctx->priv_data; @@ -1072,7 +1002,7 @@ index 37df9103ae..b83e45d333 100644 VAAPIEncodeH264Context *priv = avctx->priv_data; int err; -@@ -1134,7 +1135,7 @@ static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) +@@ -919,7 +920,7 @@ static av_cold int vaapi_encode_h264_configure(AVCodecContext *avctx) vaapi_encode_h264_sei_identifier_uuid, sizeof(priv->sei_identifier.uuid_iso_iec_11578)); @@ -1082,18 +1012,19 @@ index 37df9103ae..b83e45d333 100644 driver = "unknown driver"; diff --git a/libavcodec/vaapi_encode_h265.c b/libavcodec/vaapi_encode_h265.c -index c4aabbf5ed..9bb85af810 100644 +index 2283bcc0b4..7c624f99a9 100644 --- a/libavcodec/vaapi_encode_h265.c +++ b/libavcodec/vaapi_encode_h265.c -@@ -1199,6 +1199,7 @@ static int vaapi_encode_h265_init_slice_params(AVCodecContext *avctx, +@@ -899,6 +899,8 @@ static int vaapi_encode_h265_init_slice_params(AVCodecContext *avctx, static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) { - VAAPIEncodeContext *ctx = avctx->priv_data; + FFHWBaseEncodeContext *base_ctx = avctx->priv_data; ++ VAAPIEncodeContext *ctx = avctx->priv_data; + VAAPIDynLoadFunctions *vaf = ctx->hwctx->funcs; - VAAPIEncodeH265Context *priv = avctx->priv_data; + VAAPIEncodeH265Context *priv = avctx->priv_data; #if VA_CHECK_VERSION(1, 13, 0) -@@ -1208,7 +1209,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) +@@ -909,7 +911,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) VAStatus vas; attr.type = VAConfigAttribEncHEVCFeatures; @@ -1102,7 +1033,7 @@ index c4aabbf5ed..9bb85af810 100644 ctx->va_entrypoint, &attr, 1); if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to query encoder " -@@ -1222,7 +1223,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) +@@ -923,7 +925,7 @@ static av_cold int vaapi_encode_h265_get_encoder_caps(AVCodecContext *avctx) } attr.type = VAConfigAttribEncHEVCBlockSizes; @@ -1112,19 +1043,18 @@ index c4aabbf5ed..9bb85af810 100644 if (vas != VA_STATUS_SUCCESS) { av_log(avctx, AV_LOG_ERROR, "Failed to query encoder " diff --git a/libavutil/hwcontext_vaapi.c b/libavutil/hwcontext_vaapi.c -index 95a68e62c5..0e42a36346 100644 +index 95aa38d9d2..13451e8ad7 100644 --- a/libavutil/hwcontext_vaapi.c +++ b/libavutil/hwcontext_vaapi.c -@@ -47,7 +47,7 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) - #if HAVE_UNISTD_H +@@ -48,6 +48,7 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) # include #endif -- + +#include #include "avassert.h" #include "buffer.h" -@@ -60,6 +60,129 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) +@@ -60,6 +61,128 @@ typedef HRESULT (WINAPI *PFN_CREATE_DXGI_FACTORY)(REFIID riid, void **ppFactory) #include "pixdesc.h" #include "pixfmt.h" @@ -1237,8 +1167,7 @@ index 95a68e62c5..0e42a36346 100644 + + // Optional functions + funcs->vaSyncBuffer = dlsym(funcs->handle_va, "vaSyncBuffer"); -+ av_log(NULL, AV_LOG_ERROR, "vaSyncBuffer:%p.\n", funcs->vaSyncBuffer); // use error log level to print it out -+ ++ av_log(NULL, AV_LOG_DEBUG, "vaSyncBuffer:%p.\n", funcs->vaSyncBuffer); + + return funcs; + @@ -1457,7 +1386,7 @@ index 95a68e62c5..0e42a36346 100644 VAAPIFramesContext *ctx = hwfc->hwctx; VASurfaceID surface_id; const VAAPIFormatDescriptor *desc; -@@ -836,10 +966,10 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, +@@ -839,10 +969,10 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, map->flags = flags; map->image.image_id = VA_INVALID_ID; @@ -1470,7 +1399,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail; } -@@ -853,11 +983,11 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, +@@ -856,11 +986,11 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, // prefer not to be given direct-mapped memory if they request read access. if (ctx->derive_works && dst->format == hwfc->sw_format && ((flags & AV_HWFRAME_MAP_DIRECT) || !(flags & AV_HWFRAME_MAP_READ))) { @@ -1484,7 +1413,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail; } -@@ -870,32 +1000,32 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, +@@ -873,41 +1003,32 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, } map->flags |= AV_HWFRAME_MAP_DIRECT; } else { @@ -1514,7 +1443,16 @@ index 95a68e62c5..0e42a36346 100644 } } +-#if VA_CHECK_VERSION(1, 21, 0) +- if (flags & AV_HWFRAME_MAP_READ) +- vaflags |= VA_MAPBUFFER_FLAG_READ; +- if (flags & AV_HWFRAME_MAP_WRITE) +- vaflags |= VA_MAPBUFFER_FLAG_WRITE; +- // On drivers not implementing vaMapBuffer2 libva calls vaMapBuffer instead. +- vas = vaMapBuffer2(hwctx->display, map->image.buf, &address, vaflags); +-#else - vas = vaMapBuffer(hwctx->display, map->image.buf, &address); +-#endif + vas = vaf->vaMapBuffer(hwctx->display, map->image.buf, &address); if (vas != VA_STATUS_SUCCESS) { av_log(hwfc, AV_LOG_ERROR, "Failed to map image from surface " @@ -1523,7 +1461,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail; } -@@ -924,9 +1054,9 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, +@@ -936,9 +1057,9 @@ static int vaapi_map_frame(AVHWFramesContext *hwfc, fail: if (map) { if (address) @@ -1535,7 +1473,7 @@ index 95a68e62c5..0e42a36346 100644 av_free(map); } return err; -@@ -1068,12 +1198,12 @@ static void vaapi_unmap_from_drm(AVHWFramesContext *dst_fc, +@@ -1080,12 +1201,12 @@ static void vaapi_unmap_from_drm(AVHWFramesContext *dst_fc, HWMapDescriptor *hwmap) { AVVAAPIDeviceContext *dst_dev = dst_fc->device_ctx->hwctx; @@ -1550,7 +1488,7 @@ index 95a68e62c5..0e42a36346 100644 } static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, -@@ -1088,6 +1218,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1100,6 +1221,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, AVHWFramesContext *dst_fc = (AVHWFramesContext*)dst->hw_frames_ctx->data; AVVAAPIDeviceContext *dst_dev = dst_fc->device_ctx->hwctx; @@ -1558,7 +1496,7 @@ index 95a68e62c5..0e42a36346 100644 const AVDRMFrameDescriptor *desc; const VAAPIFormatDescriptor *format_desc; VASurfaceID surface_id; -@@ -1204,7 +1335,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1216,7 +1338,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, * Gallium seem to do the correct error checks, so lets just try the * PRIME_2 import first. */ @@ -1567,7 +1505,7 @@ index 95a68e62c5..0e42a36346 100644 src->width, src->height, &surface_id, 1, prime_attrs, FF_ARRAY_ELEMS(prime_attrs)); if (vas != VA_STATUS_SUCCESS) -@@ -1255,7 +1386,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1267,7 +1389,7 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, FFSWAP(uint32_t, buffer_desc.offsets[1], buffer_desc.offsets[2]); } @@ -1576,7 +1514,7 @@ index 95a68e62c5..0e42a36346 100644 src->width, src->height, &surface_id, 1, buffer_attrs, FF_ARRAY_ELEMS(buffer_attrs)); -@@ -1286,14 +1417,14 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, +@@ -1298,14 +1420,14 @@ static int vaapi_map_from_drm(AVHWFramesContext *src_fc, AVFrame *dst, FFSWAP(uint32_t, buffer_desc.offsets[1], buffer_desc.offsets[2]); } @@ -1593,7 +1531,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR(EIO); } av_log(dst_fc, AV_LOG_DEBUG, "Create surface %#x.\n", surface_id); -@@ -1331,6 +1462,7 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1343,6 +1465,7 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, const AVFrame *src, int flags) { AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; @@ -1601,7 +1539,7 @@ index 95a68e62c5..0e42a36346 100644 VASurfaceID surface_id; VAStatus vas; VADRMPRIMESurfaceDescriptor va_desc; -@@ -1344,10 +1476,10 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1356,10 +1479,10 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, if (flags & AV_HWFRAME_MAP_READ) { export_flags |= VA_EXPORT_SURFACE_READ_ONLY; @@ -1614,7 +1552,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR(EIO); } } -@@ -1355,14 +1487,14 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1367,14 +1490,14 @@ static int vaapi_map_to_drm_esh(AVHWFramesContext *hwfc, AVFrame *dst, if (flags & AV_HWFRAME_MAP_WRITE) export_flags |= VA_EXPORT_SURFACE_WRITE_ONLY; @@ -1631,7 +1569,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR(EIO); } -@@ -1425,6 +1557,7 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, +@@ -1437,6 +1560,7 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, HWMapDescriptor *hwmap) { AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; @@ -1639,7 +1577,7 @@ index 95a68e62c5..0e42a36346 100644 VAAPIDRMImageBufferMapping *mapping = hwmap->priv; VASurfaceID surface_id; VAStatus vas; -@@ -1436,19 +1569,19 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, +@@ -1448,19 +1572,19 @@ static void vaapi_unmap_to_drm_abh(AVHWFramesContext *hwfc, // DRM PRIME file descriptors are closed by vaReleaseBufferHandle(), // so we shouldn't close them separately. @@ -1663,7 +1601,7 @@ index 95a68e62c5..0e42a36346 100644 } av_free(mapping); -@@ -1458,6 +1591,7 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1470,6 +1594,7 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, const AVFrame *src, int flags) { AVVAAPIDeviceContext *hwctx = hwfc->device_ctx->hwctx; @@ -1671,7 +1609,7 @@ index 95a68e62c5..0e42a36346 100644 VAAPIDRMImageBufferMapping *mapping = NULL; VASurfaceID surface_id; VAStatus vas; -@@ -1471,12 +1605,12 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1483,12 +1608,12 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, if (!mapping) return AVERROR(ENOMEM); @@ -1686,7 +1624,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail; } -@@ -1531,13 +1665,13 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1543,13 +1668,13 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, } } @@ -1702,7 +1640,7 @@ index 95a68e62c5..0e42a36346 100644 err = AVERROR(EIO); goto fail_derived; } -@@ -1566,9 +1700,9 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, +@@ -1578,9 +1703,9 @@ static int vaapi_map_to_drm_abh(AVHWFramesContext *hwfc, AVFrame *dst, return 0; fail_mapped: @@ -1714,17 +1652,16 @@ index 95a68e62c5..0e42a36346 100644 fail: av_freep(&mapping); return err; -@@ -1622,9 +1756,16 @@ static void vaapi_device_free(AVHWDeviceContext *ctx) +@@ -1634,9 +1759,15 @@ static void vaapi_device_free(AVHWDeviceContext *ctx) { AVVAAPIDeviceContext *hwctx = ctx->hwctx; VAAPIDevicePriv *priv = ctx->user_opaque; + VAAPIDynLoadFunctions *vaf = hwctx->funcs; -+ -+ if (hwctx && hwctx->display && vaf && vaf->vaTerminate) -+ vaf->vaTerminate(hwctx->display); - if (hwctx->display) - vaTerminate(hwctx->display); ++ if (hwctx && hwctx->display && vaf && vaf->vaTerminate) ++ vaf->vaTerminate(hwctx->display); + + if (hwctx && hwctx->funcs) { + vaapi_free_functions(hwctx->funcs); @@ -1733,7 +1670,7 @@ index 95a68e62c5..0e42a36346 100644 #if HAVE_VAAPI_X11 if (priv->x11_display) -@@ -1657,20 +1798,21 @@ static int vaapi_device_connect(AVHWDeviceContext *ctx, +@@ -1669,20 +1800,21 @@ static int vaapi_device_connect(AVHWDeviceContext *ctx, VADisplay display) { AVVAAPIDeviceContext *hwctx = ctx->hwctx; @@ -1759,7 +1696,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR(EIO); } av_log(ctx, AV_LOG_VERBOSE, "Initialised VAAPI connection: " -@@ -1686,6 +1828,16 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, +@@ -1698,6 +1830,16 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, VADisplay display = NULL; const AVDictionaryEntry *ent; int try_drm, try_x11, try_win32, try_all; @@ -1776,7 +1713,7 @@ index 95a68e62c5..0e42a36346 100644 priv = av_mallocz(sizeof(*priv)); if (!priv) -@@ -1802,7 +1954,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, +@@ -1843,7 +1985,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, break; } @@ -1785,7 +1722,7 @@ index 95a68e62c5..0e42a36346 100644 if (!display) { av_log(ctx, AV_LOG_VERBOSE, "Cannot open a VA display " "from DRM device %s.\n", device); -@@ -1820,7 +1972,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, +@@ -1861,7 +2003,7 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, av_log(ctx, AV_LOG_VERBOSE, "Cannot open X11 display " "%s.\n", XDisplayName(device)); } else { @@ -1794,7 +1731,7 @@ index 95a68e62c5..0e42a36346 100644 if (!display) { av_log(ctx, AV_LOG_ERROR, "Cannot open a VA display " "from X11 display %s.\n", XDisplayName(device)); -@@ -1909,11 +2061,11 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, +@@ -1950,11 +2092,11 @@ static int vaapi_device_create(AVHWDeviceContext *ctx, const char *device, if (ent) { #if VA_CHECK_VERSION(0, 38, 0) VAStatus vas; @@ -1809,7 +1746,7 @@ index 95a68e62c5..0e42a36346 100644 return AVERROR_EXTERNAL; } #else -@@ -1929,6 +2081,8 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, +@@ -1970,6 +2112,8 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, AVHWDeviceContext *src_ctx, AVDictionary *opts, int flags) { @@ -1818,7 +1755,7 @@ index 95a68e62c5..0e42a36346 100644 #if HAVE_VAAPI_DRM if (src_ctx->type == AV_HWDEVICE_TYPE_DRM) { AVDRMDeviceContext *src_hwctx = src_ctx->hwctx; -@@ -2000,7 +2154,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, +@@ -2041,7 +2185,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, ctx->user_opaque = priv; ctx->free = &vaapi_device_free; @@ -1827,21 +1764,8 @@ index 95a68e62c5..0e42a36346 100644 if (!display) { av_log(ctx, AV_LOG_ERROR, "Failed to open a VA display from " "DRM device.\n"); -@@ -2010,6 +2164,7 @@ static int vaapi_device_derive(AVHWDeviceContext *ctx, - return vaapi_device_connect(ctx, display); - } - #endif -+ - return AVERROR(ENOSYS); - } - -@@ -2040,3 +2195,4 @@ const HWContextType ff_hwcontext_type_vaapi = { - AV_PIX_FMT_NONE - }, - }; -+ diff --git a/libavutil/hwcontext_vaapi.h b/libavutil/hwcontext_vaapi.h -index 0b2e071cb3..7bdb21c66a 100644 +index 0b2e071cb3..2c51223d45 100644 --- a/libavutil/hwcontext_vaapi.h +++ b/libavutil/hwcontext_vaapi.h @@ -20,6 +20,100 @@ @@ -1954,40 +1878,6 @@ index 0b2e071cb3..7bdb21c66a 100644 } AVVAAPIDeviceContext; /** -@@ -114,4 +210,5 @@ typedef struct AVVAAPIHWConfig { - VAConfigID config_id; - } AVVAAPIHWConfig; - -+ - #endif /* AVUTIL_HWCONTEXT_VAAPI_H */ -diff --git a/libavutil/hwcontext_vulkan.c b/libavutil/hwcontext_vulkan.c -index 6e3b96b73a..55ba57ea7d 100644 ---- a/libavutil/hwcontext_vulkan.c -+++ b/libavutil/hwcontext_vulkan.c -@@ -1597,6 +1597,7 @@ static int vulkan_device_derive(AVHWDeviceContext *ctx, - #if CONFIG_VAAPI - case AV_HWDEVICE_TYPE_VAAPI: { - AVVAAPIDeviceContext *src_hwctx = src_ctx->hwctx; -+ VAAPIDynLoadFunctions *vaf = src_hwctx->funcs; - VADisplay dpy = src_hwctx->display; - #if VA_CHECK_VERSION(1, 15, 0) - VAStatus vas; -@@ -1607,13 +1608,13 @@ static int vulkan_device_derive(AVHWDeviceContext *ctx, - const char *vendor; - - #if VA_CHECK_VERSION(1, 15, 0) -- vas = vaGetDisplayAttributes(dpy, &attr, 1); -+ vas = vaf->vaGetDisplayAttributes(dpy, &attr, 1); - if (vas == VA_STATUS_SUCCESS && attr.flags != VA_DISPLAY_ATTRIB_NOT_SUPPORTED) - dev_select.pci_device = (attr.value & 0xFFFF); - #endif - - if (!dev_select.pci_device) { -- vendor = vaQueryVendorString(dpy); -+ vendor = vaf->vaQueryVendorString(dpy); - if (!vendor) { - av_log(ctx, AV_LOG_ERROR, "Unable to get device info from VAAPI!\n"); - return AVERROR_EXTERNAL; -- 2.34.1 diff --git a/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch b/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch new file mode 100644 index 00000000000..21a1f4d4fe7 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0007-fix-linux-configure.patch @@ -0,0 +1,30 @@ +From 595f0468e127f204741b6c37a479d71daaf571eb Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Tue, 10 Dec 2024 21:17:14 +0800 +Subject: [PATCH] fix linux configure + +Signed-off-by: 21pages +--- + configure | 6 ------ + 1 file changed, 6 deletions(-) + +diff --git a/configure b/configure +index d77a55b653..48ca90ac5e 100755 +--- a/configure ++++ b/configure +@@ -7071,12 +7071,6 @@ enabled mmal && { check_lib mmal interface/mmal/mmal.h mmal_port_co + check_lib mmal interface/mmal/mmal.h mmal_port_connect -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host; } || + die "ERROR: mmal not found" && + check_func_headers interface/mmal/mmal.h "MMAL_PARAMETER_VIDEO_MAX_NUM_CALLBACKS"; } +-enabled openal && { check_pkg_config openal "openal >= 1.1" "AL/al.h" alGetError || +- { for al_extralibs in "${OPENAL_LIBS}" "-lopenal" "-lOpenAL32"; do +- check_lib openal 'AL/al.h' alGetError "${al_extralibs}" && break; done } || +- die "ERROR: openal not found"; } && +- { test_cpp_condition "AL/al.h" "defined(AL_VERSION_1_1)" || +- die "ERROR: openal must be installed and version must be 1.1 or compatible"; } + enabled opencl && { check_pkg_config opencl OpenCL CL/cl.h clEnqueueNDRangeKernel || + check_lib opencl OpenCL/cl.h clEnqueueNDRangeKernel "-framework OpenCL" || + check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL || +-- +2.34.1 + diff --git a/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch b/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch new file mode 100644 index 00000000000..fe08aebdadf --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0008-remove-amf-loop-query.patch @@ -0,0 +1,26 @@ +From 1440f556234d135ce58a2ef38916c6a63b05870e Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Sat, 14 Dec 2024 21:39:44 +0800 +Subject: [PATCH] remove amf loop query + +Signed-off-by: 21pages +--- + libavcodec/amfenc.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c +index f70f0109f6..a53a05b16b 100644 +--- a/libavcodec/amfenc.c ++++ b/libavcodec/amfenc.c +@@ -886,7 +886,7 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt) + av_usleep(1000); + } + } +- } while (block_and_wait); ++ } while (false); // already set query timeout + + if (res_query == AMF_EOF) { + ret = AVERROR_EOF; +-- +2.43.0.windows.1 + diff --git a/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch b/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch new file mode 100644 index 00000000000..2e8aff64aa5 --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0009-fix-nvenc-reconfigure-blur.patch @@ -0,0 +1,28 @@ +From bec8d49e75b37806e1cff39c75027860fde0bfa2 Mon Sep 17 00:00:00 2001 +From: 21pages +Date: Fri, 27 Dec 2024 08:43:12 +0800 +Subject: [PATCH] fix nvenc reconfigure blur + +Signed-off-by: 21pages +--- + libavcodec/nvenc.c | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/libavcodec/nvenc.c b/libavcodec/nvenc.c +index 2cce478be0..f4c559b7ce 100644 +--- a/libavcodec/nvenc.c ++++ b/libavcodec/nvenc.c +@@ -2741,8 +2741,8 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) + } + + if (reconfig_bitrate) { +- params.resetEncoder = 1; +- params.forceIDR = 1; ++ params.resetEncoder = 0; ++ params.forceIDR = 0; + + needs_encode_config = 1; + needs_reconfig = 1; +-- +2.43.0.windows.1 + diff --git a/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch b/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch new file mode 100644 index 00000000000..18da50b446c --- /dev/null +++ b/res/vcpkg/ffmpeg/patch/0010.disable-loading-DLLs-from-app-dir.patch @@ -0,0 +1,31 @@ +diff --git a/compat/w32dlfcn.h b/compat/w32dlfcn.h +index ac20e83..1e83aa6 100644 +--- a/compat/w32dlfcn.h ++++ b/compat/w32dlfcn.h +@@ -76,6 +76,7 @@ static inline HMODULE win32_dlopen(const char *name) + if (!name_w) + goto exit; + namelen = wcslen(name_w); ++ /* + // Try local directory first + path = get_module_filename(NULL); + if (!path) +@@ -91,6 +92,7 @@ static inline HMODULE win32_dlopen(const char *name) + path = new_path; + wcscpy(path + pathlen + 1, name_w); + module = LoadLibraryExW(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); ++ */ + if (module == NULL) { + // Next try System32 directory + pathlen = GetSystemDirectoryW(path, pathsize); +@@ -131,7 +133,9 @@ exit: + return NULL; + module = LoadPackagedLibrary(name_w, 0); + #else +-#define LOAD_FLAGS (LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32) ++// #define LOAD_FLAGS (LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32) ++// Don't dynamic-link libraries from the application directory. ++ #define LOAD_FLAGS LOAD_LIBRARY_SEARCH_SYSTEM32 + /* filename may be be in CP_ACP */ + if (!name_w) + return LoadLibraryExA(name, NULL, LOAD_FLAGS); diff --git a/res/vcpkg/ffmpeg/portfile.cmake b/res/vcpkg/ffmpeg/portfile.cmake index 0e35a9550c0..9d09c526423 100644 --- a/res/vcpkg/ffmpeg/portfile.cmake +++ b/res/vcpkg/ffmpeg/portfile.cmake @@ -2,20 +2,30 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO ffmpeg/ffmpeg REF "n${VERSION}" - SHA512 3ba02e8b979c80bf61d55f414bdac2c756578bb36498ed7486151755c6ccf8bd8ff2b8c7afa3c5d1acd862ce48314886a86a105613c05e36601984c334f8f6bf + SHA512 3b273769ef1a1b63aed0691eef317a760f8c83b1d0e1c232b67bbee26db60b4864aafbc88df0e86d6bebf07185bbd057f33e2d5258fde6d97763b9994cd48b6f HEAD_REF master PATCHES - 0002-fix-msvc-link.patch # upstreamed in future version + 0001-create-lib-libraries.patch + 0002-fix-msvc-link.patch 0003-fix-windowsinclude.patch - 0005-fix-nasm.patch # upstreamed in future version - 0012-Fix-ssl-110-detection.patch + 0004-dependencies.patch + 0005-fix-nasm.patch + 0007-fix-lib-naming.patch 0013-define-WINVER.patch + 0020-fix-aarch64-libswscale.patch + 0024-fix-osx-host-c11.patch + 0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch # Do not remove this patch. It is required by chromium + 0041-add-const-for-opengl-definition.patch + 0043-fix-miss-head.patch patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch - patch/0003-amf-colorspace.patch patch/0004-videotoolbox-changing-bitrate.patch patch/0005-mediacodec-changing-bitrate.patch patch/0006-dlopen-libva.patch + patch/0007-fix-linux-configure.patch + patch/0008-remove-amf-loop-query.patch + patch/0009-fix-nvenc-reconfigure-blur.patch + patch/0010.disable-loading-DLLs-from-app-dir.patch ) if(SOURCE_PATH MATCHES " ") @@ -51,6 +61,7 @@ set(OPTIONS "\ --disable-debug \ --disable-valgrind-backtrace \ --disable-large-tests \ +--disable-bzlib \ --disable-avdevice \ --enable-avcodec \ --enable-avformat \ diff --git a/res/vcpkg/ffmpeg/vcpkg.json b/res/vcpkg/ffmpeg/vcpkg.json index f7612d9281c..0346bb58576 100644 --- a/res/vcpkg/ffmpeg/vcpkg.json +++ b/res/vcpkg/ffmpeg/vcpkg.json @@ -1,7 +1,7 @@ { "name": "ffmpeg", - "version": "7.0.2", - "port-version": 0, + "version": "7.1", + "port-version": 1, "description": [ "a library to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created.", "FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations." diff --git a/res/vcpkg/libvpx/portfile.cmake b/res/vcpkg/libvpx/portfile.cmake index 96eab871739..ac54eafd4c9 100644 --- a/res/vcpkg/libvpx/portfile.cmake +++ b/res/vcpkg/libvpx/portfile.cmake @@ -4,7 +4,7 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO webmproject/libvpx REF "v${VERSION}" - SHA512 3e3bfad3d035c0bc3db7cb5a194d56d3c90f5963fb1ad527ae5252054e7c48ce2973de1346c97d94b59f7a95d4801bec44214cce10faf123f92b36fca79a8d1e + SHA512 8f483653a324c710fd431b87fd0d5d6f476f006bd8c8e9c6d1fa6abd105d6a40ac81c8fd5638b431c455d57ab2ee823c165e9875eb3932e6e518477422da3a7b HEAD_REF master PATCHES 0002-Fix-nasm-debug-format-flag.patch diff --git a/res/vcpkg/libvpx/vcpkg.json b/res/vcpkg/libvpx/vcpkg.json index ca4a47d309b..d19c5daca06 100644 --- a/res/vcpkg/libvpx/vcpkg.json +++ b/res/vcpkg/libvpx/vcpkg.json @@ -1,6 +1,6 @@ { "name": "libvpx", - "version": "1.14.1", + "version": "1.15.0", "port-version": 0, "description": "The reference software implementation for the video coding formats VP8 and VP9.", "homepage": "https://github.com/webmproject/libvpx", diff --git a/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch b/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch new file mode 100644 index 00000000000..676c0dd7a93 --- /dev/null +++ b/res/vcpkg/mfx-dispatch/0003-upgrade-cmake-3.14.patch @@ -0,0 +1,10 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index a8a3288..7d01d97 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -1,4 +1,4 @@ +-cmake_minimum_required(VERSION 2.6) ++cmake_minimum_required(VERSION 3.14) + + project( libmfx ) + diff --git a/res/vcpkg/mfx-dispatch/fix-pkgconf.patch b/res/vcpkg/mfx-dispatch/fix-pkgconf.patch new file mode 100644 index 00000000000..c0310e12a9c --- /dev/null +++ b/res/vcpkg/mfx-dispatch/fix-pkgconf.patch @@ -0,0 +1,39 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 9446bc4..a8a3288 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -3,16 +3,7 @@ cmake_minimum_required(VERSION 2.6) + project( libmfx ) + + # FIXME Adds support for using system/other install of intel media sdk +-find_path ( INTELMEDIASDK_PATH mfx/mfxvideo.h +- HINTS "${CMAKE_SOURCE_DIR}" +-) +- +-if (INTELMEDIASDK_PATH_NOTFOUND) +- message( FATAL_ERROR "Intel MEDIA SDK include not found" ) +-else (INTELMEDIASDK_PATH_NOTFOUND) +- message(STATUS "Intel Media SDK is here: ${INTELMEDIASDK_PATH}") +-endif (INTELMEDIASDK_PATH_NOTFOUND) +- ++set(INTELMEDIASDK_PATH "${CMAKE_CURRENT_LIST_DIR}") + + set(SOURCES + src/main.cpp +diff --git a/libmfx.pc.cmake b/libmfx.pc.cmake +index fabb541..5d248fe 100644 +--- a/libmfx.pc.cmake ++++ b/libmfx.pc.cmake +@@ -6,9 +6,9 @@ Requires.private: + Name: libmfx + Description: Intel Media SDK Dispatched static library +-Version: 2013 ++Version: 1.35 + Requires: + Requires.private: + Conflicts: +-Libs: -L${libdir} -lsupc++ ${libdir}/libmfx.lib ++Libs: -L${libdir} -llibmfx + Libs.private: +-Cflags: -I${includedir} -I@INTELMEDIASDK_PATH@ ++Cflags: -I${includedir} diff --git a/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch b/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch new file mode 100644 index 00000000000..96d9e6d90aa --- /dev/null +++ b/res/vcpkg/mfx-dispatch/fix-unresolved-symbol.patch @@ -0,0 +1,66 @@ +Subject: [PATCH] fix for vcpkg +fix missing mfx_driver_store_loader related symbols +--- +Index: CMakeLists.txt +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/CMakeLists.txt b/CMakeLists.txt +--- a/CMakeLists.txt (revision 7e4d221c36c630c1250b23a5dfa15657bc04c10c) ++++ b/CMakeLists.txt (revision 5ebef171699530ca01594a5cef10a68811f4d105) +@@ -40,6 +39,7 @@ + src/mfx_load_plugin.cpp + src/mfx_plugin_hive.cpp + src/mfx_win_reg_key.cpp ++ src/mfx_driver_store_loader.cpp + ) + endif (CMAKE_SYSTEM_NAME MATCHES "Windows") + +@@ -56,6 +56,12 @@ + configure_file (${CMAKE_SOURCE_DIR}/libmfx.pc.cmake ${CMAKE_BINARY_DIR}/libmfx.pc @ONLY) + + add_library( mfx STATIC ${SOURCES} ) ++ ++if (CMAKE_SYSTEM_NAME MATCHES "Windows") ++ set_target_properties(mfx ++ PROPERTIES PREFIX lib) ++endif (CMAKE_SYSTEM_NAME MATCHES "Windows") ++ + install (DIRECTORY ${CMAKE_SOURCE_DIR}/mfx DESTINATION ${CMAKE_INSTALL_PREFIX}/include FILES_MATCHING PATTERN "*.h") + install (FILES ${CMAKE_BINARY_DIR}/libmfx.pc DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/pkgconfig) + install (TARGETS mfx ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib) +Index: libmfx.pc.cmake +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/libmfx.pc.cmake b/libmfx.pc.cmake +--- a/libmfx.pc.cmake (revision 7e4d221c36c630c1250b23a5dfa15657bc04c10c) ++++ b/libmfx.pc.cmake (revision 388559e9e8234eb0989e1598a9beea4035a04132) +@@ -9,6 +9,6 @@ + Requires: + Requires.private: + Conflicts: +-Libs: -L${libdir} -lsupc++ ${libdir}/libmfx.a ++Libs: -L${libdir} -lsupc++ ${libdir}/libmfx.lib + Libs.private: + Cflags: -I${includedir} -I@INTELMEDIASDK_PATH@ +Index: src/mfx_driver_store_loader.cpp +IDEA additional info: +Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP +<+>UTF-8 +=================================================================== +diff --git a/src/mfx_driver_store_loader.cpp b/src/mfx_driver_store_loader.cpp +--- a/src/mfx_driver_store_loader.cpp (revision 388559e9e8234eb0989e1598a9beea4035a04132) ++++ b/src/mfx_driver_store_loader.cpp (revision 5ebef171699530ca01594a5cef10a68811f4d105) +@@ -24,6 +24,9 @@ + #include "mfx_dispatcher_log.h" + #include "mfx_load_dll.h" + ++#pragma comment(lib, "Ole32.lib") ++#pragma comment(lib, "Advapi32.lib") ++ + namespace MFX + { + diff --git a/res/vcpkg/mfx-dispatch/portfile.cmake b/res/vcpkg/mfx-dispatch/portfile.cmake new file mode 100644 index 00000000000..cb2ad7e7ced --- /dev/null +++ b/res/vcpkg/mfx-dispatch/portfile.cmake @@ -0,0 +1,40 @@ +vcpkg_download_distfile( + MISSING_CSTDINT_IMPORT_PATCH + URLS https://github.com/lu-zero/mfx_dispatch/commit/d6241243f85a0d947bdfe813006686a930edef24.patch?full_index=1 + FILENAME fix-missing-cstdint-import-d6241243f85a0d947bdfe813006686a930edef24.patch + SHA512 5d2ffc4ec2ba0e5859d01d2e072f75436ebc3e62e0f6580b5bb8b9f82fe588e7558a46a1fdfa0297a782c0eeb8f50322258d0dd9e41d927cc9be496727b61e44 +) + +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO lu-zero/mfx_dispatch + REF "${VERSION}" + SHA512 12517338342d3e653043a57e290eb9cffd190aede0c3a3948956f1c7f12f0ea859361cf3e534ab066b96b1c211f68409c67ef21fd6d76b68cc31daef541941b0 + HEAD_REF master + PATCHES + fix-unresolved-symbol.patch + fix-pkgconf.patch + 0003-upgrade-cmake-3.14.patch + ${MISSING_CSTDINT_IMPORT_PATCH} +) + +if(VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW) + vcpkg_cmake_configure( + SOURCE_PATH "${SOURCE_PATH}" + ) + vcpkg_cmake_install() + vcpkg_copy_pdbs() +else() + if(VCPKG_TARGET_IS_MINGW) + vcpkg_check_linkage(ONLY_STATIC_LIBRARY) + endif() + vcpkg_configure_make( + SOURCE_PATH "${SOURCE_PATH}" + AUTOCONFIG + ) + vcpkg_install_make() +endif() +vcpkg_fixup_pkgconfig() + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/LICENSE") diff --git a/res/vcpkg/mfx-dispatch/vcpkg.json b/res/vcpkg/mfx-dispatch/vcpkg.json new file mode 100644 index 00000000000..e8374718812 --- /dev/null +++ b/res/vcpkg/mfx-dispatch/vcpkg.json @@ -0,0 +1,16 @@ +{ + "name": "mfx-dispatch", + "version": "1.35.1", + "port-version": 5, + "description": "Open source Intel media sdk dispatcher", + "homepage": "https://github.com/lu-zero/mfx_dispatch", + "license": "BSD-3-Clause", + "supports": "((x86 | x64) & (android | linux)) | (windows & !uwp)", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true, + "platform": "windows & !mingw" + } + ] +} diff --git a/src/client.rs b/src/client.rs index a201336ac0f..4c2a3c315b4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,30 +1,43 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::clipboard_listener; use async_trait::async_trait; use bytes::Bytes; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use clipboard_master::{CallbackResult, ClipboardHandler}; +use clipboard_master::CallbackResult; +#[cfg(not(target_os = "linux"))] use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, Device, Host, StreamConfig, }; use crossbeam_queue::ArrayQueue; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; +#[cfg(not(target_os = "linux"))] use ringbuf::{ring_buffer::RbBase, Rb}; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; use std::{ collections::HashMap, ffi::c_void, - io, net::SocketAddr, ops::Deref, str::FromStr, sync::{ - mpsc::{self, RecvTimeoutError, Sender}, + mpsc::{self, RecvTimeoutError}, Arc, Mutex, RwLock, }, }; use uuid::Uuid; +use crate::{ + check_port, + common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}, + create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, + kcp_stream::KcpStream, + secure_tcp, + ui_interface::{get_builtin_option, use_texture_render}, + ui_session_interface::{InvokeUiSession, Session}, +}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::{clipboard::check_clipboard_files, clipboard_file::unix_file_clip}; pub use file_trait::FileManager; #[cfg(not(feature = "flutter"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -34,28 +47,31 @@ use hbb_common::{ anyhow::{anyhow, Context}, bail, config::{ - self, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, - PUBLIC_RS_PUB_KEY, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, + self, keys, use_ws, Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, + CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, RENDEZVOUS_PORT, RENDEZVOUS_SERVERS, }, + fs::JobType, + futures::future::{select_ok, FutureExt}, get_version_number, log, message_proto::{option_message::BoolOption, *}, protobuf::{Message as _, MessageField}, rand, rendezvous_proto::*, - socket_client::{connect_tcp, connect_tcp_local, ipv4_to_ipv6}, + sha2::{Digest, Sha256}, + socket_client::{connect_tcp, connect_tcp_local, ipv4_to_ipv6, new_direct_udp_for}, sodiumoxide::{base64, crypto::sign}, - tcp::FramedStream, timeout, tokio::{ self, + net::UdpSocket, + sync::{ + mpsc::{unbounded_channel, UnboundedReceiver}, + oneshot, + }, time::{interval, Duration, Instant}, }, AddrMangle, ResultType, Stream, }; -use hbb_common::{ - config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING, - tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver}, -}; pub use helper::*; use scrap::{ codec::Decoder, @@ -63,14 +79,6 @@ use scrap::{ CodecFormat, ImageFormat, ImageRgb, ImageTexture, }; -use crate::{ - check_port, - common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP}, - create_symmetric_key_msg, decode_id_pk, get_rs_pk, is_keyboard_mode_supported, secure_tcp, - ui_interface::{get_builtin_option, use_texture_render}, - ui_session_interface::{InvokeUiSession, Session}, -}; - #[cfg(not(target_os = "ios"))] use crate::clipboard::CLIPBOARD_INTERVAL; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -84,6 +92,7 @@ pub use super::lang::*; pub mod file_trait; pub mod helper; pub mod io_loop; +pub mod screenshot; pub const MILLI1: Duration = Duration::from_millis(1); pub const SEC30: Duration = Duration::from_secs(30); @@ -117,6 +126,7 @@ pub const SCRAP_OTHER_VERSION_OR_X11_REQUIRED: &str = pub const SCRAP_X11_REQUIRED: &str = "x11 expected"; pub const SCRAP_X11_REF_URL: &str = "https://rustdesk.com/docs/en/manual/linux/#x11-required"; +#[cfg(not(target_os = "linux"))] pub const AUDIO_BUFFER_MS: usize = 3000; #[cfg(feature = "flutter")] @@ -128,17 +138,23 @@ pub(crate) struct ClientClipboardContext; pub(crate) struct ClientClipboardContext { pub cfg: SessionPermissionConfig, pub tx: UnboundedSender, + #[cfg(feature = "unix-file-copy-paste")] + pub is_file_supported: bool, } /// Client of the remote desktop. pub struct Client; #[cfg(not(target_os = "ios"))] -struct TextClipboardState { - is_required: bool, +struct ClipboardState { + #[cfg(feature = "flutter")] + is_text_required: bool, + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + is_file_required: bool, running: bool, } +#[cfg(not(target_os = "linux"))] lazy_static::lazy_static! { static ref AUDIO_HOST: Host = cpal::default_host(); } @@ -150,7 +166,7 @@ lazy_static::lazy_static! { #[cfg(not(target_os = "ios"))] lazy_static::lazy_static! { - static ref TEXT_CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(TextClipboardState::new())); + static ref CLIPBOARD_STATE: Arc> = Arc::new(Mutex::new(ClipboardState::new())); } const PUBLIC_SERVER: &str = "public"; @@ -166,6 +182,8 @@ pub fn get_key_state(key: enigo::Key) -> bool { } impl Client { + const CLIENT_CLIPBOARD_NAME: &'static str = "client-clipboard"; + /// Start a new connection. pub async fn start( peer: &str, @@ -173,11 +191,20 @@ impl Client { token: &str, conn_type: ConnType, interface: impl Interface, - ) -> ResultType<((Stream, bool, Option>), (i32, String))> { + ) -> ResultType<( + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), + (i32, String), + )> { debug_assert!(peer == interface.get_id()); interface.update_direct(None); interface.update_received(false); - match Self::_start(peer, key, token, conn_type, interface).await { + match Self::_start(peer, key, token, conn_type, interface.clone()).await { Err(err) => { let err_str = err.to_string(); if err_str.starts_with("Failed") { @@ -186,7 +213,19 @@ impl Client { return Err(err); } } - Ok(x) => Ok(x), + Ok(x) => { + // Set x.2 to true only in the connect() function to indicate that direct_failures needs to be updated; everywhere else it should be set to false. + if x.2 { + let direct_failures = interface.get_lch().read().unwrap().direct_failures; + let direct = x.0 .1; + if !interface.is_force_relay() && (direct_failures == 0) != direct { + let n = if direct { 0 } else { 1 }; + log::info!("direct_failures updated to {}", n); + interface.get_lch().write().unwrap().set_direct_failure(n); + } + } + Ok((x.0, x.1)) + } } } @@ -197,7 +236,17 @@ impl Client { token: &str, conn_type: ConnType, interface: impl Interface, - ) -> ResultType<((Stream, bool, Option>), (i32, String))> { + ) -> ResultType<( + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), + (i32, String), + bool, + )> { if config::is_incoming_only() { bail!("Incoming only mode"); } @@ -205,18 +254,29 @@ impl Client { if hbb_common::is_ip_str(peer) { return Ok(( ( - connect_tcp(check_port(peer, RELAY_PORT + 1), CONNECT_TIMEOUT).await?, + connect_tcp_local(check_port(peer, RELAY_PORT + 1), None, CONNECT_TIMEOUT) + .await?, true, None, + None, + "TCP", ), (0, "".to_owned()), + false, )); } // Allow connect to {domain}:{port} if hbb_common::is_domain_port_str(peer) { return Ok(( - (connect_tcp(peer, CONNECT_TIMEOUT).await?, true, None), + ( + connect_tcp_local(peer, None, CONNECT_TIMEOUT).await?, + true, + None, + None, + "TCP", + ), (0, "".to_owned()), + false, )); } @@ -226,7 +286,7 @@ impl Client { } else { (peer, "", key, token) }; - let (mut rendezvous_server, servers, contained) = if other_server.is_empty() { + let (rendezvous_server, servers, contained) = if other_server.is_empty() { crate::get_rendezvous_server(1_000).await } else { if other_server == PUBLIC_SERVER { @@ -243,8 +303,93 @@ impl Client { } }; + if crate::get_ipv6_punch_enabled() { + crate::test_ipv6().await; + } + + let (stop_udp_tx, stop_udp_rx) = oneshot::channel::<()>(); + let udp = + // no need to care about multiple rendezvous servers case, since it is acutally not used any more. + // Shared state for UDP NAT test result + if crate::get_udp_punch_enabled() && !interface.is_force_relay() { + if let Ok((socket, addr)) = new_direct_udp_for(&rendezvous_server).await { + let udp_port = Arc::new(Mutex::new(0)); + let up_cloned = udp_port.clone(); + let socket_cloned = socket.clone(); + let func = async move { + allow_err!(test_udp_uat(socket_cloned, addr, up_cloned, stop_udp_rx).await); + }; + tokio::spawn(func); + (Some(socket), Some(udp_port)) + } else { + (None, None) + } + } else { + (None, None) + }; + let fut = Self::_start_inner( + peer.to_owned(), + key.to_owned(), + token.to_owned(), + conn_type, + interface.clone(), + udp.clone(), + Some(stop_udp_tx), + rendezvous_server.clone(), + servers.clone(), + contained, + ); + if udp.0.is_none() { + return fut.await; + } + let mut connect_futures = Vec::new(); + connect_futures.push(fut.boxed()); + let fut = Self::_start_inner( + peer.to_owned(), + key.to_owned(), + token.to_owned(), + conn_type, + interface, + (None, None), + None, + rendezvous_server, + servers, + contained, + ); + connect_futures.push(fut.boxed()); + match select_ok(connect_futures).await { + Ok(conn) => Ok((conn.0 .0, conn.0 .1, conn.0 .2)), + Err(e) => Err(e), + } + } + + async fn _start_inner( + peer: String, + key: String, + token: String, + conn_type: ConnType, + interface: impl Interface, + mut udp: (Option>, Option>>), + stop_udp_tx: Option>, + mut rendezvous_server: String, + servers: Vec, + contained: bool, + ) -> ResultType<( + ( + Stream, + bool, + Option>, + Option, + &'static str, + ), + (i32, String), + bool, + )> { + let mut start = Instant::now(); let mut socket = connect_tcp(&*rendezvous_server, CONNECT_TIMEOUT).await; debug_assert!(!servers.contains(&rendezvous_server)); + let rtt = start.elapsed(); + log::debug!("TCP connection establishment time used: {:?}", rtt); if socket.is_err() && !servers.is_empty() { log::info!("try the other servers: {:?}", servers); for server in servers { @@ -264,40 +409,75 @@ impl Client { let my_addr = socket.local_addr(); let mut signed_id_pk = Vec::new(); let mut relay_server = "".to_owned(); - - if !key.is_empty() && !token.is_empty() { - // mainly for the security of token - allow_err!(secure_tcp(&mut socket, key).await); - } - - let start = std::time::Instant::now(); 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; let mut feedback = 0; - for i in 1..=3 { - log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer); - let mut msg_out = RendezvousMessage::new(); - use hbb_common::protobuf::Enum; - let nat_type = if interface.is_force_relay() { - NatType::SYMMETRIC + use hbb_common::protobuf::Enum; + let nat_type = if interface.is_force_relay() { + NatType::SYMMETRIC + } else { + NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT) + }; + + if !key.is_empty() && !token.is_empty() { + // mainly for the security of token + secure_tcp(&mut socket, &key) + .await + .map_err(|e| anyhow!("Failed to secure tcp: {}", e))?; + } else if let Some(udp) = udp.1.as_ref() { + let tm = Instant::now(); + loop { + let port = *udp.lock().unwrap(); + if port > 0 { + break; + } + // await for 0.5 RTT + if tm.elapsed() > rtt / 2 { + break; + } + hbb_common::sleep(0.001).await; + } + } + // Stop UDP NAT test task if still running + stop_udp_tx.map(|tx| tx.send(())); + let mut msg_out = RendezvousMessage::new(); + let mut ipv6 = if crate::get_ipv6_punch_enabled() { + if let Some((socket, addr)) = crate::get_ipv6_socket().await { + (Some(socket), Some(addr)) } else { - NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT) - }; - msg_out.set_punch_hole_request(PunchHoleRequest { - id: peer.to_owned(), - token: token.to_owned(), - nat_type: nat_type.into(), - licence_key: key.to_owned(), - conn_type: conn_type.into(), - version: crate::VERSION.to_owned(), - ..Default::default() - }); + (None, None) + } + } else { + (None, None) + }; + let udp_nat_port = udp.1.map(|x| *x.lock().unwrap()).unwrap_or(0); + let punch_type = if udp_nat_port > 0 { "UDP" } else { "TCP" }; + msg_out.set_punch_hole_request(PunchHoleRequest { + id: peer.to_owned(), + token: token.to_owned(), + nat_type: nat_type.into(), + licence_key: key.to_owned(), + conn_type: conn_type.into(), + version: crate::VERSION.to_owned(), + udp_port: udp_nat_port as _, + force_relay: interface.is_force_relay(), + socket_addr_v6: ipv6.1.unwrap_or_default(), + ..Default::default() + }); + for i in 1..=3 { + log::info!( + "#{} {} punch attempt with {}, id: {}", + i, + punch_type, + my_addr, + peer + ); socket.send(&msg_out).await?; // below timeout should not bigger than hbbs's connection timeout. if let Some(msg_in) = - crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 6000)).await + crate::get_next_nonkeyexchange_msg(&mut socket, Some(i * 3000)).await { match msg_in.union { Some(rendezvous_message::Union::PunchHoleResponse(ph)) => { @@ -327,7 +507,24 @@ impl Client { relay_server = ph.relay_server; peer_addr = AddrMangle::decode(&ph.socket_addr); feedback = ph.feedback; - log::info!("Hole Punched {} = {}", peer, peer_addr); + let s = udp.0.take(); + if ph.is_udp && s.is_some() { + if let Some(s) = s { + allow_err!(s.connect(peer_addr).await); + udp.0 = Some(s); + } + } + let s = ipv6.0.take(); + if !ph.socket_addr_v6.is_empty() && s.is_some() { + let addr = AddrMangle::decode(&ph.socket_addr_v6); + if addr.port() > 0 { + if let Some(s) = s { + allow_err!(s.connect(addr).await); + ipv6.0 = Some(s); + } + } + } + log::info!("{} Hole Punched {} = {}", punch_type, peer, peer_addr); break; } } @@ -337,20 +534,49 @@ impl Client { start.elapsed(), rr.relay_server ); + start = Instant::now(); + let mut connect_futures = Vec::new(); + if let Some(s) = ipv6.0 { + let addr = AddrMangle::decode(&rr.socket_addr_v6); + if addr.port() > 0 { + if s.connect(addr).await.is_ok() { + connect_futures + .push(udp_nat_connect(s, "IPv6", CONNECT_TIMEOUT).boxed()); + } + } + } signed_id_pk = rr.pk().into(); - let mut conn = Self::create_relay( - peer, + let fut = Self::create_relay( + &peer, rr.uuid, rr.relay_server, - key, + &key, conn_type, my_addr.is_ipv4(), - ) - .await?; + ); + connect_futures.push( + async move { + let conn = fut.await?; + Ok((conn, None, if use_ws() { "WebSocket" } else { "Relay" })) + } + .boxed(), + ); + // Run all connection attempts concurrently, return the first successful one + let (conn, kcp, typ) = match select_ok(connect_futures).await { + Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2), + + Err(e) => (Err(e), None, ""), + }; + let mut conn = conn?; feedback = rr.feedback; + log::info!("{:?} used to establish {typ} connection", start.elapsed()); let pk = - Self::secure_connection(peer, signed_id_pk, key, &mut conn).await?; - return Ok(((conn, false, pk), (feedback, rendezvous_server))); + Self::secure_connection(&peer, signed_id_pk, &key, &mut conn).await?; + return Ok(( + (conn, typ == "IPv6", pk, kcp, typ), + (feedback, rendezvous_server), + false, + )); } _ => { log::error!("Unexpected protobuf msg received: {:?}", msg_in); @@ -364,8 +590,9 @@ impl Client { } let time_used = start.elapsed().as_millis() as u64; log::info!( - "{} ms used to punch hole, relay_server: {}, {}", + "{} ms used to {} punch hole, relay_server: {}, {}", time_used, + punch_type, relay_server, if is_local { "is_local: true".to_owned() @@ -377,7 +604,7 @@ impl Client { Self::connect( my_addr, peer_addr, - peer, + &peer, signed_id_pk, &relay_server, &rendezvous_server, @@ -385,13 +612,17 @@ impl Client { peer_nat_type, my_nat_type, is_local, - key, - token, + &key, + &token, conn_type, interface, + udp.0, + ipv6.0, + punch_type, ) .await?, (feedback, rendezvous_server), + true, )) } @@ -411,7 +642,16 @@ impl Client { token: &str, conn_type: ConnType, interface: impl Interface, - ) -> ResultType<(Stream, bool, Option>)> { + udp_socket_nat: Option>, + udp_socket_v6: Option>, + punch_type: &str, + ) -> ResultType<( + Stream, + bool, + Option>, + Option, + &'static str, + )> { let direct_failures = interface.get_lch().read().unwrap().direct_failures; let mut connect_timeout = 0; const MIN: u64 = 1000; @@ -446,10 +686,29 @@ 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 = connect_tcp_local(peer, Some(local_addr), connect_timeout).await; + + let mut connect_futures = Vec::new(); + let fut = connect_tcp_local(peer, Some(local_addr), connect_timeout); + connect_futures.push( + async move { + let conn = fut.await?; + Ok((conn, None, "TCP")) + } + .boxed(), + ); + if let Some(udp_socket_nat) = udp_socket_nat { + connect_futures.push(udp_nat_connect(udp_socket_nat, "UDP", connect_timeout).boxed()); + } + if let Some(udp_socket_v6) = udp_socket_v6 { + connect_futures.push(udp_nat_connect(udp_socket_v6, "IPv6", connect_timeout).boxed()); + } + // Run all connection attempts concurrently, return the first successful one + let (mut conn, kcp, mut typ) = match select_ok(connect_futures).await { + Ok(conn) => (Ok(conn.0 .0), conn.0 .1, conn.0 .2), + Err(e) => (Err(e), None, ""), + }; + let mut direct = !conn.is_err(); - interface.update_direct(Some(direct)); if interface.is_force_relay() || conn.is_err() { if !relay_server.is_empty() { conn = Self::request_relay( @@ -462,24 +721,34 @@ impl Client { conn_type, ) .await; - interface.update_direct(Some(false)); if let Err(e) = conn { + // this direct is mainly used by on_establish_connection_error, so we update it here before bail + interface.update_direct(Some(false)); bail!("Failed to connect via relay server: {}", e); } + typ = "Relay"; direct = false; } else { bail!("Failed to make direct connection to remote desktop"); } } - if !relay_server.is_empty() && (direct_failures == 0) != direct { - let n = if direct { 0 } else { 1 }; - log::info!("direct_failures updated to {}", n); - interface.get_lch().write().unwrap().set_direct_failure(n); - } let mut conn = conn?; - log::info!("{:?} used to establish connection", start.elapsed()); - let pk = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await?; - Ok((conn, direct, pk)) + log::info!( + "{:?} used to establish {typ} connection with {} punch", + start.elapsed(), + punch_type + ); + let res = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await; + let pk: Option> = match res { + Ok(pk) => pk, + Err(e) => { + // this direct is mainly used by on_establish_connection_error, so we update it here before bail + interface.update_direct(Some(direct)); + bail!(e); + } + }; + log::debug!("{} punch secure_connection ok", punch_type); + Ok((conn, direct, pk, kcp, typ)) } /// Establish secure connection with the server. @@ -583,7 +852,7 @@ impl Client { if !key.is_empty() && !token.is_empty() { // mainly for the security of token - allow_err!(secure_tcp(&mut socket, key).await); + secure_tcp(&mut socket, key).await?; } ipv4 = socket.local_addr().is_ipv4(); @@ -656,7 +925,13 @@ impl Client { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] pub fn set_is_text_clipboard_required(b: bool) { - TEXT_CLIPBOARD_STATE.lock().unwrap().is_required = b; + CLIPBOARD_STATE.lock().unwrap().is_text_required = b; + } + + #[inline] + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + pub fn set_is_file_clipboard_required(b: bool) { + CLIPBOARD_STATE.lock().unwrap().is_file_required = b; } #[cfg(not(target_os = "ios"))] @@ -672,68 +947,55 @@ impl Client { if crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { return; } - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + #[cfg(not(target_os = "android"))] + clipboard_listener::unsubscribe(Self::CLIENT_CLIPBOARD_NAME); + CLIPBOARD_STATE.lock().unwrap().running = false; + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + clipboard::platform::unix::fuse::uninit_fuse_context(true); } // `try_start_clipboard` is called by all session when connection is established. (When handling peer info). // This function only create one thread with a loop, the loop is shared by all sessions. // After all sessions are end, the loop exists. // - // If clipboard update is detected, the text will be sent to all sessions by `send_text_clipboard_msg`. + // If clipboard update is detected, the text will be sent to all sessions by `send_clipboard_msg`. #[cfg(not(any(target_os = "android", target_os = "ios")))] fn try_start_clipboard( _client_clip_ctx: Option, ) -> Option> { - let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + let mut clipboard_lock = CLIPBOARD_STATE.lock().unwrap(); if clipboard_lock.running { return None; } let (tx_cb_result, rx_cb_result) = mpsc::channel(); - let handler = ClientClipboardHandler { - ctx: None, - tx_cb_result, - #[cfg(not(feature = "flutter"))] - client_clip_ctx: _client_clip_ctx, - }; - - let (tx_start_res, rx_start_res) = mpsc::channel(); - let h = crate::clipboard::start_clipbard_master_thread(handler, tx_start_res); - let shutdown = match rx_start_res.recv() { - Ok((Some(s), _)) => s, - Ok((None, err)) => { - log::error!("{}", err); - return None; - } - Err(e) => { - log::error!("Failed to create clipboard listener: {}", e); - return None; - } - }; + if let Err(e) = + clipboard_listener::subscribe(Self::CLIENT_CLIPBOARD_NAME.to_owned(), tx_cb_result) + { + log::error!("Failed to subscribe clipboard listener: {}", e); + return None; + } clipboard_lock.running = true; - let (tx_started, rx_started) = unbounded_channel(); - log::info!("Start text clipboard loop"); + log::info!("Start client clipboard loop"); std::thread::spawn(move || { - let mut is_sent = false; + let mut handler = ClientClipboardHandler { + ctx: None, + #[cfg(not(feature = "flutter"))] + client_clip_ctx: _client_clip_ctx, + }; + tx_started.send(()).ok(); loop { - if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + if !CLIPBOARD_STATE.lock().unwrap().running { break; } - if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { - std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); - continue; - } - - if !is_sent { - is_sent = true; - tx_started.send(()).ok(); - } - match rx_cb_result.recv_timeout(Duration::from_millis(CLIPBOARD_INTERVAL)) { + Ok(CallbackResult::Next) => { + handler.check_clipboard(); + } Ok(CallbackResult::Stop) => { log::debug!("Clipboard listener stopped"); break; @@ -743,13 +1005,14 @@ impl Client { break; } Err(RecvTimeoutError::Timeout) => {} - _ => {} + Err(RecvTimeoutError::Disconnected) => { + log::error!("Clipboard listener disconnected"); + break; + } } } - log::info!("Stop text clipboard loop"); - shutdown.signal(); - h.join().ok(); - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + log::info!("Stop client clipboard loop"); + CLIPBOARD_STATE.lock().unwrap().running = false; }); Some(rx_started) @@ -757,31 +1020,31 @@ impl Client { #[cfg(target_os = "android")] fn try_start_clipboard(_p: Option<()>) -> Option> { - let mut clipboard_lock = TEXT_CLIPBOARD_STATE.lock().unwrap(); + let mut clipboard_lock = CLIPBOARD_STATE.lock().unwrap(); if clipboard_lock.running { return None; } clipboard_lock.running = true; - log::info!("Start text clipboard loop"); + log::info!("Start client clipboard loop"); std::thread::spawn(move || { loop { - if !TEXT_CLIPBOARD_STATE.lock().unwrap().running { + if !CLIPBOARD_STATE.lock().unwrap().running { break; } - if !TEXT_CLIPBOARD_STATE.lock().unwrap().is_required { + if !CLIPBOARD_STATE.lock().unwrap().is_text_required { std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); continue; } if let Some(msg) = crate::clipboard::get_clipboards_msg(true) { - crate::flutter::send_text_clipboard_msg(msg); + crate::flutter::send_clipboard_msg(msg, false); } std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); } - log::info!("Stop text clipboard loop"); - TEXT_CLIPBOARD_STATE.lock().unwrap().running = false; + log::info!("Stop client clipboard loop"); + CLIPBOARD_STATE.lock().unwrap().running = false; }); None @@ -789,10 +1052,13 @@ impl Client { } #[cfg(not(target_os = "ios"))] -impl TextClipboardState { +impl ClipboardState { fn new() -> Self { Self { - is_required: true, + #[cfg(feature = "flutter")] + is_text_required: true, + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + is_file_required: true, running: false, } } @@ -801,59 +1067,107 @@ impl TextClipboardState { #[cfg(not(any(target_os = "android", target_os = "ios")))] struct ClientClipboardHandler { ctx: Option, - tx_cb_result: Sender, #[cfg(not(feature = "flutter"))] client_clip_ctx: Option, } #[cfg(not(any(target_os = "android", target_os = "ios")))] impl ClientClipboardHandler { - #[inline] - #[cfg(feature = "flutter")] - fn send_msg(&self, msg: Message) { - crate::flutter::send_text_clipboard_msg(msg); + fn is_text_required(&self) -> bool { + #[cfg(feature = "flutter")] + { + CLIPBOARD_STATE.lock().unwrap().is_text_required + } + #[cfg(not(feature = "flutter"))] + { + self.client_clip_ctx + .as_ref() + .map(|ctx| ctx.cfg.is_text_clipboard_required()) + .unwrap_or(false) + } } - #[cfg(not(feature = "flutter"))] - fn send_msg(&self, msg: Message) { - if let Some(ctx) = &self.client_clip_ctx { - if ctx.cfg.is_text_clipboard_required() { - if let Some(pi) = ctx.cfg.lc.read().unwrap().peer_info.as_ref() { - if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { - if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( - &pi.version, - &pi.platform, - multi_clipboards, - ) { - let _ = ctx.tx.send(Data::Message(msg_out)); - return; + #[cfg(feature = "unix-file-copy-paste")] + fn is_file_required(&self) -> bool { + #[cfg(feature = "flutter")] + { + CLIPBOARD_STATE.lock().unwrap().is_file_required + } + #[cfg(not(feature = "flutter"))] + { + self.client_clip_ctx + .as_ref() + .map(|ctx| ctx.cfg.is_file_clipboard_required()) + .unwrap_or(false) + } + } + + fn check_clipboard(&mut self) { + if CLIPBOARD_STATE.lock().unwrap().running { + #[cfg(feature = "unix-file-copy-paste")] + if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Client, false) { + if !urls.is_empty() { + #[cfg(target_os = "macos")] + if crate::clipboard::is_file_url_set_by_rustdesk(&urls) { + return; + } + if self.is_file_required() { + match clipboard::platform::unix::serv_files::sync_files(&urls) { + Ok(()) => { + let msg = crate::clipboard_file::clip_2_msg( + unix_file_clip::get_format_list(), + ); + self.send_msg(msg, true); + } + Err(e) => { + log::error!("Failed to sync clipboard files: {}", e); + } } + return; } } - let _ = ctx.tx.send(Data::Message(msg)); } - } - } -} -#[cfg(not(any(target_os = "android", target_os = "ios")))] -impl ClipboardHandler for ClientClipboardHandler { - fn on_clipboard_change(&mut self) -> CallbackResult { - if TEXT_CLIPBOARD_STATE.lock().unwrap().running - && TEXT_CLIPBOARD_STATE.lock().unwrap().is_required - { if let Some(msg) = check_clipboard(&mut self.ctx, ClipboardSide::Client, false) { - self.send_msg(msg); + if self.is_text_required() { + self.send_msg(msg, false); + } } } - CallbackResult::Next } - fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { - self.tx_cb_result - .send(CallbackResult::StopWithError(error)) - .ok(); - CallbackResult::Next + #[inline] + #[cfg(feature = "flutter")] + fn send_msg(&self, msg: Message, _is_file: bool) { + crate::flutter::send_clipboard_msg(msg, _is_file); + } + + #[cfg(not(feature = "flutter"))] + fn send_msg(&self, msg: Message, _is_file: bool) { + if let Some(ctx) = &self.client_clip_ctx { + #[cfg(feature = "unix-file-copy-paste")] + if _is_file { + if ctx.is_file_supported { + let _ = ctx.tx.send(Data::Message(msg)); + } + return; + } + + let pi = ctx.cfg.lc.read().unwrap().peer_info.clone(); + if let Some(pi) = pi.as_ref() { + if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { + if let Some(msg_out) = crate::clipboard::get_msg_if_not_support_multi_clip( + &pi.version, + &pi.platform, + multi_clipboards, + ) { + let _ = ctx.tx.send(Data::Message(msg_out)); + return; + } + } + } + let _ = ctx.tx.send(Data::Message(msg)); + } } } @@ -861,20 +1175,28 @@ impl ClipboardHandler for ClientClipboardHandler { #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, + #[cfg(target_os = "linux")] + simple: Option, + #[cfg(not(target_os = "linux"))] audio_buffer: AudioBuffer, sample_rate: (u32, u32), + #[cfg(not(target_os = "linux"))] audio_stream: Option>, channels: u16, + #[cfg(not(target_os = "linux"))] device_channel: u16, + #[cfg(not(target_os = "linux"))] ready: Arc>, } +#[cfg(not(target_os = "linux"))] struct AudioBuffer( pub Arc>>, usize, [usize; 30], ); +#[cfg(not(target_os = "linux"))] impl Default for AudioBuffer { fn default() -> Self { Self( @@ -887,6 +1209,7 @@ impl Default for AudioBuffer { } } +#[cfg(not(target_os = "linux"))] impl AudioBuffer { pub fn resize(&mut self, sample_rate: usize, channels: usize) { let capacity = sample_rate * channels * AUDIO_BUFFER_MS / 1000; @@ -989,7 +1312,37 @@ impl AudioBuffer { } impl AudioHandler { + #[cfg(target_os = "linux")] + fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { + use psimple::Simple; + use pulse::sample::{Format, Spec}; + use pulse::stream::Direction; + + let spec = Spec { + format: Format::F32le, + channels: format0.channels as _, + rate: format0.sample_rate as _, + }; + if !spec.is_valid() { + bail!("Invalid audio format"); + } + + self.simple = Some(Simple::new( + None, // Use the default server + &crate::get_app_name(), // Our application’s name + Direction::Playback, // We want a playback stream + None, // Use the default device + "playback", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + )?); + self.sample_rate = (format0.sample_rate, format0.sample_rate); + Ok(()) + } + /// Start the audio playback. + #[cfg(not(target_os = "linux"))] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { let device = AUDIO_HOST .default_output_device() @@ -1057,13 +1410,20 @@ impl AudioHandler { /// Handle audio frame and play it. #[inline] pub fn handle_frame(&mut self, frame: AudioFrame) { + #[cfg(not(target_os = "linux"))] if self.audio_stream.is_none() || !self.ready.lock().unwrap().clone() { return; } + #[cfg(target_os = "linux")] + if self.simple.is_none() { + log::debug!("PulseAudio simple binding does not exists"); + return; + } self.audio_decoder.as_mut().map(|(d, buffer)| { if let Ok(n) = d.decode_float(&frame.data, buffer, false) { let channels = self.channels; let n = n * (channels as usize); + #[cfg(not(target_os = "linux"))] { let sample_rate0 = self.sample_rate.0; let sample_rate = self.sample_rate.1; @@ -1087,11 +1447,18 @@ impl AudioHandler { } self.audio_buffer.append_pcm(&buffer); } + #[cfg(target_os = "linux")] + { + let data_u8 = + unsafe { std::slice::from_raw_parts::(buffer.as_ptr() as _, n * 4) }; + self.simple.as_mut().map(|x| x.write(data_u8)); + } } }); } /// Build audio output stream for current device. + #[cfg(not(target_os = "linux"))] fn build_output_stream>( &mut self, config: &StreamConfig, @@ -1281,14 +1648,15 @@ impl VideoHandler { } /// Start or stop screen record. - pub fn record_screen(&mut self, start: bool, id: String, display: usize) { + pub fn record_screen(&mut self, start: bool, id: String, display_idx: usize, camera: bool) { self.record = false; if start { self.recorder = Recorder::new(RecorderContext { server: false, id, dir: crate::ui_interface::video_save_directory(false), - display, + display_idx, + camera, tx: None, }) .map_or(Default::default(), |r| Arc::new(Mutex::new(Some(r)))); @@ -1359,6 +1727,7 @@ struct ConnToken { pub struct LoginConfigHandler { id: String, pub conn_type: ConnType, + pub is_terminal_admin: bool, hash: Hash, password: Vec, // remember password for reconnect pub remember: bool, @@ -1384,7 +1753,8 @@ pub struct LoginConfigHandler { password_source: PasswordSource, // where the sent password comes from shared_password: Option, // Store the shared password pub enable_trusted_devices: bool, - pub record: bool, + pub record_state: bool, + pub record_permission: bool, } impl Deref for LoginConfigHandler { @@ -1420,7 +1790,7 @@ impl LoginConfigHandler { let server = server_key.next().unwrap_or_default(); let args = server_key.next().unwrap_or_default(); let key = if server == PUBLIC_SERVER { - PUBLIC_RS_PUB_KEY.to_owned() + config::RS_PUB_KEY.to_owned() } else { let mut args_map: HashMap = HashMap::new(); for arg in args.split('&') { @@ -1476,20 +1846,29 @@ impl LoginConfigHandler { self.restarting_remote_device = false; self.force_relay = config::option2bool("force-always-relay", &self.get_option("force-always-relay")) - || force_relay; + || force_relay + || use_ws() + || Config::is_proxy(); if let Some((real_id, server, key)) = &self.other_server { let other_server_key = self.get_option("other-server-key"); if !other_server_key.is_empty() && key.is_empty() { self.other_server = Some((real_id.to_owned(), server.to_owned(), other_server_key)); } } + self.direct = None; self.received = false; self.switch_uuid = switch_uuid; self.adapter_luid = adapter_luid; self.selected_windows_session_id = None; self.shared_password = shared_password; - self.record = LocalConfig::get_bool_option(OPTION_ALLOW_AUTO_RECORD_OUTGOING); + self.record_state = false; + self.record_permission = true; + + // `std::env::remove_var("IS_TERMINAL_ADMIN");` is called in `session_add_sync()` - `flutter_ffi.rs`. + let is_terminal_admin = conn_type == ConnType::TERMINAL + && std::env::var("IS_TERMINAL_ADMIN").map_or(false, |v| v == "Y"); + self.is_terminal_admin = is_terminal_admin; } /// Check if the client should auto login. @@ -1529,6 +1908,9 @@ impl LoginConfigHandler { /// * `v` - value of option pub fn set_option(&mut self, k: String, v: String) { let mut config = self.load_config(); + if v == self.get_option(&k) { + return; + } config.options.insert(k, v); self.save_config(config); } @@ -1697,6 +2079,14 @@ impl LoginConfigHandler { BoolOption::No }) .into(); + } else if name == keys::OPTION_TERMINAL_PERSISTENT { + config.terminal_persistent.v = !config.terminal_persistent.v; + option.terminal_persistent = (if config.terminal_persistent.v { + BoolOption::Yes + } else { + BoolOption::No + }) + .into(); } else if name == "privacy-mode" { // try toggle privacy mode option.privacy_mode = (if config.privacy_mode.v { @@ -1757,6 +2147,12 @@ impl LoginConfigHandler { self.config.store(&self.id); return None; } + + #[cfg(feature = "unix-file-copy-paste")] + if option.enable_file_transfer.enum_value() == Ok(BoolOption::No) { + crate::clipboard::try_empty_clipboard_files(crate::clipboard::ClipboardSide::Client, 0); + } + if !name.contains("block-input") { self.save_config(config); } @@ -1788,6 +2184,14 @@ impl LoginConfigHandler { return None; } let mut msg = OptionMessage::new(); + if self.conn_type.eq(&ConnType::TERMINAL) { + if self.get_toggle_option(keys::OPTION_TERMINAL_PERSISTENT) { + msg.terminal_persistent = BoolOption::Yes.into(); + return Some(msg); + } else { + return None; + } + } let q = self.image_quality.clone(); if let Some(q) = self.get_image_quality_enum(&q, ignore_default) { msg.image_quality = q.into(); @@ -1833,7 +2237,7 @@ impl LoginConfigHandler { if self.get_toggle_option("disable-audio") { msg.disable_audio = BoolOption::Yes.into(); } - if !view_only && self.get_toggle_option(config::keys::OPTION_ENABLE_FILE_COPY_PASTE) { + if !view_only && self.get_toggle_option(keys::OPTION_ENABLE_FILE_COPY_PASTE) { msg.enable_file_transfer = BoolOption::Yes.into(); } if view_only || self.get_toggle_option("disable-clipboard") { @@ -1889,9 +2293,11 @@ impl LoginConfigHandler { self.config.show_remote_cursor.v } else if name == "lock-after-session-end" { self.config.lock_after_session_end.v + } else if name == keys::OPTION_TERMINAL_PERSISTENT { + self.config.terminal_persistent.v } else if name == "privacy-mode" { self.config.privacy_mode.v - } else if name == config::keys::OPTION_ENABLE_FILE_COPY_PASTE { + } else if name == keys::OPTION_ENABLE_FILE_COPY_PASTE { self.config.enable_file_copy_paste.v } else if name == "disable-audio" { self.config.disable_audio.v @@ -1982,6 +2388,12 @@ impl LoginConfigHandler { res } + pub fn save_trackpad_speed(&mut self, speed: i32) { + let mut config = self.load_config(); + config.trackpad_speed = speed; + self.save_config(config); + } + /// Create a [`Message`] for saving custom fps. /// /// # Arguments @@ -2185,7 +2597,7 @@ impl LoginConfigHandler { } else { (my_id, self.id.clone()) }; - let mut display_name = get_builtin_option(config::keys::OPTION_DISPLAY_NAME); + let mut display_name = get_builtin_option(keys::OPTION_DISPLAY_NAME); if display_name.is_empty() { display_name = serde_json::from_str::(&LocalConfig::get_option("user_info")) @@ -2200,8 +2612,24 @@ impl LoginConfigHandler { if display_name.is_empty() { display_name = crate::username(); } + let display_name = display_name + .split_whitespace() + .map(|word| { + word.chars() + .enumerate() + .map(|(i, c)| { + if i == 0 { + c.to_uppercase().to_string() + } else { + c.to_string() + } + }) + .collect::() + }) + .collect::>() + .join(" "); #[cfg(not(target_os = "android"))] - let my_platform = whoami::platform().to_string(); + let my_platform = hbb_common::whoami::platform().to_string(); #[cfg(target_os = "android")] let my_platform = "Android".into(); let hwid = if self.get_option("trust-this-device") == "Y" { @@ -2233,11 +2661,17 @@ impl LoginConfigHandler { show_hidden: !self.get_option("remote_show_hidden").is_empty(), ..Default::default() }), + ConnType::VIEW_CAMERA => lr.set_view_camera(Default::default()), ConnType::PORT_FORWARD | ConnType::RDP => lr.set_port_forward(PortForward { host: self.port_forward.0.clone(), port: self.port_forward.1, ..Default::default() }), + ConnType::TERMINAL => { + let mut terminal = Terminal::new(); + terminal.service_id = self.get_option(self.get_key_terminal_service_id()); + lr.set_terminal(terminal); + } _ => {} } @@ -2282,6 +2716,18 @@ impl LoginConfigHandler { }) .ok() } + + pub fn get_id(&self) -> &str { + &self.id + } + + pub fn get_key_terminal_service_id(&self) -> &'static str { + if self.is_terminal_admin { + "terminal-admin-service-id" + } else { + "terminal-service-id" + } + } } /// Media data. @@ -2316,6 +2762,7 @@ pub fn start_video_thread( { let mut video_callback = video_callback; let mut last_chroma = None; + let is_view_camera = session.is_view_camera(); std::thread::spawn(move || { #[cfg(windows)] @@ -2354,10 +2801,11 @@ pub fn start_video_thread( let format = CodecFormat::from(&vf); if video_handler.is_none() { let mut handler = VideoHandler::new(format, display); - let record = session.lc.read().unwrap().record; + let record_state = session.lc.read().unwrap().record_state; + let record_permission = session.lc.read().unwrap().record_permission; let id = session.lc.read().unwrap().id.clone(); - if record { - handler.record_screen(record, id, display); + if record_state && record_permission { + handler.record_screen(true, id, display, is_view_camera); } video_handler = Some(handler); } @@ -2436,11 +2884,9 @@ pub fn start_video_thread( } } MediaData::RecordScreen(start) => { - log::info!("record screen command: start: {start}"); - session.update_record_status(start); let id = session.lc.read().unwrap().id.clone(); if let Some(handler) = video_handler.as_mut() { - handler.record_screen(start, id, display); + handler.record_screen(start, id, display, is_view_camera); } } _ => {} @@ -2949,8 +3395,7 @@ pub async fn handle_hash( } if password.is_empty() { - let p = - crate::ui_interface::get_builtin_option(config::keys::OPTION_DEFAULT_CONNECT_PASSWORD); + let p = crate::ui_interface::get_builtin_option(keys::OPTION_DEFAULT_CONNECT_PASSWORD); if !p.is_empty() { let mut hasher = Sha256::new(); hasher.update(p.clone()); @@ -2962,6 +3407,19 @@ pub async fn handle_hash( } lc.write().unwrap().password = password.clone(); + + let is_terminal_admin = lc.read().unwrap().is_terminal_admin; + let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL); + if is_terminal && is_terminal_admin { + if password.is_empty() { + interface.msgbox("terminal-admin-login-password", "", "", ""); + } else { + interface.msgbox("terminal-admin-login", "", "", ""); + } + lc.write().unwrap().hash = hash; + return; + } + let password = if password.is_empty() { // login without password, the remote side can click accept interface.msgbox("input-password", "Password Required", "", ""); @@ -2973,8 +3431,15 @@ pub async fn handle_hash( hasher.finalize()[..].into() }; - let os_username = lc.read().unwrap().get_option("os-username"); - let os_password = lc.read().unwrap().get_option("os-password"); + let is_terminal = lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL); + let (os_username, os_password) = if is_terminal { + ("".to_owned(), "".to_owned()) + } else { + ( + lc.read().unwrap().get_option("os-username"), + lc.read().unwrap().get_option("os-password"), + ) + }; send_login(lc.clone(), os_username, os_password, password, peer).await; lc.write().unwrap().hash = hash; @@ -3175,7 +3640,7 @@ pub enum Data { Close, Login((String, String, String, bool)), Message(Message), - SendFiles((i32, String, String, i32, bool, bool)), + SendFiles((i32, JobType, String, String, i32, bool, bool)), RemoveDirAll((i32, String, bool, bool)), ConfirmDeleteFiles((i32, i32)), SetNoConfirm(i32), @@ -3185,11 +3650,11 @@ pub enum Data { CancelJob(i32), RemovePortForward(i32), AddPortForward((i32, String, i32)), - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] ToggleClipboardFile, NewRDP, SetConfirmOverrideFile((i32, i32, bool, bool, bool)), - AddJob((i32, String, String, i32, bool, bool)), + AddJob((i32, JobType, String, String, i32, bool, bool)), ResumeJob((i32, bool)), RecordScreen(bool), ElevateDirect, @@ -3198,6 +3663,7 @@ pub enum Data { CloseVoiceCall, ResetDecoder(Option), RenameFile((i32, String, String, bool)), + TakeScreenshot((i32, String)), } /// Keycode for key events. @@ -3345,7 +3811,7 @@ pub fn check_if_retry(msgtype: &str, title: &str, text: &str, retry_for_relay: b && title == "Connection Error" && ((text.contains("10054") || text.contains("104")) && retry_for_relay || (!text.to_lowercase().contains("offline") - && !text.to_lowercase().contains("exist") + && !text.to_lowercase().contains("not exist") && !text.to_lowercase().contains("handshake") && !text.to_lowercase().contains("failed") && !text.to_lowercase().contains("resolve") @@ -3434,8 +3900,7 @@ pub mod peer_online { rendezvous_proto::*, sleep, socket_client::connect_tcp, - tcp::FramedStream, - ResultType, + ResultType, Stream, }; pub async fn query_online_states, Vec)>(ids: Vec, f: F) { @@ -3458,7 +3923,7 @@ pub mod peer_online { } } - async fn create_online_stream() -> ResultType { + async fn create_online_stream() -> ResultType { let (rendezvous_server, _servers, _contained) = crate::get_rendezvous_server(READ_TIMEOUT).await; let tmp: Vec<&str> = rendezvous_server.split(":").collect(); @@ -3501,11 +3966,9 @@ pub mod peer_online { } // Retry for 2 times to get the online response for _ in 0..2 { - if let Some(msg_in) = crate::common::get_next_nonkeyexchange_msg( - &mut socket, - Some(timeout.as_millis() as _), - ) - .await + if let Some(msg_in) = + crate::get_next_nonkeyexchange_msg(&mut socket, Some(timeout.as_millis() as _)) + .await { match msg_in.union { Some(rendezvous_message::Union::OnlineResponse(online_response)) => { @@ -3557,3 +4020,126 @@ pub mod peer_online { } } } + +async fn test_udp_uat( + udp_socket: Arc, + server_addr: SocketAddr, + udp_port: Arc>, + mut stop_udp_rx: oneshot::Receiver<()>, +) -> ResultType<()> { + let (tx, mut rx) = oneshot::channel::<_>(); + tokio::spawn(async { + if let Ok(v) = crate::test_nat_ipv4().await { + tx.send(v).ok(); + } + }); + + let start = Instant::now(); + let mut msg_out = RendezvousMessage::new(); + msg_out.set_test_nat_request(TestNatRequest { + ..Default::default() + }); + // Adaptive retry strategy that works within TCP RTT constraints + // Start with aggressive sending, then back off + let mut retry_interval = Duration::from_millis(20); // Start fast + const MAX_INTERVAL: Duration = Duration::from_millis(200); + let mut packets_sent = 0; + + // Send initial burst to improve reliability + let data = msg_out.write_to_bytes()?; + for _ in 0..2 { + if let Err(e) = udp_socket.send_to(&data, server_addr).await { + log::warn!("Failed to send initial UDP NAT test packet: {}", e); + } else { + packets_sent += 1; + } + } + let mut last_send_time = Instant::now(); + let mut buf = [0u8; 1500]; + + loop { + tokio::select! { + Ok((addr, server)) = &mut rx => { + *udp_port.lock().unwrap() = addr.port(); + log::debug!("UDP NAT test received response from {}: {}", addr, server); + break; + } + _ = &mut stop_udp_rx => { + log::debug!("UDP NAT test received stop signal after {} packets", packets_sent); + break; + } + _ = hbb_common::sleep(retry_interval.as_secs_f32()) => { + // Adaptive retry: send fewer packets as time goes on + let elapsed = last_send_time.elapsed(); + + if elapsed >= retry_interval { + // Send single packet (not double) to reduce network load + if let Err(e) = udp_socket.send_to(&data, server_addr).await { + log::warn!("Failed to send UDP NAT test retry packet: {}", e); + } else { + packets_sent += 1; + } + + // Exponentially increase interval to reduce network pressure + retry_interval = std::cmp::min( + Duration::from_millis((retry_interval.as_millis() as f64 * 1.5) as u64), + MAX_INTERVAL + ); + last_send_time = Instant::now(); + } + } + res = udp_socket.recv(&mut buf[..]) => { + match res { + Ok(n) => { + match RendezvousMessage::parse_from_bytes(&buf[0..n]) { + Ok(msg_in) => { + if let Some(rendezvous_message::Union::TestNatResponse(response)) = msg_in.union { + *udp_port.lock().unwrap() = response.port as u16; + break; + } + } + Err(e) => { + log::warn!("Failed to parse UDP NAT test response: {}", e); + } + } + } + Err(e) => { + log::warn!("UDP NAT test socket error: {}", e); + } + } + } + } + } + + let final_port = *udp_port.lock().unwrap(); + log::debug!( + "UDP NAT test to {:?} finished: time={:?}, port={}, packets_sent={}, success={}", + server_addr, + start.elapsed(), + final_port, + packets_sent, + final_port > 0 + ); + Ok(()) +} + +#[inline] +async fn udp_nat_connect( + socket: Arc, + typ: &'static str, + ms_timeout: u64, +) -> ResultType<(Stream, Option, &'static str)> { + crate::punch_udp(socket.clone(), false) + .await + .map_err(|err| { + log::debug!("{err}"); + anyhow!(err) + })?; + let res = KcpStream::connect(socket, Duration::from_millis(ms_timeout)) + .await + .map_err(|err| { + log::debug!("Failed to connect KCP stream: {}", err); + anyhow!(err) + })?; + Ok((res.1, Some(res.0), typ)) +} diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 88f0b14a5d6..e6b97781810 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -7,6 +7,14 @@ pub trait FileManager: Interface { fs::get_home_as_string() } + fn get_next_job_id(&self) -> i32 { + fs::get_next_job_id() + } + + fn update_next_job_id(&self, id: i32) { + fs::update_next_job_id(id); + } + #[cfg(not(any( target_os = "android", target_os = "ios", @@ -98,6 +106,7 @@ pub trait FileManager: Interface { fn send_files( &self, id: i32, + r#type: i32, path: String, to: String, file_num: i32, @@ -106,6 +115,7 @@ pub trait FileManager: Interface { ) { self.send(Data::SendFiles(( id, + r#type.into(), path, to, file_num, @@ -117,6 +127,7 @@ pub trait FileManager: Interface { fn add_job( &self, id: i32, + r#type: i32, path: String, to: String, file_num: i32, @@ -125,6 +136,7 @@ pub trait FileManager: Interface { ) { self.send(Data::AddJob(( id, + r#type.into(), path, to, file_num, diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs index df07331cfea..3b07525fb5d 100644 --- a/src/client/io_loop.rs +++ b/src/client/io_loop.rs @@ -1,13 +1,3 @@ -use std::{ - collections::HashMap, - ffi::c_void, - num::NonZeroI64, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, RwLock, - }, -}; - #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(not(any(target_os = "ios")))] @@ -20,14 +10,19 @@ use crate::{ common::get_default_sound_input, ui_session_interface::{InvokeUiSession, Session}, }; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(feature = "unix-file-copy-paste")] +use crate::{clipboard::try_empty_clipboard_files, clipboard_file::unix_file_clip}; +#[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") +))] use clipboard::ContextSend; use crossbeam_queue::ArrayQueue; #[cfg(not(target_os = "ios"))] use hbb_common::tokio::sync::mpsc::error::TryRecvError; use hbb_common::{ allow_err, - config::{self, PeerConfig, TransferSerde}, + config::{self, LocalConfig, PeerConfig, TransferSerde}, fs::{ self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, DigestCheckResult, RemoveJobMeta, @@ -44,9 +39,19 @@ use hbb_common::{ }, Stream, }; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +#[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] use hbb_common::{tokio::sync::Mutex as TokioMutex, ResultType}; use scrap::CodecFormat; +use std::{ + collections::HashMap, + ffi::c_void, + num::NonZeroI64, + path::PathBuf, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, RwLock, + }, +}; pub struct Remote { handler: Session, @@ -63,7 +68,7 @@ pub struct Remote { last_update_jobs_status: (Instant, HashMap), is_connected: bool, first_frame: bool, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: i32, // used for file clipboard data_count: Arc, video_format: CodecFormat, @@ -71,6 +76,8 @@ pub struct Remote { peer_info: ParsedPeerInfo, video_threads: HashMap, chroma: Arc>>, + last_record_state: bool, + sent_close_reason: bool, } #[derive(Default)] @@ -78,6 +85,8 @@ struct ParsedPeerInfo { platform: String, is_installed: bool, idd_impl: String, + support_view_camera: bool, + support_terminal: bool, } impl ParsedPeerInfo { @@ -106,7 +115,7 @@ impl Remote { last_update_jobs_status: (Instant::now(), Default::default()), is_connected: false, first_frame: false, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] client_conn_id: 0, data_count: Arc::new(AtomicUsize::new(0)), video_format: CodecFormat::Unknown, @@ -116,14 +125,42 @@ impl Remote { peer_info: Default::default(), video_threads: Default::default(), chroma: Default::default(), + last_record_state: false, + sent_close_reason: false, } } pub async fn io_loop(&mut self, key: &str, token: &str, round: u32) { + #[cfg(target_os = "windows")] + let _file_clip_context_holder = { + // `is_port_forward()` will not reach here, but we still check it for clarity. + if self.handler.is_default() { + // It is ok to call this function multiple times. + ContextSend::enable(true); + Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(|| { + // No need to call `enable(false)` for sciter version, because each client of sciter version is a new process. + // It's better to check if the peers are windows(support file copy&paste), but it's not necessary. + #[cfg(feature = "flutter")] + if !crate::flutter::sessions::has_sessions_running(ConnType::DEFAULT_CONN) { + ContextSend::enable(false); + }; + }), + }) + } else { + None + } + }; + let mut last_recv_time = Instant::now(); let mut received = false; let conn_type = if self.handler.is_file_transfer() { ConnType::FILE_TRANSFER + } else if self.handler.is_view_camera() { + ConnType::VIEW_CAMERA + } else if self.handler.is_terminal() { + ConnType::TERMINAL } else { ConnType::default() }; @@ -137,40 +174,45 @@ impl Remote { ) .await { - Ok(((mut peer, direct, pk), (feedback, rendezvous_server))) => { + Ok(((mut peer, direct, pk, kcp, stream_type), (feedback, rendezvous_server))) => { self.handler .connection_round_state .lock() .unwrap() .set_connected(); - self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready + self.handler + .set_connection_type(peer.is_secured(), direct, stream_type); // flutter -> connection_ready self.handler.update_direct(Some(direct)); - if conn_type == ConnType::DEFAULT_CONN { + if conn_type == ConnType::DEFAULT_CONN || conn_type == ConnType::VIEW_CAMERA { self.handler .set_fingerprint(crate::common::pk_to_fingerprint(pk.unwrap_or_default())); } // just build for now - #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] + #[cfg(not(any(target_os = "windows", feature = "unix-file-copy-paste")))] let (_tx_holder, mut rx_clip_client) = mpsc::unbounded_channel::(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] let (_tx_holder, rx) = mpsc::unbounded_channel(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let mut rx_clip_client_lock = Arc::new(TokioMutex::new(rx)); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + let mut rx_clip_client_holder = (Arc::new(TokioMutex::new(rx)), None); + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] { - let is_conn_not_default = self.handler.is_file_transfer() - || self.handler.is_port_forward() - || self.handler.is_rdp(); - if !is_conn_not_default { - log::debug!("get cliprdr client for conn_id {}", self.client_conn_id); - (self.client_conn_id, rx_clip_client_lock) = + if self.handler.is_default() { + (self.client_conn_id, rx_clip_client_holder.0) = clipboard::get_rx_cliprdr_client(&self.handler.get_id()); + log::debug!("get cliprdr client for conn_id {}", self.client_conn_id); + let client_conn_id = self.client_conn_id; + rx_clip_client_holder.1 = Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(client_conn_id); + }), + }); }; } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let mut rx_clip_client = rx_clip_client_lock.lock().await; + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + let mut rx_clip_client = rx_clip_client_holder.0.lock().await; let mut status_timer = crate::rustdesk_interval(time::interval(Duration::new(1, 0))); @@ -218,8 +260,8 @@ impl Remote { } } _msg = rx_clip_client.recv() => { - #[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))] - self.handle_local_clipboard_msg(&mut peer, _msg).await; + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + self.handle_local_clipboard_msg(&mut peer, _msg).await; } _ = self.timer.tick() => { if last_recv_time.elapsed() >= SEC30 { @@ -281,6 +323,13 @@ impl Remote { if let Some(s) = self.stop_voice_call_sender.take() { s.send(()).ok(); } + if kcp.is_some() { + // Send the close reason if it hasn't been sent yet, as KCP cannot detect the socket close event. + self.send_close_reason(&mut peer, "kcp").await; + // KCP does not send messages immediately, so wait to ensure the last message is sent. + // 1ms works in my test, but 30ms is more reliable. + tokio::time::sleep(Duration::from_millis(30)).await; + } } Err(err) => { self.handler.on_establish_connection_error(err.to_string()); @@ -295,25 +344,20 @@ impl Remote { .set_disconnected(round); #[cfg(not(target_os = "ios"))] - if _set_disconnected_ok { + if self.handler.is_default() && _set_disconnected_ok { Client::try_stop_clipboard(); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - if _set_disconnected_ok { - let conn_id = self.client_conn_id; - log::debug!("try empty cliprdr for conn_id {}", conn_id); - let _ = ContextSend::proc(|context| -> ResultType<()> { - context.empty_clipboard(conn_id)?; - Ok(()) - }); + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + if self.handler.is_default() && _set_disconnected_ok { + crate::clipboard::try_empty_clipboard_files(ClipboardSide::Client, self.client_conn_id); } } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] async fn handle_local_clipboard_msg( &self, - peer: &mut crate::client::FramedStream, + peer: &mut Stream, msg: Option, ) { match msg { @@ -341,8 +385,12 @@ impl Remote { view_only, stop, is_stopping_allowed, server_file_transfer_enabled, file_transfer_enabled ); if stop { - ContextSend::set_is_stopped(); + #[cfg(target_os = "windows")] + { + ContextSend::set_is_stopped(); + } } else { + #[cfg(target_os = "windows")] if let Err(e) = ContextSend::make_sure_enabled() { log::error!("failed to restart clipboard context: {}", e); // to-do: Show msgbox with "Don't show again" option @@ -395,7 +443,10 @@ impl Remote { // Start a voice call recorder, records audio and send to remote fn start_voice_call(&mut self) -> Option> { - if self.handler.is_file_transfer() || self.handler.is_port_forward() { + if self.handler.is_file_transfer() + || self.handler.is_port_forward() + || self.handler.is_terminal() + { return None; } // iOS does not have this server. @@ -470,14 +521,22 @@ impl Remote { } } + async fn send_close_reason(&mut self, peer: &mut Stream, reason: &str) { + if self.sent_close_reason { + return; + } + let mut misc = Misc::new(); + misc.set_close_reason(reason.to_owned()); + let mut msg = Message::new(); + msg.set_misc(misc); + allow_err!(peer.send(&msg).await); + self.sent_close_reason = true; + } + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { - let mut misc = Misc::new(); - misc.set_close_reason("".to_owned()); - let mut msg = Message::new(); - msg.set_misc(misc); - allow_err!(peer.send(&msg).await); + self.send_close_reason(peer, "").await; return false; } Data::Login((os_username, os_password, password, remember)) => { @@ -485,7 +544,7 @@ impl Remote { .handle_login_from_ui(os_username, os_password, password, remember, peer) .await; } - #[cfg(not(feature = "flutter"))] + #[cfg(all(target_os = "windows", not(feature = "flutter")))] Data::ToggleClipboardFile => { self.check_clipboard_file_context(); } @@ -508,13 +567,20 @@ impl Remote { } allow_err!(peer.send(&msg).await); } - Data::SendFiles((id, path, to, file_num, include_hidden, is_remote)) => { + Data::SendFiles((id, r#type, path, to, file_num, include_hidden, is_remote)) => { log::info!("send files, is remote {}", is_remote); let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); if is_remote { log::debug!("New job {}, write to {} from remote {}", id, to, path); + let to = match r#type { + fs::JobType::Generic => fs::DataSource::FilePath(PathBuf::from(&to)), + fs::JobType::Printer => { + fs::DataSource::MemoryCursor(std::io::Cursor::new(Vec::new())) + } + }; self.write_jobs.push(fs::TransferJob::new_write( id, + r#type, path.clone(), to, file_num, @@ -524,14 +590,15 @@ impl Remote { od, )); allow_err!( - peer.send(&fs::new_send(id, path, file_num, include_hidden)) + peer.send(&fs::new_send(id, r#type, path, file_num, include_hidden)) .await ); } else { match fs::TransferJob::new_read( id, + r#type, to.clone(), - path.clone(), + fs::DataSource::FilePath(PathBuf::from(&path)), file_num, include_hidden, is_remote, @@ -575,7 +642,7 @@ impl Remote { } } } - Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { + Data::AddJob((id, r#type, path, to, file_num, include_hidden, is_remote)) => { let od = can_enable_overwrite_detection(self.handler.lc.read().unwrap().version); if is_remote { log::debug!( @@ -586,8 +653,9 @@ impl Remote { ); let mut job = fs::TransferJob::new_write( id, + r#type, path.clone(), - to, + fs::DataSource::FilePath(PathBuf::from(&to)), file_num, include_hidden, is_remote, @@ -599,8 +667,9 @@ impl Remote { } else { match fs::TransferJob::new_read( id, + r#type, to.clone(), - path.clone(), + fs::DataSource::FilePath(PathBuf::from(&path)), file_num, include_hidden, is_remote, @@ -638,6 +707,7 @@ impl Remote { allow_err!( peer.send(&fs::new_send( id, + fs::JobType::Generic, job.remote.clone(), job.file_num, job.show_hidden @@ -647,17 +717,25 @@ impl Remote { } } else { if let Some(job) = get_job(id, &mut self.read_jobs) { - job.is_last_job = false; - allow_err!( - peer.send(&fs::new_receive( - id, - job.path.to_string_lossy().to_string(), - job.file_num, - job.files.clone(), - job.total_size(), - )) - .await - ); + match &job.data_source { + fs::DataSource::FilePath(p) => { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_receive( + id, + p.to_string_lossy().to_string(), + job.file_num, + job.files.clone(), + job.total_size(), + )) + .await + ); + } + fs::DataSource::MemoryCursor(_) => { + // unreachable!() + log::error!("Resume job with memory cursor"); + } + } } } } @@ -762,11 +840,10 @@ impl Remote { }); msg_out.set_file_action(file_action); allow_err!(peer.send(&msg_out).await); - if let Some(job) = fs::get_job(id, &mut self.write_jobs) { + if let Some(job) = fs::remove_job(id, &mut self.write_jobs) { job.remove_download_file(); - fs::remove_job(id, &mut self.write_jobs); } - fs::remove_job(id, &mut self.read_jobs); + let _ = fs::remove_job(id, &mut self.read_jobs); self.remove_jobs.remove(&id); } Data::RemoveDir((id, path)) => { @@ -846,10 +923,8 @@ impl Remote { } } Data::RecordScreen(start) => { - self.handler.lc.write().unwrap().record = start; - for (_, v) in self.video_threads.iter_mut() { - v.video_sender.send(MediaData::RecordScreen(start)).ok(); - } + self.handler.lc.write().unwrap().record_state = start; + self.update_record_state(); } Data::ElevateDirect => { let mut request = ElevationRequest::new(); @@ -904,6 +979,15 @@ impl Remote { } } }, + Data::TakeScreenshot((display, sid)) => { + let mut msg = Message::new(); + msg.set_screenshot_request(ScreenshotRequest { + display, + sid, + ..Default::default() + }); + allow_err!(peer.send(&msg).await); + } _ => {} } true @@ -1144,6 +1228,43 @@ impl Remote { } } + fn check_view_camera_support(&self, peer_version: &str, peer_platform: &str) -> bool { + if self.peer_info.support_view_camera { + return true; + } + if hbb_common::get_version_number(&peer_version) < hbb_common::get_version_number("1.3.9") + && (peer_platform == "Windows" || peer_platform == "Linux") + { + self.handler.msgbox( + "error", + "Download new version", + "upgrade_remote_rustdesk_client_to_{1.3.9}_tip", + "", + ); + } else { + self.handler.on_error("view_camera_unsupported_tip"); + } + return false; + } + + fn check_terminal_support(&self, peer_version: &str) -> bool { + if self.peer_info.support_terminal { + return true; + } + if hbb_common::get_version_number(&peer_version) < hbb_common::get_version_number("1.4.1") { + self.handler.msgbox( + "error", + "Remote terminal not supported", + "Remote terminal is not supported by the remote side. Please upgrade to version 1.4.1 or higher.", + "", + ); + } else { + self.handler + .on_error("Remote terminal is not supported by the remote side"); + } + return false; + } + async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { @@ -1198,9 +1319,22 @@ impl Remote { let peer_version = pi.version.clone(); let peer_platform = pi.platform.clone(); self.set_peer_info(&pi); + if self.handler.is_view_camera() { + if !self.check_view_camera_support(&peer_version, &peer_platform) { + self.handler.lc.write().unwrap().handle_peer_info(&pi); + return false; + } + } + if self.handler.is_terminal() { + if !self.check_terminal_support(&peer_version) { + self.handler.lc.write().unwrap().handle_peer_info(&pi); + return false; + } + } self.handler.handle_peer_info(pi); + #[cfg(all(target_os = "windows", not(feature = "flutter")))] self.check_clipboard_file_context(); - if !(self.handler.is_file_transfer() || self.handler.is_port_forward()) { + if self.handler.is_default() { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] let rx = Client::try_start_clipboard(None); @@ -1210,6 +1344,10 @@ impl Remote { crate::client::ClientClipboardContext { cfg: self.handler.get_permission_config(), tx: self.sender.clone(), + #[cfg(feature = "unix-file-copy-paste")] + is_file_supported: crate::is_support_file_copy_paste( + &peer_version, + ), }, )); // To make sure current text clipboard data is updated. @@ -1237,9 +1375,13 @@ impl Remote { // to-do: Android, is `sync_init_clipboard` really needed? // https://github.com/rustdesk/rustdesk/discussions/9010 - #[cfg(target_os = "android")] + #[cfg(feature = "flutter")] + #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); + // on connection established client #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -1293,9 +1435,9 @@ impl Remote { crate::clipboard::handle_msg_multi_clipboards(_mcb); } } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] Some(message::Union::Cliprdr(clip)) => { - self.handle_cliprdr_msg(clip); + self.handle_cliprdr_msg(clip, peer).await; } Some(message::Union::FileResponse(fr)) => { match fr.union { @@ -1326,92 +1468,105 @@ impl Remote { if digest.is_upload { if let Some(job) = fs::get_job(digest.id, &mut self.read_jobs) { if let Some(file) = job.files().get(digest.file_num as usize) { - let read_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - if let Some(overwrite) = overwrite_strategy { - let req = FileTransferSendConfirmRequest { - id: digest.id, - file_num: digest.file_num, - union: Some(if overwrite { - file_transfer_send_confirm_request::Union::OffsetBlk(0) - } else { - file_transfer_send_confirm_request::Union::Skip( - true, - ) - }), - ..Default::default() - }; - job.confirm(&req); - let msg = new_send_confirm(req); - allow_err!(peer.send(&msg).await); - } else { - self.handler.override_file_confirm( - digest.id, - digest.file_num, - read_path, - true, - digest.is_identical, - ); + if let fs::DataSource::FilePath(p) = &job.data_source { + let read_path = + get_string(&fs::TransferJob::join(p, &file.name)); + let overwrite_strategy = + job.default_overwrite_strategy(); + if let Some(overwrite) = overwrite_strategy { + let req = FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip( + true, + ) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + read_path, + true, + digest.is_identical, + ); + } } } } } else { if let Some(job) = fs::get_job(digest.id, &mut self.write_jobs) { if let Some(file) = job.files().get(digest.file_num as usize) { - let write_path = get_string(&job.join(&file.name)); - let overwrite_strategy = job.default_overwrite_strategy(); - match fs::is_write_need_confirmation(&write_path, &digest) { - Ok(res) => match res { - DigestCheckResult::IsSame => { - let req = FileTransferSendConfirmRequest { + if let fs::DataSource::FilePath(p) = &job.data_source { + let write_path = + get_string(&fs::TransferJob::join(p, &file.name)); + let overwrite_strategy = + job.default_overwrite_strategy(); + match fs::is_write_need_confirmation( + &write_path, + &digest, + ) { + Ok(res) => match res { + DigestCheckResult::IsSame => { + let 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 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( - digest.id, - digest.file_num, - write_path, - false, - digest.is_identical, - ); } - } - DigestCheckResult::NoSuchFile => { - let req = FileTransferSendConfirmRequest { + DigestCheckResult::NeedConfirm(digest) => { + if let Some(overwrite) = overwrite_strategy + { + let req = + FileTransferSendConfirmRequest { + id: digest.id, + file_num: digest.file_num, + union: Some(if overwrite { + file_transfer_send_confirm_request::Union::OffsetBlk(0) + } else { + file_transfer_send_confirm_request::Union::Skip(true) + }), + ..Default::default() + }; + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } else { + self.handler.override_file_confirm( + digest.id, + digest.file_num, + write_path, + false, + digest.is_identical, + ); + } + } + DigestCheckResult::NoSuchFile => { + 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); + job.confirm(&req); + let msg = new_send_confirm(req); + allow_err!(peer.send(&msg).await); + } + }, + Err(err) => { + println!("error receiving digest: {}", err); } - }, - Err(err) => { - println!("error receiving digest: {}", err); } } } @@ -1423,23 +1578,76 @@ impl Remote { if let Err(_err) = job.write(block).await { // to-do: add "skip" for writing job } - self.update_jobs_status(); + if job.r#type == fs::JobType::Generic { + self.update_jobs_status(); + } } } Some(file_response::Union::Done(d)) => { let mut err: Option = None; - if let Some(job) = fs::get_job(d.id, &mut self.write_jobs) { + let mut job_type = fs::JobType::Generic; + let mut printer_data = None; + if let Some(job) = fs::remove_job(d.id, &mut self.write_jobs) { job.modify_time(); err = job.job_error(); - fs::remove_job(d.id, &mut self.write_jobs); + job_type = job.r#type; + printer_data = match job.get_buf_data().await { + Ok(d) => d, + Err(e) => { + log::error!("Failed to get the printer data: {}", e); + None + } + }; + } + match job_type { + fs::JobType::Generic => { + self.handle_job_status(d.id, d.file_num, err); + } + fs::JobType::Printer => { + if let Some(err) = err { + log::error!("Receive print job failed, error {err}"); + } else { + log::info!( + "Receive print job done, data len: {:?}", + printer_data.as_ref().map(|d| d.len()).unwrap_or(0) + ); + #[cfg(target_os = "windows")] + if let Some(data) = printer_data { + let printer_name = self + .handler + .printer_names + .write() + .unwrap() + .remove(&d.id); + // Spawn a new thread to handle the print job. + // Or print job will block the ui thread. + std::thread::spawn(move || { + if let Err(e) = + crate::platform::send_raw_data_to_printer( + printer_name, + data, + ) + { + log::error!("Print job error: {}", e); + } + }); + } + } + } } - 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); + let job_type = fs::remove_job(e.id, &mut self.write_jobs) + .map(|j| j.r#type) + .unwrap_or(fs::JobType::Generic); + match job_type { + fs::JobType::Generic => { + self.handle_job_status(e.id, e.file_num, Some(e.error)); + } + fs::JobType::Printer => { + log::error!("Printer job error: {}", e.error); + } } - self.handle_job_status(e.id, e.file_num, Some(e.error)); } _ => {} } @@ -1460,6 +1668,8 @@ impl Remote { #[cfg(feature = "flutter")] #[cfg(not(target_os = "ios"))] crate::flutter::update_text_clipboard_required(); + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); self.handler.set_permission("keyboard", p.enabled); } Ok(Permission::Clipboard) => { @@ -1478,12 +1688,23 @@ impl Remote { if !p.enabled && self.handler.is_file_transfer() { return true; } + #[cfg(all(feature = "flutter", feature = "unix-file-copy-paste"))] + crate::flutter::update_file_clipboard_required(); self.handler.set_permission("file", p.enabled); + #[cfg(feature = "unix-file-copy-paste")] + if !p.enabled { + try_empty_clipboard_files( + ClipboardSide::Client, + self.client_conn_id, + ); + } } Ok(Permission::Restart) => { self.handler.set_permission("restart", p.enabled); } Ok(Permission::Recording) => { + self.handler.lc.write().unwrap().record_permission = p.enabled; + self.update_record_state(); self.handler.set_permission("recording", p.enabled); } Ok(Permission::BlockInput) => { @@ -1509,6 +1730,7 @@ impl Remote { } } Some(misc::Union::CloseReason(c)) => { + self.sent_close_reason = true; // The controlled end will close, no need to send close reason self.handler.msgbox("error", "Connection Error", &c, ""); return false; } @@ -1647,6 +1869,41 @@ impl Remote { } } Some(message::Union::FileAction(action)) => match action.union { + Some(file_action::Union::Send(_s)) => match _s.file_type.enum_value() { + #[cfg(target_os = "windows")] + Ok(file_transfer_send_request::FileType::Printer) => { + #[cfg(feature = "flutter")] + let action = LocalConfig::get_option( + config::keys::OPTION_PRINTER_INCOMING_JOB_ACTION, + ); + #[cfg(not(feature = "flutter"))] + let action = ""; + if action == "dismiss" { + // Just ignore the incoming print job. + } else { + let id = fs::get_next_job_id(); + #[cfg(feature = "flutter")] + let allow_auto_print = LocalConfig::get_bool_option( + config::keys::OPTION_PRINTER_ALLOW_AUTO_PRINT, + ); + #[cfg(not(feature = "flutter"))] + let allow_auto_print = false; + if allow_auto_print { + let printer_name = if action == "" { + "".to_string() + } else { + LocalConfig::get_option( + config::keys::OPTION_PRINTER_SELECTED_NAME, + ) + }; + self.handler.printer_response(id, _s.path, printer_name); + } else { + self.handler.printer_request(id, _s.path); + } + } + } + _ => {} + }, Some(file_action::Union::SendConfirm(c)) => { if let Some(job) = fs::get_job(c.id, &mut self.read_jobs) { job.confirm(&c); @@ -1697,6 +1954,22 @@ impl Remote { self.handler.set_displays(&pi.displays); self.handler.set_platform_additions(&pi.platform_additions); } + Some(message::Union::ScreenshotResponse(response)) => { + crate::client::screenshot::set_screenshot(response.data); + self.handler + .handle_screenshot_resp(response.sid, response.msg); + } + Some(message::Union::TerminalResponse(response)) => { + use hbb_common::message_proto::terminal_response::Union; + if let Some(Union::Opened(opened)) = &response.union { + if opened.success && !opened.service_id.is_empty() { + let mut lc = self.handler.lc.write().unwrap(); + let key = lc.get_key_terminal_service_id().to_owned(); + lc.set_option(key, opened.service_id.clone()); + } + } + self.handler.handle_terminal_response(response); + } _ => {} } } @@ -1705,6 +1978,12 @@ impl Remote { fn set_peer_info(&mut self, pi: &PeerInfo) { self.peer_info.platform = pi.platform.clone(); + + // Check features field for terminal support + if let Some(features) = pi.features.as_ref() { + self.peer_info.support_terminal = features.terminal; + } + if let Ok(platform_additions) = serde_json::from_str::>(&pi.platform_additions) { @@ -1719,6 +1998,11 @@ impl Remote { .flatten() .unwrap_or_default() .to_string(); + self.peer_info.support_view_camera = platform_additions + .get("support_view_camera") + .map(|v| v.as_bool()) + .flatten() + .unwrap_or(false); } } @@ -1896,23 +2180,19 @@ impl Remote { true } + #[cfg(all(target_os = "windows", not(feature = "flutter")))] fn check_clipboard_file_context(&self) { - #[cfg(any( - target_os = "windows", - all( - feature = "unix-file-copy-paste", - any(target_os = "linux", target_os = "macos") - ) - ))] - { - let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() - && self.handler.lc.read().unwrap().enable_file_copy_paste.v; - ContextSend::enable(enabled); - } + let enabled = *self.handler.server_file_transfer_enabled.read().unwrap() + && self.handler.lc.read().unwrap().enable_file_copy_paste.v; + ContextSend::enable(enabled); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - fn handle_cliprdr_msg(&self, clip: hbb_common::message_proto::Cliprdr) { + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + async fn handle_cliprdr_msg( + &mut self, + clip: hbb_common::message_proto::Cliprdr, + _peer: &mut Stream, + ) { log::debug!("handling cliprdr msg from server peer"); #[cfg(feature = "flutter")] if let Some(hbb_common::message_proto::cliprdr::Union::FormatList(_)) = &clip.union { @@ -1929,20 +2209,61 @@ impl Remote { }; let is_stopping_allowed = clip.is_beginning_message(); - let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_copy_paste.v; + let file_transfer_enabled = self.handler.is_file_clipboard_required(); let stop = is_stopping_allowed && !file_transfer_enabled; log::debug!( "Process clipboard message from server peer, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", stop, is_stopping_allowed, file_transfer_enabled); if !stop { + #[cfg(any( + target_os = "windows", + all(target_os = "macos", feature = "unix-file-copy-paste") + ))] if let Err(e) = ContextSend::make_sure_enabled() { log::error!("failed to restart clipboard context: {}", e); }; - let _ = ContextSend::proc(|context| -> ResultType<()> { - context - .server_clip_file(self.client_conn_id, clip) - .map_err(|e| e.into()) - }); + #[cfg(target_os = "windows")] + { + let _ = ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.client_conn_id, clip) + .map_err(|e| e.into()) + }); + } + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste_num(self.handler.lc.read().unwrap().version) { + let mut out_msg = None; + + #[cfg(target_os = "macos")] + if clipboard::platform::unix::macos::should_handle_msg(&clip) { + if let Err(e) = ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.client_conn_id, clip) + .map_err(|e| e.into()) + }) { + log::error!("failed to handle cliprdr msg: {}", e); + } + } else { + out_msg = unix_file_clip::serve_clip_messages( + ClipboardSide::Client, + clip, + self.client_conn_id, + ); + } + + #[cfg(not(target_os = "macos"))] + { + out_msg = unix_file_clip::serve_clip_messages( + ClipboardSide::Client, + clip, + self.client_conn_id, + ); + } + + if let Some(msg) = out_msg { + allow_err!(_peer.send(&msg).await); + } + } } } @@ -1983,14 +2304,38 @@ impl Remote { }, ); self.video_threads.insert(display, video_thread); - let auto_record = self.handler.lc.read().unwrap().record; - if auto_record && self.video_threads.len() == 1 { - let mut misc = Misc::new(); - misc.set_client_record_status(true); - let mut msg = Message::new(); - msg.set_misc(misc); - self.sender.send(Data::Message(msg)).ok(); + if self.video_threads.len() == 1 { + let auto_record = + LocalConfig::get_bool_option(config::keys::OPTION_ALLOW_AUTO_RECORD_OUTGOING); + self.handler.lc.write().unwrap().record_state = auto_record; + self.update_record_state(); + } + } + + fn update_record_state(&mut self) { + // state + let permission = self.handler.lc.read().unwrap().record_permission; + if !permission { + self.handler.lc.write().unwrap().record_state = false; + } + let state = self.handler.lc.read().unwrap().record_state; + let start = state && permission; + if self.last_record_state == start { + return; + } + self.last_record_state = start; + log::info!("record screen start: {start}"); + // update local + for (_, v) in self.video_threads.iter_mut() { + v.video_sender.send(MediaData::RecordScreen(start)).ok(); } + self.handler.update_record_status(start); + // update remote + let mut misc = Misc::new(); + misc.set_client_record_status(start); + let mut msg = Message::new(); + msg.set_misc(misc); + self.sender.send(Data::Message(msg)).ok(); } } @@ -2040,3 +2385,10 @@ struct VideoThread { discard_queue: Arc>, fps_control: FpsControl, } + +impl Drop for VideoThread { + fn drop(&mut self) { + // since channels are buffered, messages sent before the disconnect will still be properly received. + *self.discard_queue.write().unwrap() = true; + } +} diff --git a/src/client/screenshot.rs b/src/client/screenshot.rs new file mode 100644 index 00000000000..82a95bee96a --- /dev/null +++ b/src/client/screenshot.rs @@ -0,0 +1,99 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::clipboard::{update_clipboard, ClipboardSide}; +use hbb_common::{message_proto::*, ResultType}; +use std::sync::Mutex; + +lazy_static::lazy_static! { + static ref SCREENSHOT: Mutex = Default::default(); +} + +pub enum ScreenshotAction { + SaveAs(String), + CopyToClipboard, + Discard, +} + +impl Default for ScreenshotAction { + fn default() -> Self { + Self::Discard + } +} + +impl From<&str> for ScreenshotAction { + fn from(value: &str) -> Self { + match value.chars().next() { + Some('0') => { + if let Some((pos, _)) = value.char_indices().nth(2) { + let substring = &value[pos..]; + Self::SaveAs(substring.to_string()) + } else { + Self::default() + } + } + Some('1') => Self::CopyToClipboard, + Some('2') => Self::default(), + _ => Self::default(), + } + } +} + +impl Into for ScreenshotAction { + fn into(self) -> String { + match self { + Self::SaveAs(p) => format!("0:{p}"), + Self::CopyToClipboard => "1".to_owned(), + Self::Discard => "2".to_owned(), + } + } +} + +#[derive(Default)] +pub struct Screenshot { + data: Option, +} + +impl Screenshot { + fn set_screenshot(&mut self, data: bytes::Bytes) { + self.data.replace(data); + } + + fn handle_screenshot(&mut self, action: String) -> String { + let Some(data) = self.data.take() else { + return "No cached screenshot".to_owned(); + }; + match Self::handle_screenshot_(data, action) { + Ok(()) => "".to_owned(), + Err(e) => e.to_string(), + } + } + + fn handle_screenshot_(data: bytes::Bytes, action: String) -> ResultType<()> { + match ScreenshotAction::from(&action as &str) { + ScreenshotAction::SaveAs(p) => { + std::fs::write(p, data)?; + } + ScreenshotAction::CopyToClipboard => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let clips = vec![Clipboard { + compress: false, + content: data, + format: ClipboardFormat::ImagePng.into(), + ..Default::default() + }]; + update_clipboard(clips, ClipboardSide::Client); + } + } + ScreenshotAction::Discard => {} + } + Ok(()) + } +} + +pub fn set_screenshot(data: bytes::Bytes) { + SCREENSHOT.lock().unwrap().set_screenshot(data); +} + +pub fn handle_screenshot(action: String) -> String { + SCREENSHOT.lock().unwrap().handle_screenshot(action) +} diff --git a/src/clipboard.rs b/src/clipboard.rs index ac3a83f00f7..db8cb4cfec2 100644 --- a/src/clipboard.rs +++ b/src/clipboard.rs @@ -1,15 +1,14 @@ #[cfg(not(target_os = "android"))] use arboard::{ClipboardData, ClipboardFormat}; -#[cfg(not(target_os = "android"))] -use clipboard_master::{ClipboardHandler, Master, Shutdown}; use hbb_common::{bail, log, message_proto::*, ResultType}; use std::{ - sync::{mpsc::Sender, Arc, Mutex}, - thread::JoinHandle, + sync::{Arc, Mutex}, time::Duration, }; pub const CLIPBOARD_NAME: &'static str = "clipboard"; +#[cfg(feature = "unix-file-copy-paste")] +pub const FILE_CLIPBOARD_NAME: &'static str = "file-clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; // This format is used to store the flag in the clipboard. @@ -43,115 +42,12 @@ const SUPPORTED_FORMATS: &[ClipboardFormat] = &[ ClipboardFormat::ImageRgba, ClipboardFormat::ImagePng, ClipboardFormat::ImageSvg, + #[cfg(feature = "unix-file-copy-paste")] + ClipboardFormat::FileUrl, ClipboardFormat::Special(CLIPBOARD_FORMAT_EXCEL_XML_SPREADSHEET), ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), ]; -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -static X11_CLIPBOARD: once_cell::sync::OnceCell = - once_cell::sync::OnceCell::new(); - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -fn get_clipboard() -> Result<&'static x11_clipboard::Clipboard, String> { - X11_CLIPBOARD - .get_or_try_init(|| x11_clipboard::Clipboard::new()) - .map_err(|e| e.to_string()) -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -pub struct ClipboardContext { - string_setter: x11rb::protocol::xproto::Atom, - string_getter: x11rb::protocol::xproto::Atom, - text_uri_list: x11rb::protocol::xproto::Atom, - - clip: x11rb::protocol::xproto::Atom, - prop: x11rb::protocol::xproto::Atom, -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -fn parse_plain_uri_list(v: Vec) -> Result { - let text = String::from_utf8(v).map_err(|_| "ConversionFailure".to_owned())?; - let mut list = String::new(); - for line in text.lines() { - if !line.starts_with("file://") { - continue; - } - let decoded = percent_encoding::percent_decode_str(line) - .decode_utf8() - .map_err(|_| "ConversionFailure".to_owned())?; - list = list + "\n" + decoded.trim_start_matches("file://"); - } - list = list.trim().to_owned(); - Ok(list) -} - -#[cfg(all(target_os = "linux", feature = "unix-file-copy-paste"))] -impl ClipboardContext { - pub fn new() -> Result { - let clipboard = get_clipboard()?; - let string_getter = clipboard - .getter - .get_atom("UTF8_STRING") - .map_err(|e| e.to_string())?; - let string_setter = clipboard - .setter - .get_atom("UTF8_STRING") - .map_err(|e| e.to_string())?; - let text_uri_list = clipboard - .getter - .get_atom("text/uri-list") - .map_err(|e| e.to_string())?; - let prop = clipboard.getter.atoms.property; - let clip = clipboard.getter.atoms.clipboard; - Ok(Self { - text_uri_list, - string_setter, - string_getter, - clip, - prop, - }) - } - - pub fn get_text(&mut self) -> Result { - let clip = self.clip; - let prop = self.prop; - - const TIMEOUT: std::time::Duration = std::time::Duration::from_millis(120); - - let text_content = get_clipboard()? - .load(clip, self.string_getter, prop, TIMEOUT) - .map_err(|e| e.to_string())?; - - let file_urls = get_clipboard()?.load(clip, self.text_uri_list, prop, TIMEOUT)?; - - if file_urls.is_err() || file_urls.as_ref().is_empty() { - log::trace!("clipboard get text, no file urls"); - return String::from_utf8(text_content).map_err(|e| e.to_string()); - } - - let file_urls = parse_plain_uri_list(file_urls)?; - - let text_content = String::from_utf8(text_content).map_err(|e| e.to_string())?; - - if text_content.trim() == file_urls.trim() { - log::trace!("clipboard got text but polluted"); - return Err(String::from("polluted text")); - } - - Ok(text_content) - } - - pub fn set_text(&mut self, content: String) -> Result<(), String> { - let clip = self.clip; - - let value = content.clone().into_bytes(); - get_clipboard()? - .store(clip, self.string_setter, value) - .map_err(|e| e.to_string())?; - Ok(()) - } -} - #[cfg(not(target_os = "android"))] pub fn check_clipboard( ctx: &mut Option, @@ -179,6 +75,103 @@ pub fn check_clipboard( None } +#[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] +pub fn is_file_url_set_by_rustdesk(url: &Vec) -> bool { + if url.len() != 1 { + return false; + } + url.iter() + .next() + .map(|s| { + for prefix in &["file:///tmp/.rustdesk_", "//tmp/.rustdesk_"] { + if s.starts_with(prefix) { + return s[prefix.len()..].parse::().is_ok(); + } + } + false + }) + .unwrap_or(false) +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn check_clipboard_files( + ctx: &mut Option, + side: ClipboardSide, + force: bool, +) -> Option> { + if ctx.is_none() { + *ctx = ClipboardContext::new().ok(); + } + let ctx2 = ctx.as_mut()?; + match ctx2.get_files(side, force) { + Ok(Some(urls)) => { + if !urls.is_empty() { + return Some(urls); + } + } + Err(e) => { + log::error!("Failed to get clipboard file urls. {}", e); + } + _ => {} + } + None +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn update_clipboard_files(files: Vec, side: ClipboardSide) { + if !files.is_empty() { + std::thread::spawn(move || { + do_update_clipboard_(vec![ClipboardData::FileUrl(files)], side); + }); + } +} + +#[cfg(feature = "unix-file-copy-paste")] +pub fn try_empty_clipboard_files(_side: ClipboardSide, _conn_id: i32) { + std::thread::spawn(move || { + let mut ctx = CLIPBOARD_CTX.lock().unwrap(); + if ctx.is_none() { + match ClipboardContext::new() { + Ok(x) => { + *ctx = Some(x); + } + Err(e) => { + log::error!("Failed to create clipboard context: {}", e); + return; + } + } + } + if let Some(mut ctx) = ctx.as_mut() { + #[cfg(target_os = "linux")] + { + use clipboard::platform::unix; + if unix::fuse::empty_local_files(_side == ClipboardSide::Client, _conn_id) { + ctx.try_empty_clipboard_files(_side); + } + } + #[cfg(target_os = "macos")] + { + ctx.try_empty_clipboard_files(_side); + // No need to make sure the context is enabled. + clipboard::ContextSend::proc(|context| -> ResultType<()> { + context.empty_clipboard(_conn_id).ok(); + Ok(()) + }) + .ok(); + } + } + }); +} + +#[cfg(target_os = "windows")] +pub fn try_empty_clipboard_files(side: ClipboardSide, conn_id: i32) { + log::debug!("try to empty {} cliprdr for conn_id {}", side, conn_id); + let _ = clipboard::ContextSend::proc(|context| -> ResultType<()> { + context.empty_clipboard(conn_id)?; + Ok(()) + }); +} + #[cfg(target_os = "windows")] pub fn check_clipboard_cm() -> ResultType { let mut ctx = CLIPBOARD_CTX.lock().unwrap(); @@ -203,10 +196,15 @@ pub fn check_clipboard_cm() -> ResultType { #[cfg(not(target_os = "android"))] fn update_clipboard_(multi_clipboards: Vec, side: ClipboardSide) { - let mut to_update_data = proto::from_multi_clipbards(multi_clipboards); + let to_update_data = proto::from_multi_clipbards(multi_clipboards); if to_update_data.is_empty() { return; } + do_update_clipboard_(to_update_data, side); +} + +#[cfg(not(target_os = "android"))] +fn do_update_clipboard_(mut to_update_data: Vec, side: ClipboardSide) { let mut ctx = CLIPBOARD_CTX.lock().unwrap(); if ctx.is_none() { match ClipboardContext::new() { @@ -240,13 +238,11 @@ pub fn update_clipboard(multi_clipboards: Vec, side: ClipboardSide) { } #[cfg(not(target_os = "android"))] -#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] pub struct ClipboardContext { inner: arboard::Clipboard, } #[cfg(not(target_os = "android"))] -#[cfg(not(any(all(target_os = "linux", feature = "unix-file-copy-paste"))))] #[allow(unreachable_code)] impl ClipboardContext { pub fn new() -> ResultType { @@ -293,7 +289,7 @@ impl ClipboardContext { // https://github.com/rustdesk/rustdesk/issues/9263 // https://github.com/rustdesk/rustdesk/issues/9222#issuecomment-2329233175 for i in 0..CLIPBOARD_GET_MAX_RETRY { - match self.inner.get_formats(SUPPORTED_FORMATS) { + match self.inner.get_formats(formats) { Ok(data) => { return Ok(data .into_iter() @@ -316,8 +312,26 @@ impl ClipboardContext { } pub fn get(&mut self, side: ClipboardSide, force: bool) -> ResultType> { + let data = self.get_formats_filter(SUPPORTED_FORMATS, side, force)?; + // We have a seperate service named `file-clipboard` to handle file copy-paste. + // We need to read the file urls because file copy may set the other clipboard formats such as text. + #[cfg(feature = "unix-file-copy-paste")] + { + if data.iter().any(|c| matches!(c, ClipboardData::FileUrl(_))) { + return Ok(vec![]); + } + } + Ok(data) + } + + fn get_formats_filter( + &mut self, + formats: &[ClipboardFormat], + side: ClipboardSide, + force: bool, + ) -> ResultType> { let _lock = ARBOARD_MTX.lock().unwrap(); - let data = self.get_formats(SUPPORTED_FORMATS)?; + let data = self.get_formats(formats)?; if data.is_empty() { return Ok(data); } @@ -334,16 +348,115 @@ impl ClipboardContext { .into_iter() .filter(|c| match c { ClipboardData::Special((s, _)) => s != RUSTDESK_CLIPBOARD_OWNER_FORMAT, + // Skip synchronizing empty text to the remote clipboard + ClipboardData::Text(text) => !text.is_empty(), _ => true, }) .collect()) } + #[cfg(feature = "unix-file-copy-paste")] + pub fn get_files( + &mut self, + side: ClipboardSide, + force: bool, + ) -> ResultType>> { + let data = self.get_formats_filter( + &[ + ClipboardFormat::FileUrl, + ClipboardFormat::Special(RUSTDESK_CLIPBOARD_OWNER_FORMAT), + ], + side, + force, + )?; + Ok(data.into_iter().find_map(|c| match c { + ClipboardData::FileUrl(urls) => Some(urls), + _ => None, + })) + } + fn set(&mut self, data: &[ClipboardData]) -> ResultType<()> { let _lock = ARBOARD_MTX.lock().unwrap(); self.inner.set_formats(data)?; Ok(()) } + + #[cfg(all(feature = "unix-file-copy-paste", target_os = "macos"))] + fn get_file_urls_set_by_rustdesk( + data: Vec, + _side: ClipboardSide, + ) -> Vec { + for item in data.into_iter() { + if let ClipboardData::FileUrl(urls) = item { + if is_file_url_set_by_rustdesk(&urls) { + return urls; + } + } + } + vec![] + } + + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + fn get_file_urls_set_by_rustdesk(data: Vec, side: ClipboardSide) -> Vec { + let exclude_path = + clipboard::platform::unix::fuse::get_exclude_paths(side == ClipboardSide::Client); + data.into_iter() + .filter_map(|c| match c { + ClipboardData::FileUrl(urls) => Some( + urls.into_iter() + .filter(|s| s.starts_with(&*exclude_path)) + .collect::>(), + ), + _ => None, + }) + .flatten() + .collect::>() + } + + #[cfg(feature = "unix-file-copy-paste")] + fn try_empty_clipboard_files(&mut self, side: ClipboardSide) { + let _lock = ARBOARD_MTX.lock().unwrap(); + if let Ok(data) = self.get_formats(&[ClipboardFormat::FileUrl]) { + let urls = Self::get_file_urls_set_by_rustdesk(data, side); + if !urls.is_empty() { + // FIXME: + // The host-side clear file clipboard `let _ = self.inner.clear();`, + // does not work on KDE Plasma for the installed version. + + // Don't use `hbb_common::platform::linux::is_kde()` here. + // It's not correct in the server process. + #[cfg(target_os = "linux")] + let is_kde_x11 = { + use hbb_common::platform::linux::CMD_SH; + let is_kde = std::process::Command::new(CMD_SH.as_str()) + .arg("-c") + .arg("ps -e | grep -E kded[0-9]+ | grep -v grep") + .stdout(std::process::Stdio::piped()) + .output() + .map(|o| !o.stdout.is_empty()) + .unwrap_or(false); + is_kde && crate::platform::linux::is_x11() + }; + #[cfg(target_os = "macos")] + let is_kde_x11 = false; + let clear_holder_text = if is_kde_x11 { + "RustDesk placeholder to clear the file clipbard" + } else { + "" + } + .to_string(); + self.inner + .set_formats(&[ + ClipboardData::Text(clear_holder_text), + ClipboardData::Special(( + RUSTDESK_CLIPBOARD_OWNER_FORMAT.to_owned(), + side.get_owner_data(), + )), + ]) + .ok(); + } + } + } } pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bool { @@ -351,7 +464,7 @@ pub fn is_support_multi_clipboard(peer_version: &str, peer_platform: &str) -> bo if get_version_number(peer_version) < get_version_number("1.3.0") { return false; } - if ["", &whoami::Platform::Ios.to_string()].contains(&peer_platform) { + if ["", &hbb_common::whoami::Platform::Ios.to_string()].contains(&peer_platform) { return false; } if "Android" == peer_platform && get_version_number(peer_version) < get_version_number("1.3.3") @@ -427,36 +540,6 @@ impl std::fmt::Display for ClipboardSide { } } -#[cfg(not(target_os = "android"))] -pub fn start_clipbard_master_thread( - handler: impl ClipboardHandler + Send + 'static, - tx_start_res: Sender<(Option, String)>, -) -> JoinHandle<()> { - // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread. - let h = std::thread::spawn(move || match Master::new(handler) { - Ok(mut master) => { - tx_start_res - .send((Some(master.shutdown_channel()), "".to_owned())) - .ok(); - log::debug!("Clipboard listener started"); - if let Err(err) = master.run() { - log::error!("Failed to run clipboard listener: {}", err); - } else { - log::debug!("Clipboard listener stopped"); - } - } - Err(err) => { - tx_start_res - .send(( - None, - format!("Failed to create clipboard listener: {}", err), - )) - .ok(); - } - }); - h -} - pub use proto::get_msg_if_not_support_multi_clip; mod proto { #[cfg(not(target_os = "android"))] @@ -671,3 +754,140 @@ pub fn get_clipboards_msg(client: bool) -> Option { msg.set_multi_clipboards(clipboards); Some(msg) } + +// We need this mod to notify multiple subscribers when the clipboard changes. +// Because only one clipboard master(listener) can tigger the clipboard change event multiple listeners are created on Linux(x11). +// https://github.com/rustdesk-org/clipboard-master/blob/4fb62e5b62fb6350d82b571ec7ba94b3cd466695/src/master/x11.rs#L226 +#[cfg(not(target_os = "android"))] +pub mod clipboard_listener { + use clipboard_master::{CallbackResult, ClipboardHandler, Master, Shutdown}; + use hbb_common::{bail, log, ResultType}; + use std::{ + collections::HashMap, + io, + sync::mpsc::{channel, Sender}, + sync::{Arc, Mutex}, + thread::JoinHandle, + }; + + lazy_static::lazy_static! { + pub static ref CLIPBOARD_LISTENER: Arc> = Default::default(); + } + + struct Handler { + subscribers: Arc>>>, + } + + impl ClipboardHandler for Handler { + fn on_clipboard_change(&mut self) -> CallbackResult { + let sub_lock = self.subscribers.lock().unwrap(); + for tx in sub_lock.values() { + tx.send(CallbackResult::Next).ok(); + } + CallbackResult::Next + } + + fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { + let msg = format!("Clipboard listener error: {}", error); + let sub_lock = self.subscribers.lock().unwrap(); + for tx in sub_lock.values() { + tx.send(CallbackResult::StopWithError(io::Error::new( + io::ErrorKind::Other, + msg.clone(), + ))) + .ok(); + } + CallbackResult::Next + } + } + + #[derive(Default)] + pub struct ClipboardListener { + subscribers: Arc>>>, + handle: Option<(Shutdown, JoinHandle<()>)>, + } + + pub fn subscribe(name: String, tx: Sender) -> ResultType<()> { + log::info!("Subscribe clipboard listener: {}", &name); + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + listener_lock + .subscribers + .lock() + .unwrap() + .insert(name.clone(), tx); + + if listener_lock.handle.is_none() { + log::info!("Start clipboard listener thread"); + let handler = Handler { + subscribers: listener_lock.subscribers.clone(), + }; + let (tx_start_res, rx_start_res) = channel(); + let h = start_clipbard_master_thread(handler, tx_start_res); + let shutdown = match rx_start_res.recv() { + Ok((Some(s), _)) => s, + Ok((None, err)) => { + bail!(err); + } + + Err(e) => { + bail!("Failed to create clipboard listener: {}", e); + } + }; + listener_lock.handle = Some((shutdown, h)); + log::info!("Clipboard listener thread started"); + } + + log::info!("Clipboard listener subscribed: {}", name); + Ok(()) + } + + pub fn unsubscribe(name: &str) { + log::info!("Unsubscribe clipboard listener: {}", name); + let mut listener_lock = CLIPBOARD_LISTENER.lock().unwrap(); + let is_empty = { + let mut sub_lock = listener_lock.subscribers.lock().unwrap(); + if let Some(tx) = sub_lock.remove(name) { + tx.send(CallbackResult::Stop).ok(); + } + sub_lock.is_empty() + }; + if is_empty { + if let Some((shutdown, h)) = listener_lock.handle.take() { + log::info!("Stop clipboard listener thread"); + shutdown.signal(); + h.join().ok(); + log::info!("Clipboard listener thread stopped"); + } + } + log::info!("Clipboard listener unsubscribed: {}", name); + } + + fn start_clipbard_master_thread( + handler: impl ClipboardHandler + Send + 'static, + tx_start_res: Sender<(Option, String)>, + ) -> JoinHandle<()> { + // https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage#:~:text=The%20window%20must%20belong%20to%20the%20current%20thread. + let h = std::thread::spawn(move || match Master::new(handler) { + Ok(mut master) => { + tx_start_res + .send((Some(master.shutdown_channel()), "".to_owned())) + .ok(); + log::debug!("Clipboard listener started"); + if let Err(err) = master.run() { + log::error!("Failed to run clipboard listener: {}", err); + } else { + log::debug!("Clipboard listener stopped"); + } + } + Err(err) => { + tx_start_res + .send(( + None, + format!("Failed to create clipboard listener: {}", err), + )) + .ok(); + } + }); + h + } +} diff --git a/src/clipboard_file.rs b/src/clipboard_file.rs index a4bfc1aef69..d7c72f981c5 100644 --- a/src/clipboard_file.rs +++ b/src/clipboard_file.rs @@ -134,6 +134,15 @@ pub fn clip_2_msg(clip: ClipboardFile) -> Message { })), ..Default::default() }, + ClipboardFile::TryEmpty => Message { + union: Some(message::Union::Cliprdr(Cliprdr { + union: Some(cliprdr::Union::TryEmpty(CliprdrTryEmpty { + ..Default::default() + })), + ..Default::default() + })), + ..Default::default() + }, } } @@ -176,6 +185,210 @@ pub fn msg_2_clip(msg: Cliprdr) -> Option { requested_data: data.requested_data.into(), }) } + Some(cliprdr::Union::TryEmpty(_)) => Some(ClipboardFile::TryEmpty), _ => None, } } + +#[cfg(feature = "unix-file-copy-paste")] +pub mod unix_file_clip { + use crate::clipboard::try_empty_clipboard_files; + + use super::{ + super::clipboard::{update_clipboard_files, ClipboardSide}, + *, + }; + #[cfg(target_os = "linux")] + use clipboard::platform::unix::fuse; + use clipboard::platform::unix::{ + get_local_format, serv_files, FILECONTENTS_FORMAT_ID, FILECONTENTS_FORMAT_NAME, + FILEDESCRIPTORW_FORMAT_NAME, FILEDESCRIPTOR_FORMAT_ID, + }; + use hbb_common::log; + use std::sync::{Arc, Mutex}; + + lazy_static::lazy_static! { + static ref CLIPBOARD_CTX: Arc>> = Arc::new(Mutex::new(None)); + } + + pub fn get_format_list() -> ClipboardFile { + let fd_format_name = get_local_format(FILEDESCRIPTOR_FORMAT_ID) + .unwrap_or(FILEDESCRIPTORW_FORMAT_NAME.to_string()); + let fc_format_name = get_local_format(FILECONTENTS_FORMAT_ID) + .unwrap_or(FILECONTENTS_FORMAT_NAME.to_string()); + ClipboardFile::FormatList { + format_list: vec![ + (FILEDESCRIPTOR_FORMAT_ID, fd_format_name), + (FILECONTENTS_FORMAT_ID, fc_format_name), + ], + } + } + + #[inline] + fn msg_resp_format_data_failure() -> Message { + clip_2_msg(ClipboardFile::FormatDataResponse { + msg_flags: 0x2, + format_data: vec![], + }) + } + + #[inline] + fn resp_file_contents_fail(stream_id: i32) -> Message { + clip_2_msg(ClipboardFile::FileContentsResponse { + msg_flags: 0x2, + stream_id, + requested_data: vec![], + }) + } + + pub fn serve_clip_messages( + side: ClipboardSide, + clip: ClipboardFile, + conn_id: i32, + ) -> Option { + log::debug!("got clipfile from client peer"); + match clip { + ClipboardFile::MonitorReady => { + log::debug!("client is ready for clipboard"); + } + ClipboardFile::FormatList { format_list } => { + if !format_list + .iter() + .find(|(_, name)| name == FILECONTENTS_FORMAT_NAME) + .map(|(id, _)| *id) + .is_some() + { + log::error!("no file contents format found"); + return None; + }; + let Some(file_descriptor_id) = format_list + .iter() + .find(|(_, name)| name == FILEDESCRIPTORW_FORMAT_NAME) + .map(|(id, _)| *id) + else { + log::error!("no file descriptor format found"); + return None; + }; + // sync file system from peer + let data = ClipboardFile::FormatDataRequest { + requested_format_id: file_descriptor_id, + }; + return Some(clip_2_msg(data)); + } + ClipboardFile::FormatListResponse { + msg_flags: _msg_flags, + } => {} + ClipboardFile::FormatDataRequest { + requested_format_id: _requested_format_id, + } => { + log::debug!("requested format id: {}", _requested_format_id); + let format_data = serv_files::get_file_list_pdu(); + if !format_data.is_empty() { + return Some(clip_2_msg(ClipboardFile::FormatDataResponse { + msg_flags: 1, + format_data, + })); + } + // empty file list, send failure message + return Some(msg_resp_format_data_failure()); + } + #[cfg(target_os = "linux")] + ClipboardFile::FormatDataResponse { + msg_flags, + format_data, + } => { + log::debug!("format data response: msg_flags: {}", msg_flags); + + if msg_flags != 0x1 { + // return failure message? + } + + log::debug!("parsing file descriptors"); + if fuse::init_fuse_context(true).is_ok() { + match fuse::format_data_response_to_urls( + side == ClipboardSide::Client, + format_data, + conn_id, + ) { + Ok(files) => { + update_clipboard_files(files, side); + } + Err(e) => { + log::error!("failed to parse file descriptors: {:?}", e); + } + } + } else { + // send error message to server + } + } + ClipboardFile::FileContentsRequest { + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + .. + } => { + log::debug!("file contents request: stream_id: {}, list_index: {}, dw_flags: {}, n_position_low: {}, n_position_high: {}, cb_requested: {}", stream_id, list_index, dw_flags, n_position_low, n_position_high, cb_requested); + match serv_files::read_file_contents( + conn_id, + stream_id, + list_index, + dw_flags, + n_position_low, + n_position_high, + cb_requested, + ) { + Ok(data) => { + return Some(clip_2_msg(data)); + } + Err(e) => { + log::error!("failed to read file contents: {:?}", e); + return Some(resp_file_contents_fail(stream_id)); + } + } + } + #[cfg(target_os = "linux")] + ClipboardFile::FileContentsResponse { + msg_flags, + stream_id, + .. + } => { + log::debug!( + "file contents response: msg_flags: {}, stream_id: {}", + msg_flags, + stream_id, + ); + if fuse::init_fuse_context(true).is_ok() { + hbb_common::allow_err!(fuse::handle_file_content_response( + side == ClipboardSide::Client, + clip + )); + } else { + // send error message to server + } + } + ClipboardFile::NotifyCallback { + r#type, + title, + text, + } => { + // unreachable, but still log it + log::debug!( + "notify callback: type: {}, title: {}, text: {}", + r#type, + title, + text + ); + } + ClipboardFile::TryEmpty => { + try_empty_clipboard_files(side, conn_id); + } + _ => { + log::error!("unsupported clipboard file type"); + } + } + None + } +} diff --git a/src/common.rs b/src/common.rs index 294ab97cc4f..214d6c1ab8d 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,18 +1,23 @@ use std::{ collections::HashMap, future::Future, + net::{SocketAddr, ToSocketAddrs}, sync::{Arc, Mutex, RwLock}, task::Poll, }; use serde_json::{json, Map, Value}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use hbb_common::whoami; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, bail, base64, bytes::Bytes, - config::{self, Config, CONNECT_TIMEOUT, READ_TIMEOUT, RENDEZVOUS_PORT}, + config::{ + self, keys, use_ws, Config, LocalConfig, CONNECT_TIMEOUT, READ_TIMEOUT, RENDEZVOUS_PORT, + }, futures::future::join_all, futures_util::future::poll_fn, get_version_number, log, @@ -21,13 +26,13 @@ use hbb_common::{ rendezvous_proto::*, socket_client, sodiumoxide::crypto::{box_, secretbox, sign}, - tcp::FramedStream, timeout, tokio::{ self, + net::UdpSocket, time::{Duration, Instant, Interval}, }, - ResultType, + ResultType, Stream, }; use crate::{ @@ -76,6 +81,7 @@ lazy_static::lazy_static! { pub static ref SOFTWARE_UPDATE_URL: Arc> = Default::default(); pub static ref DEVICE_ID: Arc> = Default::default(); pub static ref DEVICE_NAME: Arc> = Default::default(); + static ref PUBLIC_IPV6_ADDR: Arc, Option)>> = Default::default(); } lazy_static::lazy_static! { @@ -89,7 +95,7 @@ lazy_static::lazy_static! { pub struct SimpleCallOnReturn { pub b: bool, - pub f: Box, + pub f: Box, } impl Drop for SimpleCallOnReturn { @@ -127,6 +133,36 @@ pub fn is_support_multi_ui_session_num(ver: i64) -> bool { ver >= hbb_common::get_version_number(MIN_VER_MULTI_UI_SESSION) } +#[inline] +#[cfg(feature = "unix-file-copy-paste")] +pub fn is_support_file_copy_paste(ver: &str) -> bool { + is_support_file_copy_paste_num(hbb_common::get_version_number(ver)) +} + +#[inline] +#[cfg(feature = "unix-file-copy-paste")] +pub fn is_support_file_copy_paste_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number("1.3.8") +} + +pub fn is_support_remote_print(ver: &str) -> bool { + hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") +} + +pub fn is_support_file_paste_if_macos(ver: &str) -> bool { + hbb_common::get_version_number(ver) >= hbb_common::get_version_number("1.3.9") +} + +#[inline] +pub fn is_support_screenshot(ver: &str) -> bool { + is_support_multi_ui_session_num(hbb_common::get_version_number(ver)) +} + +#[inline] +pub fn is_support_screenshot_num(ver: i64) -> bool { + ver >= hbb_common::get_version_number("1.4.0") +} + // is server process, with "--server" args #[inline] pub fn is_server() -> bool { @@ -472,41 +508,74 @@ audio_rechannel!(audio_rechannel_8_5, 8, 5); audio_rechannel!(audio_rechannel_8_6, 8, 6); audio_rechannel!(audio_rechannel_8_7, 8, 7); +pub struct CheckTestNatType { + is_direct: bool, +} + +impl CheckTestNatType { + pub fn new() -> Self { + Self { + is_direct: Config::get_socks().is_none() && !config::use_ws(), + } + } +} + +impl Drop for CheckTestNatType { + fn drop(&mut self) { + let is_direct = Config::get_socks().is_none() && !config::use_ws(); + if self.is_direct != is_direct { + test_nat_type(); + } + } +} + pub fn test_nat_type() { - let mut i = 0; - std::thread::spawn(move || loop { - match test_nat_type_() { - Ok(true) => break, - Err(err) => { - log::error!("test nat: {}", err); - } - _ => {} + test_ipv6_sync(); + use std::sync::atomic::{AtomicBool, Ordering}; + std::thread::spawn(move || { + static IS_RUNNING: AtomicBool = AtomicBool::new(false); + if IS_RUNNING.load(Ordering::SeqCst) { + return; } - if Config::get_nat_type() != 0 { - break; + IS_RUNNING.store(true, Ordering::SeqCst); + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::ipc::get_socks_ws(); + let is_direct = Config::get_socks().is_none() && !config::use_ws(); + if !is_direct { + Config::set_nat_type(NatType::SYMMETRIC as _); + IS_RUNNING.store(false, Ordering::SeqCst); + return; } - i = i * 2 + 1; - if i > 300 { - i = 300; + + let mut i = 0; + loop { + match test_nat_type_() { + Ok(true) => break, + Err(err) => { + log::error!("test nat: {}", err); + } + _ => {} + } + if Config::get_nat_type() != 0 { + break; + } + i = i * 2 + 1; + if i > 300 { + i = 300; + } + std::thread::sleep(std::time::Duration::from_secs(i)); } - std::thread::sleep(std::time::Duration::from_secs(i)); + + IS_RUNNING.store(false, Ordering::SeqCst); }); } #[tokio::main(flavor = "current_thread")] async fn test_nat_type_() -> ResultType { log::info!("Testing nat ..."); - #[cfg(not(any(target_os = "android", target_os = "ios")))] - let is_direct = crate::ipc::get_socks_async(1_000).await.is_none(); // sync socks BTW - #[cfg(any(target_os = "android", target_os = "ios"))] - let is_direct = Config::get_socks().is_none(); // sync socks BTW - if !is_direct { - Config::set_nat_type(NatType::SYMMETRIC as _); - return Ok(true); - } let start = std::time::Instant::now(); - let (rendezvous_server, _, _) = get_rendezvous_server(1_000).await; - let server1 = rendezvous_server; + let server1 = Config::get_rendezvous_server(); let server2 = crate::increase_port(&server1, -1); let mut msg_out = RendezvousMessage::new(); let serial = Config::get_serial(); @@ -751,7 +820,6 @@ pub fn get_sysinfo() -> serde_json::Value { os = format!("{os} - {}", system.os_version().unwrap_or_default()); } let hostname = hostname(); // sys.hostname() return localhost on android in my test - use serde_json::json; #[cfg(any(target_os = "android", target_os = "ios"))] let out; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -811,21 +879,28 @@ pub fn is_modifier(evt: &KeyEvent) -> bool { } pub fn check_software_update() { - std::thread::spawn(move || allow_err!(check_software_update_())); + if is_custom_client() { + return; + } + let opt = LocalConfig::get_option(keys::OPTION_ENABLE_CHECK_UPDATE); + if config::option2bool(keys::OPTION_ENABLE_CHECK_UPDATE, &opt) { + std::thread::spawn(move || allow_err!(do_check_software_update())); + } } #[tokio::main(flavor = "current_thread")] -async fn check_software_update_() -> hbb_common::ResultType<()> { - let url = "https://github.com/rustdesk/rustdesk/releases/latest"; - let latest_release_response = create_http_client_async().get(url).send().await?; - let latest_release_version = latest_release_response - .url() - .path() - .rsplit('/') - .next() - .unwrap_or_default(); - - let response_url = latest_release_response.url().to_string(); +pub async fn do_check_software_update() -> hbb_common::ResultType<()> { + let (request, url) = + hbb_common::version_check_request(hbb_common::VER_TYPE_RUSTDESK_CLIENT.to_string()); + let latest_release_response = create_http_client_async() + .post(url) + .json(&request) + .send() + .await?; + let bytes = latest_release_response.bytes().await?; + let resp: hbb_common::VersionCheckResponse = serde_json::from_slice(&bytes)?; + let response_url = resp.url; + let latest_release_version = response_url.rsplit('/').next().unwrap_or_default(); if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) { #[cfg(feature = "flutter")] @@ -838,6 +913,8 @@ async fn check_software_update_() -> hbb_common::ResultType<()> { } } *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url; + } else { + *SOFTWARE_UPDATE_URL.lock().unwrap() = "".to_string(); } Ok(()) } @@ -886,7 +963,25 @@ pub fn get_custom_rendezvous_server(custom: String) -> String { "".to_owned() } +#[inline] pub fn get_api_server(api: String, custom: String) -> String { + if Config::no_register_device() { + return "".to_owned(); + } + let mut res = get_api_server_(api, custom); + if res.ends_with('/') { + res.pop(); + } + if res.starts_with("https") + && res.ends_with(":21114") + && get_builtin_option(keys::OPTION_ALLOW_HTTPS_21114) != "Y" + { + return res.replace(":21114", ""); + } + res +} + +fn get_api_server_(api: String, custom: String) -> String { #[cfg(windows)] if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() { if !lic.api.is_empty() { @@ -912,9 +1007,40 @@ pub fn get_api_server(api: String, custom: String) -> String { "https://admin.rustdesk.com".to_owned() } +#[inline] +pub fn is_public(url: &str) -> bool { + url.contains("rustdesk.com") +} + +pub fn get_udp_punch_enabled() -> bool { + config::option2bool( + keys::OPTION_ENABLE_UDP_PUNCH, + &get_local_option(keys::OPTION_ENABLE_UDP_PUNCH), + ) +} + +pub fn get_ipv6_punch_enabled() -> bool { + config::option2bool( + keys::OPTION_ENABLE_IPV6_PUNCH, + &get_local_option(keys::OPTION_ENABLE_IPV6_PUNCH), + ) +} + +pub fn get_local_option(key: &str) -> String { + let v = LocalConfig::get_option(key); + if key == keys::OPTION_ENABLE_UDP_PUNCH || key == keys::OPTION_ENABLE_IPV6_PUNCH { + if v.is_empty() { + if !is_public(&Config::get_rendezvous_server()) { + return "N".to_owned(); + } + } + } + v +} + 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") { + if url.is_empty() || is_public(&url) { return "".to_owned(); } format!("{}/api/audit/{}", url, typ) @@ -1056,7 +1182,6 @@ pub fn make_fd_to_json(id: i32, path: String, entries: &Vec) -> Strin } pub fn _make_fd_to_json(id: i32, path: String, entries: &Vec) -> Map { - 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)); @@ -1162,7 +1287,7 @@ pub fn pk_to_fingerprint(pk: Vec) -> String { #[inline] pub async fn get_next_nonkeyexchange_msg( - conn: &mut FramedStream, + conn: &mut Stream, timeout: Option, ) -> Option { let timeout = timeout.unwrap_or(READ_TIMEOUT); @@ -1184,7 +1309,34 @@ pub async fn get_next_nonkeyexchange_msg( None } +#[cfg(all(target_os = "windows", not(target_pointer_width = "64")))] +pub fn check_process(arg: &str, same_session_id: bool) -> bool { + let mut path = std::env::current_exe().unwrap_or_default(); + if let Ok(linked) = path.read_link() { + path = linked; + } + let Some(filename) = path.file_name() else { + return false; + }; + let filename = filename.to_string_lossy().to_string(); + match crate::platform::windows::get_pids_with_first_arg_check_session( + &filename, + arg, + same_session_id, + ) { + Ok(pids) => { + let self_pid = hbb_common::sysinfo::Pid::from_u32(std::process::id()); + pids.into_iter().filter(|pid| *pid != self_pid).count() > 0 + } + Err(e) => { + log::error!("Failed to check process with arg: \"{}\", {}", arg, e); + false + } + } +} + #[allow(unused_mut)] +#[cfg(not(all(target_os = "windows", not(target_pointer_width = "64"))))] #[cfg(not(any(target_os = "android", target_os = "ios")))] pub fn check_process(arg: &str, mut same_uid: bool) -> bool { #[cfg(target_os = "macos")] @@ -1231,7 +1383,14 @@ pub fn check_process(arg: &str, mut same_uid: bool) -> bool { false } -pub async fn secure_tcp(conn: &mut FramedStream, key: &str) -> ResultType<()> { +pub async fn secure_tcp(conn: &mut Stream, key: &str) -> ResultType<()> { + // Skip additional encryption when using WebSocket connections (wss://) + // as WebSocket Secure (wss://) already provides transport layer encryption. + // This doesn't affect the end-to-end encryption between clients, + // it only avoids redundant encryption between client and server. + if use_ws() { + return Ok(()); + } let rs_pk = get_rs_pk(key); let Some(rs_pk) = rs_pk else { bail!("Handshake failed: invalid public key from rendezvous server"); @@ -1486,19 +1645,19 @@ pub fn read_custom_client(config: &str) { } let mut map_display_settings = HashMap::new(); - for s in config::keys::KEYS_DISPLAY_SETTINGS { + for s in keys::KEYS_DISPLAY_SETTINGS { map_display_settings.insert(s.replace("_", "-"), s); } let mut map_local_settings = HashMap::new(); - for s in config::keys::KEYS_LOCAL_SETTINGS { + for s in keys::KEYS_LOCAL_SETTINGS { map_local_settings.insert(s.replace("_", "-"), s); } let mut map_settings = HashMap::new(); - for s in config::keys::KEYS_SETTINGS { + for s in keys::KEYS_SETTINGS { map_settings.insert(s.replace("_", "-"), s); } let mut buildin_settings = HashMap::new(); - for s in config::keys::KEYS_BUILDIN_SETTINGS { + for s in keys::KEYS_BUILDIN_SETTINGS { buildin_settings.insert(s.replace("_", "-"), s); } if let Some(default_settings) = data.remove("default-settings") { @@ -1541,7 +1700,7 @@ pub fn is_empty_uni_link(arg: &str) -> bool { } pub fn get_hwid() -> Bytes { - use sha2::{Digest, Sha256}; + use hbb_common::sha2::{Digest, Sha256}; let uuid = hbb_common::get_uuid(); let mut hasher = Sha256::new(); @@ -1549,6 +1708,318 @@ pub fn get_hwid() -> Bytes { Bytes::from(hasher.finalize().to_vec()) } +#[inline] +pub fn get_builtin_option(key: &str) -> String { + config::BUILTIN_SETTINGS + .read() + .unwrap() + .get(key) + .cloned() + .unwrap_or_default() +} + +#[inline] +pub fn is_custom_client() -> bool { + get_app_name() != "RustDesk" +} + +pub fn verify_login(raw: &str, id: &str) -> bool { + true + /* + if is_custom_client() { + return true; + } + #[cfg(debug_assertions)] + return true; + let Ok(pk) = crate::decode64("IycjQd4TmWvjjLnYd796Rd+XkK+KG+7GU1Ia7u4+vSw=") else { + return false; + }; + let Some(key) = get_pk(&pk).map(|x| sign::PublicKey(x)) else { + return false; + }; + let Ok(v) = crate::decode64(raw) else { + return false; + }; + let raw = sign::verify(&v, &key).unwrap_or_default(); + let v_str = std::str::from_utf8(&raw) + .unwrap_or_default() + .split(":") + .next() + .unwrap_or_default(); + v_str == id + */ +} + +#[inline] +pub fn is_udp_disabled() -> bool { + get_builtin_option(keys::OPTION_DISABLE_UDP) == "Y" +} + +// this crate https://github.com/yoshd/stun-client supports nat type +async fn stun_ipv6_test(stun_server: &str) -> ResultType<(SocketAddr, String)> { + use std::net::ToSocketAddrs; + use stunclient::StunClient; + let local_addr = SocketAddr::from(([0u16; 8], 0)); // [::]:0 + let socket = UdpSocket::bind(&local_addr).await?; + let Some(stun_addr) = stun_server + .to_socket_addrs()? + .filter(|x| x.is_ipv6()) + .next() + else { + bail!( + "Failed to resolve STUN ipv6 server address: {}", + stun_server + ); + }; + let client = StunClient::new(stun_addr); + let addr = client.query_external_address_async(&socket).await?; + Ok(if addr.ip().is_ipv6() { + (addr, stun_server.to_owned()) + } else { + bail!("STUN server returned non-IPv6 address: {}", addr) + }) +} + +async fn stun_ipv4_test(stun_server: &str) -> ResultType<(SocketAddr, String)> { + use std::net::ToSocketAddrs; + use stunclient::StunClient; + let local_addr = SocketAddr::from(([0u8; 4], 0)); + let socket = UdpSocket::bind(&local_addr).await?; + let Some(stun_addr) = stun_server + .to_socket_addrs()? + .filter(|x| x.is_ipv4()) + .next() + else { + bail!( + "Failed to resolve STUN ipv4 server address: {}", + stun_server + ); + }; + let client = StunClient::new(stun_addr); + let addr = client.query_external_address_async(&socket).await?; + Ok(if addr.ip().is_ipv4() { + (addr, stun_server.to_owned()) + } else { + bail!("STUN server returned non-IPv6 address: {}", addr) + }) +} + +static STUNS_V4: [&str; 3] = [ + "stun.l.google.com:19302", + "stun.cloudflare.com:3478", + "stun.nextcloud.com:3478", +]; + +static STUNS_V6: [&str; 3] = [ + "stun.l.google.com:19302", + "stun.cloudflare.com:3478", + "stun.nextcloud.com:3478", +]; + +pub async fn test_nat_ipv4() -> ResultType<(SocketAddr, String)> { + use hbb_common::futures::future::{select_ok, FutureExt}; + let tests = STUNS_V4 + .iter() + .map(|&stun| stun_ipv4_test(stun).boxed()) + .collect::>(); + + match select_ok(tests).await { + Ok(res) => { + return Ok(res.0); + } + Err(e) => { + bail!( + "Failed to get public IPv4 address via public STUN servers: {}", + e + ); + } + }; +} + +async fn test_bind_ipv6() -> ResultType { + let local_addr = SocketAddr::from(([0u16; 8], 0)); // [::]:0 + let socket = UdpSocket::bind(local_addr).await?; + let addr = STUNS_V6[0] + .to_socket_addrs()? + .filter(|x| x.is_ipv6()) + .next() + .ok_or_else(|| { + anyhow!( + "Failed to resolve STUN ipv6 server address: {}", + STUNS_V6[0] + ) + })?; + socket.connect(addr).await?; + Ok(socket.local_addr()?) +} + +pub async fn test_ipv6() -> Option> { + if PUBLIC_IPV6_ADDR + .lock() + .unwrap() + .1 + .map(|x| x.elapsed().as_secs() < 60) + .unwrap_or(false) + { + return None; + } + PUBLIC_IPV6_ADDR.lock().unwrap().1 = Some(Instant::now()); + + match test_bind_ipv6().await { + Ok(mut addr) => { + if let std::net::IpAddr::V6(ip) = addr.ip() { + if !ip.is_loopback() + && !ip.is_unspecified() + && !ip.is_multicast() + && (ip.segments()[0] & 0xe000) == 0x2000 + { + addr.set_port(0); + PUBLIC_IPV6_ADDR.lock().unwrap().0 = Some(addr); + log::debug!("Found public IPv6 address locally: {}", addr); + } + } + } + Err(e) => { + log::warn!("Failed to bind IPv6 socket: {}", e); + } + } + // Interestingly, on my macOS, sometimes my ipv6 works, sometimes not (test with ping6 or https://test-ipv6.com/). + // I checked ifconfig, could not see any difference. Both secure ipv6 and temporary ipv6 are there. + // So we can not rely on the local ipv6 address queries with if_addrs. + // above test_bind_ipv6 is safer, because it can fail in this case. + /* + std::thread::spawn(|| { + if let Ok(ifaces) = if_addrs::get_if_addrs() { + for iface in ifaces { + if let if_addrs::IfAddr::V6(v6) = iface.addr { + let ip = v6.ip; + if !ip.is_loopback() + && !ip.is_unspecified() + && !ip.is_multicast() + && !ip.is_unique_local() + && !ip.is_unicast_link_local() + && (ip.segments()[0] & 0xe000) == 0x2000 + { + // only use the first one, on mac, the first one is the stable + // one, the last one is the temporary one. The middle ones are deperecated. + *PUBLIC_IPV6_ADDR.lock().unwrap() = + Some((SocketAddr::from((ip, 0)), Instant::now())); + log::debug!("Found public IPv6 address locally: {}", ip); + break; + } + } + } + } + }); + */ + + Some(tokio::spawn(async { + use hbb_common::futures::future::{select_ok, FutureExt}; + let tests = STUNS_V6 + .iter() + .map(|&stun| stun_ipv6_test(stun).boxed()) + .collect::>(); + + match select_ok(tests).await { + Ok(res) => { + let mut addr = res.0 .0; + addr.set_port(0); // Set port to 0 to avoid conflicts + PUBLIC_IPV6_ADDR.lock().unwrap().0 = Some(addr); + log::debug!( + "Found public IPv6 address via STUN server {}: {}", + res.0 .1, + addr + ); + } + Err(e) => { + log::error!("Failed to get public IPv6 address: {}", e); + } + }; + })) +} + +pub async fn punch_udp( + socket: Arc, + listen: bool, +) -> ResultType> { + let mut retry_interval = Duration::from_millis(20); + const MAX_INTERVAL: Duration = Duration::from_millis(200); + const MAX_TIME: Duration = Duration::from_secs(20); + let mut packets_sent = 0; + socket.send(&[]).await.ok(); + packets_sent += 1; + let mut last_send_time = Instant::now(); + let tm = Instant::now(); + let mut data = [0u8; 1500]; + + loop { + tokio::select! { + _ = hbb_common::sleep(retry_interval.as_secs_f32()) => { + if tm.elapsed() > MAX_TIME { + bail!("UDP punch is timed out, stop sending packets after {:?} packets", packets_sent); + } + let elapsed = last_send_time.elapsed(); + + if elapsed >= retry_interval { + socket.send(&[]).await.ok(); + packets_sent += 1; + + // Exponentially increase interval to reduce network pressure + retry_interval = std::cmp::min( + Duration::from_millis((retry_interval.as_millis() as f64 * 1.5) as u64), + MAX_INTERVAL + ); + last_send_time = Instant::now(); + } + } + res = socket.recv(&mut data) => match res { + Err(e) => bail!("UDP punch failed, {packets_sent} packets sent: {e}"), + Ok(n) => { + // log::debug!("UDP punch succeeded after sending {} packets after {:?}", packets_sent, tm.elapsed()); + if listen { + if n == 0 { + continue; + } + return Ok(Some(bytes::BytesMut::from(&data[..n]))); + } + return Ok(None); + } + } + } + } +} + +fn test_ipv6_sync() { + #[tokio::main(flavor = "current_thread")] + async fn func() { + if let Some(job) = test_ipv6().await { + job.await.ok(); + } + } + std::thread::spawn(func); +} + +pub async fn get_ipv6_socket() -> Option<(Arc, bytes::Bytes)> { + let Some(addr) = PUBLIC_IPV6_ADDR.lock().unwrap().0 else { + return None; + }; + + match UdpSocket::bind(addr).await { + Err(err) => { + log::warn!("Failed to create UDP socket for IPv6: {err}"); + } + Ok(socket) => { + if let Ok(local_addr_v6) = socket.local_addr() { + return Some(( + Arc::new(socket), + hbb_common::AddrMangle::encode(local_addr_v6).into(), + )); + } + } + } + None +} + #[cfg(test)] mod tests { use super::*; @@ -1690,13 +2161,3 @@ mod tests { ); } } - -#[inline] -pub fn get_builtin_option(key: &str) -> String { - config::BUILTIN_SETTINGS - .read() - .unwrap() - .get(key) - .cloned() - .unwrap_or_default() -} diff --git a/src/core_main.rs b/src/core_main.rs index 23d7706d473..cee6ac0b9fb 100644 --- a/src/core_main.rs +++ b/src/core_main.rs @@ -1,4 +1,4 @@ -#[cfg(windows)] +#[cfg(any(target_os = "windows", target_os = "macos"))] use crate::client::translate; #[cfg(not(debug_assertions))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -31,7 +31,10 @@ macro_rules! my_println{ pub fn core_main() -> Option> { crate::load_custom_client(); #[cfg(windows)] - crate::platform::windows::bootstrap(); + if !crate::platform::windows::bootstrap() { + // return None to terminate the process + return None; + } let mut args = Vec::new(); let mut flutter_args = Vec::new(); let mut i = 0; @@ -50,6 +53,7 @@ pub fn core_main() -> Option> { "--connect", "--play", "--file-transfer", + "--view-camera", "--port-forward", "--rdp", ] @@ -73,7 +77,15 @@ pub fn core_main() -> Option> { } #[cfg(any(target_os = "linux", target_os = "windows"))] if args.is_empty() { - if crate::check_process("--server", false) && !crate::check_process("--tray", true) { + #[cfg(target_os = "linux")] + let should_check_start_tray = crate::check_process("--server", false); + // We can use `crate::check_process("--server", false)` on Windows. + // Because `--server` process is the System user's process. We can't get the arguments in `check_process()`. + // We can assume that self service running means the server is also running on Windows. + #[cfg(target_os = "windows")] + let should_check_start_tray = crate::platform::is_self_service_running() + && crate::platform::is_cur_exe_the_installed(); + if should_check_start_tray && !crate::check_process("--tray", true) { #[cfg(target_os = "linux")] hbb_common::allow_err!(crate::platform::check_autostart_config()); hbb_common::allow_err!(crate::run_me(vec!["--tray"])); @@ -96,7 +108,7 @@ pub fn core_main() -> Option> { } } #[cfg(windows)] - if args.contains(&"--connect".to_string()) { + if args.contains(&"--connect".to_string()) || args.contains(&"--view-camera".to_string()) { hbb_common::platform::windows::start_cpu_performance_monitor(); } #[cfg(feature = "flutter")] @@ -166,6 +178,8 @@ pub fn core_main() -> Option> { #[cfg(not(any(target_os = "android", target_os = "ios")))] init_plugins(&args); if args.is_empty() || crate::common::is_empty_uni_link(&args[0]) { + #[cfg(windows)] + hbb_common::config::PeerConfig::preload_peers(); std::thread::spawn(move || crate::start_server(false, no_server)); } else { #[cfg(windows)] @@ -176,6 +190,26 @@ pub fn core_main() -> Option> { log::error!("Failed to uninstall: {}", err); } return None; + } else if args[0] == "--update" { + if config::is_disable_installation() { + return None; + } + let res = platform::update_me(false); + let text = match res { + Ok(_) => translate("Update successfully!".to_string()), + Err(err) => { + log::error!("Failed with error: {err}"); + translate("Update failed!".to_string()) + } + }; + Toast::new(Toast::POWERSHELL_APP_ID) + .title(&config::APP_NAME.read().unwrap()) + .text1(&text) + .sound(Some(Sound::Default)) + .duration(Duration::Short) + .show() + .ok(); + return None; } else if args[0] == "--after-install" { if let Err(err) = platform::run_after_install() { log::error!("Failed to after-install: {}", err); @@ -190,12 +224,11 @@ pub fn core_main() -> Option> { if config::is_disable_installation() { return None; } - let res = platform::install_me( - "desktopicon startmenu", - "".to_owned(), - true, - args.len() > 1, - ); + #[cfg(not(windows))] + let options = "desktopicon startmenu"; + #[cfg(windows)] + let options = "desktopicon startmenu printer"; + let res = platform::install_me(options, "".to_owned(), true, args.len() > 1); let text = match res { Ok(_) => translate("Installation Successful!".to_string()), Err(err) => { @@ -236,6 +269,43 @@ pub fn core_main() -> Option> { crate::virtual_display_manager::amyuni_idd::uninstall_driver() ); return None; + } else if args[0] == "--install-remote-printer" { + #[cfg(windows)] + if crate::platform::is_win_10_or_greater() { + match remote_printer::install_update_printer(&crate::get_app_name()) { + Ok(_) => { + log::info!("Remote printer installed/updated successfully"); + } + Err(e) => { + log::error!("Failed to install/update the remote printer: {}", e); + } + } + } else { + log::error!("Win10 or greater required!"); + } + return None; + } else if args[0] == "--uninstall-remote-printer" { + #[cfg(windows)] + if crate::platform::is_win_10_or_greater() { + remote_printer::uninstall_printer(&crate::get_app_name()); + log::info!("Remote printer uninstalled"); + } + return None; + } + } + #[cfg(target_os = "macos")] + { + use crate::platform; + if args[0] == "--update" { + let _text = match platform::update_me() { + Ok(_) => { + log::info!("{}", translate("Update successfully!".to_string())); + } + Err(err) => { + log::error!("Update failed with error: {err}"); + } + }; + return None; } } if args[0] == "--remove" { @@ -272,14 +342,10 @@ pub fn core_main() -> Option> { .arg(&format!("{} --tray", crate::get_app_name().to_lowercase())) .status() .ok(); - hbb_common::allow_err!(crate::platform::run_as_user( - vec!["--tray"], - None, - None::<(&str, &str)>, - )); + hbb_common::allow_err!(crate::run_me(vec!["--tray"])); } #[cfg(windows)] - crate::privacy_mode::restore_reg_connectivity(true); + crate::privacy_mode::restore_reg_connectivity(true, false); #[cfg(any(target_os = "linux", target_os = "windows"))] { crate::start_server(true, false); @@ -388,7 +454,9 @@ pub fn core_main() -> Option> { } return None; } else if args[0] == "--assign" { - if crate::platform::is_installed() && is_root() { + if config::Config::no_register_device() { + println!("Cannot assign an unregistrable device!"); + } else if crate::platform::is_installed() && is_root() { let max = args.len() - 1; let pos = args.iter().position(|x| x == "--token").unwrap_or(max); if pos < max { @@ -424,15 +492,34 @@ pub fn core_main() -> Option> { if pos < max { address_book_tag = Some(args[pos + 1].to_owned()); } + let mut address_book_alias = None; + let pos = args + .iter() + .position(|x| x == "--address_book_alias") + .unwrap_or(max); + if pos < max { + address_book_alias = Some(args[pos + 1].to_owned()); + } + let mut device_group_name = None; + let pos = args + .iter() + .position(|x| x == "--device_group_name") + .unwrap_or(max); + if pos < max { + device_group_name = Some(args[pos + 1].to_owned()); + } let mut body = serde_json::json!({ "id": id, "uuid": uuid, }); let header = "Authorization: Bearer ".to_owned() + &token; - if user_name.is_none() && strategy_name.is_none() && address_book_name.is_none() + if user_name.is_none() + && strategy_name.is_none() + && address_book_name.is_none() + && device_group_name.is_none() { println!( - "--user_name or --strategy_name or --address_book_name is required!" + "--user_name or --strategy_name or --address_book_name or --device_group_name is required!" ); } else { if let Some(name) = user_name { @@ -446,6 +533,12 @@ pub fn core_main() -> Option> { if let Some(name) = address_book_tag { body["address_book_tag"] = serde_json::json!(name); } + if let Some(name) = address_book_alias { + body["address_book_alias"] = serde_json::json!(name); + } + } + if let Some(name) = device_group_name { + body["device_group_name"] = serde_json::json!(name); } let url = crate::ui_interface::get_api_server() + "/api/devices/cli"; match crate::post_request_sync(url, body.to_string(), &header) { @@ -570,7 +663,8 @@ fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option { + "--connect" | "--play" | "--file-transfer" | "--view-camera" | "--port-forward" + | "--rdp" => { authority = Some((&arg.to_string()[2..]).to_owned()); id = args.next(); } diff --git a/src/flutter.rs b/src/flutter.rs index fe0a77e39d3..198d685051b 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -19,6 +19,7 @@ use serde_json::json; use std::{ collections::{HashMap, HashSet}, ffi::CString, + io::{Error as IoError, ErrorKind as IoErrorKind}, os::raw::{c_char, c_int, c_void}, str::FromStr, sync::{ @@ -50,7 +51,7 @@ lazy_static::lazy_static! { #[cfg(target_os = "windows")] lazy_static::lazy_static! { - pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = Library::open("texture_rgba_renderer_plugin.dll"); + pub static ref TEXTURE_RGBA_RENDERER_PLUGIN: Result = load_plugin_in_app_path("texture_rgba_renderer_plugin.dll"); } #[cfg(target_os = "linux")] @@ -65,7 +66,37 @@ lazy_static::lazy_static! { #[cfg(target_os = "windows")] lazy_static::lazy_static! { - pub static ref TEXTURE_GPU_RENDERER_PLUGIN: Result = Library::open("flutter_gpu_texture_renderer_plugin.dll"); + pub static ref TEXTURE_GPU_RENDERER_PLUGIN: Result = load_plugin_in_app_path("flutter_gpu_texture_renderer_plugin.dll"); +} + +// Move this function into `src/platform/windows.rs` if there're more calls to load plugins. +// Load dll with full path. +#[cfg(target_os = "windows")] +fn load_plugin_in_app_path(dll_name: &str) -> Result { + match std::env::current_exe() { + Ok(exe_file) => { + if let Some(cur_dir) = exe_file.parent() { + let full_path = cur_dir.join(dll_name); + if !full_path.exists() { + Err(LibError::OpeningLibraryError(IoError::new( + IoErrorKind::NotFound, + format!("{} not found", dll_name), + ))) + } else { + Library::open(full_path) + } + } else { + Err(LibError::OpeningLibraryError(IoError::new( + IoErrorKind::Other, + format!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ), + ))) + } + } + Err(e) => Err(LibError::OpeningLibraryError(e)), + } } /// FFI for rustdesk core's main entry. @@ -512,6 +543,25 @@ impl FlutterHandler { pub fn push_event(&self, name: &str, event: &[(&str, V)], excludes: &[&SessionID]) where V: Sized + Serialize + Clone, + { + self.push_event_(name, event, &[], excludes); + } + + pub fn push_event_to(&self, name: &str, event: &[(&str, V)], include: &[&SessionID]) + where + V: Sized + Serialize + Clone, + { + self.push_event_(name, event, include, &[]); + } + + pub fn push_event_( + &self, + name: &str, + event: &[(&str, V)], + includes: &[&SessionID], + excludes: &[&SessionID], + ) where + V: Sized + Serialize + Clone, { let mut h: HashMap<&str, serde_json::Value> = event.iter().map(|(k, v)| (*k, json!(*v))).collect(); @@ -519,11 +569,20 @@ impl FlutterHandler { h.insert("name", json!(name)); let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); for (sid, session) in self.session_handlers.read().unwrap().iter() { - if excludes.contains(&sid) { - continue; + let mut push = false; + if includes.is_empty() { + if !excludes.contains(&sid) { + push = true; + } + } else { + if includes.contains(&sid) { + push = true; + } } - if let Some(stream) = &session.event_stream { - stream.add(EventToUI::Event(out.clone())); + if push { + if let Some(stream) = &session.event_stream { + stream.add(EventToUI::Event(out.clone())); + } } } } @@ -657,12 +716,13 @@ impl InvokeUiSession for FlutterHandler { ); } - fn set_connection_type(&self, is_secured: bool, direct: bool) { + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str) { self.push_event( "connection_ready", &[ ("secure", &is_secured.to_string()), ("direct", &direct.to_string()), + ("stream_type", &stream_type.to_string()), ], &[], ); @@ -1028,6 +1088,81 @@ impl InvokeUiSession for FlutterHandler { fn update_record_status(&self, start: bool) { self.push_event("record_status", &[("start", &start.to_string())], &[]); } + + fn printer_request(&self, id: i32, path: String) { + self.push_event( + "printer_request", + &[("id", json!(id)), ("path", json!(path))], + &[], + ); + } + + fn handle_screenshot_resp(&self, sid: String, msg: String) { + match SessionID::from_str(&sid) { + Ok(sid) => self.push_event_to("screenshot", &[("msg", json!(msg))], &[&sid]), + Err(e) => { + // Unreachable! + log::error!("Failed to parse sid \"{}\", {}", sid, e); + } + } + } + + fn handle_terminal_response(&self, response: TerminalResponse) { + use hbb_common::message_proto::terminal_response::Union; + + match response.union { + Some(Union::Opened(opened)) => { + let mut event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("opened")), + ("terminal_id", json!(opened.terminal_id)), + ("success", json!(opened.success)), + ("message", json!(&opened.message)), + ("pid", json!(opened.pid)), + ("service_id", json!(&opened.service_id)), + ]; + if !opened.persistent_sessions.is_empty() { + event_data.push(("persistent_sessions", json!(opened.persistent_sessions))); + } + self.push_event_("terminal_response", &event_data, &[], &[]); + } + Some(Union::Data(data)) => { + // Decompress data if needed + let output_data = if data.compressed { + hbb_common::compress::decompress(&data.data) + } else { + data.data.to_vec() + }; + + let encoded = crate::encode64(&output_data); + let event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("data")), + ("terminal_id", json!(data.terminal_id)), + ("data", json!(&encoded)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + Some(Union::Closed(closed)) => { + let event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("closed")), + ("terminal_id", json!(closed.terminal_id)), + ("exit_code", json!(closed.exit_code)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + Some(Union::Error(error)) => { + let event_data: Vec<(&str, serde_json::Value)> = vec![ + ("type", json!("error")), + ("terminal_id", json!(error.terminal_id)), + ("message", json!(&error.message)), + ]; + self.push_event_("terminal_response", &event_data, &[], &[]); + } + None => {} + Some(_) => { + log::warn!("Unhandled terminal response type"); + } + } + } } impl FlutterHandler { @@ -1118,8 +1253,14 @@ pub fn session_add_existed( peer_id: String, session_id: SessionID, displays: Vec, + is_view_camera: bool, ) -> ResultType<()> { - sessions::insert_peer_session_id(peer_id, ConnType::DEFAULT_CONN, session_id, displays); + let conn_type = if is_view_camera { + ConnType::VIEW_CAMERA + } else { + ConnType::DEFAULT_CONN + }; + sessions::insert_peer_session_id(peer_id, conn_type, session_id, displays); Ok(()) } @@ -1129,13 +1270,16 @@ pub fn session_add_existed( /// /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ /// * `is_file_transfer` - If the session is used for file transfer. +/// * `is_view_camera` - If the session is used for view camera. /// * `is_port_forward` - If the session is used for port forward. pub fn session_add( session_id: &SessionID, id: &str, is_file_transfer: bool, + is_view_camera: bool, is_port_forward: bool, is_rdp: bool, + is_terminal: bool, switch_uuid: &str, force_relay: bool, password: String, @@ -1144,6 +1288,10 @@ pub fn session_add( ) -> ResultType { let conn_type = if is_file_transfer { ConnType::FILE_TRANSFER + } else if is_view_camera { + ConnType::VIEW_CAMERA + } else if is_terminal { + ConnType::TERMINAL } else if is_port_forward { if is_rdp { ConnType::RDP @@ -1274,9 +1422,26 @@ pub fn update_text_clipboard_required() { Client::set_is_text_clipboard_required(is_required); } +#[cfg(feature = "unix-file-copy-paste")] +pub fn update_file_clipboard_required() { + let is_required = sessions::get_sessions() + .iter() + .any(|s| s.is_file_clipboard_required()); + Client::set_is_file_clipboard_required(is_required); +} + #[cfg(not(target_os = "ios"))] -pub fn send_text_clipboard_msg(msg: Message) { +pub fn send_clipboard_msg(msg: Message, _is_file: bool) { for s in sessions::get_sessions() { + #[cfg(feature = "unix-file-copy-paste")] + if _is_file { + if crate::is_support_file_copy_paste_num(s.lc.read().unwrap().version) + && s.is_file_clipboard_required() + { + s.send(Data::Message(msg.clone())); + } + continue; + } if s.is_text_clipboard_required() { // Check if the client supports multi clipboards if let Some(message::Union::MultiClipboards(multi_clipboards)) = &msg.union { @@ -1930,7 +2095,10 @@ pub mod sessions { None => {} } } - SESSIONS.write().unwrap().remove(&remove_peer_key?) + let s = SESSIONS.write().unwrap().remove(&remove_peer_key?); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_session_count_to_server(); + s } fn check_remove_unused_displays( @@ -1978,6 +2146,8 @@ pub mod sessions { // This operation will also cause the peer to send a switch display message. // The switch display message will contain `SupportedResolutions`, which is useful when changing resolutions. s.switch_display(value[0]); + // Reset the valid flag of the display. + s.next_rgba(value[0] as usize); if !is_desktop { s.capture_displays(vec![], vec![], value); @@ -2030,6 +2200,14 @@ pub mod sessions { .write() .unwrap() .insert(session_id, Default::default()); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + update_session_count_to_server(); + } + + #[inline] + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn update_session_count_to_server() { + crate::ipc::update_controlling_session_count(SESSIONS.read().unwrap().len()).ok(); } #[inline] @@ -2055,6 +2233,11 @@ pub mod sessions { .write() .unwrap() .insert(session_id, h); + // If the session is a single display session, it may be a software rgba rendered display. + // If this is the second time the display is opened, the old valid flag may be true. + if displays.len() == 1 { + s.ui_handler.next_rgba(displays[0] as usize); + } true } else { false @@ -2076,11 +2259,7 @@ pub mod sessions { } pub(super) mod async_tasks { - use hbb_common::{ - bail, - tokio::{self, select}, - ResultType, - }; + use hbb_common::{bail, tokio, ResultType}; use std::{ collections::HashMap, sync::{ diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs index 0bb17c9036d..3e947609f85 100644 --- a/src/flutter_ffi.rs +++ b/src/flutter_ffi.rs @@ -1,3 +1,5 @@ +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::keyboard::input_source::{change_input_source, get_cur_session_input_source}; use crate::{ client::file_trait::FileManager, common::{make_fd_to_json, make_vec_fd_to_json}, @@ -7,11 +9,6 @@ use crate::{ input::*, ui_interface::{self, *}, }; -#[cfg(not(any(target_os = "android", target_os = "ios")))] -use crate::{ - common::get_default_sound_input, - keyboard::input_source::{change_input_source, get_cur_session_input_source}, -}; use flutter_rust_bridge::{StreamSink, SyncReturn}; #[cfg(feature = "plugin_framework")] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -24,11 +21,12 @@ use hbb_common::{ }; use std::{ collections::HashMap, + path::PathBuf, sync::{ atomic::{AtomicI32, Ordering}, Arc, }, - time::SystemTime, + time::{Duration, SystemTime}, }; pub type SessionID = uuid::Uuid; @@ -66,6 +64,7 @@ fn initialize(app_dir: &str, custom_client_config: &str) { { use hbb_common::env_logger::*; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")); + crate::common::test_nat_type(); } #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -95,16 +94,30 @@ pub fn host_stop_system_key_propagate(_stopped: bool) { } // This function is only used to count the number of control sessions. -pub fn peer_get_default_sessions_count(id: String) -> SyncReturn { - SyncReturn(sessions::get_session_count(id, ConnType::DEFAULT_CONN)) +pub fn peer_get_sessions_count(id: String, conn_type: i32) -> SyncReturn { + let conn_type = if conn_type == ConnType::VIEW_CAMERA as i32 { + ConnType::VIEW_CAMERA + } else if conn_type == ConnType::FILE_TRANSFER as i32 { + ConnType::FILE_TRANSFER + } else if conn_type == ConnType::PORT_FORWARD as i32 { + ConnType::PORT_FORWARD + } else if conn_type == ConnType::RDP as i32 { + ConnType::RDP + } else if conn_type == ConnType::TERMINAL as i32 { + ConnType::TERMINAL + } else { + ConnType::DEFAULT_CONN + }; + SyncReturn(sessions::get_session_count(id, conn_type)) } pub fn session_add_existed_sync( id: String, session_id: SessionID, displays: Vec, + is_view_camera: bool, ) -> SyncReturn { - if let Err(e) = session_add_existed(id.clone(), session_id, displays) { + if let Err(e) = session_add_existed(id.clone(), session_id, displays, is_view_camera) { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -115,26 +128,37 @@ pub fn session_add_sync( session_id: SessionID, id: String, is_file_transfer: bool, + is_view_camera: bool, is_port_forward: bool, is_rdp: bool, + is_terminal: bool, switch_uuid: String, force_relay: bool, password: String, is_shared_password: bool, conn_token: Option, ) -> SyncReturn { - if let Err(e) = session_add( + let add_res = session_add( &session_id, &id, is_file_transfer, + is_view_camera, is_port_forward, is_rdp, + is_terminal, &switch_uuid, force_relay, password, is_shared_password, conn_token, - ) { + ); + // We can't put the remove call together with `std::env::var("IS_TERMINAL_ADMIN")`. + // Because there are some `bail!` in `session_add()`, we must make sure `IS_TERMINAL_ADMIN` is removed at last. + if is_terminal { + std::env::remove_var("IS_TERMINAL_ADMIN"); + } + + if let Err(e) = add_res { SyncReturn(format!("Failed to add session with id {}, {}", &id, e)) } else { SyncReturn("".to_owned()) @@ -239,6 +263,16 @@ pub fn session_refresh(session_id: SessionID, display: usize) { } } +pub fn session_take_screenshot(session_id: SessionID, display: usize) { + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + s.take_screenshot(display as _, session_id.to_string()); + } +} + +pub fn session_handle_screenshot(session_id: SessionID, action: String) -> String { + crate::client::screenshot::handle_screenshot(action) +} + pub fn session_is_multi_ui_session(session_id: SessionID) -> SyncReturn { if let Some(session) = sessions::get_session_by_session_id(&session_id) { SyncReturn(session.is_multi_ui_session()) @@ -278,6 +312,12 @@ pub fn session_toggle_option(session_id: SessionID, value: String) { if sessions::get_session_by_session_id(&session_id).is_some() && value == "disable-clipboard" { crate::flutter::update_text_clipboard_required(); } + #[cfg(feature = "unix-file-copy-paste")] + if sessions::get_session_by_session_id(&session_id).is_some() + && value == config::keys::OPTION_ENABLE_FILE_COPY_PASTE + { + crate::flutter::update_file_clipboard_required(); + } } pub fn session_toggle_privacy_mode(session_id: SessionID, impl_key: String, on: bool) { @@ -464,6 +504,20 @@ pub fn session_set_custom_fps(session_id: SessionID, fps: i32) { } } +pub fn session_get_trackpad_speed(session_id: SessionID) -> Option { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + Some(session.get_trackpad_speed()) + } else { + None + } +} + +pub fn session_set_trackpad_speed(session_id: SessionID, value: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.save_trackpad_speed(value); + } +} + pub fn session_lock_screen(session_id: SessionID) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.lock_screen(); @@ -570,6 +624,36 @@ pub fn session_send_chat(session_id: SessionID, text: String) { } } +// Terminal functions +pub fn session_open_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.open_terminal(terminal_id, rows, cols); + } else { + log::error!( + "[flutter_ffi] Session not found for session_id: {}", + session_id + ); + } +} + +pub fn session_send_terminal_input(session_id: SessionID, terminal_id: i32, data: String) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.send_terminal_input(terminal_id, data); + } +} + +pub fn session_resize_terminal(session_id: SessionID, terminal_id: i32, rows: u32, cols: u32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.resize_terminal(terminal_id, rows, cols); + } +} + +pub fn session_close_terminal(session_id: SessionID, terminal_id: i32) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.close_terminal(terminal_id); + } +} + pub fn session_peer_option(session_id: SessionID, name: String, value: String) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { session.set_option(name, value); @@ -607,7 +691,15 @@ pub fn session_send_files( _is_dir: bool, ) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.send_files(act_id, path, to, file_num, include_hidden, is_remote); + session.send_files( + act_id, + fs::JobType::Generic.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ); } } @@ -732,7 +824,15 @@ pub fn session_add_job( is_remote: bool, ) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.add_job(act_id, path, to, file_num, include_hidden, is_remote); + session.add_job( + act_id, + fs::JobType::Generic.into(), + path, + to, + file_num, + include_hidden, + is_remote, + ); } } @@ -847,7 +947,10 @@ pub fn main_set_option(key: String, value: String) { ); } - if key.eq("custom-rendezvous-server") { + if key.eq("custom-rendezvous-server") + || key.eq(config::keys::OPTION_ALLOW_WEBSOCKET) + || key.eq("api-server") + { set_option(key, value.clone()); #[cfg(target_os = "android")] crate::rendezvous_mediator::RendezvousMediator::restart(); @@ -974,8 +1077,38 @@ pub fn main_get_env(key: String) -> SyncReturn { SyncReturn(std::env::var(key).unwrap_or_default()) } +// Dart does not support changing environment variables. +// `Platform.environment['MY_VAR'] = 'VAR';` will throw an error +// `Unsupported operation: Cannot modify unmodifiable map`. +// +// And we need to share the environment variables between rust and dart isolates sometimes. +pub fn main_set_env(key: String, value: Option) -> SyncReturn<()> { + let is_valid_key = !key.is_empty() && !key.contains('=') && !key.contains('\0'); + debug_assert!(is_valid_key, "Invalid environment variable key: {}", key); + if !is_valid_key { + log::error!("Invalid environment variable key: {}", key); + return SyncReturn(()); + } + + match value { + Some(v) => { + let is_valid_value = !v.contains('\0'); + debug_assert!(is_valid_value, "Invalid environment variable value: {}", v); + if !is_valid_value { + log::error!("Invalid environment variable value: {}", v); + return SyncReturn(()); + } + std::env::set_var(key, v); + } + None => std::env::remove_var(key), + } + + SyncReturn(()) +} + pub fn main_set_local_option(key: String, value: String) { let is_texture_render_key = key.eq(config::keys::OPTION_TEXTURE_RENDER); + let is_d3d_render_key = key.eq(config::keys::OPTION_ALLOW_D3D_RENDER); set_local_option(key, value.clone()); if is_texture_render_key { let session_event = [("v", &value)]; @@ -985,6 +1118,11 @@ pub fn main_set_local_option(key: String, value: String) { session.ui_handler.update_use_texture_render(); } } + if is_d3d_render_key { + for session in sessions::get_sessions() { + session.update_supported_decodings(); + } + } } // We do use use `main_get_local_option` and `main_set_local_option`. @@ -1098,55 +1236,76 @@ pub fn main_peer_exists(id: String) -> bool { peer_exists(&id) } -pub fn main_load_recent_peers() { - if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec> = PeerConfig::peers(None) - .drain(..) - .map(|(id, _, p)| peer_to_map(id, p)) - .collect(); +fn load_recent_peers( + vec_id_modified_time_path: &Vec<(String, SystemTime, std::path::PathBuf)>, + to_end: bool, + all_peers: &mut Vec>, + from: usize, +) -> usize { + let to = if to_end { + Some(vec_id_modified_time_path.len()) + } else { + None + }; + let mut peers_next = PeerConfig::batch_peers(vec_id_modified_time_path, from, to); + // There may be less peers than the batch size. + // But no need to consider this case, because it is a rare case. + let peers = peers_next.0.drain(..).map(|(id, _, p)| peer_to_map(id, p)); + all_peers.extend(peers); + peers_next.1 +} - let data = HashMap::from([ - ("name", "load_recent_peers".to_owned()), - ( - "peers", - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), - ), - ]); +pub fn main_load_recent_peers() { + let push_to_flutter = |peers, ids| { + let mut data = HashMap::from([("name", "load_recent_peers".to_owned()), ("peers", peers)]); + if let Some(ids) = ids { + data.insert("ids", ids); + } let _res = flutter::push_global_event( flutter::APP_TYPE_MAIN, serde_json::ser::to_string(&data).unwrap_or("".to_owned()), ); - } -} + }; -pub fn main_load_recent_peers_sync() -> SyncReturn { if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec> = PeerConfig::peers(None) - .drain(..) - .map(|(id, _, p)| peer_to_map(id, p)) - .collect(); + let vec_id_modified_time_path = PeerConfig::get_vec_id_modified_time_path(&None); + if vec_id_modified_time_path.is_empty() { + push_to_flutter("".to_owned(), None); + return; + } - let data = HashMap::from([ - ("name", "load_recent_peers".to_owned()), - ( - "peers", - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), - ), - ]); - return SyncReturn(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + let load_two_times = vec_id_modified_time_path.len() > PeerConfig::BATCH_LOADING_COUNT + && cfg!(target_os = "windows"); + let mut all_peers = vec![]; + if load_two_times { + let next_from = load_recent_peers(&vec_id_modified_time_path, false, &mut all_peers, 0); + let rest_ids = if next_from < vec_id_modified_time_path.len() { + Some( + vec_id_modified_time_path[next_from..] + .iter() + .map(|(id, _, _)| id.clone()) + .collect::>() + .join(", "), + ) + } else { + None + }; + push_to_flutter( + serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()), + rest_ids, + ); + let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, next_from); + } else { + let _ = load_recent_peers(&vec_id_modified_time_path, true, &mut all_peers, 0); + } + // Don't check if `all_peers` is empty, because we need this message to update the state in the flutter side. + push_to_flutter( + serde_json::ser::to_string(&all_peers).unwrap_or("".to_owned()), + None, + ); + } else { + push_to_flutter("".to_owned(), None) } - SyncReturn("".to_string()) -} - -pub fn main_load_lan_peers_sync() -> SyncReturn { - let data = HashMap::from([ - ("name", "load_lan_peers".to_owned()), - ( - "peers", - serde_json::to_string(&get_lan_peers()).unwrap_or_default(), - ), - ]); - return SyncReturn(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); } pub fn main_load_recent_peers_for_ab(filter: String) -> String { @@ -1167,13 +1326,20 @@ pub fn main_load_recent_peers_for_ab(filter: String) -> String { } pub fn main_load_fav_peers() { + let push_to_flutter = |peers| { + let data = HashMap::from([("name", "load_fav_peers".to_owned()), ("peers", peers)]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }; if !config::APP_DIR.read().unwrap().is_empty() { let favs = get_fav(); - let mut recent = PeerConfig::peers(None); + let mut recent = PeerConfig::peers(Some(favs.clone())); let mut lan = config::LanPeers::load() .peers .iter() - .filter(|d| recent.iter().all(|r| r.0 != d.id)) + .filter(|d| favs.contains(&d.id) && recent.iter().all(|r| r.0 != d.id)) .map(|d| { ( d.id.clone(), @@ -1192,26 +1358,12 @@ pub fn main_load_fav_peers() { recent.append(&mut lan); let peers: Vec> = recent .into_iter() - .filter_map(|(id, _, p)| { - if favs.contains(&id) { - Some(peer_to_map(id, p)) - } else { - None - } - }) + .map(|(id, _, p)| peer_to_map(id, p)) .collect(); - let data = HashMap::from([ - ("name", "load_fav_peers".to_owned()), - ( - "peers", - serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), - ), - ]); - let _res = flutter::push_global_event( - flutter::APP_TYPE_MAIN, - serde_json::ser::to_string(&data).unwrap_or("".to_owned()), - ); + push_to_flutter(serde_json::ser::to_string(&peers).unwrap_or("".to_owned())); + } else { + push_to_flutter("".to_owned()); } } @@ -1409,10 +1561,7 @@ pub fn main_get_last_remote_id() -> String { } pub fn main_get_software_update_url() { - let opt = get_local_option(config::keys::OPTION_ENABLE_CHECK_UPDATE.to_string()); - if config::option2bool(config::keys::OPTION_ENABLE_CHECK_UPDATE, &opt) { - crate::common::check_software_update(); - } + crate::common::check_software_update(); } pub fn main_get_home_dir() -> String { @@ -1619,7 +1768,7 @@ pub fn session_alternative_codecs(session_id: SessionID) -> String { pub fn session_change_prefer_codec(session_id: SessionID) { if let Some(session) = sessions::get_session_by_session_id(&session_id) { - session.change_prefer_codec(); + session.update_supported_decodings(); } } @@ -1634,6 +1783,17 @@ pub fn session_toggle_virtual_display(session_id: SessionID, index: i32, on: boo } } +pub fn session_printer_response( + session_id: SessionID, + id: i32, + path: String, + printer_name: String, +) { + if let Some(session) = sessions::get_session_by_session_id(&session_id) { + session.printer_response(id, path, printer_name); + } +} + pub fn main_set_home_dir(_home: String) { #[cfg(any(target_os = "android", target_os = "ios"))] { @@ -1951,13 +2111,7 @@ pub fn main_hide_dock() -> SyncReturn { } pub fn main_has_file_clipboard() -> SyncReturn { - let ret = cfg!(any( - target_os = "windows", - all( - feature = "unix-file-copy-paste", - any(target_os = "linux", target_os = "macos") - ) - )); + let ret = cfg!(any(target_os = "windows", feature = "unix-file-copy-paste",)); SyncReturn(ret) } @@ -2004,7 +2158,7 @@ pub fn is_outgoing_only() -> SyncReturn { } pub fn is_custom_client() -> SyncReturn { - SyncReturn(get_app_name() != "RustDesk") + SyncReturn(crate::common::is_custom_client()) } pub fn is_disable_settings() -> SyncReturn { @@ -2334,6 +2488,218 @@ pub fn main_audio_support_loopback() -> SyncReturn { SyncReturn(is_surpport) } +pub fn main_get_printer_names() -> SyncReturn { + #[cfg(target_os = "windows")] + return SyncReturn( + serde_json::to_string(&crate::platform::windows::get_printer_names().unwrap_or_default()) + .unwrap_or_default(), + ); + #[cfg(not(target_os = "windows"))] + return SyncReturn("".to_owned()); +} + +pub fn main_get_common(key: String) -> String { + if key == "is-printer-installed" { + #[cfg(target_os = "windows")] + { + return match remote_printer::is_rd_printer_installed(&get_app_name()) { + Ok(r) => r.to_string(), + Err(e) => e.to_string(), + }; + } + #[cfg(not(target_os = "windows"))] + return false.to_string(); + } else if key == "is-support-printer-driver" { + #[cfg(target_os = "windows")] + return crate::platform::is_win_10_or_greater().to_string(); + #[cfg(not(target_os = "windows"))] + return false.to_string(); + } else if key == "transfer-job-id" { + return hbb_common::fs::get_next_job_id().to_string(); + } else { + if key.starts_with("download-data-") { + let id = key.replace("download-data-", ""); + match crate::hbbs_http::downloader::get_download_data(&id) { + Ok(data) => serde_json::to_string(&data).unwrap_or_default(), + Err(e) => { + format!("error:{}", e) + } + } + } else if key.starts_with("download-file-") { + let _version = key.replace("download-file-", ""); + #[cfg(target_os = "windows")] + return match crate::platform::windows::is_msi_installed() { + Ok(true) => format!("rustdesk-{_version}-x86_64.msi"), + Ok(false) => format!("rustdesk-{_version}-x86_64.exe"), + Err(e) => { + log::error!("Failed to check if is msi: {}", e); + format!("error:update-failed-check-msi-tip") + } + }; + #[cfg(target_os = "macos")] + { + return if cfg!(target_arch = "x86_64") { + format!("rustdesk-{_version}-x86_64.dmg") + } else if cfg!(target_arch = "aarch64") { + format!("rustdesk-{_version}-aarch64.dmg") + } else { + "error:unsupported".to_owned() + }; + } + #[cfg(not(any(target_os = "windows", target_os = "macos")))] + { + "error:unsupported".to_owned() + } + } else { + "".to_owned() + } + } +} + +pub fn main_get_common_sync(key: String) -> SyncReturn { + SyncReturn(main_get_common(key)) +} + +pub fn main_set_common(_key: String, _value: String) { + #[cfg(target_os = "windows")] + if _key == "install-printer" && crate::platform::is_win_10_or_greater() { + std::thread::spawn(move || { + let (success, msg) = match remote_printer::install_update_printer(&get_app_name()) { + Ok(_) => (true, "".to_owned()), + Err(e) => { + let err = e.to_string(); + log::error!("Failed to install/update rd printer: {}", &err); + (false, err) + } + }; + if success { + // Use `ipc` to notify the server process to update the install option in the registry. + // Because `install_update_printer()` may prompt for permissions, there is no need to prompt again here. + if let Err(e) = crate::ipc::set_install_option( + crate::platform::REG_NAME_INSTALL_PRINTER.to_string(), + "1".to_string(), + ) { + log::error!("Failed to set install printer option: {}", e); + } + } + let data = HashMap::from([ + ("name", serde_json::json!("install-printer-res")), + ("success", serde_json::json!(success)), + ("msg", serde_json::json!(msg)), + ]); + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + }); + } + #[cfg(any(target_os = "windows", target_os = "macos"))] + { + use crate::updater::get_download_file_from_url; + if _key == "download-new-version" { + let download_url = _value.clone(); + let event_key = "download-new-version".to_owned(); + let data = if let Some(download_file) = get_download_file_from_url(&download_url) { + std::fs::remove_file(&download_file).ok(); + match crate::hbbs_http::downloader::download_file( + download_url, + Some(PathBuf::from(download_file)), + Some(Duration::from_secs(3)), + ) { + Ok(id) => HashMap::from([("name", event_key), ("id", id)]), + Err(e) => HashMap::from([("name", event_key), ("error", e.to_string())]), + } + } else { + HashMap::from([ + ("name", event_key), + ("error", "Invalid download url".to_string()), + ]) + }; + let _res = flutter::push_global_event( + flutter::APP_TYPE_MAIN, + serde_json::ser::to_string(&data).unwrap_or("".to_owned()), + ); + } else if _key == "update-me" { + if let Some(new_version_file) = get_download_file_from_url(&_value) { + log::debug!( + "New version file is downloaed, update begin, {:?}", + new_version_file.to_str() + ); + if let Some(f) = new_version_file.to_str() { + // 1.4.0 does not support "--update" + // But we can assume that the new version supports it. + #[cfg(target_os = "windows")] + if f.ends_with(".exe") { + if let Err(e) = + crate::platform::run_exe_in_cur_session(f, vec!["--update"], false) + { + log::error!("Failed to run the update exe: {}", e); + } + } else if f.ends_with(".msi") { + if let Err(e) = crate::platform::update_me_msi(f, false) { + log::error!("Failed to run the update msi: {}", e); + } + } else { + // unreachable!() + } + #[cfg(target_os = "macos")] + match crate::platform::update_to(f) { + Ok(_) => { + log::info!("Update successfully!"); + } + Err(e) => { + log::error!("Failed to update to new version, {}", e); + } + } + fs::remove_file(f).ok(); + } + } + } else if _key == "extract-update-dmg" { + #[cfg(target_os = "macos")] + { + if let Some(new_version_file) = get_download_file_from_url(&_value) { + if let Some(f) = new_version_file.to_str() { + crate::platform::macos::extract_update_dmg(f); + } else { + // unreachable!() + log::error!("Failed to get the new version file path"); + } + } else { + // unreachable!() + log::error!("Failed to get the new version file from url: {}", _value); + } + } + } + } + + if _key == "remove-downloader" { + crate::hbbs_http::downloader::remove(&_value); + } else if _key == "cancel-downloader" { + crate::hbbs_http::downloader::cancel(&_value); + } +} + +pub fn session_get_common_sync( + session_id: SessionID, + key: String, + param: String, +) -> SyncReturn> { + SyncReturn(session_get_common(session_id, key, param)) +} + +pub fn session_get_common(session_id: SessionID, key: String, param: String) -> Option { + if let Some(s) = sessions::get_session_by_session_id(&session_id) { + let v = if key == "is_screenshot_supported" { + s.is_screenshot_supported().to_string() + } else { + "".to_owned() + }; + Some(v) + } else { + None + } +} + #[cfg(target_os = "android")] pub mod server_side { use hbb_common::{config, log}; diff --git a/src/hbbs_http.rs b/src/hbbs_http.rs index 71fff7ca899..e79534e2c47 100644 --- a/src/hbbs_http.rs +++ b/src/hbbs_http.rs @@ -7,6 +7,7 @@ pub mod account; mod http_client; pub mod record_upload; pub mod sync; +pub mod downloader; pub use http_client::create_http_client; pub use http_client::create_http_client_async; diff --git a/src/hbbs_http/downloader.rs b/src/hbbs_http/downloader.rs new file mode 100644 index 00000000000..4821b0814d6 --- /dev/null +++ b/src/hbbs_http/downloader.rs @@ -0,0 +1,274 @@ +use super::create_http_client_async; +use hbb_common::{ + bail, + lazy_static::lazy_static, + log, + tokio::{ + self, + fs::File, + io::AsyncWriteExt, + sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + }, + ResultType, +}; +use serde_derive::Serialize; +use std::{collections::HashMap, path::PathBuf, sync::Mutex, time::Duration}; + +lazy_static! { + static ref DOWNLOADERS: Mutex> = Default::default(); +} + +/// This struct is used to return the download data to the caller. +/// The caller should check if the file is downloaded successfully and remove the job from the map. +/// If the file is not downloaded successfully, the `data` field will be empty. +/// If the file is downloaded successfully, the `data` field will contain the downloaded data if `path` is None. +#[derive(Serialize, Debug)] +pub struct DownloadData { + #[serde(skip_serializing_if = "Vec::is_empty")] + pub data: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub total_size: Option, + pub downloaded_size: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +struct Downloader { + data: Vec, + path: Option, + // Some file may be empty, so we use Option to indicate if the size is known + total_size: Option, + downloaded_size: u64, + error: Option, + finished: bool, + tx_cancel: UnboundedSender<()>, +} + +// The caller should check if the file is downloaded successfully and remove the job from the map. +pub fn download_file( + url: String, + path: Option, + auto_del_dur: Option, +) -> ResultType { + let id = url.clone(); + if DOWNLOADERS.lock().unwrap().contains_key(&id) { + return Ok(id); + } + + if let Some(path) = path.as_ref() { + if path.exists() { + bail!("File {} already exists", path.display()); + } + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + } + let (tx, rx) = unbounded_channel(); + let downloader = Downloader { + data: Vec::new(), + path: path.clone(), + total_size: None, + downloaded_size: 0, + error: None, + tx_cancel: tx, + finished: false, + }; + let mut downloaders = DOWNLOADERS.lock().unwrap(); + downloaders.insert(id.clone(), downloader); + + let id2 = id.clone(); + std::thread::spawn( + move || match do_download(&id2, url, path, auto_del_dur, rx) { + Ok(is_all_downloaded) => { + let mut downloaded_size = 0; + let mut total_size = 0; + DOWNLOADERS.lock().unwrap().get_mut(&id2).map(|downloader| { + downloaded_size = downloader.downloaded_size; + total_size = downloader.total_size.unwrap_or(0); + }); + log::info!( + "Download {} end, {}/{}, {:.2} %", + &id2, + downloaded_size, + total_size, + if total_size == 0 { + 0.0 + } else { + downloaded_size as f64 / total_size as f64 * 100.0 + } + ); + + let is_canceled = !is_all_downloaded; + if is_canceled { + if let Some(downloader) = DOWNLOADERS.lock().unwrap().remove(&id2) { + if let Some(p) = downloader.path { + if p.exists() { + std::fs::remove_file(p).ok(); + } + } + } + } + } + Err(e) => { + let err = e.to_string(); + log::error!("Download {}, failed: {}", &id2, &err); + DOWNLOADERS.lock().unwrap().get_mut(&id2).map(|downloader| { + downloader.error = Some(err); + }); + } + }, + ); + + Ok(id) +} + +#[tokio::main(flavor = "current_thread")] +async fn do_download( + id: &str, + url: String, + path: Option, + auto_del_dur: Option, + mut rx_cancel: UnboundedReceiver<()>, +) -> ResultType { + let client = create_http_client_async(); + + let mut is_all_downloaded = false; + tokio::select! { + _ = rx_cancel.recv() => { + return Ok(is_all_downloaded); + } + head_resp = client.head(&url).send() => { + match head_resp { + Ok(resp) => { + if resp.status().is_success() { + let total_size = resp + .headers() + .get(reqwest::header::CONTENT_LENGTH) + .and_then(|ct_len| ct_len.to_str().ok()) + .and_then(|ct_len| ct_len.parse::().ok()); + let Some(total_size) = total_size else { + bail!("Failed to get content length"); + }; + DOWNLOADERS.lock().unwrap().get_mut(id).map(|downloader| { + downloader.total_size = Some(total_size); + }); + } else { + bail!("Failed to get content length: {}", resp.status()); + } + } + Err(e) => { + return Err(e.into()); + } + } + } + } + + let mut response; + tokio::select! { + _ = rx_cancel.recv() => { + return Ok(is_all_downloaded); + } + resp = client.get(url).send() => { + response = resp?; + } + } + + let mut dest: Option = None; + if let Some(p) = path { + dest = Some(File::create(p).await?); + } + + loop { + tokio::select! { + _ = rx_cancel.recv() => { + break; + } + chunk = response.chunk() => { + match chunk { + Ok(Some(chunk)) => { + match dest { + Some(ref mut f) => { + f.write_all(&chunk).await?; + f.flush().await?; + DOWNLOADERS.lock().unwrap().get_mut(id).map(|downloader| { + downloader.downloaded_size += chunk.len() as u64; + }); + } + None => { + DOWNLOADERS.lock().unwrap().get_mut(id).map(|downloader| { + downloader.data.extend_from_slice(&chunk); + downloader.downloaded_size += chunk.len() as u64; + }); + } + } + } + Ok(None) => { + is_all_downloaded = true; + break; + }, + Err(e) => { + log::error!("Download {} failed: {}", id, e); + return Err(e.into()); + } + } + } + } + } + + if let Some(mut f) = dest.take() { + f.flush().await?; + } + + if let Some(ref mut downloader) = DOWNLOADERS.lock().unwrap().get_mut(id) { + downloader.finished = true; + } + if is_all_downloaded { + let id_del = id.to_string(); + if let Some(dur) = auto_del_dur { + tokio::spawn(async move { + tokio::time::sleep(dur).await; + DOWNLOADERS.lock().unwrap().remove(&id_del); + }); + } + } + Ok(is_all_downloaded) +} + +pub fn get_download_data(id: &str) -> ResultType { + let downloaders = DOWNLOADERS.lock().unwrap(); + if let Some(downloader) = downloaders.get(id) { + let downloaded_size = downloader.downloaded_size; + let total_size = downloader.total_size.clone(); + let error = downloader.error.clone(); + let data = if total_size.unwrap_or(0) == downloaded_size && downloader.path.is_none() { + downloader.data.clone() + } else { + Vec::new() + }; + let path = downloader.path.clone(); + let download_data = DownloadData { + data, + path, + total_size, + downloaded_size, + error, + }; + Ok(download_data) + } else { + bail!("Downloader not found") + } +} + +pub fn cancel(id: &str) { + if let Some(downloader) = DOWNLOADERS.lock().unwrap().get(id) { + // downloader.is_canceled.store(true, Ordering::SeqCst); + // The receiver may not be able to receive the cancel signal, so we also set the atomic bool to true + let _ = downloader.tx_cancel.send(()); + } +} + +pub fn remove(id: &str) { + let _ = DOWNLOADERS.lock().unwrap().remove(id); +} diff --git a/src/hbbs_http/http_client.rs b/src/hbbs_http/http_client.rs index 944e84ae6ff..8d6f529b75a 100644 --- a/src/hbbs_http/http_client.rs +++ b/src/hbbs_http/http_client.rs @@ -6,15 +6,17 @@ use reqwest::Client as AsyncClient; macro_rules! configure_http_client { ($builder:expr, $Client: ty) => {{ - let mut builder = $builder; + // https://github.com/rustdesk/rustdesk/issues/11569 + // https://docs.rs/reqwest/latest/reqwest/struct.ClientBuilder.html#method.no_proxy + let mut builder = $builder.no_proxy(); let client = if let Some(conf) = Config::get_socks() { let proxy_result = Proxy::from_conf(&conf, None); match proxy_result { Ok(proxy) => { let proxy_setup = match &proxy.intercept { - ProxyScheme::Http { host, .. } =>{ reqwest::Proxy::http(format!("http://{}", host))}, - ProxyScheme::Https { host, .. } => {reqwest::Proxy::https(format!("https://{}", host))}, + ProxyScheme::Http { host, .. } =>{ reqwest::Proxy::all(format!("http://{}", host))}, + ProxyScheme::Https { host, .. } => {reqwest::Proxy::all(format!("https://{}", host))}, ProxyScheme::Socks5 { addr, .. } => { reqwest::Proxy::all(&format!("socks5://{}", addr)) } }; diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs index 91c18c4b206..b82464b24a2 100644 --- a/src/hbbs_http/sync.rs +++ b/src/hbbs_http/sync.rs @@ -7,7 +7,8 @@ use std::{ #[cfg(not(any(target_os = "ios")))] use crate::{ui_interface::get_builtin_option, Connection}; use hbb_common::{ - config::{keys, Config, LocalConfig}, + config::{self, keys, Config, LocalConfig}, + log, tokio::{self, sync::broadcast, time::Instant}, }; use serde::{Deserialize, Serialize}; @@ -48,6 +49,38 @@ pub struct StrategyOptions { pub extra: HashMap, } +struct InfoUploaded { + uploaded: bool, + url: String, + last_uploaded: Option, + id: String, + username: Option, +} + +impl Default for InfoUploaded { + fn default() -> Self { + Self { + uploaded: false, + url: "".to_owned(), + last_uploaded: None, + id: "".to_owned(), + username: None, + } + } +} + +impl InfoUploaded { + fn uploaded(url: String, id: String, username: String) -> Self { + Self { + uploaded: true, + url, + last_uploaded: None, + id, + username: Some(username), + } + } +} + #[cfg(not(any(target_os = "ios")))] #[tokio::main(flavor = "current_thread")] async fn start_hbbs_sync_async() { @@ -56,8 +89,8 @@ async fn start_hbbs_sync_async() { TIME_CONN, )); let mut last_sent: Option = None; - let mut info_uploaded: (bool, String, Option, String) = - (false, "".to_owned(), None, "".to_owned()); + let mut info_uploaded = InfoUploaded::default(); + let mut sysinfo_ver = "".to_owned(); loop { tokio::select! { _ = interval.tick() => { @@ -67,54 +100,108 @@ async fn start_hbbs_sync_async() { *PRO.lock().unwrap() = false; continue; } - if hbb_common::config::option2bool("stop-service", &Config::get_option("stop-service")) { + if config::option2bool("stop-service", &Config::get_option("stop-service")) { continue; } let conns = Connection::alive_conns(); - if info_uploaded.0 && (url != info_uploaded.1 || id != info_uploaded.3) { - info_uploaded.0 = false; + if info_uploaded.uploaded && (url != info_uploaded.url || id != info_uploaded.id) { + info_uploaded.uploaded = false; *PRO.lock().unwrap() = false; } - if !info_uploaded.0 && info_uploaded.2.map(|x| x.elapsed() >= UPLOAD_SYSINFO_TIMEOUT).unwrap_or(true) { - let mut v = crate::get_sysinfo(); - // username is empty in login screen of windows, but here we only upload sysinfo once, causing - // real user name not uploaded after login screen. https://github.com/rustdesk/rustdesk/discussions/8031 - if !cfg!(windows) || !v["username"].as_str().unwrap_or_default().is_empty() { - v["version"] = json!(crate::VERSION); - v["id"] = json!(id); - v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); - let ab_name = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_NAME); - if !ab_name.is_empty() { - v[keys::OPTION_PRESET_ADDRESS_BOOK_NAME] = json!(ab_name); - } - let ab_tag = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_TAG); - if !ab_tag.is_empty() { - v[keys::OPTION_PRESET_ADDRESS_BOOK_TAG] = json!(ab_tag); - } - let username = get_builtin_option(keys::OPTION_PRESET_USERNAME); - if !username.is_empty() { - v[keys::OPTION_PRESET_USERNAME] = json!(username); - } - let strategy_name = get_builtin_option(keys::OPTION_PRESET_STRATEGY_NAME); - if !strategy_name.is_empty() { - v[keys::OPTION_PRESET_STRATEGY_NAME] = json!(strategy_name); - } - match crate::post_request(url.replace("heartbeat", "sysinfo"), v.to_string(), "").await { - Ok(x) => { - if x == "SYSINFO_UPDATED" { - info_uploaded = (true, url.clone(), None, id.clone()); - hbb_common::log::info!("sysinfo updated"); + // For Windows: + // We can't skip uploading sysinfo when the username is empty, because the username may + // always be empty before login. We also need to upload the other sysinfo info. + // + // https://github.com/rustdesk/rustdesk/discussions/8031 + // We still need to check the username after uploading sysinfo, because + // 1. The username may be empty when logining in, and it can be fetched after a while. + // In this case, we need to upload sysinfo again. + // 2. The username may be changed after uploading sysinfo, and we need to upload sysinfo again. + // + // The Windows session will switch to the last user session before the restart, + // so it may be able to get the username before login. + // But strangely, sometimes we can get the username before login, + // we may not be able to get the username before login after the next restart. + let mut v = crate::get_sysinfo(); + let sys_username = v["username"].as_str().unwrap_or_default().to_string(); + // Though the username comparison is only necessary on Windows, + // we still keep the comparison on other platforms for consistency. + let need_upload = (!info_uploaded.uploaded || info_uploaded.username.as_ref() != Some(&sys_username)) && + info_uploaded.last_uploaded.map(|x| x.elapsed() >= UPLOAD_SYSINFO_TIMEOUT).unwrap_or(true); + if need_upload { + v["version"] = json!(crate::VERSION); + v["id"] = json!(id); + v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); + let ab_name = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_NAME); + if !ab_name.is_empty() { + v[keys::OPTION_PRESET_ADDRESS_BOOK_NAME] = json!(ab_name); + } + let ab_tag = Config::get_option(keys::OPTION_PRESET_ADDRESS_BOOK_TAG); + if !ab_tag.is_empty() { + v[keys::OPTION_PRESET_ADDRESS_BOOK_TAG] = json!(ab_tag); + } + let username = get_builtin_option(keys::OPTION_PRESET_USERNAME); + if !username.is_empty() { + v[keys::OPTION_PRESET_USERNAME] = json!(username); + } + let strategy_name = get_builtin_option(keys::OPTION_PRESET_STRATEGY_NAME); + if !strategy_name.is_empty() { + v[keys::OPTION_PRESET_STRATEGY_NAME] = json!(strategy_name); + } + let device_group_name = get_builtin_option(keys::OPTION_PRESET_DEVICE_GROUP_NAME); + if !device_group_name.is_empty() { + v[keys::OPTION_PRESET_DEVICE_GROUP_NAME] = json!(device_group_name); + } + let v = v.to_string(); + let mut hash = "".to_owned(); + if crate::is_public(&url) { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(url.as_bytes()); + hasher.update(&v.as_bytes()); + let res = hasher.finalize(); + hash = hbb_common::base64::encode(&res[..]); + let old_hash = config::Status::get("sysinfo_hash"); + let ver = config::Status::get("sysinfo_ver"); // sysinfo_ver is the version of sysinfo on server's side + if hash == old_hash { + // When the api doesn't exist, Ok("") will be returned in test. + let samever = match crate::post_request(url.replace("heartbeat", "sysinfo_ver"), "".to_owned(), "").await { + Ok(x) => { + sysinfo_ver = x.clone(); *PRO.lock().unwrap() = true; - } else if x == "ID_NOT_FOUND" { - info_uploaded.2 = None; // next heartbeat will upload sysinfo again - } else { - info_uploaded.2 = Some(Instant::now()); + x == ver } + _ => { + false // to make sure Pro can be assigned in below post for old + // hbbs pro not supporting sysinfo_ver, use false for ensuring + } + }; + if samever { + info_uploaded = InfoUploaded::uploaded(url.clone(), id.clone(), sys_username); + log::info!("sysinfo not changed, skip upload"); + continue; } - _ => { - info_uploaded.2 = Some(Instant::now()); + } + } + match crate::post_request(url.replace("heartbeat", "sysinfo"), v, "").await { + Ok(x) => { + if x == "SYSINFO_UPDATED" { + info_uploaded = InfoUploaded::uploaded(url.clone(), id.clone(), sys_username); + log::info!("sysinfo updated"); + if !hash.is_empty() { + config::Status::set("sysinfo_hash", hash); + config::Status::set("sysinfo_ver", sysinfo_ver.clone()); + } + *PRO.lock().unwrap() = true; + } else if x == "ID_NOT_FOUND" { + info_uploaded.last_uploaded = None; // next heartbeat will upload sysinfo again + } else { + info_uploaded.last_uploaded = Some(Instant::now()); } } + _ => { + info_uploaded.last_uploaded = Some(Instant::now()); + } } } if conns.is_empty() && last_sent.map(|x| x.elapsed() < TIME_HEARTBEAT).unwrap_or(false) { @@ -132,6 +219,11 @@ async fn start_hbbs_sync_async() { 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 rsp.remove("sysinfo").is_some() { + info_uploaded.uploaded = false; + config::Status::set("sysinfo_hash", "".to_owned()); + log::info!("sysinfo required to forcely update"); + } if let Some(conns) = rsp.remove("disconnect") { if let Ok(conns) = serde_json::from_value::>(conns) { SENDER.lock().unwrap().send(conns).ok(); @@ -146,6 +238,7 @@ async fn start_hbbs_sync_async() { } if let Some(strategy) = rsp.remove("strategy") { if let Ok(strategy) = serde_json::from_value::(strategy) { + log::info!("strategy updated"); handle_config_options(strategy.config_options); } } diff --git a/src/ipc.rs b/src/ipc.rs index f1deb5ba8e5..1ae0481622f 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,4 +1,5 @@ use crate::{ + common::CheckTestNatType, privacy_mode::PrivacyModeState, ui_interface::{get_local_option, set_local_option}, }; @@ -22,12 +23,10 @@ pub use clipboard::ClipboardFile; use hbb_common::{ allow_err, bail, bytes, bytes_codec::BytesCodec, - config::{self, Config, Config2}, + config::{self, keys::OPTION_ALLOW_WEBSOCKET, Config, Config2}, futures::StreamExt as _, futures_util::sink::SinkExt, - log, password_security as password, - sodiumoxide::base64, - timeout, + log, password_security as password, timeout, tokio::{ self, io::{AsyncRead, AsyncWrite}, @@ -190,6 +189,8 @@ pub enum Data { Login { id: i32, is_file_transfer: bool, + is_view_camera: bool, + is_terminal: bool, peer_id: String, name: String, authorized: bool, @@ -230,7 +231,7 @@ pub enum Data { FS(FS), Test, SyncConfig(Option>), - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(target_os = "windows")] ClipboardFile(ClipboardFile), ClipboardFileEnabled(bool), #[cfg(target_os = "windows")] @@ -273,6 +274,19 @@ pub enum Data { HwCodecConfig(Option), RemoveTrustedDevices(Vec), ClearTrustedDevices, + #[cfg(all(target_os = "windows", feature = "flutter"))] + PrinterData(Vec), + InstallOption(Option<(String, String)>), + #[cfg(all( + feature = "flutter", + not(any(target_os = "android", target_os = "ios")) + ))] + ControllingSessionCount(usize), + #[cfg(target_os = "linux")] + TerminalSessionCount(usize), + #[cfg(target_os = "windows")] + PortForwardSessionCount(Option), + SocksWs(Option, String)>>), } #[tokio::main(flavor = "current_thread")] @@ -339,29 +353,40 @@ pub async fn new_listener(postfix: &str) -> ResultType { } } -pub struct CheckIfRestart(String, Vec, String, String); +pub struct CheckIfRestart { + stop_service: String, + rendezvous_servers: Vec, + audio_input: String, + voice_call_input: String, + ws: String, + api_server: String, +} impl CheckIfRestart { pub fn new() -> CheckIfRestart { - CheckIfRestart( - Config::get_option("stop-service"), - Config::get_rendezvous_servers(), - Config::get_option("audio-input"), - Config::get_option("voice-call-input"), - ) + CheckIfRestart { + stop_service: Config::get_option("stop-service"), + rendezvous_servers: Config::get_rendezvous_servers(), + audio_input: Config::get_option("audio-input"), + voice_call_input: Config::get_option("voice-call-input"), + ws: Config::get_option(OPTION_ALLOW_WEBSOCKET), + api_server: Config::get_option("api-server"), + } } } impl Drop for CheckIfRestart { fn drop(&mut self) { - if self.0 != Config::get_option("stop-service") - || self.1 != Config::get_rendezvous_servers() + if self.stop_service != Config::get_option("stop-service") + || self.rendezvous_servers != Config::get_rendezvous_servers() + || self.ws != Config::get_option(OPTION_ALLOW_WEBSOCKET) + || self.api_server != Config::get_option("api-server") { RendezvousMediator::restart(); } - if self.2 != Config::get_option("audio-input") { + if self.audio_input != Config::get_option("audio-input") { crate::audio_service::restart(); } - if self.3 != Config::get_option("voice-call-input") { + if self.voice_call_input != Config::get_option("voice-call-input") { crate::audio_service::set_voice_call_input_device( Some(Config::get_option("voice-call-input")), true, @@ -446,23 +471,36 @@ async fn handle(data: Data, stream: &mut Connection) { allow_err!(stream.send(&Data::Socks(Config::get_socks())).await); } Some(data) => { + let _nat = CheckTestNatType::new(); if data.proxy.is_empty() { Config::set_socks(None); } else { Config::set_socks(Some(data)); } - crate::common::test_nat_type(); RendezvousMediator::restart(); log::info!("socks updated"); } }, + Data::SocksWs(s) => match s { + None => { + allow_err!( + stream + .send(&Data::SocksWs(Some(Box::new(( + Config::get_socks(), + Config::get_option(OPTION_ALLOW_WEBSOCKET) + ))))) + .await + ); + } + _ => {} + }, #[cfg(feature = "flutter")] Data::VideoConnCount(None) => { let n = crate::server::AUTHED_CONNS .lock() .unwrap() .iter() - .filter(|x| x.1 == crate::server::AuthConnType::Remote) + .filter(|x| x.conn_type == crate::server::AuthConnType::Remote) .count(); allow_err!(stream.send(&Data::VideoConnCount(Some(n))).await); } @@ -492,7 +530,8 @@ async fn handle(data: Data, stream: &mut Connection) { None }; } else if name == "hide_cm" { - value = if crate::hbbs_http::sync::is_pro() { + value = if crate::hbbs_http::sync::is_pro() || crate::common::is_custom_client() + { Some(hbb_common::password_security::hide_cm().to_string()) } else { None @@ -535,6 +574,7 @@ async fn handle(data: Data, stream: &mut Connection) { } Some(value) => { let _chk = CheckIfRestart::new(); + let _nat = CheckTestNatType::new(); if let Some(v) = value.get("privacy-mode-impl-key") { crate::privacy_mode::switch(v); } @@ -597,6 +637,18 @@ async fn handle(data: Data, stream: &mut Connection) { .await ); } + #[cfg(all( + feature = "flutter", + not(any(target_os = "android", target_os = "ios")) + ))] + Data::ControllingSessionCount(count) => { + crate::updater::update_controlling_session_count(count); + } + #[cfg(target_os = "linux")] + Data::TerminalSessionCount(_) => { + let count = crate::terminal_service::get_terminal_session_count(true); + allow_err!(stream.send(&Data::TerminalSessionCount(count)).await); + } #[cfg(feature = "hwcodec")] #[cfg(not(any(target_os = "android", target_os = "ios")))] Data::CheckHwcodec => { @@ -661,6 +713,42 @@ async fn handle(data: Data, stream: &mut Connection) { Data::ClearTrustedDevices => { Config::clear_trusted_devices(); } + Data::InstallOption(opt) => match opt { + Some((_k, _v)) => { + #[cfg(target_os = "windows")] + if let Err(e) = crate::platform::windows::update_install_option(&_k, &_v) { + log::error!( + "Failed to update install option \"{}\" to \"{}\", error: {}", + &_k, + &_v, + e + ); + } + } + None => { + // `None` is usually used to get values. + // This branch is left blank for unification and further use. + } + }, + #[cfg(target_os = "windows")] + Data::PortForwardSessionCount(c) => match c { + None => { + let count = crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.conn_type == crate::server::AuthConnType::PortForward) + .count(); + allow_err!( + stream + .send(&Data::PortForwardSessionCount(Some(count))) + .await + ); + } + _ => { + // Port forward session count is only a get value. + } + }, _ => {} } } @@ -1061,6 +1149,7 @@ pub fn set_option(key: &str, value: &str) { #[tokio::main(flavor = "current_thread")] pub async fn set_options(value: HashMap) -> ResultType<()> { + let _nat = CheckTestNatType::new(); if let Ok(mut c) = connect(1000, "").await { c.send(&Data::Options(Some(value.clone()))).await?; // do not put below before connect, because we need to check should_exit @@ -1118,6 +1207,7 @@ pub async fn get_socks() -> Option { #[tokio::main(flavor = "current_thread")] pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> { + let _nat = CheckTestNatType::new(); Config::set_socks(if value.proxy.is_empty() { None } else { @@ -1130,6 +1220,29 @@ pub async fn set_socks(value: config::Socks5Server) -> ResultType<()> { Ok(()) } +async fn get_socks_ws_(ms_timeout: u64) -> ResultType<(Option, String)> { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::SocksWs(None)).await?; + if let Some(Data::SocksWs(Some(value))) = c.next_timeout(ms_timeout).await? { + Config::set_socks(value.0.clone()); + Config::set_option(OPTION_ALLOW_WEBSOCKET.to_string(), value.1.clone()); + Ok(*value) + } else { + Ok(( + Config::get_socks(), + Config::get_option(OPTION_ALLOW_WEBSOCKET), + )) + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn get_socks_ws() -> (Option, String) { + get_socks_ws_(1_000).await.unwrap_or(( + Config::get_socks(), + Config::get_option(OPTION_ALLOW_WEBSOCKET), + )) +} + pub fn get_proxy_status() -> bool { Config::get_socks().is_some() } @@ -1170,6 +1283,16 @@ pub async fn notify_server_to_check_hwcodec() -> ResultType<()> { Ok(()) } +#[cfg(target_os = "windows")] +pub async fn get_port_forward_session_count(ms_timeout: u64) -> ResultType { + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::PortForwardSessionCount(None)).await?; + if let Some(Data::PortForwardSessionCount(Some(count))) = c.next_timeout(ms_timeout).await? { + return Ok(count); + } + bail!("Failed to get port forward session count"); +} + #[cfg(feature = "hwcodec")] #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main(flavor = "current_thread")] @@ -1261,6 +1384,29 @@ pub async fn clear_wayland_screencast_restore_token(key: String) -> ResultType ResultType<()> { + let mut c = connect(1000, "").await?; + c.send(&Data::ControllingSessionCount(count)).await?; + Ok(()) +} + +#[cfg(target_os = "linux")] +#[tokio::main(flavor = "current_thread")] +pub async fn get_terminal_session_count() -> ResultType { + let ms_timeout = 1_000; + let mut c = connect(ms_timeout, "").await?; + c.send(&Data::TerminalSessionCount(0)).await?; + if let Some(Data::TerminalSessionCount(c)) = c.next_timeout(ms_timeout).await? { + return Ok(c); + } + Ok(0) +} + async fn handle_wayland_screencast_restore_token( key: String, value: String, @@ -1276,12 +1422,22 @@ async fn handle_wayland_screencast_restore_token( return Ok(None); } +#[tokio::main(flavor = "current_thread")] +pub async fn set_install_option(k: String, v: String) -> ResultType<()> { + if let Ok(mut c) = connect(1000, "").await { + c.send(&&Data::InstallOption(Some((k, v)))).await?; + // do not put below before connect, because we need to check should_exit + c.next_timeout(1000).await.ok(); + } + Ok(()) +} + #[cfg(test)] mod test { use super::*; #[test] fn verify_ffi_enum_data_size() { println!("{}", std::mem::size_of::()); - assert!(std::mem::size_of::() < 96); + assert!(std::mem::size_of::() <= 96); } } diff --git a/src/kcp_stream.rs b/src/kcp_stream.rs new file mode 100644 index 00000000000..74bd84130ff --- /dev/null +++ b/src/kcp_stream.rs @@ -0,0 +1,151 @@ +use hbb_common::{ + anyhow, + bytes::{Bytes, BytesMut}, + bytes_codec::BytesCodec, + config, log, + tcp::{DynTcpStream, FramedStream}, + tokio::{self, net::UdpSocket, sync::mpsc, sync::oneshot}, + tokio_util, ResultType, Stream, +}; +use kcp_sys::{ + endpoint::KcpEndpoint, + packet_def::{KcpPacket, KcpPacketHeader}, + stream, +}; +use std::{net::SocketAddr, sync::Arc}; + +pub struct KcpStream { + _endpoint: KcpEndpoint, + stop_sender: Option>, +} + +impl KcpStream { + fn create_framed(stream: stream::KcpStream, local_addr: Option) -> Stream { + Stream::Tcp(FramedStream( + tokio_util::codec::Framed::new(DynTcpStream(Box::new(stream)), BytesCodec::new()), + local_addr.unwrap_or(config::Config::get_any_listen_addr(true)), + None, + 0, + )) + } + + pub async fn accept( + udp_socket: Arc, + timeout: std::time::Duration, + init_packet: Option, + ) -> ResultType<(Self, Stream)> { + let mut endpoint = KcpEndpoint::new(); + endpoint.run().await; + + let (input, output) = ( + endpoint.input_sender(), + endpoint + .output_receiver() + .ok_or_else(|| anyhow::anyhow!("Failed to get output receiver"))?, + ); + let (stop_sender, stop_receiver) = oneshot::channel(); + if let Some(packet) = init_packet { + if packet.len() >= std::mem::size_of::() { + input.send(packet.into()).await?; + } + } + Self::kcp_io(udp_socket.clone(), input, output, stop_receiver).await; + + let conn_id = tokio::time::timeout(timeout, endpoint.accept()).await??; + if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) { + Ok(( + Self { + _endpoint: endpoint, + stop_sender: Some(stop_sender), + }, + Self::create_framed(stream, udp_socket.local_addr().ok()), + )) + } else { + Err(anyhow::anyhow!("Failed to create KcpStream")) + } + } + + pub async fn connect( + udp_socket: Arc, + timeout: std::time::Duration, + ) -> ResultType<(Self, Stream)> { + let mut endpoint = KcpEndpoint::new(); + endpoint.run().await; + + let (input, output) = ( + endpoint.input_sender(), + endpoint + .output_receiver() + .ok_or_else(|| anyhow::anyhow!("Failed to get output receiver"))?, + ); + let (stop_sender, stop_receiver) = oneshot::channel(); + Self::kcp_io(udp_socket.clone(), input, output, stop_receiver).await; + + let conn_id = endpoint.connect(timeout, 0, 0, Bytes::new()).await?; + if let Some(stream) = stream::KcpStream::new(&endpoint, conn_id) { + Ok(( + Self { + _endpoint: endpoint, + stop_sender: Some(stop_sender), + }, + Self::create_framed(stream, udp_socket.local_addr().ok()), + )) + } else { + Err(anyhow::anyhow!("Failed to create KcpStream")) + } + } + + async fn kcp_io( + udp_socket: Arc, + input: mpsc::Sender, + mut output: mpsc::Receiver, + mut stop_receiver: oneshot::Receiver<()>, + ) { + let udp = udp_socket.clone(); + tokio::spawn(async move { + let mut buf = vec![0; 1500]; + loop { + tokio::select! { + _ = &mut stop_receiver => { + log::debug!("KCP io loop received stop signal"); + break; + } + Some(data) = output.recv() => { + if let Err(e) = udp.send(&data.inner()).await { + log::debug!("KCP send error: {:?}", e); + break; + } + } + result = udp.recv_from(&mut buf) => { + match result { + Ok((size, _)) => { + if size < std::mem::size_of::() { + continue; + } + input + .send(BytesMut::from(&buf[..size]).into()) + .await.ok(); + } + Err(e) => { + log::debug!("KCP recv_from error: {:?}", e); + break; + } + } + } + else => { + log::debug!("KCP endpoint input closed"); + break; + } + } + } + }); + } +} + +impl Drop for KcpStream { + fn drop(&mut self) { + if let Some(sender) = self.stop_sender.take() { + let _ = sender.send(()); + } + } +} diff --git a/src/lan.rs b/src/lan.rs index 7d3f4f05fed..f2f370587f1 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -1,7 +1,7 @@ -use hbb_common::config::Config; use hbb_common::{ allow_err, anyhow::bail, + config::Config, config::{self, RENDEZVOUS_PORT}, log, protobuf::Message as _, @@ -10,7 +10,7 @@ use hbb_common::{ self, sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, }, - ResultType, + whoami, ResultType, }; use std::{ diff --git a/src/lang.rs b/src/lang.rs index 70682267803..a4a68905c8d 100644 --- a/src/lang.rs +++ b/src/lang.rs @@ -33,6 +33,7 @@ mod pl; mod ptbr; mod ro; mod ru; +mod sc; mod sk; mod sl; mod sq; @@ -42,7 +43,9 @@ mod th; mod tr; mod tw; mod uk; -mod vn; +mod vi; +mod ta; +mod ge; pub const LANGS: &[(&str, &str)] = &[ ("en", "English"), @@ -67,7 +70,7 @@ pub const LANGS: &[(&str, &str)] = &[ ("da", "Dansk"), ("eo", "Esperanto"), ("tr", "Türkçe"), - ("vn", "Tiếng Việt"), + ("vi", "Tiếng Việt"), ("pl", "Polski"), ("ja", "日本語"), ("ko", "한국어"), @@ -87,6 +90,9 @@ pub const LANGS: &[(&str, &str)] = &[ ("ar", "العربية"), ("he", "עברית"), ("hr", "Hrvatski"), + ("sc", "Sardu"), + ("ta", "தமிழà¯"), + ("ge", "ქáƒáƒ áƒ—ული"), ]; #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -139,7 +145,7 @@ pub fn translate_locale(name: String, locale: &str) -> String { "cs" => cs::T.deref(), "da" => da::T.deref(), "sk" => sk::T.deref(), - "vn" => vn::T.deref(), + "vi" => vi::T.deref(), "pl" => pl::T.deref(), "ja" => ja::T.deref(), "ko" => ko::T.deref(), @@ -161,6 +167,9 @@ pub fn translate_locale(name: String, locale: &str) -> String { "be" => be::T.deref(), "he" => he::T.deref(), "hr" => hr::T.deref(), + "sc" => sc::T.deref(), + "ta" => ta::T.deref(), + "ge" => ge::T.deref(), _ => en::T.deref(), }; let (name, placeholder_value) = extract_placeholder(&name); diff --git a/src/lang/ar.rs b/src/lang/ar.rs index 08eae5dbd5b..a1a84de5971 100644 --- a/src/lang/ar.rs +++ b/src/lang/ar.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "الطول من %min% الى %max%"), ("starts with a letter", "يبدأ بحرÙ"), ("allowed characters", "الحرو٠المسموح بها"), - ("id_change_tip", "Ùقط a-z, A-Z, 0-9 Ùˆ _ مسموح بها. اول حر٠يجب ان يكون a-z او A-Z. الطول بين 6 Ùˆ 16."), + ("id_change_tip", "Ùقط a-z, A-Z, 0-9, - (dash) Ùˆ _ مسموح بها. اول حر٠يجب ان يكون a-z او A-Z. الطول بين 6 Ùˆ 16."), ("Website", "الموقع"), ("About", "عن"), ("Slogan_tip", "صنع بحب ÙÙŠ هذا العالم الÙوضوي!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "كلمة مرور نظام التشغيل"), ("install_tip", "بسبب صلاحيات تحكم حساب المستخدم. RustDesk قد لا يعمل بشكل صحيح ÙÙŠ جهة البعيد ÙÙŠ بعض الحالات. Ù„ØªÙØ§Ø¯ÙŠ Ø°Ù„Ùƒ. الرجاء الضغط على الزر ادناه لتثبيت RustDesk ÙÙŠ جهازك."), ("Click to upgrade", "اضغط للارتقاء"), - ("Click to download", "اضغط للتنزيل"), - ("Click to update", "ضغط للتحديث"), ("Configure", "تهيئة"), ("config_acc", "لتتمكن من التحكم بسطح مكتبك البعيد, تحتاج الى منح RustDesk اذونات \"امكانية الوصول\"."), ("config_screen", "لتتمكن من الوصول الى سطح مكتبك البعيد, تحتاج الى منح RustDesk اذونات \"تسجيل الشاشة\"."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "لا يوجد اذن نقل الملÙ"), ("Note", "ملاحظة"), ("Connection", "الاتصال"), - ("Share Screen", "مشاركة الشاشة"), + ("Share screen", "مشاركة الشاشة"), ("Chat", "محادثة"), ("Total", "الاجمالي"), ("items", "عناصر"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "لقط الشاشة"), ("Input Control", "تحكم الادخال"), ("Audio Capture", "لقط الصوت"), - ("File Connection", "اتصال الملÙ"), - ("Screen Connection", "اتصال الشاشة"), ("Do you accept?", "هل تقبل؟"), ("Open System Setting", "ÙØªØ­ اعدادات النظام"), ("How to get Android input permission?", "كي٠تحصل على اذن الادخال ÙÙŠ اندرويد؟"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "لا يوجد اقران Ù…ÙØ¶Ù„ين حتى الان؟\nحسنا لنبحث عن شخص للاتصال معه ومن ثم Ø§Ø¶Ø§ÙØªÙ‡ Ù„Ù„Ù…ÙØ¶Ù„Ø©."), ("empty_lan_tip", "اه لا, يبدو انك لم تكتش٠اي قرين بعد."), ("empty_address_book_tip", "يا عزيزي, يبدو انه لايوجد حاليا اي اقران ÙÙŠ كتاب العناوين."), - ("eg: admin", "مثلا: admin"), ("Empty Username", "اسم مستخدم ÙØ§Ø±Øº"), ("Empty Password", "كلمة مرور ÙØ§Ø±ØºØ©"), ("Me", "انا"), @@ -528,133 +523,191 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("pull_ab_failed_tip", "ÙØ´Ù„ تحديث كتاب العناوين"), ("push_ab_failed_tip", "ÙØ´Ù„ مزامنة كتاب العناوين مع الخادم"), ("synced_peer_readded_tip", "الاجهزة الموجودة ÙÙŠ الجلسات الحديثة سيتم مزامنتها مع كتاب العناوين"), - ("Change Color", ""), - ("Primary Color", ""), - ("HSV Color", ""), - ("Installation Successful!", ""), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", ""), - ("scam_text1", ""), - ("scam_text2", ""), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", ""), - ("Connection failed due to inactivity", ""), - ("Check for software update on startup", ""), - ("upgrade_rustdesk_server_pro_to_{}_tip", ""), - ("pull_group_failed_tip", ""), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), - ("display_is_plugged_out_msg", ""), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), - ("selinux_tip", ""), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), - ("id_input_tip", ""), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", ""), - ("input_source_1_tip", ""), - ("input_source_2_tip", ""), - ("Swap control-command key", ""), - ("swap-left-right-mouse", ""), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Change Color", "تغيير اللون"), + ("Primary Color", "اللون الأساسي"), + ("HSV Color", "اللون بنظام HSV"), + ("Installation Successful!", "تم التثبيت بنجاح!"), + ("Installation failed!", "ÙØ´Ù„ التثبيت!"), + ("Reverse mouse wheel", "عكس عجلة الماوس"), + ("{} sessions", "{} جلسات"), + ("scam_title", "عنوان الاحتيال"), + ("scam_text1", "تحذير! هذا قد يكون هجوم احتيالي."), + ("scam_text2", "يرجى توخي الحذر وعدم المواÙقة على الاتصال إذا كنت غير متأكد."), + ("Don't show again", "لا تظهر مرة أخرى"), + ("I Agree", "أواÙÙ‚"), + ("Decline", "Ø±ÙØ¶"), + ("Timeout in minutes", "مهلة بالدقائق"), + ("auto_disconnect_option_tip", "سيتم قطع الاتصال تلقائيًا إذا تم تجاوز المهلة."), + ("Connection failed due to inactivity", "ÙØ´Ù„ الاتصال بسبب عدم النشاط"), + ("Check for software update on startup", "البحث عن تحديثات البرنامج عند بدء التشغيل"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "ترقية خادم RustDesk Pro إلى {}"), + ("pull_group_failed_tip", "ÙØ´Ù„ سحب المجموعة"), + ("Filter by intersection", "تصÙية حسب التقاطع"), + ("Remove wallpaper during incoming sessions", "إزالة الخلÙية أثناء الجلسات الواردة"), + ("Test", "اختبار"), + ("display_is_plugged_out_msg", "تم ÙØµÙ„ الشاشة"), + ("No displays", "لا توجد شاشات"), + ("Open in new window", "ÙØªØ­ ÙÙŠ Ù†Ø§ÙØ°Ø© جديدة"), + ("Show displays as individual windows", "عرض الشاشات ÙƒÙ†Ø§ÙØ°Ø§Øª Ù…Ù†ÙØµÙ„Ø©"), + ("Use all my displays for the remote session", "استخدام جميع شاشاتي للجلسة عن Ø¨ÙØ¹Ø¯"), + ("selinux_tip", "يجب تكوين SELinux بشكل صحيح لضمان التشغيل السلس."), + ("Change view", "تغيير العرض"), + ("Big tiles", "بلاط كبير"), + ("Small tiles", "بلاط صغير"), + ("List", "قائمة"), + ("Virtual display", "الشاشة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ©"), + ("Plug out all", "ÙØµÙ„ الكل"), + ("True color (4:4:4)", "اللون الحقيقي (4:4:4)"), + ("Enable blocking user input", "تمكين حظر إدخال المستخدم"), + ("id_input_tip", "يرجى إدخال المعر٠بشكل صحيح"), + ("privacy_mode_impl_mag_tip", "وضع الخصوصية Ù…ÙØ¹Ù„. سيتم تعطيل بعض الميزات."), + ("privacy_mode_impl_virtual_display_tip", "وضع الخصوصية Ù…ÙØ¹Ù„. يتم استخدام شاشة Ø§ÙØªØ±Ø§Ø¶ÙŠØ©."), + ("Enter privacy mode", "دخول وضع الخصوصية"), + ("Exit privacy mode", "الخروج من وضع الخصوصية"), + ("idd_not_support_under_win10_2004_tip", "لا يدعم IDD ÙÙŠ Windows 10 الإصدار 2004 أو أقدم."), + ("input_source_1_tip", "المصدر الأول للإدخال"), + ("input_source_2_tip", "المصدر الثاني للإدخال"), + ("Swap control-command key", "تبديل Ù…ÙØªØ§Ø­ التحكم-الأمر"), + ("swap-left-right-mouse", "تبديل زر الماوس الأيسر مع الأيمن"), + ("2FA code", "رمز التحقق الثنائي"), + ("More", "المزيد"), + ("enable-2fa-title", "تمكين التحقق الثنائي"), + ("enable-2fa-desc", "زيادة الأمان عن طريق التحقق الثنائي."), + ("wrong-2fa-code", "رمز التحقق الثنائي غير صحيح"), + ("enter-2fa-title", "إدخال رمز التحقق الثنائي"), + ("Email verification code must be 6 characters.", "يجب أن يتكون رمز التحقق بالبريد الإلكتروني من 6 أحرÙ."), + ("2FA code must be 6 digits.", "يجب أن يتكون رمز التحقق الثنائي من 6 أرقام."), + ("Multiple Windows sessions found", "تم العثور على جلسات متعددة Ù„Ù„Ù†ÙˆØ§ÙØ°"), + ("Please select the session you want to connect to", "يرجى اختيار الجلسة التي ترغب ÙÙŠ الاتصال بها"), + ("powered_by_me", "مدعوم بواسطة"), + ("outgoing_only_desk_tip", "اتصال الصادر Ùقط"), + ("preset_password_warning", "تحذير: كلمة المرور المحÙوظة قد تكون Ø¶Ø¹ÙŠÙØ©."), + ("Security Alert", "تنبيه أمني"), + ("My address book", "دليل العناوين الخاص بي"), + ("Personal", "شخصي"), + ("Owner", "المالك"), + ("Set shared password", "تعيين كلمة مرور مشتركة"), + ("Exist in", "موجود ÙÙŠ"), + ("Read-only", "للقراءة Ùقط"), + ("Read/Write", "قراءة/كتابة"), + ("Full Control", "تحكم كامل"), + ("share_warning_tip", "تحذير: قد يتمكن الآخرون من الوصول إلى معلوماتك."), + ("Everyone", "الجميع"), + ("ab_web_console_tip", "وحدة التحكم عبر الويب متاحة."), + ("allow-only-conn-window-open-tip", "السماح Ø¨ÙØªØ­ Ø§Ù„Ù†Ø§ÙØ°Ø© Ùقط للاتصال."), + ("no_need_privacy_mode_no_physical_displays_tip", "لا حاجة لوضع الخصوصية إذا لم تكن هناك شاشات ÙØ¹Ù„ية."), + ("Follow remote cursor", "مواكبة المؤشر عن Ø¨ÙØ¹Ø¯"), + ("Follow remote window focus", "مواكبة تركيز Ø§Ù„Ù†Ø§ÙØ°Ø© عن Ø¨ÙØ¹Ø¯"), + ("default_proxy_tip", "تعيين الخادم الوكيل Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠ"), + ("no_audio_input_device_tip", "لا يوجد جهاز إدخال صوتي"), + ("Incoming", "وارد"), + ("Outgoing", "صادر"), + ("Clear Wayland screen selection", "مسح تحديد الشاشة Wayland"), + ("clear_Wayland_screen_selection_tip", "مسح اختيار الشاشة Wayland الحالي."), + ("confirm_clear_Wayland_screen_selection_tip", "هل أنت متأكد من مسح تحديد الشاشة WaylandØŸ"), + ("android_new_voice_call_tip", "مكالمة صوتية جديدة على الأندرويد"), + ("texture_render_tip", "تمكين عرض الرسوميات باستخدام الخامات"), + ("Use texture rendering", "استخدام عرض الخامات"), + ("Floating window", "Ù†Ø§ÙØ°Ø© عائمة"), + ("floating_window_tip", "تمكين Ø§Ù„Ù†ÙˆØ§ÙØ° العائمة"), + ("Keep screen on", "ابق الشاشة مشغولة"), + ("Never", "أبدًا"), + ("During controlled", "أثناء التحكم"), + ("During service is on", "أثناء تشغيل الخدمة"), + ("Capture screen using DirectX", "التقاط الشاشة باستخدام DirectX"), + ("Back", "رجوع"), + ("Apps", "التطبيقات"), + ("Volume up", "زيادة الصوت"), + ("Volume down", "Ø®ÙØ¶ الصوت"), + ("Power", "الطاقة"), + ("Telegram bot", "بوت تيليجرام"), + ("enable-bot-tip", "تمكين البوت Ù„Ù„ØªÙØ§Ø¹Ù„ مع RustDesk"), + ("enable-bot-desc", "يمكنك استخدام بوت تيليجرام للتحكم ÙÙŠ الجلسات."), + ("cancel-2fa-confirm-tip", "إلغاء تأكيد التحقق الثنائي."), + ("cancel-bot-confirm-tip", "إلغاء تأكيد بوت تيليجرام."), + ("About RustDesk", "حول RustDesk"), + ("Send clipboard keystrokes", "إرسال ضغطات Ø§Ù„Ù…ÙØ§ØªÙŠØ­ من Ø§Ù„Ø­Ø§ÙØ¸Ø©"), + ("network_error_tip", "خطأ ÙÙŠ الشبكة، يرجى المحاولة لاحقًا."), + ("Unlock with PIN", "ÙØªØ­ باستخدام الرقم السري"), + ("Requires at least {} characters", "يتطلب على الأقل {} حرÙًا"), + ("Wrong PIN", "الرقم السري خاطئ"), + ("Set PIN", "تعيين الرقم السري"), + ("Enable trusted devices", "تمكين الأجهزة الموثوقة"), + ("Manage trusted devices", "إدارة الأجهزة الموثوقة"), + ("Platform", "المنصة"), + ("Days remaining", "الأيام المتبقية"), + ("enable-trusted-devices-tip", "تمكين الأجهزة الموثوقة لتسهيل الوصول."), + ("Parent directory", "الدليل الأب"), + ("Resume", "استئناÙ"), + ("Invalid file name", "اسم مل٠غير صالح"), + ("one-way-file-transfer-tip", "نقل Ø§Ù„Ù…Ù„ÙØ§Øª ÙÙŠ اتجاه واحد Ùقط."), + ("Authentication Required", "التوثيق مطلوب"), + ("Authenticate", "توثيق"), + ("web_id_input_tip", "يرجى إدخال المعر٠بشكل صحيح"), + ("Download", "تحميل"), + ("Upload folder", "Ø±ÙØ¹ المجلد"), + ("Upload files", "Ø±ÙØ¹ Ø§Ù„Ù…Ù„ÙØ§Øª"), + ("Clipboard is synchronized", "تمت مزامنة Ø§Ù„Ø­Ø§ÙØ¸Ø©"), + ("Update client clipboard", "تحديث Ø­Ø§ÙØ¸Ø© العميل"), + ("Untagged", "غير موسوم"), + ("new-version-of-{}-tip", "تحديث جديد متاح لـ {}"), + ("Accessible devices", "الأجهزة القابلة للوصول"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "ترقية عميل RustDesk البعيد إلى {}"), + ("d3d_render_tip", "تمكين العرض باستخدام D3D"), + ("Use D3D rendering", "استخدام عرض D3D"), + ("Printer", "الطابعة"), + ("printer-os-requirement-tip", "يتطلب تثبيت الطابعة على النظام."), + ("printer-requires-installed-{}-client-tip", "الطابعة تتطلب عميل {} المثبت."), + ("printer-{}-not-installed-tip", "الطابعة {} غير مثبتة"), + ("printer-{}-ready-tip", "الطابعة {} جاهزة"), + ("Install {} Printer", "تثبيت طابعة {}"), + ("Outgoing Print Jobs", "وظائ٠الطباعة الصادرة"), + ("Incoming Print Jobs", "وظائ٠الطباعة الواردة"), + ("Incoming Print Job", "ÙˆØ¸ÙŠÙØ© طباعة واردة"), + ("use-the-default-printer-tip", "استخدم الطابعة Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ©"), + ("use-the-selected-printer-tip", "استخدم الطابعة المحددة"), + ("auto-print-tip", "تمكين الطباعة التلقائية"), + ("print-incoming-job-confirm-tip", "هل أنت متأكد من طباعة هذه Ø§Ù„ÙˆØ¸ÙŠÙØ©ØŸ"), + ("remote-printing-disallowed-tile-tip", "الطباعة عن Ø¨ÙØ¹Ø¯ غير مسموح بها"), + ("remote-printing-disallowed-text-tip", "الطباعة عن Ø¨ÙØ¹Ø¯ غير مسموح بها على هذا الجهاز"), + ("save-settings-tip", "Ø­ÙØ¸ الإعدادات"), + ("dont-show-again-tip", "لا تظهر هذا مرة أخرى"), + ("Take screenshot", "التقاط لقطة شاشة"), + ("Taking screenshot", "جار٠التقاط لقطة الشاشة"), + ("screenshot-merged-screen-not-supported-tip", "لقطة الشاشة للشاشات المدمجة غير مدعومة"), + ("screenshot-action-tip", "إجراء لقطة الشاشة"), + ("Save as", "Ø­ÙØ¸ باسم"), + ("Copy to clipboard", "نسخ إلى Ø§Ù„Ø­Ø§ÙØ¸Ø©"), + ("Enable remote printer", "تمكين الطابعة عن Ø¨ÙØ¹Ø¯"), + ("Downloading {}", "جار٠تنزيل {}"), + ("{} Update", "تحديث {}"), + ("{}-to-update-tip", "يرجى تحديث {}"), + ("download-new-version-failed-tip", "ÙØ´Ù„ ÙÙŠ تنزيل الإصدار الجديد"), + ("Auto update", "التحديث التلقائي"), + ("update-failed-check-msi-tip", "ÙØ´Ù„ التحقق من طريقة التثبيت. يرجى النقر على زر 'تنزيل' من ØµÙØ­Ø© الإصدارات للترقية يدويًا."), + ("websocket_tip", "يتم دعم الاتصالات عبر Relay Ùقط، WebSocket عند استخدام WebRelay."), + ("Use WebSocket", "استخدام WebSocket"), + ("Trackpad speed", "سرعة لوحة التتبع"), + ("Default trackpad speed", "سرعة لوحة التتبع Ø§Ù„Ø§ÙØªØ±Ø§Ø¶ÙŠØ©"), + ("Numeric one-time password", "كلمة مرور رقمية لمرة واحدة"), + ("Enable IPv6 P2P connection", "تمكين اتصال نظير إلى نظير عبر IPv6"), + ("Enable UDP hole punching", "تمكين تقنية Ø­ÙØ± الثغرات عبر UDP"), + ("View camera", "عرض الكاميرا"), + ("Enable camera", "تمكين الكاميرا"), + ("No cameras", "لا توجد كاميرات"), + ("view_camera_unsupported_tip", "عرض الكاميرا غير مدعوم ÙÙŠ هذا الجهاز"), + ("Terminal", "الطرÙية"), + ("Enable terminal", "تمكين الطرÙية"), + ("New tab", "تبويب جديد"), + ("Keep terminal sessions on disconnect", "Ø§Ù„Ø§Ø­ØªÙØ§Ø¸ بجلسات الطرÙية عند قطع الاتصال"), + ("Terminal (Run as administrator)", "الطرÙية (تشغيل كمسؤول)"), + ("terminal-admin-login-tip", "لتشغيل الطرÙية كمسؤول، يرجى إدخال اسم المستخدم وكلمة المرور للمسؤول."), + ("Failed to get user token.", "ÙØ´Ù„ ÙÙŠ الحصول على رمز المستخدم."), + ("Incorrect username or password.", "اسم المستخدم أو كلمة المرور غير صحيحة."), + ("The user is not an administrator.", "المستخدم ليس لديه صلاحيات المسؤول."), + ("Failed to check if the user is an administrator.", "ÙØ´Ù„ التحقق مما إذا كان المستخدم لديه صلاحيات المسؤول."), + ("Supported only in the installed version.", "مدعوم Ùقط ÙÙŠ النسخة Ø§Ù„Ù…ÙØ«Ø¨ØªØ©."), + ("elevation_username_tip", "يرجى إدخال اسم مستخدم بصلاحيات المسؤول للمتابعة."), + ("Preparing for installation ...", "جار٠التحضير للتثبيت...") ].iter().cloned().collect(); } diff --git a/src/lang/be.rs b/src/lang/be.rs index c2314377609..61a4ed6c31c 100644 --- a/src/lang/be.rs +++ b/src/lang/be.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "Ð´Ð°ÑžÐ¶Ñ‹Ð½Ñ %min%...%max%"), ("starts with a letter", "пачынаецца з літары"), ("allowed characters", "Ð´Ð°Ð·Ð²Ð¾Ð»ÐµÐ½Ñ‹Ñ Ñімвалы"), - ("id_change_tip", "ДапуÑкаюцца толькі Ñімвалы a-z, A-Z, 0-9 Ñ– _ (падкрÑÑліванне). Першай павінна быць літара a-z, A-Z. Ð”Ð°ÑžÐ¶Ñ‹Ð½Ñ Ð°Ð´ 6 да 16."), + ("id_change_tip", "ДапуÑкаюцца толькі Ñімвалы a-z, A-Z, 0-9, - (dash) Ñ– _ (падкрÑÑліванне). Першай павінна быць літара a-z, A-Z. Ð”Ð°ÑžÐ¶Ñ‹Ð½Ñ Ð°Ð´ 6 да 16."), ("Website", "Сайт"), ("About", "Пра праграму"), ("Slogan_tip", "Зроблена з душой у гÑтым вар'Ñцкім Ñвеце!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Пароль ўваходу Ñž аперацыйную ÑÑ–ÑÑ‚Ñму"), ("install_tip", "У некаторых выпадках RustDesk можа працаваць нÑправільна на аддаленым вузле з-за UAC. Каб пазбегнуць магчымых праблем з UAC, націÑніце кнопку ніжÑй Ð´Ð»Ñ ÑžÑтаноўкі RustDesk у ÑÑ–ÑÑ‚Ñме."), ("Click to upgrade", "Ðбнавіць"), - ("Click to download", "Спампаваць"), - ("Click to update", "Ðбнавіць"), ("Configure", "Ðаладзіць"), ("config_acc", "Каб аддаленна кіраваць Ñваім працоўным Ñталом, вам неабходна дазволіць RustDesk правы доÑтупу."), ("config_screen", "Ð”Ð»Ñ Ð°Ð´Ð´Ð°Ð»ÐµÐ½Ð°Ð³Ð° доÑтупу да працоўнага Ñталу вам неабходна дазволіць RustDesk правы здымку Ñкрана."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "ÐÑма дазволу на перадачу файлаў"), ("Note", "Ðататка"), ("Connection", "ПадключÑнне"), - ("Share Screen", "ДзÑліцца Ñкранам"), + ("Share screen", "ДзÑліцца Ñкранам"), ("Chat", "Чат"), ("Total", "УÑÑго"), ("items", "Ñлементы"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Захоп Ñкрана"), ("Input Control", "Кіраванне ўводам"), ("Audio Capture", "Захоп аўдыё"), - ("File Connection", "ПадлучÑнне перадачы файлаў"), - ("Screen Connection", "ПадлучÑнне праглÑду/ÐºÑ–Ñ€Ð°Ð²Ð°Ð½Ð½Ñ Ñкранам"), ("Do you accept?", "Ці вы згодны?"), ("Open System Setting", "Ðдкрыць налады ÑÑ–ÑÑ‚Ñмы"), ("How to get Android input permission?", "Як атрымаць дазвол на ўвод Android?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ð¯ÑˆÑ‡Ñ Ð½Ñма выбраных аддаленых вузлоў?\nДавайце знойдзем, каго можна дадаць у выбранае."), ("empty_lan_tip", "Ðе знойдзены Ð°Ð´Ð´Ð°Ð»ÐµÐ½Ñ‹Ñ Ð²ÑƒÐ·Ð»Ñ‹."), ("empty_address_book_tip", "У адраÑнай кнізе нÑма аддаленых вузлоў."), - ("eg: admin", "напрыклад: admin"), ("Empty Username", "ПуÑтае Ñ–Ð¼Ñ ÐºÐ°Ñ€Ñ‹Ñтальніка"), ("Empty Password", "ПуÑты пароль"), ("Me", "Я"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Калі лаÑка, абнавіце кліент RustDesk да верÑÑ–Ñ– {} або навейшай на аддаленым баку!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "ПраглÑд камеры"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/bg.rs b/src/lang/bg.rs index 469b1b4fb39..5b71674d3eb 100644 --- a/src/lang/bg.rs +++ b/src/lang/bg.rs @@ -1,7 +1,7 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Положение"), + ("Status", "СъÑтоÑне"), ("Your Desktop", "Вашата работна Ñреда"), ("desk_tip", "Вашата работна Ñреда не може да бъде доÑтъпена Ñ Ñ‚Ð¾Ð·Ð¸ потребителÑки код и парола."), ("Password", "Парола"), @@ -20,53 +20,53 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Address book", "ÐдреÑник"), ("Confirmation", "Потвърждение"), ("TCP tunneling", "TCP тунел"), - ("Remove", "Премахване"), - ("Refresh random password", "ОпреÑнÑване на произволна парола"), + ("Remove", "Премахни"), + ("Refresh random password", "Ðова Ñлучйна парола"), ("Set your own password", "Задайте ÑобÑтвена парола"), ("Enable keyboard/mouse", "ПозволÑване на клавиатура/мишка"), ("Enable clipboard", "ПозволÑване доÑтъп до клипборда"), - ("Enable file transfer", "ПозволÑване прехвърлÑне на файлове"), + ("Enable file transfer", "ПозволÑване на прехвърлÑне на файлове"), ("Enable TCP tunneling", "ПозволÑване на TCP тунели"), - ("IP Whitelisting", "ОпределÑне на позволени IP по ÑпиÑък"), + ("IP Whitelisting", "Позволени IP"), ("ID/Relay Server", "ID/Препредаващ Ñървър"), - ("Import server config", "ВнаÑÑне Ñървър наÑтройки за "), - ("Export Server Config", "ИзнаÑÑне наÑтройки на Ñървър"), - ("Import server configuration successfully", "УÑпешно внаÑÑне на Ñървърни наÑтройки"), - ("Export server configuration successfully", "УÑпешно изнаÑÑне на Ñървърни наÑтройки"), - ("Invalid server configuration", "ÐедопуÑтими Ñървърни наÑтройки"), + ("Import server config", "ВъзÑтановÑване на Ñървърните наÑтройки"), + ("Export Server Config", "СъхранÑване на Ñървърни наÑтройки"), + ("Import server configuration successfully", "УÑпешно възÑтановÑване на Ñървърни наÑтройки"), + ("Export server configuration successfully", "УÑпешно ÑъхранÑване на Ñървърни наÑтройки"), + ("Invalid server configuration", "Ðевалидни Ñървърни наÑтройки"), ("Clipboard is empty", "Клипбордът е празен"), - ("Stop service", "Спираане на уÑлуга"), - ("Change ID", "ПромÑна определител (ID)"), - ("Your new ID", "ВашиÑÑ‚ нов определител (ID)"), + ("Stop service", "Спиране на уÑлуга"), + ("Change ID", "ПромÑна идентификатор (ID)"), + ("Your new ID", "ВашиÑÑ‚ нов идентификатор (ID)"), ("length %min% to %max%", "дължина %min% до %max%"), ("starts with a letter", "започва Ñ Ð±ÑƒÐºÐ²Ð°"), ("allowed characters", "разрешени знаци"), - ("id_change_tip", "Само a-z, A-Z, 0-9 и _ (долна черта) Ñа Ñред позволени. Първа буква Ñледва да е a-z, A-Z. С дължина мержу 6 и 16."), + ("id_change_tip", "Само a-z, A-Z, 0-9, - (тире) и _ (долна черта) Ñа Ñред позволени. Първата буква Ñледва да е a-z, A-Z. С дължина мержу 6 и 16."), ("Website", "УебÑайт"), - ("About", "ОтноÑно"), + ("About", "За програмата"), ("Slogan_tip", "Ðаправено от Ñърце в този хаотичен ÑвÑÑ‚!"), ("Privacy Statement", "Ð”ÐµÐºÐ»Ð°Ñ€Ð°Ñ†Ð¸Ñ Ð·Ð° поверителноÑÑ‚"), ("Mute", "Без звук"), - ("Build Date", "Дата на изграждане"), + ("Build Date", "Дата на Ñъздаване"), ("Version", "ВерÑиÑ"), ("Home", "Ðачало"), ("Audio Input", "Ðудио вход"), ("Enhancements", "ПодобрениÑ"), ("Hardware Codec", "Хардуерен кодек"), - ("Adaptive bitrate", "ПриÑпоÑобÑваще Ñе ÑкороÑÑ‚ на предаване наданни"), + ("Adaptive bitrate", "Ðдаптивна ÑкороÑÑ‚ на предаване"), ("ID Server", "ID Ñървър"), ("Relay Server", "Препращащ Ñървър"), ("API Server", "API Ñървър"), ("invalid_http", "трÑбва да започва Ñ http:// или https://"), - ("Invalid IP", "ÐедопуÑтим IP"), - ("Invalid format", "ÐедопуÑтим формат"), + ("Invalid IP", "Ðевалиден IP"), + ("Invalid format", "Ðевалиден формат"), ("server_not_support", "Ð’Ñе още не Ñе поддържа от Ñървъра"), ("Not available", "Ðе е наличен"), ("Too frequent", "Твърде чеÑто"), - ("Cancel", "Отказ"), - ("Skip", "ПропуÑкане"), - ("Close", "ЗатварÑне"), - ("Retry", "Преповтори"), + ("Cancel", "Откажи"), + ("Skip", "ПропуÑни"), + ("Close", "Затвори"), + ("Retry", "Повтори"), ("OK", "Добре"), ("Password Required", "ИзиÑква Ñе парола"), ("Please enter your password", "ÐœÐ¾Ð»Ñ Ð²ÑŠÐ²ÐµÐ´ÐµÑ‚Ðµ парола"), @@ -77,10 +77,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Error", "Грешка"), ("Reset by the peer", "Ðулирано от партньора"), ("Connecting...", "Свързване..."), - ("Connection in progress. Please wait.", "Връзката Ñе извършва. ÐœÐ¾Ð»Ñ Ð˜Ð·Ñ‡Ð°ÐºÐ°Ð¹Ñ‚Ðµ."), + ("Connection in progress. Please wait.", "Свързването Ñе оÑъщеÑтвÑва. ÐœÐ¾Ð»Ñ Ð˜Ð·Ñ‡Ð°ÐºÐ°Ð¹Ñ‚Ðµ."), ("Please try 1 minute later", "МолÑ, опитайте 1 минута по-къÑно"), ("Login Error", "Грешка при впиÑване"), - ("Successful", "УÑпешен опит"), + ("Successful", "УÑпешно"), ("Connected, waiting for image...", "Свързано, чака Ñе изображение..."), ("Name", "Име"), ("Type", "Тип"), @@ -88,7 +88,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Size", "Размер"), ("Show Hidden Files", "Показване на Ñкрити файлове"), ("Receive", "Получаване"), - ("Send", "Пращане"), + ("Send", "Изпращане"), ("Refresh File", "ОпреÑнÑване на файла"), ("Local", "Локално"), ("Remote", "Отдалечено"), @@ -105,7 +105,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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", "Разреши така вÑички конфликти"), + ("Do this for all conflicts", "Същото за вÑички конфликти"), ("This is irreversible!", "Това е необратимо!"), ("Deleting", "Изтриване"), ("files", "файлове"), @@ -114,54 +114,52 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Speed", "СкороÑÑ‚"), ("Custom Image Quality", "КачеÑтво на изображението по Ñвой избор"), ("Privacy mode", "Режим на поверителноÑÑ‚"), - ("Block user input", "Забрана за потребителÑки вход"), - ("Unblock user input", "Разрешаване на потребителÑки въвеждане"), + ("Block user input", "Забрана за потребителÑко въвеждане"), + ("Unblock user input", "Разрешаване на потребителÑко въвеждане"), ("Adjust Window", "ÐаглаÑи прозореца"), ("Original", "Оригинално"), ("Shrink", "Свиване"), - ("Stretch", "Разтегнат"), + ("Stretch", "РазтÑгане"), ("Scrollbar", "Плъзгач"), - ("ScrollAuto", "Ðвтоматичено приплъзване"), + ("ScrollAuto", "Ðвтоматично Ñкролиране"), ("Good image quality", "Добро качеÑтво на изображението"), ("Balanced", "УравновеÑен"), - ("Optimize reaction time", "С оглед времето на реакциÑ"), - ("Custom", "По ÑобÑтвено желание"), + ("Optimize reaction time", "Оптимизирай времето за реакциÑ"), + ("Custom", "По избор"), ("Show remote cursor", "Показвай Ð¾Ñ‚Ð´Ð°Ð»ÐµÑ‡ÐµÐ½Ð¸Ñ ÐºÑƒÑ€Ñор"), ("Show quality monitor", "Показвай прозорец за качеÑтво"), - ("Disable clipboard", "Забрана за доÑтъп до клипборд"), + ("Disable clipboard", "Забрана на клипборда"), ("Lock after session end", "Заключване Ñлед край на ползване"), - ("Insert Ctrl + Alt + Del", "ПоÑтавÑне Ctrl + Alt + Del"), - ("Insert Lock", "ЗаÑвка за заключване"), + ("Insert Ctrl + Alt + Del", "Въведи Ctrl + Alt + Del"), + ("Insert Lock", "Въведи заключване"), ("Refresh", "ОбновÑване"), - ("ID does not exist", "ÐеÑъщеÑтвуващ определител (ID)"), + ("ID does not exist", "ÐеÑъщеÑтвуващ идентификатор (ID)"), ("Failed to connect to rendezvous server", "ÐеуÑпешно Ñвързване към Ñървъра за Ñреща (rendezvous)"), ("Please try later", "ÐœÐ¾Ð»Ñ Ð¾Ð¿Ð¸Ñ‚Ð°Ð¹Ñ‚Ðµ по-къÑно"), ("Remote desktop is offline", "Отдалечената работна Ñреда не е налична"), - ("Key mismatch", "Ключово неÑъответÑтвие"), - ("Timeout", "Изтичане на времето"), - ("Failed to connect to relay server", "Провал при Ñвързване към препредаващ Ñървър"), - ("Failed to connect via rendezvous server", "Провал при Ñвързване към Ñървър за Ñрещи (rendezvous)"), - ("Failed to connect via relay server", "Провал при Ñвързване чрез препредаващ Ñървър"), - ("Failed to make direct connection to remote desktop", "Провал при уÑтановÑване на прÑка връзка Ñ Ð¾Ñ‚Ð´Ð°Ð»ÐµÑ‡ÐµÐ½Ð° работна Ñреда"), + ("Key mismatch", "ÐеÑъответÑтвие на ключове"), + ("Timeout", "Таймаут"), + ("Failed to connect to relay server", "ÐеуÑпешно Ñвързване към препредаващ Ñървър"), + ("Failed to connect via rendezvous server", "ÐеуÑпешно Ñвързване към Ñървър за Ñрещи (rendezvous)"), + ("Failed to connect via relay server", "ÐеуÑпешно Ñвързване чрез препредаващ Ñървър"), + ("Failed to make direct connection to remote desktop", "ÐеуÑпешно уÑтановÑване на прÑка връзка Ñ Ð¾Ñ‚Ð´Ð°Ð»ÐµÑ‡ÐµÐ½Ð° работна Ñреда"), ("Set Password", "Задаване на парола"), ("OS Password", "Парола на Операционната ÑиÑтема"), ("install_tip", "Поради UAC, RustDesk в нÑкои Ñлучай не може да работи правилно за отдалечена доÑтъп. За да заобиколите UAC, молÑ, натиÑнете копчето по-долу, за да поÑтавите RustDesk като ÑиÑтемна уÑлуга."), ("Click to upgrade", "ÐатиÑнете, за да надÑтроите"), - ("Click to download", "ÐатиÑнете, за да изтеглите"), - ("Click to update", "ÐатиÑнете, за да обновите"), ("Configure", "ÐаÑтройване"), ("config_acc", "За да управлÑвате Ð²Ð°ÑˆÐ¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ð½Ð° Ñреда отдалечено, трÑбва да предоÑтавите на RustDesk права от раздел \"ДоÑтъпноÑÑ‚\"."), ("config_screen", "За да управлÑвате Ð²Ð°ÑˆÐ¸Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ð½Ð° Ñреда отдалечено, трÑбва да предоÑтавите на RustDesk права от раздел \"Ð—Ð°Ð¿Ð¸Ñ Ð½Ð° екрана\"."), - ("Installing ...", "ПоÑтавÑне..."), - ("Install", "ПоÑтави"), - ("Installation", "ПоÑтавÑне"), - ("Installation Path", "Път към мÑÑто за поÑтавÑне"), - ("Create start menu shortcuts", "Бърз доÑтъп от меню 'Старт'."), - ("Create desktop icon", "Създайте икона на Ñ€Ð°Ð±Ð¾Ñ‚Ð½Ð¸Ñ Ð¿Ð»Ð¾Ñ‚"), - ("agreement_tip", "Започвайки поÑтавÑнето, вие приемате лицензионното Ñпоразумение."), - ("Accept and Install", "Приемете и поÑтавÑте"), + ("Installing ...", "ИнÑталиране..."), + ("Install", "ИнÑталирай"), + ("Installation", "ИнÑталациÑ"), + ("Installation Path", "Път за инÑталациÑ"), + ("Create start menu shortcuts", "Създай връзка от меню 'Старт'."), + ("Create desktop icon", "Създай иконка на Ñ€Ð°Ð±Ð¾Ñ‚Ð½Ð¸Ñ Ð¿Ð»Ð¾Ñ‚"), + ("agreement_tip", "Започвайки инÑталациÑта, вие приемате лицензионното Ñпоразумение."), + ("Accept and Install", "Приемам и инÑталирам"), ("End-user license agreement", "Споразумение Ñ Ð¿Ð¾Ñ‚Ñ€ÐµÐ±Ð¸Ñ‚ÐµÐ»Ñ"), - ("Generating ...", "Пораждане..."), + ("Generating ...", "Създаване..."), ("Your installation is lower version.", "Вашата инÑÑ‚Ð°Ð»Ð°Ñ†Ð¸Ñ Ðµ по-ниÑка верÑиÑ."), ("not_close_tcp_tip", "Ðе затварÑйте този прозорец, докато използвате тунела"), ("Listening ...", "Слушане..."), @@ -177,9 +175,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("The confirmation is not identical.", "Потвърждението не Ñъвпада"), ("Permissions", "РазрешениÑ"), ("Accept", "Приеми"), - ("Dismiss", "ОтхвърлÑне"), - ("Disconnect", "ПрекъÑване"), - ("Enable file copy and paste", "Разрешаване прехвърлÑне на файлове"), + ("Dismiss", "Отхвърли"), + ("Disconnect", "ПрекъÑни"), + ("Enable file copy and paste", "Разрешаване копирането и поÑтавÑне на файлове"), ("Connected", "Свързан"), ("Direct and encrypted connection", "ПрÑка защитена връзка"), ("Relayed and encrypted connection", "Препредадена защитена връзка"), @@ -193,55 +191,55 @@ 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", "МолÑ, въведете име на папката"), ("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", "Очаква Ñе x11"), ("Port", "Порт"), ("Settings", "ÐаÑтройки"), ("Username", "ПотребителÑко име"), - ("Invalid port", "ÐедопуÑтим порт"), + ("Invalid port", "Ðевалиден порт"), ("Closed manually by the peer", "Затворено ръчно от другата Ñтрана"), ("Enable remote configuration modification", "Разрешаване на отдалечена промÑна на конфигурациÑта"), ("Run without install", "Стартирайте без инÑталиране"), ("Connect via relay", "Свързване чрез препращане"), ("Always connect via relay", "Винаги чрез препращане"), ("whitelist_tip", "Само IP адреÑите от Ð±ÐµÐ»Ð¸Ñ ÑпиÑък имат доÑтъп до мен"), - ("Login", "Влизане"), + ("Login", "ВпиÑване"), ("Verify", "Потвърди"), ("Remember me", "Запомни ме"), ("Trust this device", "ДоверÑване на това уÑтройÑтво"), ("Verification code", "Код за потвърждение"), - ("verification_tip", "Ðа поÑÐ¾Ñ‡ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÐ¹Ð» е изпратен код за потвърждение. ÐœÐ¾Ð»Ñ Ð²ÑŠÐ²ÐµÐ´ÐµÑ‚Ðµ го, за да продължите Ñ Ð²Ð»Ð¸Ð·Ð°Ð½ÐµÑ‚Ð¾."), + ("verification_tip", "Ðа поÑÐ¾Ñ‡ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÐ¹Ð» е изпратен код за потвърждение. ÐœÐ¾Ð»Ñ Ð²ÑŠÐ²ÐµÐ´ÐµÑ‚Ðµ го, за да продължите Ñ Ð²Ð¿Ð¸Ñването."), ("Logout", "ОтпиÑване (Изход)"), - ("Tags", "Белези"), + ("Tags", "Етикети"), ("Search ID", "ТърÑи ID"), ("whitelist_sep", "Разделени ÑÑŠÑ Ð·Ð°Ð¿ÐµÑ‚Ð°Ñ, точка и запетаÑ, празни Ñимволи или нов ред"), ("Add ID", "Добави ID"), ("Add Tag", "Добави етикет"), - ("Unselect all tags", "Премахнете избора на вÑички белези (tags)"), + ("Unselect all tags", "Премахнете избора на вÑички етикети (tags)"), ("Network error", "Мрежова грешка"), - ("Username missed", "ЛипÑващо потребителÑко име"), - ("Password missed", "ЛипÑваща парола"), + ("Username missed", "ЛипÑва потребителÑко име"), + ("Password missed", "ЛипÑва парола"), ("Wrong credentials", "Грешни пълномощиÑ"), - ("The verification code is incorrect or has expired", "Кодът за проверка е неправилен или Ñ Ð¸Ð·Ñ‚ÐµÐºÐ»Ð° давноÑÑ‚."), - ("Edit Tag", "Промени белег"), + ("The verification code is incorrect or has expired", "Кодът за потвърждение е неправилен или Ñ Ð¸Ð·Ñ‚ÐµÐºÐ»Ð° давноÑÑ‚."), + ("Edit Tag", "Редактирай етикет"), ("Forget Password", "Забравена парола"), ("Favorites", "Любими"), ("Add to Favorites", "Добави към любими"), ("Remove from Favorites", "Премахване от любими"), ("Empty", "Празно"), - ("Invalid folder name", "Ðепозволено име на папка"), - ("Socks5 Proxy", "Socks5 поÑредник"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) поÑредник"), + ("Invalid folder name", "Ðевалидно име на папка"), + ("Socks5 Proxy", "Socks5 ПрокÑи"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) прокÑи"), ("Discovered", "Открит"), - ("install_daemon_tip", "За зареждане при Ñтартиране на ОС Ñледва да поÑтавите RustDesk като ÑиÑтемна уÑлуга."), + ("install_daemon_tip", "За зареждане при Ñтартиране на ОС трÑбва да инÑталирате RustDesk като ÑиÑтемна уÑлуга."), ("Remote ID", "Отдалечено ID"), ("Paste", "ПоÑтави"), ("Paste here?", "ПоÑтави тук?"), @@ -267,28 +265,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "ÐÑма разрешение за прехвърлÑне на файлове"), ("Note", "Бележка"), ("Connection", "Връзка"), - ("Share Screen", "Сподели екран"), - ("Chat", "Говор"), + ("Share screen", "Сподели екран"), + ("Chat", "Чат"), ("Total", "Общо"), ("items", "неща"), ("Selected", "Избрано"), - ("Screen Capture", "Снемане на екрана"), - ("Input Control", "Управление на вход"), + ("Screen Capture", "ЗаÑнемане на екрана"), + ("Input Control", "Управление на въвеждане"), ("Audio Capture", "ÐудиозапиÑ"), - ("File Connection", "Файлова връзка"), - ("Screen Connection", "Екранна връзка"), ("Do you accept?", "Приемате ли?"), ("Open System Setting", "Отворете ÑиÑтемните наÑтройки"), - ("How to get Android input permission?", "Как да получим право за въвеждане под Ðндрид?"), + ("How to get Android input permission?", "Как да получим право за въвеждане при Ðндроид?"), ("android_input_permission_tip1", "За да може отдалечено уÑтройÑтво да управлÑва вашето Android уÑтройÑтво чрез мишка или допир, трÑбва да разрешите на RustDesk да използва уÑлугата \"ДоÑтъпноÑÑ‚\"."), - ("android_input_permission_tip2", "МолÑ, отидете на Ñледващата Ñтраница Ñ ÑиÑтемни наÑтройки, намерете и въведете [Installed Services], включете уÑлугата [RustDesk Input]."), + ("android_input_permission_tip2", "МолÑ, отидете на Ñледващата Ñтраница ÑÑŠÑ ÑиÑтемни наÑтройки, намерете и въведете [Installed Services], включете уÑлугата [RustDesk Input]."), ("android_new_connection_tip", "Получена е нова заÑвка за отдалечено управление на вашето текущо уÑтройÑтво."), - ("android_service_will_start_tip", "Включването на \"Снемане на екрана\" автоматично ще Ñтартира уÑлугата, позволÑвайки на други уÑтройÑтва да поиÑкат връзка Ñ Ð²Ð°ÑˆÐµÑ‚Ð¾ уÑтройÑтво."), + ("android_service_will_start_tip", "Включването на \"ЗаÑнемане на екрана\" автоматично ще Ñтартира уÑлугата, позволÑвайки на други уÑтройÑтва да поиÑкат връзка Ñ Ð²Ð°ÑˆÐµÑ‚Ð¾ уÑтройÑтво."), ("android_stop_service_tip", "ЗатварÑнето на уÑлугата автоматично ще затвори вÑички уÑтановени връзки."), ("android_version_audio_tip", "Текущата верÑÐ¸Ñ Ð½Ð° Android не поддържа аудиозапиÑ. МолÑ, актуализирайте уÑтройÑтвото Ñ Android 10 или по-нов."), ("android_start_service_tip", "ДокоÑнете [Start service] или позволете [Screen Capture], за да започне уÑлугата по ÑподелÑне на екрана."), ("android_permission_may_not_change_tip", "РазрешениÑта за уÑтановени връзки може да не Ñе променÑÑ‚ незабавно, а ще изиÑкват да Ñе Ñвържете отново."), - ("Account", "Сметка"), + ("Account", "Профил"), ("Overwrite", "ПрезапиÑване"), ("This file exists, skip or overwrite this file?", "Този файл ÑъщеÑтвува вече. ПропуÑкане или презапиÑване?"), ("Quit", "Изход"), @@ -298,12 +294,12 @@ 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", "Изкключен"), ("Language", "Език"), - ("Keep RustDesk background service", "Запази работеща фонова уÑлуга Ñ RustDesk"), + ("Keep RustDesk background service", "Запази RustDesk фоновата уÑлуга"), ("Ignore Battery Optimizations", "Игнорирай оптимизациите на батериÑта"), ("android_open_battery_optimizations_tip", "Ðко иÑкате да деактивирате тази функциÑ, молÑ, отидете на Ñледващата Ñтраница Ñ Ð½Ð°Ñтройки на приложението RustDesk, намерете и въведете [Battery], премахнете отметката от [Unrestricted]"), ("Start on boot", "Стартирайте при зареждане"), @@ -311,7 +307,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Connection not allowed", "Връзката непозволена"), ("Legacy mode", "По оÑтарÑл начин"), ("Map mode", "По начин ÑÑŠÑ ÑъответÑтвие (map)"), - ("Translate mode", "По нчаин Ñ Ð¿Ñ€ÐµÐ²Ð¾Ð´"), + ("Translate mode", "По начин Ñ Ð¿Ñ€ÐµÐ²Ð¾Ð´"), ("Use permanent password", "Използване на поÑтоÑнна парола"), ("Use both passwords", "Използване и на двете пароли"), ("Set permanent password", "Задаване поÑтоÑнна парола"), @@ -346,9 +342,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark", "Тъмна"), ("Light", "Светла"), ("Follow System", "Следвай ÑиÑтема"), - ("Enable hardware codec", "ПозволÑване хардуерен кодек"), + ("Enable hardware codec", "ПозволÑване на хардуерен кодек"), ("Unlock Security Settings", "Отключи наÑтройките за ÑигурноÑÑ‚"), - ("Enable audio", "Разрешете аудиото"), + ("Enable audio", "Позволи звук"), ("Unlock Network Settings", "Отключи мрежовите наÑтройки"), ("Server", "Сървър"), ("Direct IP Access", "ПрÑк IP доÑтъп"), @@ -364,11 +360,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "ЗапиÑване"), ("Directory", "ДиректориÑ"), ("Automatically record incoming sessions", "Ðвтоматичен Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° входÑщи ÑеÑии"), - ("Automatically record outgoing sessions", ""), - ("Change", "ПромÑна"), - ("Start session recording", "Започванена запиÑ"), - ("Stop session recording", "Край на запиÑ"), - ("Enable recording session", "ПозволÑване запиÑ"), + ("Automatically record outgoing sessions", "Ðвтоматичен Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° изходÑщи ÑеÑии"), + ("Change", "Промени"), + ("Start session recording", "Старт на Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° ÑеÑиÑта"), + ("Stop session recording", "Стоип на Ð·Ð°Ð¿Ð¸Ñ Ð½Ð° ÑеÑиÑта"), + ("Enable recording session", "ПозволÑване на запиÑване на ÑеÑиÑта"), ("Enable LAN discovery", "ПозволÑване откриване във вътрешна мрежа"), ("Deny LAN discovery", "Забрана за откриване във вътрешна мрежа"), ("Write a message", "Ðапишете Ñъобщение"), @@ -392,14 +388,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Elevate", "Повишаване"), ("Zoom cursor", "УголемÑване курÑор"), ("Accept sessions via password", "Приемане ÑеÑии чрез парола"), - ("Accept sessions via click", "Приемане ÑеÑии чрез цъкване"), + ("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 connection management window", "Скриване на прозореца за управление на връзка"), ("hide_cm_tip", "Разрешаване Ñкриване Ñамо ако Ñе приемат ÑеÑии чрез поÑтоÑнна парола"), ("wayland_experiment_tip", "Поддръжката на Wayland е в екÑпериментален Ñтадий, молÑ, използвайте X11, ако Ñе нуждаете от безконтролен доÑтъп.."), ("Right click to select tabs", "ДеÑен бутон за избор на раздел"), @@ -408,22 +404,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Group", "Група"), ("Search", "ТърÑене"), ("Closed manually by web console", "Затворен ръчно от уеб конзола"), - ("Local keyboard type", "Тип на тукашната клавиатура"), - ("Select local keyboard type", "Избор на тип на тукашната клавиатура"), + ("Local keyboard type", "Тип на локалната клавиатура"), + ("Select local keyboard type", "Избор на тип на локалната клавиатура"), ("software_render_tip", "Ðко използвате графична карта Nvidia под Linux и отдалечениÑÑ‚ прозорец Ñе Ð·Ð°Ñ‚Ð²Ð°Ñ€Ñ Ð²ÐµÐ´Ð½Ð°Ð³Ð° Ñлед Ñвързване, превключването към драйвера Nouveau Ñ Ð¾Ñ‚Ð²Ð¾Ñ€ÐµÐ½ код и изборът да използвате Ñофтуерно изобразÑване може да помогне. ИзиÑква Ñе реÑтартиране на Ñофтуера."), ("Always use software rendering", "Винаги ползвай Ñофтуерно изграждане на картината"), ("config_input", "За да управлÑвате отдалечена Ñреда Ñ ÐºÐ»Ð°Ð²Ð¸Ð°Ñ‚ÑƒÑ€Ð°, трÑбва да предоÑтавите на RustDesk право за \"Input Monitoring\"."), ("config_microphone", "За да говорите отдалечено, трÑбва да предоÑтавите на RustDesk право за \"Ð—Ð°Ð¿Ð¸Ñ Ð½Ð° звук\"."), ("request_elevation_tip", "Можете Ñъщо така да поиÑкате разширени права, ако има нÑкой от отдалечената Ñтрана."), ("Wait", "Изчакване"), - ("Elevation Error", "Грешка при добвиане на разширени права"), + ("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", "ПоиÑкайте разширени права"), + ("Transmit the username and password of administrator", "Предаване на потребителÑкото име и паролата на админиÑтратор"), + ("still_click_uac_tip", "Ð’Ñе още изиÑква отдалечениÑÑ‚ потребител да натиÑне върху OK в прозореца на UAC при Ñтартиран RustDesk."), + ("Request Elevation", "ПоиÑкайте повишени права"), ("wait_accept_uac_tip", "МолÑ, изчакайте отдалечениÑÑ‚ потребител да приеме Ð´Ð¸Ð°Ð»Ð¾Ð³Ð¾Ð²Ð¸Ñ Ð¿Ñ€Ð¾Ð·Ð¾Ñ€ÐµÑ† на UAC."), - ("Elevate successfully", "УÑпешно получаване на разширени права"), + ("Elevate successfully", "УÑпешно получаване на повишени права"), ("uppercase", "големи букви"), ("lowercase", "малки букви"), ("digit", "цифра"), @@ -433,7 +429,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Средна"), ("Strong", "Силна"), ("Switch Sides", "РазмÑна на Ñтраните"), - ("Please confirm if you want to share your desktop?", "МолÑ, потвърдете дали иÑкате да Ñподелите работното Ñи проÑтранÑтво"), + ("Please confirm if you want to share your desktop?", "МолÑ, потвърдете ако иÑкате да Ñподелите работното Ñи проÑтранÑтво"), ("Display", "Екран"), ("Default View Style", "Стил на изглед по подразбиране"), ("Default Scroll Style", "Стил на превъртане по подразбиране"), @@ -444,44 +440,43 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Auto", "Ðвтоматично"), ("Other Default Options", "Други опции по подразбиране"), ("Voice call", "ГлаÑови обажданиÑ"), - ("Text chat", "ТекÑтов разговор"), - ("Stop voice call", "ПрекратÑване глаÑово обаждане"), - ("relay_hint_tip", "Може да не е възможно да Ñе Ñвържете директно; можете да опитате да Ñе Ñвържете чрез реле. ОÑвен това, ако иÑкате да използвате реле при Ð¿ÑŠÑ€Ð²Ð¸Ñ Ñи опит, добавете наÑтавка \"/r\" към идентификатора или да изберете опциÑта \"Винаги Ñвързване чрез реле\" в картата на поÑледните ÑеÑии, ако ÑъщеÑтвува."), + ("Text chat", "ТекÑтов чат"), + ("Stop voice call", "ПрекратÑване на глаÑово обаждане"), + ("relay_hint_tip", "Може да не е възможно да Ñе Ñвържете директно; можете да опитате да Ñе Ñвържете чрез препращаш Ñървър. ОÑвен това, ако иÑкате да използвате препращаш Ñървър при Ð¿ÑŠÑ€Ð²Ð¸Ñ Ñи опит, добавете наÑтавка \"/r\" към идентификатора или да изберете опциÑта \"Винаги Ñвързване чрез препращаш Ñървър\" в картата на поÑледните ÑеÑии, ако ÑъщеÑтвува."), ("Reconnect", "Повторно Ñвързане"), ("Codec", "Кодек"), ("Resolution", "Разделителна ÑпоÑобноÑÑ‚"), ("No transfers in progress", "ÐÑма текущи прехвърлÑниÑ"), - ("Set one-time password length", "Задаване дължаина на еднократна парола"), + ("Set one-time password length", "Задаване дължина на еднократна парола"), ("RDP Settings", "RDP наÑтройки"), - ("Sort by", "Подредба по"), - ("New Connection", "Ðово Ñвързване"), - ("Restore", "ВъзÑтановÑване"), - ("Minimize", "СмалÑване"), - ("Maximize", "УголемÑване"), + ("Sort by", "Сортирай по"), + ("New Connection", "Ðова Връзка"), + ("Restore", "ВъзÑтанови"), + ("Minimize", "Минимизирай"), + ("Maximize", "Ðа цÑл екран"), ("Your Device", "Вашето уÑтройÑтво"), ("empty_recent_tip", "Ðми Ñега, нÑма Ñкорошни ÑеÑии!\nВреме е да планирате нова."), - ("empty_favorite_tip", "Ð’Ñе още нÑмате любими връÑтници?\nÐека намерим нÑкой, Ñ ÐºÐ¾Ð³Ð¾Ñ‚Ð¾ да Ñе Ñвържете, и да го добавим към вашите любими!"), - ("empty_lan_tip", "О, не, изглежда, че вÑе още не Ñме открили връÑтници."), - ("empty_address_book_tip", "Изглежда, че в момента нÑма изброени връÑтници във вашата адреÑна книга."), - ("eg: admin", "напр. admin"), + ("empty_favorite_tip", "Ð’Ñе още нÑмате любими връзки?\nÐека намерим нÑкой, Ñ ÐºÐ¾Ð³Ð¾Ñ‚Ð¾ да Ñе Ñвържете, и да го добавим към вашите любими!"), + ("empty_lan_tip", "О, не, изглежда, че вÑе още не Ñме открили връзки."), + ("empty_address_book_tip", "Изглежда, че в момента нÑма изброени връзки във вашата адреÑна книга."), ("Empty Username", "Празно потребителÑко име"), ("Empty Password", "Празна парола"), - ("Me", "Мен"), + ("Me", "Ðз"), ("identical_file_tip", "Файлът Ñъвпада Ñ Ñ‚Ð¾Ð·Ð¸ от другата Ñтрана."), ("show_monitors_tip", "Показване на мониторите в лентата Ñ Ð¸Ð½Ñтрументи"), - ("View Mode", "Режим на преглед"), + ("View Mode", "Режим на изглед"), ("login_linux_tip", "ТрÑбва да влезете в отдалечен Linux акаунт, за да активирате X ÑеÑÐ¸Ñ Ð½Ð° Ñ€Ð°Ð±Ð¾Ñ‚Ð½Ð¸Ñ Ð¿Ð»Ð¾Ñ‚"), ("verify_rustdesk_password_tip", "Проверете RustDesk паролата"), ("remember_account_tip", "Запомнете този акаунт"), - ("os_account_desk_tip", "Този акаунт Ñе използва за влизане в отдалечената операционна ÑиÑтема и позволÑва на деÑктоп ÑеÑиÑта без глава"), - ("OS Account", "Операционната ÑиÑтема акаунт"), + ("os_account_desk_tip", "Този акаунт Ñе използва за влизане в отдалечената операционна ÑиÑтема и позволÑва на деÑктоп ÑеÑÐ¸Ñ Ð±ÐµÐ· моинитор"), + ("OS Account", "Профил в операционната ÑиÑтема"), ("another_user_login_title_tip", "Друг потребител вече е влÑзъл"), ("another_user_login_text_tip", "ПрекъÑнете връзката"), ("xorg_not_found_title_tip", "Xorg не е намерен"), ("xorg_not_found_text_tip", "МолÑ, инÑталирайте Xorg"), ("no_desktop_title_tip", "ÐÑма наличен работен плот"), ("no_desktop_text_tip", "МолÑ, инÑталирайте работен плот GNOME"), - ("No need to elevate", ""), + ("No need to elevate", "ÐÑма нужда за повишаване на права"), ("System Sound", "СиÑтемен звук"), ("Default", "По подразбиране"), ("New RDP", "Ðов RDP"), @@ -490,7 +485,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no fingerprints", "ÐÑма пръÑтови отпечатъци"), ("Select a peer", "Избери отдалечена Ñтрана"), ("Select peers", "Избери отдалечени Ñтрани"), - ("Plugins", "ПриÑтавки"), + ("Plugins", "Плъгини"), ("Uninstall", "Премахни"), ("Update", "ОбновÑване"), ("Enable", "ПозволÑване"), @@ -513,17 +508,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Stop", "Спиране"), ("exceed_max_devices", "ДоÑтигнахте макÑÐ¸Ð¼Ð°Ð»Ð½Ð¸Ñ Ð±Ñ€Ð¾Ð¹ управлÑвани уÑтройÑтва."), ("Sync with recent sessions", "Синхронизиране Ñ Ð¿Ð¾Ñледните ÑеÑии"), - ("Sort tags", "Подреди белези"), - ("Open connection in new tab", "Разкриване на връзка в нов раздел"), - ("Move tab to new window", "ОтделÑне на раздела в нов прозорец"), + ("Sort tags", "Подреди етикети"), + ("Open connection in new tab", "ОтварÑне на връзката в нов раздел"), + ("Move tab to new window", "ПревмеÑтване на раздела в нов прозорец"), ("Can not be empty", "Ðе може да е празно"), ("Already exists", "Вече ÑъщеÑтвува"), ("Change Password", "ПромÑна на парола"), ("Refresh Password", "ОбновÑване парола"), - ("ID", "Определител (ID)"), - ("Grid View", "Мрежов изглед"), + ("ID", "Идентификатор (ID)"), + ("Grid View", "Табличен изглед"), ("List View", "СпиÑъчен изглед"), - ("Select", "Избиране"), + ("Select", "Избор"), ("Toggle Tags", "Превключване на етикети"), ("pull_ab_failed_tip", "ÐеуÑпешно опреÑнÑване на адреÑната книга"), ("push_ab_failed_tip", "ÐеуÑпешно Ñинхронизиране на адреÑната книга ÑÑŠÑ Ñървъра"), @@ -531,20 +526,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Change Color", "ПромÑна на цвета"), ("Primary Color", "ОÑновен цвÑÑ‚"), ("HSV Color", "HSV цвÑÑ‚"), - ("Installation Successful!", "УÑпешно поÑтавÑне!"), - ("Installation failed!", "Провал при поÑтавÑне"), + ("Installation Successful!", "УÑпешно инÑталиране!"), + ("Installation failed!", "ÐеуÑпешно инÑталиране"), ("Reverse mouse wheel", "Обърнато колелото на мишката"), ("{} sessions", "{} ÑеÑии"), ("scam_title", "Възможно е да Ñте ИЗМÐМЕÐИ!"), ("scam_text1", "Ðко разговарÑте по телефона Ñ Ð½Ñкой, когото ÐЕ ПОЗÐÐÐ’ÐТЕ и ÐЯМÐТЕ ДОВЕРИЕ, който ви е помолил да използвате RustDesk и да Ñтартирате уÑлугата, не продължавайте и затворете незабавно."), ("scam_text2", "Те вероÑтно Ñа измамник, който Ñе опитва да открадне вашите пари или друга лична информациÑ."), ("Don't show again", "Ðе показвай отново"), - ("I Agree", "СъглаÑен"), + ("I Agree", "СъглаÑен Ñъм"), ("Decline", "Отказвам"), ("Timeout in minutes", "Време за отговор в минути"), ("auto_disconnect_option_tip", "Ðвтоматично затварÑне на входÑщите ÑеÑии при неактивноÑÑ‚ на потребителÑ"), ("Connection failed due to inactivity", "Ðвтоматично прекъÑване на връзката поради неактивноÑÑ‚"), - ("Check for software update on startup", ""), + ("Check for software update on startup", "ПроверÑвай за Ð¾Ð±Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¿Ñ€Ð¸ Ñтартиране"), ("upgrade_rustdesk_server_pro_to_{}_tip", "ÐœÐ¾Ð»Ñ Ð¾Ð±Ð½Ð¾Ð²ÐµÑ‚Ðµ RustDesk Server Pro на верÑÐ¸Ñ {} или по-нова!"), ("pull_group_failed_tip", "ÐеуÑпешно опреÑнÑване на групата"), ("Filter by intersection", "ОтÑÑване по преÑичане"), @@ -554,14 +549,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No displays", "ÐÑма екрани"), ("Open in new window", "ОтварÑне в нов прозорец"), ("Show displays as individual windows", "Показване на екраните в отделни прозорци"), - ("Use all my displays for the remote session", "Използване на вÑички тукашни екрани за отдалечена работа"), + ("Use all my displays for the remote session", "Използвай вÑички мои екрани за отдалечена връзка"), ("selinux_tip", "SELinux е активиран на вашето уÑтройÑтво, което може да попречи на RustDesk да работи правилно като контролирана Ñтрана."), ("Change view", "ПромÑна изглед"), ("Big tiles", "Големи заглавиÑ"), ("Small tiles", "Малки заглавиÑ"), ("List", "СпиÑък"), ("Virtual display", "Виртуален екран"), - ("Plug out all", "Изтръгване на вÑички"), + ("Plug out all", "Разкачане на вÑички"), ("True color (4:4:4)", ""), ("Enable blocking user input", "Разрешаване на блокиране на потребителÑко въвеждане"), ("id_input_tip", "Можете да въведете ID, директен IP Ð°Ð´Ñ€ÐµÑ Ð¸Ð»Ð¸ домейн Ñ Ð¿Ð¾Ñ€Ñ‚ (:).\nÐко иÑкате да получите доÑтъп до уÑтройÑтво на друг Ñървър, молÑ, добавете адреÑа на Ñървъра (@?key=), например\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nÐко иÑкате да получите доÑтъп до уÑтройÑтво на общеÑтвен Ñървър, молÑ, въведете \"@public\" , ключът не е необходим за публичен Ñървър"), @@ -591,7 +586,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("My address book", "МоÑта адреÑна книга"), ("Personal", "Личен"), ("Owner", "СобÑтвеник"), - ("Set shared password", "ОпределÑне Ñподелена парола"), + ("Set shared password", "Задай Ñподелена парола"), ("Exist in", "СъщеÑтвува в"), ("Read-only", "Само четене"), ("Read/Write", "ПиÑане/четене"), @@ -612,25 +607,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("confirm_clear_Wayland_screen_selection_tip", ""), ("android_new_voice_call_tip", ""), ("texture_render_tip", ""), - ("Use texture rendering", "Използвай текÑтово изграждане"), + ("Use texture rendering", "Използвай рендер на текÑтури"), ("Floating window", "Плаващ прозорец"), ("floating_window_tip", ""), ("Keep screen on", "Запази екранът включен"), ("Never", "Ðикога"), ("During controlled", "Докато е обект на управление"), ("During service is on", "Докато уÑлугата е включена"), - ("Capture screen using DirectX", "Снемай екрана ползвайки DirectX"), + ("Capture screen using DirectX", "ЗаÑнемай екрана ползвайки DirectX"), ("Back", "Ðазад"), ("Apps", "ПриложениÑ"), ("Volume up", "УÑилване звук"), - ("Volume down", "ÐамалÑне звук"), + ("Volume down", "ÐамалÑване звук"), ("Power", "МощноÑÑ‚"), ("Telegram bot", "Телеграм бот"), ("enable-bot-tip", ""), ("enable-bot-desc", ""), ("cancel-2fa-confirm-tip", ""), ("cancel-bot-confirm-tip", ""), - ("About RustDesk", "ОтноÑно RustDesk"), + ("About RustDesk", "За RustDesk"), ("Send clipboard keystrokes", ""), ("network_error_tip", ""), ("Unlock with PIN", "Отключване Ñ PIN"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "МолÑ, надÑтройте клиента RustDesk до верÑÐ¸Ñ {} или по-нова от отдалечената Ñтрана!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Преглед на камерата"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ca.rs b/src/lang/ca.rs index 33992a3386b..66067b261c9 100644 --- a/src/lang/ca.rs +++ b/src/lang/ca.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "Entre %min% i %max% caràcters"), ("starts with a letter", "Comença amb una lletra"), ("allowed characters", "Caràcters admesos"), - ("id_change_tip", "Els caràcters admesos són: a-z, A-Z, 0-9, _ (guió baix). El primer caràcter ha de ser a-z/A-Z, i una mida de 6 a 16 caràcters."), + ("id_change_tip", "Els caràcters admesos són: a-z, A-Z, 0-9, - (dash), _ (guió baix). El primer caràcter ha de ser a-z/A-Z, i una mida de 6 a 16 caràcters."), ("Website", "Lloc web"), ("About", "Quant al RustDesk"), ("Slogan_tip", "Fet de tot cor dins d'aquest món caòtic!\nTraducció: Benet R. i Camps (BennyBeat)."), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Contrasenya del sistema"), ("install_tip", "En alguns casos és possible que el RustDesk no funcioni correctament per les restriccions UAC («User Account Control»; Control de comptes d'usuari). Per evitar aquest problema, instal·leu el RustDesk al vostre sistema."), ("Click to upgrade", "Feu clic per a actualitzar"), - ("Click to download", "Feu clic per a baixar"), - ("Click to update", "Feu clic per a actualitzar"), ("Configure", "Configura"), ("config_acc", "Per a poder controlar el dispositiu remotament, faciliteu al RustDesk els permisos d'accessibilitat."), ("config_screen", "Per a poder controlar el dispositiu remotament, faciliteu al RustDesk els permisos de gravació de pantalla."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Cap permís per a transferència de fitxers"), ("Note", "Nota"), ("Connection", "Connexió"), - ("Share Screen", "Compartició de pantalla"), + ("Share screen", "Compartició de pantalla"), ("Chat", "Xat"), ("Total", "Total"), ("items", "elements"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de pantalla"), ("Input Control", "Control d'entrada"), ("Audio Capture", "Captura d'àudio"), - ("File Connection", "Connexió de fitxer"), - ("Screen Connection", "Connexió de pantalla"), ("Do you accept?", "Voleu acceptar?"), ("Open System Setting", "Obre la configuració del sistema"), ("How to get Android input permission?", "Com modificar els permisos a Android?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "No heu afegit cap dispositiu aquí!\nPodeu afegir dispositius favorits en qualsevol moment."), ("empty_lan_tip", "No s'ha trobat cap dispositiu proper."), ("empty_address_book_tip", "Sembla que no teniu cap dispositiu a la vostra llista d'adreces."), - ("eg: admin", "p. ex.:admin"), ("Empty Username", "Nom d'usuari buit"), ("Empty Password", "Contrasenya buida"), ("Me", "Vós"), @@ -649,12 +644,70 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Autenticació requerida"), ("Authenticate", "Autentica"), ("web_id_input_tip", "Podeu inserir el número ID al propi servidor; l'accés directe per IP no és compatible amb el client web.\nSi voleu accedir a un dispositiu d'un altre servidor, afegiu l'adreça del servidor, com ara @?key= (p. ex.\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi voleu accedir a un dispositiu en un servidor públic, no cal que inseriu la clau pública «@» per al servidor públic."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), + ("Download", "Descarrega"), + ("Upload folder", "Puja una carpeta"), + ("Upload files", "Puja fitxers"), + ("Clipboard is synchronized", "El porta-retalls està sincronitzat"), + ("Update client clipboard", "Actualitza el porta-retalls del client"), + ("Untagged", "Sense etiquetar"), ("new-version-of-{}-tip", ""), + ("Accessible devices", "Dispositius accessibles"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre à niveau le client RustDesk vers la version {} ou plus récente du côté distant !"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Mostra la càmera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index a3e3666c680..ef28c34fc35 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "长度在 %min% 与 %max% 之间"), ("starts with a letter", "以字æ¯å¼€å¤´"), ("allowed characters", "使用å…许的字符"), - ("id_change_tip", "åªå¯ä»¥ä½¿ç”¨å­—æ¯ a-z, A-Z, 0-9, _ (下划线)。首字æ¯å¿…须是 a-z, A-Z。长度在 6 与 16 之间。"), + ("id_change_tip", "åªå¯ä»¥ä½¿ç”¨å­—æ¯ a-z, A-Z, 0-9, - (dash), _ (下划线)。首字æ¯å¿…须是 a-z, A-Z。长度在 6 与 16 之间。"), ("Website", "网站"), ("About", "关于"), ("Slogan_tip", "在这个混乱的世界中,用心制作ï¼"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "æ“作系统密ç "), ("install_tip", "你正在è¿è¡Œæœªå®‰è£…版本,由于 UAC é™åˆ¶ï¼Œä½œä¸ºè¢«æŽ§ç«¯ï¼Œä¼šåœ¨æŸäº›æƒ…况下无法控制鼠标键盘,或者录制å±å¹•,请点击下é¢çš„æŒ‰é’®å°† RustDesk 安装到系统,从而规é¿ä¸Šè¿°é—®é¢˜ã€‚"), ("Click to upgrade", "点击这里å‡çº§"), - ("Click to download", "点击这里下载"), - ("Click to update", "点击这里更新"), ("Configure", "é…ç½®"), ("config_acc", "为了能够远程控制你的桌é¢, 请给予 RustDesk \"辅助功能\" æƒé™ã€‚"), ("config_screen", "为了能够远程访问你的桌é¢, 请给予 RustDesk \"å±å¹•录制\" æƒé™ã€‚"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "没有文件传输æƒé™"), ("Note", "备注"), ("Connection", "连接"), - ("Share Screen", "共享å±å¹•"), + ("Share screen", "共享å±å¹•"), ("Chat", "èŠå¤©æ¶ˆæ¯"), ("Total", "总计"), ("items", "个项目"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "å±å¹•录制"), ("Input Control", "输入控制"), ("Audio Capture", "音频录制"), - ("File Connection", "文件连接"), - ("Screen Connection", "å±å¹•连接"), ("Do you accept?", "æ˜¯å¦æŽ¥å—?"), ("Open System Setting", "打开系统设置"), ("How to get Android input permission?", "如何获å–安å“的输入æƒé™ï¼Ÿ"), @@ -346,7 +342,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dark", "黑暗"), ("Light", "明亮"), ("Follow System", "è·Ÿéšç³»ç»Ÿ"), - ("Enable hardware codec", "使能硬件编解ç "), + ("Enable hardware codec", "å¯ç”¨ç¡¬ä»¶ç¼–è§£ç "), ("Unlock Security Settings", "è§£é”安全设置"), ("Enable audio", "å…许传输音频"), ("Unlock Network Settings", "è§£é”网络设置"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "还没有收è—的被控端?找一个人连接并将其添加到收è—å§ï¼"), ("empty_lan_tip", "情况ä¸å¦™ï¼Œä¼¼ä¹Žæœªå‘现任何被控端ï¼"), ("empty_address_book_tip", "似乎目å‰åœ°å€ç°¿å†…无被控端"), - ("eg: admin", "例如:admin"), ("Empty Username", "空用户å"), ("Empty Password", "空密ç "), ("Me", "我"), @@ -653,8 +648,66 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload folder", "上传文件夹"), ("Upload files", "上传文件"), ("Clipboard is synchronized", "剪贴æ¿å·²åŒæ­¥"), - ("Update client clipboard", "更新客户端的粘贴æ¿"), + ("Update client clipboard", "更新客户端的剪贴æ¿"), ("Untagged", "无标签"), ("new-version-of-{}-tip", "{} 版本更新"), + ("Accessible devices", "å¯è®¿é—®çš„设备"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "请在远程端将 RustDesk 客户端å‡çº§è‡³ç‰ˆæœ¬ {} 或更新版本ï¼"), + ("d3d_render_tip", "当å¯ç”¨ D3D 渲染时,æŸäº›æœºå™¨å¯èƒ½æ— æ³•显示远程画é¢ã€‚"), + ("Use D3D rendering", "使用 D3D 渲染"), + ("Printer", "æ‰“å°æœº"), + ("printer-os-requirement-tip", "æ‰“å°æœºçš„ä¼ å‡ºåŠŸèƒ½éœ€è¦ Windows 10 或更高版本。"), + ("printer-requires-installed-{}-client-tip", "请先安装 {} 客户端。"), + ("printer-{}-not-installed-tip", "未安装 {} æ‰“å°æœºã€‚"), + ("printer-{}-ready-tip", "{} æ‰“å°æœºå·²å®‰è£…,您å¯ä»¥ä½¿ç”¨æ‰“å°åŠŸèƒ½äº†ã€‚"), + ("Install {} Printer", "安装 {} æ‰“å°æœº"), + ("Outgoing Print Jobs", "传出的打å°ä»»åŠ¡"), + ("Incoming Print Jobs", "传入的打å°ä»»åŠ¡"), + ("Incoming Print Job", "传入的打å°ä»»åŠ¡"), + ("use-the-default-printer-tip", "ä½¿ç”¨é»˜è®¤çš„æ‰“å°æœºæ‰§è¡Œ"), + ("use-the-selected-printer-tip", "ä½¿ç”¨é€‰æ‹©çš„æ‰“å°æœºæ‰§è¡Œ"), + ("auto-print-tip", "ä½¿ç”¨é€‰æ‹©çš„æ‰“å°æœºè‡ªåŠ¨æ‰§è¡Œ"), + ("print-incoming-job-confirm-tip", "您收到一个远程打å°ä»»åŠ¡ï¼Œæ‚¨æƒ³åœ¨æœ¬åœ°æ‰§è¡Œå®ƒå—?"), + ("remote-printing-disallowed-tile-tip", "ä¸å…许远程打å°"), + ("remote-printing-disallowed-text-tip", "被控端的æƒé™è®¾ç½®æ‹’ç»äº†è¿œç¨‹æ‰“å°ã€‚"), + ("save-settings-tip", "ä¿å­˜è®¾ç½®"), + ("dont-show-again-tip", "ä¸å†æ˜¾ç¤ºæ­¤ä¿¡æ¯"), + ("Take screenshot", "截å±"), + ("Taking screenshot", "正在截å±"), + ("screenshot-merged-screen-not-supported-tip", "当å‰ä¸æ”¯æŒå¤šä¸ªå±å¹•çš„åˆå¹¶æˆªå±ï¼Œè¯·åˆ‡æ¢åˆ°å•个å±å¹•é‡è¯•。"), + ("screenshot-action-tip", "请选择如何继续截å±ã€‚"), + ("Save as", "å¦å­˜ä¸º"), + ("Copy to clipboard", "å¤åˆ¶åˆ°å‰ªè´´æ¿"), + ("Enable remote printer", "å¯ç”¨è¿œç¨‹æ‰“å°æœº"), + ("Downloading {}", "正在下载 {}"), + ("{} Update", "{} æ›´æ–°"), + ("{}-to-update-tip", "å³å°†å…³é—­ {} ,并安装新版本。"), + ("download-new-version-failed-tip", "下载失败,您å¯ä»¥é‡è¯•或者点击\"下载\"按钮,从å‘布网å€ä¸‹è½½ï¼Œå¹¶æ‰‹åЍå‡çº§ã€‚"), + ("Auto update", "自动更新"), + ("update-failed-check-msi-tip", "å®‰è£…æ–¹å¼æ£€æµ‹å¤±è´¥ã€‚请点击\"下载\"按钮,从å‘布网å€ä¸‹è½½ï¼Œå¹¶æ‰‹åЍå‡çº§ã€‚"), + ("websocket_tip", "使用 WebSocket 时,仅支æŒä¸­ç»§è¿žæŽ¥ã€‚"), + ("Use WebSocket", "使用 WebSocket"), + ("Trackpad speed", "触控æ¿é€Ÿåº¦"), + ("Default trackpad speed", "默认触控æ¿é€Ÿåº¦"), + ("Numeric one-time password", "一次性密ç ä¸ºæ•°å­—"), + ("Enable IPv6 P2P connection", "å¯ç”¨ IPv6 P2P 连接"), + ("Enable UDP hole punching", "å¯ç”¨ UDP 打洞"), + ("View camera", "查看摄åƒå¤´"), + ("Enable camera", "å…许查看摄åƒå¤´"), + ("No cameras", "没有摄åƒå¤´"), + ("view_camera_unsupported_tip", "æ‚¨çš„è¿œç¨‹ç«¯ä¸æ”¯æŒæŸ¥çœ‹æ‘„åƒå¤´ã€‚"), + ("Terminal", "终端"), + ("Enable terminal", "å¯ç”¨ç»ˆç«¯"), + ("New tab", "新建选项å¡"), + ("Keep terminal sessions on disconnect", "æ–­å¼€è¿žæŽ¥æ—¶ä¿æŒç»ˆç«¯ä¼šè¯"), + ("Terminal (Run as administrator)", "终端(以管ç†å‘˜èº«ä»½è¿è¡Œï¼‰"), + ("terminal-admin-login-tip", "请输入被控端的管ç†å‘˜è´¦å·å¯†ç ã€‚"), + ("Failed to get user token.", "获å–用户令牌时出错。"), + ("Incorrect username or password.", "ç”¨æˆ·åæˆ–密ç ä¸æ­£ç¡®ã€‚"), + ("The user is not an administrator.", "ç”¨æˆ·ä¸æ˜¯ç®¡ç†å‘˜ã€‚"), + ("Failed to check if the user is an administrator.", "检查用户是å¦ä¸ºç®¡ç†å‘˜æ—¶å‡ºé”™ã€‚"), + ("Supported only in the installed version.", "ä»…åœ¨ä»¥å®‰è£…ç‰ˆæœ¬å—æ”¯æŒã€‚"), + ("elevation_username_tip", "è¾“å…¥ç”¨æˆ·åæˆ–域å\\用户å"), + ("Preparing for installation ...", "准备安装..."), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index e36c493618b..dc4e0f214e7 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "délka mezi %min% a %max%"), ("starts with a letter", "zaÄíná písmenem"), ("allowed characters", "povolené znaky"), - ("id_change_tip", "Použít je možné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je tÅ™eba aby zaÄínalo písmenem a-z, A-Z. Délka mezi 6 a 16 znaky."), + ("id_change_tip", "Použít je možné pouze znaky a-z, A-Z, 0-9, - (dash) a _ (podtržítko). Dále je tÅ™eba aby zaÄínalo písmenem a-z, A-Z. Délka mezi 6 a 16 znaky."), ("Website", "Webové stránky"), ("About", "O aplikaci"), ("Slogan_tip", "VytvoÅ™eno srdcem v tomto chaotickém svÄ›tÄ›!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Heslo do operaÄního systému"), ("install_tip", "Kvůli řízení oprávnÄ›ní v systému (UAC), RustDesk v nÄ›kterých případech na protistranÄ› nefunguje správnÄ›. Abyste se UAC vyhnuli, kliknÄ›te na níže uvedené tlaÄítko a nainstalujte tak RustDesk do systému."), ("Click to upgrade", "Aktualizovat"), - ("Click to download", "Stáhnout"), - ("Click to update", "Aktualizovat"), ("Configure", "Nastavit"), ("config_acc", "Aby bylo možné na dálku ovládat vaÅ¡i plochu, je tÅ™eba aplikaci RustDesk udÄ›lit oprávnÄ›ní pro \"ZpřístupnÄ›ní pro hendikepované\"."), ("config_screen", "Aby bylo možné pÅ™istupovat k vaší ploÅ¡e na dálku, je tÅ™eba aplikaci RustDesk udÄ›lit oprávnÄ›ní pro \"Nahrávání obsahu obrazovky\"."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Žádné oprávnÄ›ní k pÅ™enosu souborů"), ("Note", "Poznámka"), ("Connection", "PÅ™ipojení"), - ("Share Screen", "Sdílet obrazovku"), + ("Share screen", "Sdílet obrazovku"), ("Chat", "Chat"), ("Total", "Celkem"), ("items", "Položek"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Zachytávání obrazovky"), ("Input Control", "Ovládání vstupních zařízení"), ("Audio Capture", "Zachytávání zvuku"), - ("File Connection", "Souborové spojení"), - ("Screen Connection", "Spojení obrazovky"), ("Do you accept?", "PÅ™ijímáte?"), ("Open System Setting", "Otevřít nastavení systému"), ("How to get Android input permission?", "Jak v systému Android získat oprávnÄ›ní pro vstupní zařízení?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "JeÅ¡tÄ› nemáte oblíbené protistrany?\nNajdÄ›te nÄ›koho, s kým se můžete spojit, a pÅ™idejte si ho do oblíbených!"), ("empty_lan_tip", "Ale ne, vypadá, že jsme jeÅ¡tÄ› neobjevili žádné protistrany."), ("empty_address_book_tip", "Ach bože, zdá se, že ve vaÅ¡em adresáři nejsou v souÄasné dobÄ› uvedeni žádní kolegové."), - ("eg: admin", "napÅ™. admin"), ("Empty Username", "Prázdné uživatelské jméno"), ("Empty Password", "Prázdné heslo"), ("Me", "Já"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgradujte prosím klienta RustDesk na verzi {} nebo novÄ›jší na vzdálené stranÄ›!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Zobrazit kameru"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 7988da242e7..b180c5856a5 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "længde %min% til %max%"), ("starts with a letter", "starter med ét bogstav"), ("allowed characters", "tilladte tegn"), - ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Antal tegn skal være mellem 6 og 16."), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9, - (dash) og _ (understregning) er tilladt. Det første bogstav skal være a-z, A-Z. Antal tegn skal være mellem 6 og 16."), ("Website", "Hjemmeside"), ("About", "Om"), ("Slogan_tip", "Lavet med kærlighed i denne kaotiske verden!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Operativsystemadgangskode"), ("install_tip", "PÃ¥ grund af UAC kan RustDesk ikke fungere korrekt i nogle tilfælde pÃ¥ fjernskrivebordet. For at undgÃ¥ UAC skal du klikke pÃ¥ knappen nedenfor for at installere RustDesk pÃ¥ systemet"), ("Click to upgrade", "Klik for at opgradere"), - ("Click to download", "Klik for at downloade"), - ("Click to update", "Klik for at opdatere"), ("Configure", "Konfigurer"), ("config_acc", "For at kontrollere dit skrivebord pÃ¥ afstand skal du give RustDesk \"Access \" Rettigheder."), ("config_screen", "For at kunne fÃ¥ adgang til dit skrivebord langtfra, skal du give RustDesk \"skærmstøtte \" tilladelser."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ingen tilladelse til at overføre filen"), ("Note", "Note"), ("Connection", "Forbindelse"), - ("Share Screen", "Del skærmen"), + ("Share screen", "Del skærmen"), ("Chat", "Chat"), ("Total", "Total"), ("items", "artikel"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Skærmoptagelse"), ("Input Control", "Inputkontrol"), ("Audio Capture", "Lydoptagelse"), - ("File Connection", "Filforbindelse"), - ("Screen Connection", "Færdiggørelse"), ("Do you accept?", "Accepterer du?"), ("Open System Setting", "Ã…bn systemindstillingen"), ("How to get Android input permission?", "Hvordan fÃ¥r jeg en Android-input tilladelse?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ingen yndlings modparter endnu?\nLad os finde én at forbinde til, og tilføje den til dine favoritter!"), ("empty_lan_tip", "Ã…h nej, det ser ud til, at vi ikke kunne finde nogen modparter endnu."), ("empty_address_book_tip", "Ã…h nej, det ser ud til at der ikke er nogle modparter der er tilføjet til din adressebog."), - ("eg: admin", "fx: admin"), ("Empty Username", "Tom brugernavn"), ("Empty Password", "Tom adgangskode"), ("Me", "Mig"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Opgrader venligst RustDesk-klienten til version {} eller nyere pÃ¥ fjernsiden!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Se kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index dbc6efc2d39..4a703f4ba83 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "Länge %min% bis %max%"), ("starts with a letter", "Beginnt mit Buchstabe"), ("allowed characters", "Erlaubte Zeichen"), - ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9 und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), + ("id_change_tip", "Nur die Zeichen a-z, A-Z, 0-9, - (Bindestrich) und _ (Unterstrich) sind erlaubt. Der erste Buchstabe muss a-z, A-Z sein und die Länge zwischen 6 und 16 Zeichen betragen."), ("Website", "Webseite"), ("About", "Über"), ("Slogan_tip", "Mit Herzblut programmiert - in einer Welt, die im Chaos versinkt!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Betriebssystem-Passwort"), ("install_tip", "Aufgrund der Benutzerkontensteuerung (UAC) kann RustDesk in manchen Fällen nicht ordnungsgemäß funktionieren. Um die Benutzerkontensteuerung zu umgehen, klicken Sie bitte auf die Schaltfläche unten und installieren RustDesk auf dem System."), ("Click to upgrade", "Zum Upgraden klicken"), - ("Click to download", "Zum Herunterladen klicken"), - ("Click to update", "Zum Aktualisieren klicken"), ("Configure", "Konfigurieren"), ("config_acc", "Um Ihren PC aus der Ferne zu steuern, müssen Sie RustDesk Zugriffsrechte erteilen."), ("config_screen", "Um aus der Ferne auf Ihren PC zugreifen zu können, müssen Sie RustDesk die Berechtigung \"Bildschirmaufnahme\" erteilen."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Keine Berechtigung für die Dateiübertragung"), ("Note", "Hinweis"), ("Connection", "Verbindung"), - ("Share Screen", "Bildschirm freigeben"), + ("Share screen", "Bildschirm freigeben"), ("Chat", "Chat"), ("Total", "Gesamt"), ("items", "Einträge"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Bildschirmaufnahme"), ("Input Control", "Eingabesteuerung"), ("Audio Capture", "Audioaufnahme"), - ("File Connection", "Dateiverbindung"), - ("Screen Connection", "Bildschirmverbindung"), ("Do you accept?", "Verbindung zulassen?"), ("Open System Setting", "Systemeinstellung öffnen"), ("How to get Android input permission?", "Wie erhalte ich eine Android-Eingabeberechtigung?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Noch keine favorisierte Gegenstelle?\nLassen Sie uns jemanden finden, mit dem wir uns verbinden können und fügen Sie ihn zu Ihren Favoriten hinzu!"), ("empty_lan_tip", "Oh nein, es sieht so aus, als hätten wir noch keine Gegenstelle entdeckt."), ("empty_address_book_tip", "Oh je, es scheint, dass in Ihrem Adressbuch derzeit keine Gegenstellen aufgeführt sind."), - ("eg: admin", "z. B.: admin"), ("Empty Username", "Leerer Benutzername"), ("Empty Password", "Leeres Passwort"), ("Me", "Ich"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Client-Zwischenablage aktualisieren"), ("Untagged", "Unmarkiert"), ("new-version-of-{}-tip", "Es ist eine neue Version von {} verfügbar"), + ("Accessible devices", "Erreichbare Geräte"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Bitte aktualisieren Sie den RustDesk-Client auf der Remote-Seite auf Version {} oder neuer!"), + ("d3d_render_tip", "Wenn das D3D-Rendering aktiviert ist, kann der entfernte Bildschirm auf manchen Rechnern schwarz sein."), + ("Use D3D rendering", "D3D-Rendering verwenden"), + ("Printer", "Drucker"), + ("printer-os-requirement-tip", "Für die Funktion des Druckerausgangs ist Windows 10 oder höher erforderlich."), + ("printer-requires-installed-{}-client-tip", "Um den entfernten Druck nutzen zu können, muss {} auf diesem Gerät installiert sein."), + ("printer-{}-not-installed-tip", "Der Drucker {} ist nicht installiert."), + ("printer-{}-ready-tip", "Der Drucker {} ist installiert und einsatzbereit."), + ("Install {} Printer", "Drucker {} installieren"), + ("Outgoing Print Jobs", "Ausgehende Druckaufträge"), + ("Incoming Print Jobs", "Eingehende Druckaufträge"), + ("Incoming Print Job", "Eingehender Druckauftrag"), + ("use-the-default-printer-tip", "Standarddrucker verwenden"), + ("use-the-selected-printer-tip", "Ausgewählten Drucker verwenden"), + ("auto-print-tip", "Automatischer Druck mit dem ausgewählten Drucker."), + ("print-incoming-job-confirm-tip", "Sie haben einen Druckauftrag aus der Ferne erhalten. Möchten Sie ihn bei sich selbst ausführen?"), + ("remote-printing-disallowed-tile-tip", "Entferntes Drucken nicht erlaubt"), + ("remote-printing-disallowed-text-tip", "Die Berechtigungseinstellungen der kontrollierten Seite verweigern den entfernten Druck."), + ("save-settings-tip", "Einstellungen speichern"), + ("dont-show-again-tip", "Nicht mehr anzeigen"), + ("Take screenshot", "Screenshot aufnehmen"), + ("Taking screenshot", "Screenshot aufnehmen …"), + ("screenshot-merged-screen-not-supported-tip", "Das Zusammenführen von Screenshots von mehreren Bildschirmen wird derzeit nicht unterstützt. Bitte wechseln Sie zu einem einzelnen Bildschirm und versuchen Sie es erneut."), + ("screenshot-action-tip", "Bitte wählen Sie aus, wie Sie mit dem Screenshot fortfahren möchten."), + ("Save as", "Speichern unter"), + ("Copy to clipboard", "In Zwischenablage kopieren"), + ("Enable remote printer", "Entfernten Drucker aktivieren"), + ("Downloading {}", "{} herunterladen"), + ("{} Update", "{} aktualisieren"), + ("{}-to-update-tip", "{} wird jetzt geschlossen und die neue Version installiert."), + ("download-new-version-failed-tip", "Download fehlgeschlagen. Sie können es erneut versuchen oder auf die Schaltfläche \"Herunterladen\" klicken, um von der Versionsseite herunterzuladen und manuell zu aktualisieren."), + ("Auto update", "Automatisch aktualisieren"), + ("update-failed-check-msi-tip", "Prüfung der Installationsmethode fehlgeschlagen. Bitte klicken Sie auf die Schaltfläche \"Herunterladen\", um von der Versionsseite herunterzuladen und manuell zu aktualisieren."), + ("websocket_tip", "Bei der Verwendung von WebSocket werden nur Relay-Verbindungen unterstützt."), + ("Use WebSocket", "WebSocket verwenden"), + ("Trackpad speed", "Geschwindigkeit des Trackpads"), + ("Default trackpad speed", "Standardgeschwindigkeit des Trackpads"), + ("Numeric one-time password", "Numerisches Einmalpasswort"), + ("Enable IPv6 P2P connection", "IPv6-P2P-Verbindung aktivieren"), + ("Enable UDP hole punching", "UDP-Hole-Punching aktivieren"), + ("View camera", "Kamera anzeigen"), + ("Enable camera", "Kamera zulassen"), + ("No cameras", "Keine Kameras"), + ("view_camera_unsupported_tip", "Das entfernte Gerät kann die Kamera nicht anzeigen."), + ("Terminal", "Terminal"), + ("Enable terminal", "Terminal zulassen"), + ("New tab", "Neuer Tab"), + ("Keep terminal sessions on disconnect", "Terminalsitzungen beim Trennen der Verbindung beibehalten"), + ("Terminal (Run as administrator)", "Terminal (als Administrator ausführen)"), + ("terminal-admin-login-tip", "Bitte geben Sie den Benutzernamen und das Passwort des Administrators der kontrollierten Seite ein."), + ("Failed to get user token.", "Benutzer-Token konnte nicht abgerufen werden."), + ("Incorrect username or password.", "Falscher Benutzername oder falsches Passwort."), + ("The user is not an administrator.", "Der Benutzer ist kein Administrator."), + ("Failed to check if the user is an administrator.", "Es konnte nicht geprüft werden, ob der Benutzer ein Administrator ist."), + ("Supported only in the installed version.", "Wird nur in der installierten Version unterstützt."), + ("elevation_username_tip", "Geben Sie Benutzername oder Domäne\\Benutzername ein"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/el.rs b/src/lang/el.rs index 0b78aa68560..56704fb392b 100644 --- a/src/lang/el.rs +++ b/src/lang/el.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "μέγεθος από %min% έως %max%"), ("starts with a letter", "ξεκινά με γÏάμμα"), ("allowed characters", "επιτÏεπόμενοι χαÏακτήÏες"), - ("id_change_tip", "ΕπιτÏέπονται μόνο οι χαÏακτήÏες a-z, A-Z, 0-9 και _ (υπογÏάμμιση). Το Ï€Ïώτο γÏάμμα Ï€Ïέπει να είναι a-z, A-Z και το μήκος Ï€Ïέπει να είναι Î¼ÎµÏ„Î±Î¾Ï 6 και 16 χαÏακτήÏων."), + ("id_change_tip", "ΕπιτÏέπονται μόνο οι χαÏακτήÏες a-z, A-Z, 0-9, - (παÏλα) και _ (κάτω παÏλα). Το Ï€Ïώτο γÏάμμα Ï€Ïέπει να είναι a-z, A-Z και το μήκος Ï€Ïέπει να είναι Î¼ÎµÏ„Î±Î¾Ï 6 και 16 χαÏακτήÏων."), ("Website", "Ιστότοπος"), ("About", "ΠληÏοφοÏίες"), ("Slogan_tip", "Φτιαγμένο με πάθος - σε έναν κόσμο που βυθίζεται στο χάος!"), @@ -146,9 +146,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set Password", "ΟÏίστε κωδικό Ï€Ïόσβασης"), ("OS Password", "Κωδικός Ï€Ïόσβασης λειτουÏÎ³Î¹ÎºÎ¿Ï ÏƒÏ…ÏƒÏ„Î®Î¼Î±Ï„Î¿Ï‚"), ("install_tip", "Λόγω UAC, το RustDesk ενδέχεται να μην λειτουÏγεί σωστά σε οÏισμένες πεÏιπτώσεις. Για να αποφÏγετε το UAC, κάντε κλικ στο κουμπί παÏακάτω για να εγκαταστήσετε το RustDesk στο σÏστημα"), - ("Click to upgrade", "Πιέστε για αναβάθμιση"), - ("Click to download", "Πιέστε για λήψη"), - ("Click to update", "Πιέστε για ενημέÏωση"), + ("Click to upgrade", "Αναβάθμιση τώÏα"), ("Configure", "ΔιαμόÏφωση"), ("config_acc", "Για τον απομακÏυσμένο έλεγχο του υπολογιστή σας, Ï€Ïέπει να εκχωÏήσετε δικαιώματα Ï€Ïόσβασης στο RustDesk."), ("config_screen", "Για να αποκτήσετε απομακÏυσμένη Ï€Ïόσβαση στον υπολογιστή σας, Ï€Ïέπει να εκχωÏήσετε το δικαίωμα RustDesk \"Screen Capture\"."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Δεν υπάÏχει άδεια για μεταφοÏά αÏχείων"), ("Note", "Σημείωση"), ("Connection", "ΣÏνδεση"), - ("Share Screen", "Κοινή χÏήση οθόνης"), + ("Share screen", "Κοινή χÏήση οθόνης"), ("Chat", "Κουβέντα"), ("Total", "ΣÏνολο"), ("items", "στοιχεία"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "ΑποτÏπωση οθόνης"), ("Input Control", "Έλεγχος εισόδου"), ("Audio Capture", "ΕγγÏαφή ήχου"), - ("File Connection", "ΣÏνδεση αÏχείου"), - ("Screen Connection", "ΣÏνδεση οθόνης"), ("Do you accept?", "Δέχεσαι;"), ("Open System Setting", "Άνοιγμα Ïυθμίσεων συστήματος"), ("How to get Android input permission?", "Πώς να αποκτήσω άδεια εισαγωγής Android;"), @@ -364,7 +360,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "ΕγγÏαφή"), ("Directory", "Φάκελος εγγÏαφών"), ("Automatically record incoming sessions", "Αυτόματη εγγÏαφή εισεÏχόμενων συνεδÏιών"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Αυτόματη εγγÏαφή εξεÏχόμενων συνεδÏιών"), ("Change", "Αλλαγή"), ("Start session recording", "ΈναÏξη εγγÏαφής συνεδÏίας"), ("Stop session recording", "Διακοπή εγγÏαφής συνεδÏίας"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Δεν υπάÏχουν ακόμη αγαπημένες συνδέσεις;\nÎ‘Ï†Î¿Ï Ï€Ïαγματοποιήσετε σÏνδεση με κάποιο απομακÏυσμένο σταθμό, μποÏείτε να τον Ï€Ïοσθέσετε στα αγαπημένα σας!"), ("empty_lan_tip", "Δεν έχουμε ανακαλυφθεί ακόμη απομακÏυσμένοι σταθμοί."), ("empty_address_book_tip", "Φαίνεται ότι αυτή τη στιγμή δεν υπάÏχουν αγαπημένες συνδέσεις στο βιβλίο διευθÏνσεών σας."), - ("eg: admin", "Ï€.χ. admin"), ("Empty Username", "Κενό όνομα χÏήστη"), ("Empty Password", "Κενός κωδικός Ï€Ïόσβασης"), ("Me", "Εγώ"), @@ -511,7 +506,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Service", "ΥπηÏεσία"), ("Start", "ΈναÏξη"), ("Stop", "Διακοπή"), - ("exceed_max_devices", "Έχετε ξεπεÏάσει το μέγιστο ÏŒÏιο αποθηκευμένων συνδέσεων"), + ("exceed_max_devices", "ΥπέÏβαση μέγιστου οÏίου αποθηκευμένων συνδέσεων"), ("Sync with recent sessions", "ΣυγχÏονισμός των Ï€Ïόσφατων συνεδÏιών"), ("Sort tags", "Ταξινόμηση ετικετών"), ("Open connection in new tab", "Άνοιγμα σÏνδεσης σε νέα καÏτέλα"), @@ -592,19 +587,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Personal", "ΠÏοσωπικό"), ("Owner", "Ιδιοκτήτης"), ("Set shared password", "ΟÏίστε κοινόχÏηστο κωδικό Ï€Ïόσβασης"), - ("Exist in", ""), + ("Exist in", "ΥπάÏχει στο"), ("Read-only", "Μόνο για ανάγνωση"), - ("Read/Write", ""), + ("Read/Write", "Ανάγνωση/ΕγγÏαφή"), ("Full Control", "ΠλήÏης Έλεγχος"), ("share_warning_tip", "Τα παÏαπάνω πεδία είναι κοινόχÏηστα και οÏατά σε άλλους."), - ("Everyone", ""), - ("ab_web_console_tip", ""), + ("Everyone", "Όλοι"), + ("ab_web_console_tip", "ΠεÏισσότεÏα στην κονσόλα web"), ("allow-only-conn-window-open-tip", "Îα επιτÏέπεται η σÏνδεση μόνο εάν το παÏάθυÏο RustDesk είναι ανοιχτό"), ("no_need_privacy_mode_no_physical_displays_tip", "Δεν υπάÏχουν φυσικές οθόνες, δεν χÏειάζεται να χÏησιμοποιήσετε τη λειτουÏγία αποÏÏήτου."), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), + ("Follow remote cursor", "ΠαÏακολοÏθηση απομακÏυσμένου κέÏσοÏα"), + ("Follow remote window focus", "ΠαÏακολοÏθηση απομακÏυσμένου ενεÏÎ³Î¿Ï Ï€Î±ÏαθÏÏου"), + ("default_proxy_tip", "ΠÏοκαθοÏισμένο Ï€Ïωτόκολλο Socks5 στην πόÏτα 1080"), + ("no_audio_input_device_tip", "Δεν βÏέθηκε συσκευή εισόδου ήχου."), ("Incoming", "ΕισεÏχόμενη"), ("Outgoing", "ΕξεÏχόμενη"), ("Clear Wayland screen selection", ""), @@ -619,42 +614,100 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Never", "Ποτέ"), ("During controlled", "Κατα την διάÏκεια απομακÏυσμένου ελέγχου"), ("During service is on", "Κατα την εκκίνηση της υπηÏεσίας Rustdesk"), - ("Capture screen using DirectX", ""), + ("Capture screen using DirectX", "ΚαταγÏαφή οθόνης με χÏήση DirectX"), ("Back", "Πίσω"), ("Apps", "ΕφαÏμογές"), - ("Volume up", ""), - ("Volume down", ""), + ("Volume up", "ΑÏξηση έντασης"), + ("Volume down", "Μείωση έντασης"), ("Power", ""), ("Telegram bot", ""), ("enable-bot-tip", "Εάν ενεÏγοποιήσετε αυτήν τη δυνατότητα, μποÏείτε να λάβετε τον κωδικό 2FA από το bot σας. ΜποÏεί επίσης να λειτουÏγήσει ως ειδοποίηση σÏνδεσης."), ("enable-bot-desc", "1, Ανοίξτε μια συνομιλία με τον @BotFather., Στείλτε την εντολή \"/newbot\". Θα λάβετε ένα διακÏιτικό Î±Ï†Î¿Ï Î¿Î»Î¿ÎºÎ»Î·Ïώσετε αυτό το βήμα.3, Ξεκινήστε μια συνομιλία με το bot που μόλις δημιουÏγήσατε. Στείλτε ένα μήνυμα που αÏχίζει με κάθετο (\"/\") όπως \"/hello\" για να το ενεÏγοποιήσετε."), ("cancel-2fa-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυÏώσετε το 2FA;"), ("cancel-bot-confirm-tip", "Είστε βέβαιοι ότι θέλετε να ακυÏώσετε το Telegram bot;"), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), + ("About RustDesk", "ΠληÏοφοÏίες για το RustDesk"), + ("Send clipboard keystrokes", "Αποστολή Ï€ÏοχείÏου με πλήκτÏα συντόμευσης"), + ("network_error_tip", "Ελέγξτε τη σÏνδεσή σας στο δίκτυο και, στη συνέχεια, κάντε κλικ στην επανάληψη."), + ("Unlock with PIN", "Ξεκλείδωμα με PIN"), + ("Requires at least {} characters", "ΑπαιτοÏνται τουλάχιστον {} χαÏακτήÏες"), + ("Wrong PIN", "Λάθος PIN"), + ("Set PIN", "ΟÏισμός PIN"), + ("Enable trusted devices", "ΕνεÏγοποίηση αξιόπιστων συσκευών"), + ("Manage trusted devices", "ΔιαχείÏιση αξιόπιστων συσκευών"), + ("Platform", "ΠλατφόÏμα"), + ("Days remaining", "ΗμέÏες που απομένουν"), + ("enable-trusted-devices-tip", "ΠαÏάβλεψη επαλήθευσης 2FA σε αξιόπιστες συσκευές."), + ("Parent directory", "Γονικός φάκελος"), + ("Resume", "Συνέχεια"), + ("Invalid file name", "Μη έγκυÏο όνομα αÏχείου"), ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), + ("Authentication Required", "Απαιτείται έλεγχος ταυτότητας"), + ("Authenticate", "Πιστοποίηση"), ("web_id_input_tip", ""), ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Upload folder", "ΜεταφόÏτωση φακέλου"), + ("Upload files", "ΜεταφόÏτωση αÏχείων"), + ("Clipboard is synchronized", "Το Ï€ÏόχειÏο έχει συγχÏονιστεί"), + ("Update client clipboard", "ΕνημέÏωση απομακÏισμένου Ï€ÏοχείÏου"), + ("Untagged", "ΧωÏίς ετικέτα"), + ("new-version-of-{}-tip", "ΥπάÏχει διαθέσιμη νέα έκδοση του {}"), + ("Accessible devices", "ΠÏοσβάσιμες συσκευές"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Αναβαθμίστε τον πελάτη RustDesk στην έκδοση {} ή νεότεÏη στην απομακÏυσμένη πλευÏά!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "ΠÏοβολή κάμεÏας"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/en.rs b/src/lang/en.rs index 2295fd05cef..dafa8f0702c 100644 --- a/src/lang/en.rs +++ b/src/lang/en.rs @@ -5,7 +5,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("connecting_status", "Connecting to the RustDesk network..."), ("not_ready_status", "Not ready. Please check your connection"), ("ID/Relay Server", "ID/Relay server"), - ("id_change_tip", "Only a-z, A-Z, 0-9 and _ (underscore) characters allowed. The first letter must be a-z, A-Z. Length between 6 and 16."), + ("id_change_tip", "Only a-z, A-Z, 0-9, - (dash) and _ (underscore) characters allowed. The first letter must be a-z, A-Z. Length between 6 and 16."), ("Slogan_tip", "Made with heart in this chaotic world!"), ("Build Date", "Build date"), ("Audio Input", "Audio input"), @@ -77,12 +77,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Move", "Canvas move"), ("Pinch to Zoom", "Pinch to zoom"), ("Canvas Zoom", "Canvas zoom"), - ("Share Screen", "Share screen"), ("Screen Capture", "Screen capture"), ("Input Control", "Input control"), ("Audio Capture", "Audio capture"), - ("File Connection", "File connection"), - ("Screen Connection", "Screen connection"), ("Open System Setting", "Open system setting"), ("android_input_permission_tip1", "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."), ("android_input_permission_tip2", "Please go to the next system settings page, find and enter [Installed Services], turn on [RustDesk Input] service."), @@ -237,5 +234,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "One-way file transfer is enabled on the controlled side."), ("web_id_input_tip", "You can input an ID in the same server, direct IP access is not supported in web client.\nIf you want to access a device on another server, please append the server address (@?key=), for example,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nIf you want to access a device on a public server, please input \"@public\", the key is not needed for public server."), ("new-version-of-{}-tip", "There is a new version of {} available"), + ("View camera", "View camera"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Please upgrade the RustDesk client to version {} or newer on the remote side!"), + ("view_camera_unsupported_tip", "The remote device does not support viewing the camera."), + ("d3d_render_tip", "When D3D rendering is enabled, the remote control screen may be black on some machines."), + ("printer-requires-installed-{}-client-tip", "In order to use remote printing, {} needs to be installed on this device."), + ("printer-os-requirement-tip", "The printer outgoing function requires Windows 10 or higher."), + ("printer-{}-not-installed-tip", "The {} Printer is not installed."), + ("printer-{}-ready-tip", "The {} Printer is installed and ready to use."), + ("auto-print-tip", "Print automatically using the selected printer."), + ("print-incoming-job-confirm-tip", "You received a print job from remote. Do you want to execute it at your side?"), + ("use-the-default-printer-tip", "Use the default printer"), + ("use-the-selected-printer-tip", "Use the selected printer"), + ("remote-printing-disallowed-tile-tip", "Remote Printing disallowed"), + ("remote-printing-disallowed-text-tip", "The permission settings of the controlled side deny Remote Printing."), + ("save-settings-tip", "Save settings"), + ("dont-show-again-tip", "Don't show this again"), + ("screenshot-merged-screen-not-supported-tip", "Merging screenshots of multiple displays is currently not supported. Please switch to a single display and try again."), + ("screenshot-action-tip", "Please select how to continue with the screenshot."), + ("{}-to-update-tip", "{} will close now and install the new version."), + ("download-new-version-failed-tip", "Download failed. You can try again or click the \"Download\" button to download from the release page and upgrade manually."), + ("update-failed-check-msi-tip", "Installation method check failed. Please click the \"Download\" button to download from the release page and upgrade manually."), + ("websocket_tip", "When using WebSocket, only relay connections are supported."), + ("terminal-admin-login-tip", "Please input the administrator username and password of the controlled side."), + ("elevation_username_tip", "Input username or domain\\username"), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index 7370c2429fb..3447df9f442 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "longeco %min% al %max%"), ("starts with a letter", "komencas kun letero"), ("allowed characters", "permesitaj signoj"), - ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), + ("id_change_tip", "Nur la signoj a-z, A-Z, 0-9, - (dash), _ (substreko) povas esti uzataj. La unua litero povas esti inter a-z, A-Z. La longeco devas esti inter 6 kaj 16."), ("Website", "Retejo"), ("About", "Pri"), ("Slogan_tip", "Farita kun koro en ĉi tiu Ä¥aosa mondo!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Pasvorto de la operaciumo"), ("install_tip", "Vi ne uzas instalita versio. Pro limigoj pro UAC, kiel aparato kontrolata, en kelkaj kazoj, ne estos ebla kontroli la muson kaj klavaron aÅ­ registri la ekranon. Bonvolu alkliku la butonon malsupre por instali RustDesk sur la operaciumo por eviti la demando supre."), ("Click to upgrade", "Alklaki por plibonigi"), - ("Click to download", "Alklaki por elÅuti"), - ("Click to update", "Alklaki por Äisdatigi"), ("Configure", "Konfiguri"), ("config_acc", "Por uzi vian foran aparaton, bonvolu doni la permeson \"alirebleco\" al RustDesk."), ("config_screen", "Por uzi vian foran aparaton, bonvolu doni la permeson \"ekranregistrado\" al RustDesk."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Neniu permeso de dosiertransigo"), ("Note", "Notu"), ("Connection", "Konekto"), - ("Share Screen", "Kunhavigi Ekranon"), + ("Share screen", "Kunhavigi Ekranon"), ("Chat", "Babilo"), ("Total", "Sumo"), ("items", "eroj"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ekrankapto"), ("Input Control", "Eniga Kontrolo"), ("Audio Capture", "Sonkontrolo"), - ("File Connection", "Dosiero Konekto"), - ("Screen Connection", "Ekrono konekto"), ("Do you accept?", "Ĉu vi akceptas?"), ("Open System Setting", "Malfermi Sistemajn Agordojn"), ("How to get Android input permission?", "Kiel akiri Android enigajn permesojn"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Rigardi kameron"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index a3437e01f0b..471d7bd73db 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "de %min% a %max% de longitud"), ("starts with a letter", "comenzar con una letra"), ("allowed characters", "Caracteres permitidos"), - ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), + ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9, - (dash) e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."), ("Website", "Sitio web"), ("About", "Acerca de"), ("Slogan_tip", "¡Hecho con corazón en este mundo caótico!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Contraseña del sistema operativo"), ("install_tip", "Debido al Control de cuentas de usuario, es posible que RustDesk no funcione correctamente como escritorio remoto. Para evitar este problema, haga clic en el botón de abajo para instalar RustDesk a nivel de sistema."), ("Click to upgrade", "Clic para actualizar"), - ("Click to download", "Clic para descargar"), - ("Click to update", "Clic para refrescar"), ("Configure", "Configurar"), ("config_acc", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Accesibilidad\"."), ("config_screen", "Para controlar su escritorio desde el exterior, debe otorgar permiso a RustDesk de \"Grabación de pantalla\"."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Sin permiso de transferencia de archivos"), ("Note", "Nota"), ("Connection", "Conexión"), - ("Share Screen", "Compartir pantalla"), + ("Share screen", "Compartir pantalla"), ("Chat", "Chat"), ("Total", "Total"), ("items", "items"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de pantalla"), ("Input Control", "Control de entrada"), ("Audio Capture", "Captura de audio"), - ("File Connection", "Conexión de archivos"), - ("Screen Connection", "Conexión de pantalla"), ("Do you accept?", "¿Aceptas?"), ("Open System Setting", "Configuración del sistema abierto"), ("How to get Android input permission?", "¿Cómo obtener el permiso de entrada de Android?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "¿Sin pares favoritos aún?\nEncontremos uno al que conectarte y ¡añádelo a tus favoritos!"), ("empty_lan_tip", "Oh no, parece que aún no has descubierto ningún par."), ("empty_address_book_tip", "Parece que actualmente no hay pares en tu directorio."), - ("eg: admin", "ej.: admin"), ("Empty Username", "Nombre de usuario vacío"), ("Empty Password", "Contraseña vacía"), ("Me", "Yo"), @@ -654,7 +649,65 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "Subir archivos"), ("Clipboard is synchronized", "Portapapeles sincronizado"), ("Update client clipboard", "Actualizar portapapeles del cliente"), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Untagged", "Sin itiquetar"), + ("new-version-of-{}-tip", "Hay una nueva versión de {} disponible"), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Por favor, actualiza el cliente RustDesk a la versión {} o superior en el lado remoto"), + ("d3d_render_tip", "Al activar el renderizado D3D, la pantalla de control remoto puede verse negra en algunos equipos."), + ("Use D3D rendering", "Usar renderizado D3D"), + ("Printer", "Impresora"), + ("printer-os-requirement-tip", "La función de salida de impresora necesita Windows 10 o superior."), + ("printer-requires-installed-{}-client-tip", "Para usar la impresión remota, {} necesita estar instalado en tu dispositivo."), + ("printer-{}-not-installed-tip", "La impresora {} no está instalada."), + ("printer-{}-ready-tip", "La impresora {} está instalada y lista para usar."), + ("Install {} Printer", "Instalar la impresora {}"), + ("Outgoing Print Jobs", "Tareas salientes de impresión"), + ("Incoming Print Jobs", "Tareas entrantes de impresión"), + ("Incoming Print Job", "Trabajo entrante de impresión"), + ("use-the-default-printer-tip", "Usar la impresora predeterminada"), + ("use-the-selected-printer-tip", "Usar la impresora seleccionada"), + ("auto-print-tip", "Imprimir automáticamente usando la impresora seleccionada."), + ("print-incoming-job-confirm-tip", "Has recibido una tarea de impresión remota. ¿Deseas ejecutarla en tu lado?"), + ("remote-printing-disallowed-tile-tip", "Impresión remota inhabilitada"), + ("remote-printing-disallowed-text-tip", "Los ajustes de permisos del lado controlado no permiten la impresión remota."), + ("save-settings-tip", "Guardar ajustes"), + ("dont-show-again-tip", "No volver a mostrar"), + ("Take screenshot", "Tomar captura de pantalla"), + ("Taking screenshot", "Tomando captura de pantalla"), + ("screenshot-merged-screen-not-supported-tip", "La fusión de capturas de pantalla de múltiples monitores no está soportada. Por favor, cambie a un monitor e inténtelo de nuevo."), + ("screenshot-action-tip", "Por favor, seleccione cómo continuar con la captura de pantalla."), + ("Save as", "Guardar como"), + ("Copy to clipboard", "Copiar al portapapeles"), + ("Enable remote printer", "Habilitar impresora remota"), + ("Downloading {}", "Descarngando {}"), + ("{} Update", "{} Actualizar"), + ("{}-to-update-tip", "{} Se cerrará ahora e instalará la nueva versión."), + ("download-new-version-failed-tip", "Descarga fallida. Puedes volver a intentarlo o hacer clic en el botón \"Download\" para descargar desde la página de lanzamientos y actualizar manualmente."), + ("Auto update", "Auto actualizar"), + ("update-failed-check-msi-tip", "Comprobación de método de instalación fallida. Por favor, haz clic en el botón \"Download\" para descargar desde la página de lanzamientos y actualizar manualmente."), + ("websocket_tip", "Al usar WebSocket, solo se permiten conexiones relay."), + ("Use WebSocket", "Usar WebSocket"), + ("Trackpad speed", "Velocidad de trackpad"), + ("Default trackpad speed", "Velocidad predeterminada de trackpad"), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Ver cámara"), + ("Enable camera", "Habilitar cámara"), + ("No cameras", "No hay cámaras"), + ("view_camera_unsupported_tip", "El dispositivo remoto no soporta la visualización de la cámara."), + ("Terminal", ""), + ("Enable terminal", "Habilitar terminal"), + ("New tab", "Nueva pestaña"), + ("Keep terminal sessions on disconnect", "Mantener sesiones de terminal al desconectar"), + ("Terminal (Run as administrator)", "Terminal (Ejecutar como administrador)"), + ("terminal-admin-login-tip", "Por favor, introduzca el usuario y la contrasseña del administrador en el lado controlado."), + ("Failed to get user token.", "No se ha podido obtener el token de usuario"), + ("Incorrect username or password.", "Nombre y contraseña incorrectos"), + ("The user is not an administrator.", "El usuario no es un administrador."), + ("Failed to check if the user is an administrator.", "No se ha podido comprobar si el usuario es un administrador."), + ("Supported only in the installed version.", "Soportado solo en la versión instalada."), + ("elevation_username_tip", "Introduzca el nombre de usuario o dominio\\NombreDeUsuario"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/et.rs b/src/lang/et.rs index b38b55bd615..507b580c4f5 100644 --- a/src/lang/et.rs +++ b/src/lang/et.rs @@ -1,254 +1,252 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", ""), - ("Your Desktop", ""), + ("Status", "Olek"), + ("Your Desktop", "Sinu töölaud"), ("desk_tip", "Sinu töölauale saab selle ID ja parooliga ligi pääseda."), - ("Password", ""), - ("Ready", ""), - ("Established", ""), + ("Password", "Parool"), + ("Ready", "Valmis"), + ("Established", "Ühendus loodud"), ("connecting_status", "RustDeski võrguga ühendumine..."), - ("Enable service", ""), - ("Start service", ""), - ("Service is running", ""), - ("Service is not running", ""), + ("Enable service", "Luba teenus"), + ("Start service", "Käivita teenus"), + ("Service is running", "Teenus töötab"), + ("Service is not running", "Teenus ei tööta"), ("not_ready_status", "Pole valmis. Palun kontrolli oma ühendust"), - ("Control Remote Desktop", ""), - ("Transfer file", ""), - ("Connect", ""), - ("Recent sessions", ""), - ("Address book", ""), - ("Confirmation", ""), - ("TCP tunneling", ""), - ("Remove", ""), - ("Refresh random password", ""), - ("Set your own password", ""), - ("Enable keyboard/mouse", ""), - ("Enable clipboard", ""), - ("Enable file transfer", ""), - ("Enable TCP tunneling", ""), - ("IP Whitelisting", ""), + ("Control Remote Desktop", "Juhi kaugtöölauda"), + ("Transfer file", "Edasta fail"), + ("Connect", "Ühenda"), + ("Recent sessions", "Viimatised seansid"), + ("Address book", "Aadressiraamat"), + ("Confirmation", "Kinnitus"), + ("TCP tunneling", "TCP-tunneldamine"), + ("Remove", "Eemalda"), + ("Refresh random password", "Värskenda juhuslik parool"), + ("Set your own password", "Määra oma parool"), + ("Enable keyboard/mouse", "Luba klaviatuur/hiir"), + ("Enable clipboard", "Luba lõikelaud"), + ("Enable file transfer", "Luba failiedastus"), + ("Enable TCP tunneling", "Luba TCP-tunneldamine"), + ("IP Whitelisting", "IP lubamisloend"), ("ID/Relay Server", "ID-/releeserver"), - ("Import server config", ""), - ("Export Server Config", ""), - ("Import server configuration successfully", ""), - ("Export server configuration successfully", ""), - ("Invalid server configuration", ""), - ("Clipboard is empty", ""), - ("Stop service", ""), - ("Change ID", ""), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", "Lubatud on vaid a-z, A-Z, 0-9 ja _ (alakriips) tähemärgid. Esimene täht peab olema a-z või A-Z. Pikkus vahemikus 6-16."), - ("Website", ""), - ("About", ""), + ("Import server config", "Impordi serveriseadistus"), + ("Export Server Config", "Ekspordi serveriseadistus"), + ("Import server configuration successfully", "Serveriseadistus edukalt imporditud"), + ("Export server configuration successfully", "Serveriseadistus edukalt eksporditud"), + ("Invalid server configuration", "Sobimatu serveriseadistus"), + ("Clipboard is empty", "Lõikelaud on tühi"), + ("Stop service", "Peata teenus"), + ("Change ID", "Muuda ID-d"), + ("Your new ID", "Sinu uus ID"), + ("length %min% to %max%", "pikkus %min%-%max%"), + ("starts with a letter", "algab tähega"), + ("allowed characters", "lubatud tähemärgid"), + ("id_change_tip", "Lubatud on vaid a-z, A-Z, 0-9, - (kriips) ja _ (alakriips) tähemärgid. Esimene täht peab olema a-z või A-Z. Pikkus vahemikus 6-16."), + ("Website", "Veebileht"), + ("About", "Meist"), ("Slogan_tip", "Loodud südamega selles kaootilises maailmas!"), - ("Privacy Statement", ""), - ("Mute", ""), + ("Privacy Statement", "Privaatsusteatis"), + ("Mute", "Hääletu"), ("Build Date", "Ehituskuupäev"), - ("Version", ""), - ("Home", ""), + ("Version", "Versioon"), + ("Home", "Kodu"), ("Audio Input", "Helisisend"), - ("Enhancements", ""), + ("Enhancements", "Täiendused"), ("Hardware Codec", "Riistvarakoodek"), - ("Adaptive bitrate", ""), + ("Adaptive bitrate", "Kohanduv bitikiirus"), ("ID Server", "ID-server"), ("Relay Server", "Releeserver"), ("API Server", "Rakendusliidese server"), ("invalid_http", "peab algama: http:// või https://"), - ("Invalid IP", ""), - ("Invalid format", ""), + ("Invalid IP", "Sobimatu IP"), + ("Invalid format", "Sobimatu vorming"), ("server_not_support", "Pole veel serveri poolt toetatud"), - ("Not available", ""), - ("Too frequent", ""), - ("Cancel", ""), - ("Skip", ""), - ("Close", ""), - ("Retry", ""), - ("OK", ""), + ("Not available", "Pole saadaval"), + ("Too frequent", "Liiga sagedane"), + ("Cancel", "Tühista"), + ("Skip", "Jäta vahele"), + ("Close", "Sulge"), + ("Retry", "Proovi uuesti"), + ("OK", "OK"), ("Password Required", "Parool on nõutud"), - ("Please enter your password", ""), - ("Remember password", ""), + ("Please enter your password", "Palun sisesta oma parool"), + ("Remember password", "Jäta parool meelde"), ("Wrong Password", "Vale parool"), - ("Do you want to enter again?", ""), + ("Do you want to enter again?", "Kas soovid uuesti sisestada?"), ("Connection Error", "Ühenduse viga"), - ("Error", ""), - ("Reset by the peer", ""), - ("Connecting...", ""), - ("Connection in progress. Please wait.", ""), - ("Please try 1 minute later", ""), + ("Error", "Viga"), + ("Reset by the peer", "Partneri poolt lähtestatud"), + ("Connecting...", "Ühendamine..."), + ("Connection in progress. Please wait.", "Ühendus on käimas. Palun oota."), + ("Please try 1 minute later", "Palun proovi 1 minuti pärast"), ("Login Error", "Sisselogimise viga"), - ("Successful", ""), - ("Connected, waiting for image...", ""), - ("Name", ""), - ("Type", ""), - ("Modified", ""), - ("Size", ""), + ("Successful", "Edukas"), + ("Connected, waiting for image...", "Ühendatud, pildi ootamine..."), + ("Name", "Nimi"), + ("Type", "Tüüp"), + ("Modified", "Muudetud"), + ("Size", "Suurus"), ("Show Hidden Files", "Kuva peidetud faile"), - ("Receive", ""), - ("Send", ""), + ("Receive", "Võta vastu"), + ("Send", "Saada"), ("Refresh File", "Värskenda faili"), - ("Local", ""), - ("Remote", ""), + ("Local", "Kohalik"), + ("Remote", "Kaugjuhitav"), ("Remote Computer", "Kaugarvuti"), ("Local Computer", "Kohalik arvuti"), ("Confirm Delete", "Kinnita kustutamist"), - ("Delete", ""), - ("Properties", ""), + ("Delete", "Kustuta"), + ("Properties", "Atribuudid"), ("Multi Select", "Mitmikvalik"), ("Select All", "Vali kõik"), ("Unselect All", "Tühista kõigi valik"), ("Empty Directory", "Tühi kaust"), - ("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", ""), + ("Not an empty directory", "Pole tühi kaust"), + ("Are you sure you want to delete this file?", "Kas soovid kindlasti selle faili eemaldada?"), + ("Are you sure you want to delete this empty directory?", "Kas soovid kindlasti selle tühja kausta eemaldada?"), + ("Are you sure you want to delete the file of this directory?", "Kas soovid kindlasti selle kausta faili eemaldada?"), + ("Do this for all conflicts", "Tee see kõigi konfliktide puhul"), + ("This is irreversible!", "See on pöördumatu!"), + ("Deleting", "Kustutamine"), + ("files", "failid"), + ("Waiting", "Ootamine"), + ("Finished", "Lõpetatud"), + ("Speed", "Kiirus"), ("Custom Image Quality", "Kohandatud pildikvaliteet"), - ("Privacy mode", ""), - ("Block user input", ""), - ("Unblock user input", ""), + ("Privacy mode", "Privaatsusrežiim"), + ("Block user input", "Blokeeri kasutajasisend"), + ("Unblock user input", "Eemalda kasutajasisendi blokeering"), ("Adjust Window", "Kohanda akent"), - ("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 Ctrl + Alt + Del", ""), + ("Original", "Algne"), + ("Shrink", "Vähenda"), + ("Stretch", "Venita"), + ("Scrollbar", "Kerimisriba"), + ("ScrollAuto", "Automaatne kerimine"), + ("Good image quality", "Hea pildikvaliteet"), + ("Balanced", "Tasakaalustatud"), + ("Optimize reaction time", "Optimeeri reageerimisaeg"), + ("Custom", "Kohandatud"), + ("Show remote cursor", "Kuva kaugkursorit"), + ("Show quality monitor", "Kuva kvaliteedijälgija"), + ("Disable clipboard", "Keela lõikelaud"), + ("Lock after session end", "Lukusta pärast seansi lõppu"), + ("Insert Ctrl + Alt + Del", "Sisesta Ctrl + Alt + Del"), ("Insert Lock", "Sisesta lukk"), - ("Refresh", ""), - ("ID does not exist", ""), - ("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", ""), + ("Refresh", "Värskenda"), + ("ID does not exist", "ID-d ei eksisteeri"), + ("Failed to connect to rendezvous server", "Kohtumisserveriga ühendumine ebaõnnestus"), + ("Please try later", "Palun proovi hiljem"), + ("Remote desktop is offline", "Kaugtöölaud on väljas"), + ("Key mismatch", "Võtme sobimatus"), + ("Timeout", "Ajalõpp"), + ("Failed to connect to relay server", "Releeserveriga ühendumine ebaõnnestus"), + ("Failed to connect via rendezvous server", "Kohtumisserveri kaudu ühendumine ebaõnnestus"), + ("Failed to connect via relay server", "Releeserveri kaudu ühendumine ebaõnnestus"), + ("Failed to make direct connection to remote desktop", "Kaugtöölauaga otsese ühenduse loomine ebaõnnestus"), ("Set Password", "Määra parool"), ("OS Password", "Opsüsteemi parool"), ("install_tip", "Kasutajakonto kontrolli (UAC) tõttu ei saa RustDesk mõnel juhul korralikult kaugjuhtimispoolena töötada. Kontrolli vältimiseks palun klõpsa alloleval nupul, et RustDesk oma süsteemi paigaldada."), - ("Click to upgrade", ""), - ("Click to download", ""), - ("Click to update", ""), - ("Configure", ""), + ("Click to upgrade", "Vajuta täiendamiseks"), + ("Configure", "Seadista"), ("config_acc", "Töölaua kaugjuhtimiseks tuleb RustDeskile anda \"juurdepääsetavuse\" õigused."), ("config_screen", "Töölaua kaugjuhtimiseks tuleb RustDeskile anda \"ekraanisalvestuse\" õigused."), - ("Installing ...", ""), - ("Install", ""), - ("Installation", ""), + ("Installing ...", "Installimine..."), + ("Install", "Installi"), + ("Installation", "Paigaldus"), ("Installation Path", "Paigaldustee"), - ("Create start menu shortcuts", ""), - ("Create desktop icon", ""), + ("Create start menu shortcuts", "Loo Start-menüü otseteed"), + ("Create desktop icon", "Loo töölauaikoon"), ("agreement_tip", "Paigalduse alustamisel nõustud litsentsilepinguga."), ("Accept and Install", "Nõustu ja paigalda"), - ("End-user license agreement", ""), - ("Generating ...", ""), - ("Your installation is lower version.", ""), - ("not_close_tcp_tip", "Ara sulge seda akent, kuni kasutad tunnelit"), - ("Listening ...", ""), + ("End-user license agreement", "Lõppkasutaja litsentsileping"), + ("Generating ...", "Loomine..."), + ("Your installation is lower version.", "Sinu paigaldus kasutab vanemat versiooni."), + ("not_close_tcp_tip", "Ära sulge seda akent, kuni kasutad tunnelit"), + ("Listening ...", "Kuulamine..."), ("Remote Host", "Kaughost"), ("Remote Port", "Kaugport"), - ("Action", ""), - ("Add", ""), + ("Action", "Tegevus"), + ("Add", "Lisa"), ("Local Port", "Kohalik port"), ("Local Address", "Kohalik aadress"), ("Change Local Port", "Muuda kohalikku porti"), ("setup_server_tip", "Kiirema ühenduse jaoks palun seadista oma server"), - ("Too short, at least 6 characters.", ""), - ("The confirmation is not identical.", ""), - ("Permissions", ""), - ("Accept", ""), - ("Dismiss", ""), - ("Disconnect", ""), - ("Enable file copy and paste", ""), - ("Connected", ""), - ("Direct and encrypted connection", ""), - ("Relayed and encrypted connection", ""), - ("Direct and unencrypted connection", ""), - ("Relayed and unencrypted connection", ""), + ("Too short, at least 6 characters.", "Liiga lühike, peab olema vähemalt 6 tähemärki."), + ("The confirmation is not identical.", "Kinnitus ei ole identne."), + ("Permissions", "Õigused"), + ("Accept", "Nõustu"), + ("Dismiss", "Loobu"), + ("Disconnect", "Ühendu lahti"), + ("Enable file copy and paste", "Luba failide kopeerimine ja kleepimine"), + ("Connected", "Ühendatud"), + ("Direct and encrypted connection", "Otsene ja krüpteeritud ühendus"), + ("Relayed and encrypted connection", "Vahendatud ja krüpteeritud ühendus"), + ("Direct and unencrypted connection", "Otsene ja krüpteerimata ühendus"), + ("Relayed and unencrypted connection", "Vahendatud ja krüpteerimata ühendus"), ("Enter Remote ID", "Sisesta kaug-ID"), - ("Enter your password", ""), - ("Logging in...", ""), - ("Enable RDP session sharing", ""), + ("Enter your password", "Sisesta oma parool"), + ("Logging in...", "Sisselogimine..."), + ("Enable RDP session sharing", "Luba RDP-seansi jagamine"), ("Auto Login", "Logi automaatselt sisse (Kehtib vaid valiku \"lukusta pärast seansi lõppu\" lubamisel)"), - ("Enable direct IP access", ""), - ("Rename", ""), - ("Space", ""), - ("Create desktop shortcut", ""), + ("Enable direct IP access", "Luba otsene IP-juurdepääs"), + ("Rename", "Nimeta ümber"), + ("Space", "Ruum"), + ("Create desktop shortcut", "Loo töölauaotsetee"), ("Change Path", "Muuda failiteed"), ("Create Folder", "Loo kaust"), - ("Please enter the folder name", ""), - ("Fix it", ""), - ("Warning", ""), - ("Login screen using Wayland is not supported", ""), - ("Reboot required", ""), - ("Unsupported display server", ""), - ("x11 expected", ""), - ("Port", ""), - ("Settings", ""), - ("Username", ""), - ("Invalid port", ""), - ("Closed manually by the peer", ""), - ("Enable remote configuration modification", ""), - ("Run without install", ""), - ("Connect via relay", ""), - ("Always connect via relay", ""), + ("Please enter the folder name", "Palun sisesta kausta nimi"), + ("Fix it", "Paranda see"), + ("Warning", "Hoiatus"), + ("Login screen using Wayland is not supported", "Waylandi kasutav sisselogimisekraan ei ole toetatud"), + ("Reboot required", "Taaskäivitus nõutud"), + ("Unsupported display server", "Mittetoetatud kuvaserver"), + ("x11 expected", "Oodatakse x11"), + ("Port", "Port"), + ("Settings", "Seaded"), + ("Username", "Kasutajanimi"), + ("Invalid port", "Sobimatu port"), + ("Closed manually by the peer", "Partneri poolt käsitsi suletud"), + ("Enable remote configuration modification", "Luba kaugtöölaua seadistuse muutmine"), + ("Run without install", "Käivita paigaldamata"), + ("Connect via relay", "Ühenda relee kaudu"), + ("Always connect via relay", "Ühenda alati relee kaudu"), ("whitelist_tip", "Ainult lubamisloendis IP saab mulle ligi"), - ("Login", ""), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), + ("Login", "Logi sisse"), + ("Verify", "Kinnita"), + ("Remember me", "Jäta mind meelde"), + ("Trust this device", "Usalda seda seadet"), + ("Verification code", "Kinnituskood"), ("verification_tip", "Registreeritud e-posti aadressile on saadetud kinnituskood, sisselogimise jätkamiseks sisesta kinnituskood."), - ("Logout", ""), - ("Tags", ""), - ("Search ID", ""), + ("Logout", "Logi välja"), + ("Tags", "Sildid"), + ("Search ID", "Otsi ID-d"), ("whitelist_sep", "Eraldatud koma, semikooloni, tühikute või uue reaga"), - ("Add ID", ""), + ("Add ID", "Lisa ID"), ("Add Tag", "Lisa silt"), - ("Unselect all tags", ""), - ("Network error", ""), - ("Username missed", ""), - ("Password missed", ""), + ("Unselect all tags", "Tühista kõik sildid"), + ("Network error", "Võrgu viga"), + ("Username missed", "Kasutajanimi on puudu"), + ("Password missed", "Parool on puudu"), ("Wrong credentials", "Vale kasutajanimi või parool"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "Kinnituskood on vale või aegunud"), ("Edit Tag", "Muuda silti"), ("Forget Password", "Unusta parool"), - ("Favorites", ""), + ("Favorites", "Lemmikud"), ("Add to Favorites", "Lisa lemmikutesse"), ("Remove from Favorites", "Eemalda lemmikutest"), - ("Empty", ""), - ("Invalid folder name", ""), + ("Empty", "Tühi"), + ("Invalid folder name", "Kehtetu kaustanimi"), ("Socks5 Proxy", "Socks5 proksi"), ("Socks5/Http(s) Proxy", "Socks5/Http(s) proksi"), - ("Discovered", ""), + ("Discovered", "Avastatud"), ("install_daemon_tip", "Süsteemikäivitusel käivitamiseks tuleb paigaldada süsteemiteenus."), - ("Remote ID", ""), - ("Paste", ""), - ("Paste here?", ""), + ("Remote ID", "Kaug-ID"), + ("Paste", "Kleebi"), + ("Paste here?", "Kleebid siia?"), ("Are you sure to close the connection?", "Kas soovid kindlasti ühenduse sulgeda?"), - ("Download new version", ""), - ("Touch mode", ""), - ("Mouse mode", ""), + ("Download new version", "Laadi alla uus versioon"), + ("Touch mode", "Puuterežiim"), + ("Mouse mode", "Hiirerežiim"), ("One-Finger Tap", "Ühe sõrme koputus"), ("Left Mouse", "Vasak hiireklahv"), ("One-Long Tap", "Üks pikk koputus"), @@ -263,23 +261,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Canvas Move", "Lõuendi liigutus"), ("Pinch to Zoom", "Näpistus-suum"), ("Canvas Zoom", "Lõuendi suum"), - ("Reset canvas", ""), - ("No permission of file transfer", ""), - ("Note", ""), - ("Connection", ""), - ("Share Screen", "Jaga ekraani"), - ("Chat", ""), - ("Total", ""), - ("items", ""), - ("Selected", ""), + ("Reset canvas", "Lähtesta lõuend"), + ("No permission of file transfer", "Failiülekande luba puudub"), + ("Note", "Märkus"), + ("Connection", "Ühendus"), + ("Share screen", "Jaga ekraani"), + ("Chat", "Vestlus"), + ("Total", "Kokku"), + ("items", "üksust"), + ("Selected", "Valitud"), ("Screen Capture", "Ekraanisalvestus"), ("Input Control", "Sisendjuhtimine"), ("Audio Capture", "Helisalvestus"), - ("File Connection", "Failiühendus"), - ("Screen Connection", "Kuvaühendus"), - ("Do you accept?", ""), + ("Do you accept?", "Kas nõustud?"), ("Open System Setting", "Ava süsteemisätted"), - ("How to get Android input permission?", ""), + ("How to get Android input permission?", "Kuidas saada Androidi sisendi luba?"), ("android_input_permission_tip1", "Selleks, et kaugseade saaks sinu Androidi seadet juhtida hiire või puute abil, pead andma RustDeskile \"juurdepääsetavuse\" loa."), ("android_input_permission_tip2", "Palun mine järgmisele süsteemiseadete lehele, leia ja sisesta [Paigaldatud teenused], lülita teenus [RustDesk Input] sisse."), ("android_new_connection_tip", "Saabunud on uus juhtimistaotlus, mis soovib sinu praegust seadet juhtida."), @@ -288,46 +284,46 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("android_version_audio_tip", "Kasutatav Androidi versioon ei toeta helisalvestust, palun täienda Android 10 või uuemale versioonile."), ("android_start_service_tip", "Koputa [Alusta teenust] või anna [Ekraanisalvestuse] luba, et ekraanijagamisteenust alustada."), ("android_permission_may_not_change_tip", "Loodud ühenduste õigused ei pruugi muutuda enne taasühendamist koheselt."), - ("Account", ""), - ("Overwrite", ""), - ("This file exists, skip or overwrite this file?", ""), - ("Quit", ""), - ("Help", ""), - ("Failed", ""), - ("Succeeded", ""), - ("Someone turns on privacy mode, exit", ""), - ("Unsupported", ""), - ("Peer denied", ""), - ("Please install plugins", ""), - ("Peer exit", ""), - ("Failed to turn off", ""), - ("Turned off", ""), - ("Language", ""), - ("Keep RustDesk background service", ""), + ("Account", "Konto"), + ("Overwrite", "Ülekirjutamine"), + ("This file exists, skip or overwrite this file?", "See fail eksisteerib, kas soovid selle vahele jätta või ülekirjutada?"), + ("Quit", "Välju"), + ("Help", "Abi"), + ("Failed", "Ebaõnnestus"), + ("Succeeded", "Õnnestus"), + ("Someone turns on privacy mode, exit", "Keegi lülitab sisse privaatsusrežiimi, välju"), + ("Unsupported", "Mittetoetatud"), + ("Peer denied", "Partner keeldus"), + ("Please install plugins", "Palun paigalda pluginad"), + ("Peer exit", "Partner väljub"), + ("Failed to turn off", "Väljalülitamine ebaõnnestus"), + ("Turned off", "Väljalülitatud"), + ("Language", "Keel"), + ("Keep RustDesk background service", "Säilita RustDeski taustateenus"), ("Ignore Battery Optimizations", "Ignoreeri akuoptimeerimisi"), ("android_open_battery_optimizations_tip", "Kui soovid selle funktsiooni keelata, palun mine järgmisele RustDeski rakenduse seadete lehele, leia ja sisesta [Aku], eemalda linnuke valikult [Piiramata]"), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), - ("Use permanent password", ""), - ("Use both passwords", ""), - ("Set permanent password", ""), - ("Enable remote restart", ""), - ("Restart remote device", ""), - ("Are you sure you want to restart", ""), - ("Restarting remote device", ""), + ("Start on boot", "Käivita seadme käivitamisel"), + ("Start the screen sharing service on boot, requires special permissions", "Käivita ekraanijagamise teenus seadme käivitamisel, nõuab eriõigusi"), + ("Connection not allowed", "Ühendus ei ole lubatud"), + ("Legacy mode", "Pärandrežiim"), + ("Map mode", "Kaardirežiim"), + ("Translate mode", "Tõlkerežiim"), + ("Use permanent password", "Kasuta püsiparooli"), + ("Use both passwords", "Kasuta mõlemat parooli"), + ("Set permanent password", "Seadista püsiparool"), + ("Enable remote restart", "Luba kaugtaaskäivitamine"), + ("Restart remote device", "Taaskäivita kaugseade"), + ("Are you sure you want to restart", "Kas oled kindel, et soovid taaskäivitada"), + ("Restarting remote device", "Kaugseadme taaskäivitamine"), ("remote_restarting_tip", "Kaugseade taaskäivitub, palun sulge see sõnumikast ja ühendu mõne aja pärast uuesti püsiva parooliga."), - ("Copied", ""), - ("Exit Fullscreen", "Lahku täisekraanist"), - ("Fullscreen", ""), + ("Copied", "Kopeeritud"), + ("Exit Fullscreen", "Välju täisekraanist"), + ("Fullscreen", "Täisekraan"), ("Mobile Actions", "Mobiilitegevused"), ("Select Monitor", "Vali kuvar"), ("Control Actions", "Juhtimistegevused"), ("Display Settings", "Kuvasätted"), - ("Ratio", ""), + ("Ratio", "Kuvasuhe"), ("Image Quality", "Pildikvaliteet"), ("Scroll Style", "Kerimisstiil"), ("Show Toolbar", "Kuva tööriistariba"), @@ -336,142 +332,141 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Releeühendus"), ("Secure Connection", "Turvaline ühendus"), ("Insecure Connection", "Ebaturvaline ühendus"), - ("Scale original", ""), - ("Scale adaptive", ""), - ("General", ""), - ("Security", ""), - ("Theme", ""), + ("Scale original", "Originaalskaala"), + ("Scale adaptive", "Kohanduv skaala"), + ("General", "Üldine"), + ("Security", "Turvalisus"), + ("Theme", "Teema"), ("Dark Theme", "Tume teema"), ("Light Theme", "Hele teema"), - ("Dark", ""), - ("Light", ""), + ("Dark", "Tume"), + ("Light", "Hele"), ("Follow System", "Järgi süsteemi"), - ("Enable hardware codec", ""), + ("Enable hardware codec", "Luba riistvarakooder"), ("Unlock Security Settings", "Lukusta lahti turvasätted"), - ("Enable audio", ""), + ("Enable audio", "Luba heli"), ("Unlock Network Settings", "Lukusta lahti võrgusätted"), - ("Server", ""), + ("Server", "Server"), ("Direct IP Access", "Otsene IP-ligipääs"), - ("Proxy", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), + ("Proxy", "Proksi"), + ("Apply", "Rakenda"), + ("Disconnect all devices?", "Ühendad kõik seadmed lahti?"), + ("Clear", "Tühjenda"), ("Audio Input Device", "Heli sisendseade"), ("Use IP Whitelisting", "Kasuta IP-lubamisloendit"), - ("Network", ""), + ("Network", "Võrk"), ("Pin Toolbar", "Kinnita tööriistariba"), ("Unpin Toolbar", "Eemalda tööriistariba kinnitus"), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Automatically record outgoing sessions", ""), - ("Change", ""), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable recording session", ""), - ("Enable LAN discovery", ""), - ("Deny LAN discovery", ""), - ("Write a message", ""), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), + ("Recording", "Salvestamine"), + ("Directory", "Kaust"), + ("Automatically record incoming sessions", "Salvesta alati sisenevad ühendused"), + ("Automatically record outgoing sessions", "Salvesta alati väljuvad ühendused"), + ("Change", "Muuda"), + ("Start session recording", "Alusta seansisalvestust"), + ("Stop session recording", "Peata seansisalvestus"), + ("Enable recording session", "Luba seansisalvestus"), + ("Enable LAN discovery", "Luba LAN-avastamine"), + ("Deny LAN discovery", "Keela LAN-avastamine"), + ("Write a message", "Kirjuta sõnum"), + ("Prompt", "Küsi"), + ("Please wait for confirmation of UAC...", "Palun oota UAC (kasutajakonto kontroll) kinnitust..."), ("elevated_foreground_window_tip", "Kaugtöölaua praegune aken nõuab töötamiseks kõrgemaid õigusi, mistõttu ei saa see ajutiselt hiirt ja klaviatuuri kasutada. Võid kaugkasutajal paluda minimeerida praegune aken või klõpsata ühenduse haldamise aknas kõrgendatud loa nuppu. Selle probleemi vältimiseks on soovitatav kaugseadmesse tarkvara paigaldada."), - ("Disconnected", ""), - ("Other", ""), - ("Confirm before closing multiple tabs", ""), + ("Disconnected", "Ühendus katkestatud"), + ("Other", "Muu"), + ("Confirm before closing multiple tabs", "Kinnita enne mitme vahekaardi sulgemist"), ("Keyboard Settings", "Klaviatuurisätted"), ("Full Access", "Täielik ligipääs"), ("Screen Share", "Ekraanijagamine"), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), - ("JumpLink", "Kuva"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nõuab Ubuntu 21.04 või uuemat versiooni."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nõuab Linuxi distributsiooni uuemat versiooni. Palun proovi X11 töölaual või muuda oma operatsioonisüsteemi."), + ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Palun vali jagatav ekraan (tegutse partneri poolel)."), - ("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...", ""), + ("Show RustDesk", "Kuva RustDesk"), + ("This PC", "See arvuti"), + ("or", "või"), + ("Continue with", "Jätka koos"), + ("Elevate", "Tõsta"), + ("Zoom cursor", "Suumi kursorit"), + ("Accept sessions via password", "Aktsepteeri seansid parooli kaudu"), + ("Accept sessions via click", "Aktsepteeri seansid klõpsamise kaudu"), + ("Accept sessions via both", "Aktsepteeri seansid mõlema kaudu"), + ("Please wait for the remote side to accept your session request...", "Palun oota, kuni kaugpool aktsepteerib sinu seansitaotluse..."), ("One-time Password", "Ühekordne parool"), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), + ("Use one-time password", "Kasuta ühekordset parooli"), + ("One-time password length", "Ühekordse parooli pikkus"), + ("Request access to your device", "Taotle oma seadmele juurdepääsu"), + ("Hide connection management window", "Peida ühenduse haldamise aken"), ("hide_cm_tip", "Luba varjamine ainult siis, kui parooliga seansse võetakse vastu ning kasutatakse püsivat parooli."), ("wayland_experiment_tip", "Waylandi tugi on katsetusjärgus, järelvalveta juurdepääsu vajadusel palun kasuta X11."), - ("Right click to select tabs", ""), - ("Skipped", ""), - ("Add to address book", ""), - ("Group", ""), - ("Search", ""), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), + ("Right click to select tabs", "Paremklõpsa vahekaartide valimiseks"), + ("Skipped", "Vahelejäetud"), + ("Add to address book", "Lisa aadressiraamatusse"), + ("Group", "Grupeeri"), + ("Search", "Otsi"), + ("Closed manually by web console", "Veebikonsooli kaudu käsitsi suletud"), + ("Local keyboard type", "Kohalik klaviatuuritüüp"), + ("Select local keyboard type", "Vali kohalik klaviatuuritüüp"), ("software_render_tip", "Kui kasutad Linuxis Nvidia graafikakaarti ja kaugaken sulgub kohe pärast ühendamist, võib aidata üleminek avatud lähtekoodiga Nouveau draiverile ja valida tarkvaraline renderdamise. Vajalik on tarkvara taaskäivitamine."), - ("Always use software rendering", ""), + ("Always use software rendering", "Kasuta alati tarkvaralist renderdust"), ("config_input", "Kaugtöölaua klaviatuuriga juhtimiseks pead andma RustDeskile \"sisendi jälgimise\" õigused."), ("config_microphone", "Kaugelt rääkimiseks pead andma RustDeskile \"heli salvestamise\" õigused."), ("request_elevation_tip", "Sa võid kõrgendatud õigusi taotleda ka siis, kui keegi on kaugpoolel."), - ("Wait", ""), + ("Wait", "Oota"), ("Elevation Error", "Kõrgendatud õiguste viga"), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), + ("Ask the remote user for authentication", "Küsi kaugkasutajalt autentimist"), + ("Choose this if the remote account is administrator", "Vali see, kui kaugkonto on administraator"), + ("Transmit the username and password of administrator", "Edasta administraatori kasutajanimi ja parool"), ("still_click_uac_tip", "Kaugkasutaja peab siiski ise vajutama käitatud RustDeski kasutajakonto kontrollis (UAC) OK-nuppu."), ("Request Elevation", "Taotle kõrgendatud õigusi"), ("wait_accept_uac_tip", "Palun oota, kuni kaugkasutaja nõustub UAC-dialoogiga (kasutajakonto kontroll)."), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("Elevate successfully", "Kõrgendamine õnnestus"), + ("uppercase", "suurtäht"), + ("lowercase", "väiketäht"), + ("digit", "number"), + ("special character", "erimärk"), + ("length>=8", "pikkus>=8"), + ("Weak", "Nõrk"), + ("Medium", "Keskmine"), + ("Strong", "Tugev"), ("Switch Sides", "Vaheta pooli"), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("Please confirm if you want to share your desktop?", "Palun kinnita, kas soovid oma töölauda jagada?"), + ("Display", "Kuva"), ("Default View Style", "Vaikimisi kuvastiil"), ("Default Scroll Style", "Vaikimisi kerimisstiil"), ("Default Image Quality", "Vaikimisi pildikvaliteet"), ("Default Codec", "Vaikimisi koodek"), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), + ("Bitrate", "Bitikiirus"), + ("FPS", "FPS"), + ("Auto", "Automaatne"), ("Other Default Options", "Teised vaikevalikud"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), + ("Voice call", "Häälkõne"), + ("Text chat", "Tekstivestlus"), + ("Stop voice call", "Peata häälkõne"), ("relay_hint_tip", "Otseühendust ei pruugi olla võimalik luua; võid proovida ühendust relee kaudu. Lisaks, kui soovid esimesel katsel kasutada releed, võid lisada ID-le järelliite \"/r\" või valida viimaste seansside kaardil - kui see on olemas - valiku \"Ühenda alati relee kaudu\"."), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), - ("Set one-time password length", ""), + ("Reconnect", "Ühenda uuesti"), + ("Codec", "Koodek"), + ("Resolution", "Resolutsioon"), + ("No transfers in progress", "Ülekandeid ei toimu"), + ("Set one-time password length", "Seadista ühekordse parooli pikkus"), ("RDP Settings", "RDP seaded"), - ("Sort by", ""), + ("Sort by", "Sorteeri"), ("New Connection", "Uus ühendus"), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), + ("Restore", "Taasta"), + ("Minimize", "Minimeeri"), + ("Maximize", "Maksimeeri"), ("Your Device", "Sinu seade"), ("empty_recent_tip", "Ups, hiljutised seansid puuduvad!\nAeg uus planeerida."), ("empty_favorite_tip", "Ei ole veel ühtegi lemmikpartnerit?\nLeia keegi, kellega suhelda ja lisa ta oma lemmikute hulka!"), ("empty_lan_tip", "Oh ei, tundub, et me pole veel ühtegi partnerit avastanud."), ("empty_address_book_tip", "Oh ei, tundub et sinu aadressiraamatus ei ole hetkel ühtegi partnerit."), - ("eg: admin", ""), ("Empty Username", "Tühi kasutajanimi"), ("Empty Password", "Tühi parool"), - ("Me", ""), + ("Me", "Mina"), ("identical_file_tip", "See fail on partneri omaga identne."), ("show_monitors_tip", "Kuva kuvarid tööriistaribal"), ("View Mode", "Kuvarežiim"), ("login_linux_tip", "X-töölaua seansi lubamiseks pead sisse logima Linuxi kaugkontosse."), - ("verify_rustdesk_password_tip", "Kinnita RustDeski parooli"), + ("verify_rustdesk_password_tip", "Kinnita RustDeski parool"), ("remember_account_tip", "Jäta see konto meelde"), ("os_account_desk_tip", "Seda kontot kasutatakse kaug-opsüsteemi sisselogimiseks ja töölaua seansi lubamiseks headless-režiimis."), ("OS Account", "Opsüsteemi konto"), @@ -481,49 +476,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("xorg_not_found_text_tip", "Palun paigalda Xorg"), ("no_desktop_title_tip", "Töölaud pole saadaval"), ("no_desktop_text_tip", "Palun paigalda GNOME Desktop"), - ("No need to elevate", ""), + ("No need to elevate", "Kõrgendamine pole vajalik"), ("System Sound", "Süsteemiheli"), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), + ("Default", "Vaikimisi"), + ("New RDP", "Uus RDP"), + ("Fingerprint", "Sõrmejälg"), ("Copy Fingerprint", "Kopeeri sõrmejälg"), ("no fingerprints", "Sõrmejäljed puuduvad"), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), + ("Select a peer", "Vali partner"), + ("Select peers", "Vali partnerid"), + ("Plugins", "Pluginad"), + ("Uninstall", "Desinstalli"), + ("Update", "Uuenda"), + ("Enable", "Luba"), + ("Disable", "Keela"), + ("Options", "Valikud"), ("resolution_original_tip", "Originaalne eraldusvõime"), ("resolution_fit_local_tip", "Ühita kohaliku eraldusvõimega"), ("resolution_custom_tip", "Kohandatud eraldusvõime"), - ("Collapse toolbar", ""), - ("Accept and Elevate", "Luba kõrgemate õigustega"), + ("Collapse toolbar", "Kompaktne tööriistariba"), + ("Accept and Elevate", "Luba kõrgendatud õigustega"), ("accept_and_elevate_btn_tooltip", "Võta ühendus vastu ja anna kõrgemad UAC-õigused (kasutajakonto kontroll)."), ("clipboard_wait_response_timeout_tip", "Koopia vastuse ootamisel tekkis ajalõpp."), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), + ("Incoming connection", "Sissetulev ühendus"), + ("Outgoing connection", "Väljuv ühendus"), + ("Exit", "Välju"), + ("Open", "Ava"), ("logout_tip", "Kas soovid kindlasti välja logida?"), - ("Service", ""), - ("Start", ""), - ("Stop", ""), + ("Service", "Teenused"), + ("Start", "Käivita"), + ("Stop", "Peata"), ("exceed_max_devices", "Oled saavutanud hallatavate seadmete maksimaalse arvu."), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), + ("Sync with recent sessions", "Sünkroniseeri viimaste seanssidega"), + ("Sort tags", "Sorteeri silte"), + ("Open connection in new tab", "Ava ühendus uuel vahekaardil"), + ("Move tab to new window", "Liiguta vahekaart uude aknasse"), + ("Can not be empty", "Ei tohi olla tühi"), + ("Already exists", "Juba eksisteerib"), ("Change Password", "Vaheta parooli"), ("Refresh Password", "Värskenda parool"), - ("ID", ""), + ("ID", "ID"), ("Grid View", "Ruudustikuvaade"), ("List View", "Loendivaade"), - ("Select", ""), + ("Select", "Vali"), ("Toggle Tags", "Lülita silte"), ("pull_ab_failed_tip", "Aadressiraamatu värskendamine ebaõnnestus"), ("push_ab_failed_tip", "Aadressiraamatu sünkroonimine serveriga ebaõnnestus"), @@ -532,129 +527,187 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Primary Color", "Põhivärv"), ("HSV Color", "HSV-värv"), ("Installation Successful!", "Paigaldus oli edukas!"), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), + ("Installation failed!", "Paigaldus ebaõnnestus!"), + ("Reverse mouse wheel", "Pööra hiireratas"), + ("{} sessions", "{} seanssi"), ("scam_title", "Võid olla KELMUSE ohver!"), ("scam_text1", "Kui räägid telefoniga kellegagi, keda EI TUNNE ja EI USALDA, kes on palunud sul RustDeski kasutada ja teenus käivitada, ära jätka ning lõpeta kõne koheselt."), ("scam_text2", "Tõenäoliselt on tegemist petturiga, kes üritab sinu raha või muid privaatseid andmeid varastada."), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), + ("Don't show again", "Ära kuva uuesti"), + ("I Agree", "Nõustun"), + ("Decline", "Keeldu"), + ("Timeout in minutes", "Ajalõpp minutites"), ("auto_disconnect_option_tip", "Sissetulevate seansside automaatne sulgemine kasutaja mitteaktiivsuse korral"), ("Connection failed due to inactivity", "Mitteaktiivsuse tõttu automaatselt lahti ühendatud"), - ("Check for software update on startup", ""), + ("Check for software update on startup", "Kontrolli käivitusel tarkvarauuendust"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Palun täienda RustDesk Server Pro versioonile {} või uuem!"), ("pull_group_failed_tip", "Grupi värskendamine ebaõnnestus"), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), + ("Filter by intersection", "Filtreeri ristumiste järgi"), + ("Remove wallpaper during incoming sessions", "Eemalda taustapilt sissetulevate seansside ajal"), + ("Test", "Test"), ("display_is_plugged_out_msg", "See kuvar on välja lülitatud, lülita esmasele kuvarile."), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), + ("No displays", "Kuvarid puuduvad"), + ("Open in new window", "Ava uues aknas"), + ("Show displays as individual windows", "Kuva kuvarid eraldi akendena"), + ("Use all my displays for the remote session", "Kasuta kõiki minu kuvarid kaugseansi jaoks"), ("selinux_tip", "SELinux on su seadmes lubatud, mis võib RustDeskil takistada juhitud poolel toimimist."), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), + ("Change view", "Muuda vaadet"), + ("Big tiles", "Suured plaadid"), + ("Small tiles", "Väikesed plaadid"), + ("List", "Loend"), + ("Virtual display", "Virtuaalne kuvar"), + ("Plug out all", "Eemalda kõik"), + ("True color (4:4:4)", "Tõeline värv (4:4:4)"), + ("Enable blocking user input", "Luba kasutaja sisendi blokeerimine"), ("id_input_tip", "Võid sisestada ID, otsese IP või domeeni koos pordiga (:).\nKui soovid juurdepääsu seadmele mõnes teises serveris, lisa palun serveri aadress (@?key=), näiteks,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nKui soovid juurdepääsu seadmele avalikus serveris, sisesta \"@public\", avaliku serveri puhul ei ole võtit vaja."), ("privacy_mode_impl_mag_tip", "Režiim 1"), ("privacy_mode_impl_virtual_display_tip", "Režiim 2"), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), + ("Enter privacy mode", "Sisene privaatsusrežiimi"), + ("Exit privacy mode", "Välju privaatsusrežiimist"), ("idd_not_support_under_win10_2004_tip", "Kaudse kuvari draiver ei ole toetatud. Vajalik on Windows 10, versioon 2004 või uuem."), ("input_source_1_tip", "Sisendallikas 1"), ("input_source_2_tip", "Sisendallikas 2"), - ("Swap control-command key", ""), + ("Swap control-command key", "Vaheta klahvid Control ja Command"), ("swap-left-right-mouse", "Vaheta vasak ja parem hiirenupp"), - ("2FA code", ""), - ("More", ""), - ("enable-2fa-title", ""), - ("enable-2fa-desc", ""), - ("wrong-2fa-code", ""), - ("enter-2fa-title", ""), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", ""), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("2FA code", "2FA kood"), + ("More", "Rohkem"), + ("enable-2fa-title", "Luba kaheastmeline autentimine"), + ("enable-2fa-desc", "Palun seadista oma autentimisrakendus nüüd. Sa saad kasutada autentimisrakendust nagu Authy, Microsoft või Google Authenticator oma telefonis või lauaarvutis.\n\nSkaneeri QR-kood oma rakendusega ja sisesta kood, mida sinu rakendus näitab, et lubada kaheastmeline autentimine."), + ("wrong-2fa-code", "Koodi ei saa kinnitada. Kontrolli, et kood ja kohalikud ajaseaded oleksid õiged."), + ("enter-2fa-title", "Kaheastmeline autentimine"), + ("Email verification code must be 6 characters.", "E-posti kinnituskood peab olema 6 tähemärki."), + ("2FA code must be 6 digits.", "2FA kood peab olema 6 numbrit."), + ("Multiple Windows sessions found", "Leitud mitu Windowsi seanssi"), + ("Please select the session you want to connect to", "Palun vali seanss, millega soovid ühendada"), + ("powered_by_me", "Põhineb RustDeskil"), + ("outgoing_only_desk_tip", "See on kohandatud versioon.\nSa saad ühenduda teiste seadmetega, kuid teised seadmed ei saa sinu seadmega ühenduda."), + ("preset_password_warning", "See kohandatud versioon sisaldab eelmääratud parooli. Igaüks, kes seda parooli teab, võib saada täieliku kontrolli sinu seadme üle. Kui sa ei oodanud seda, desinstalli tarkvara kohe."), + ("Security Alert", "Turvahoiatus"), + ("My address book", "Minu aadressiraamat"), + ("Personal", "Isiklik"), + ("Owner", "Omanik"), + ("Set shared password", "Seadista jagatud parool"), + ("Exist in", "Eksisteerib"), + ("Read-only", "Ainult lugemiseks"), + ("Read/Write", "Lugemiseks/Kirjutamiseks"), + ("Full Control", "Täielik kontroll"), + ("share_warning_tip", "Ülalolevad väljad on teistele jagatud ja nähtavad."), + ("Everyone", "Igaüks"), + ("ab_web_console_tip", "Rohkem leiad veebikonsoolist"), + ("allow-only-conn-window-open-tip", "Luba ühendus ainult siis, kui RustDeski aken on avatud."), + ("no_need_privacy_mode_no_physical_displays_tip", "Füüsilisi ekraane pole, privaatsusrežiimi kasutamine pole vajalik."), + ("Follow remote cursor", "Jälgi kaugkursorit"), + ("Follow remote window focus", "Jälgi kaugakna fookust"), + ("default_proxy_tip", "Vaikimisi protokoll ja port on Socks5 ja 1080."), + ("no_audio_input_device_tip", "Heli sisendseadet ei leitud."), + ("Incoming", "Sissetulev"), + ("Outgoing", "Väljuv"), + ("Clear Wayland screen selection", "Tühjenda Waylandi ekraanivalik"), + ("clear_Wayland_screen_selection_tip", "Pärast ekraanivaliku tühistamist saad uuesti jagatava ekraani valida."), + ("confirm_clear_Wayland_screen_selection_tip", "Kas oled kindel, et soovid Waylandi ekraanivaliku tühistada?"), + ("android_new_voice_call_tip", "Uus häälkõne taotlus on saadud. Vastu võtmisel lülitub heli häälkommunikatsioonile."), + ("texture_render_tip", "Kasuta tekstuurirenderdust, et muuta pildid sujuvamaks. Renderdusprobleemide esinemisel võid proovida selle valiku keelata."), + ("Use texture rendering", "Kasuta tekstuurirenderdust"), + ("Floating window", "Ujuv aken"), + ("floating_window_tip", "See aitab säilitada RustDeski taustateenust."), + ("Keep screen on", "Hoia ekraan sees"), + ("Never", "Mitte kunagi"), + ("During controlled", "Juhtimise ajal"), + ("During service is on", "Teenuse käitamisel"), + ("Capture screen using DirectX", "Salvesta ekraani DirectX abil"), + ("Back", "Tagasi"), + ("Apps", "Rakendused"), + ("Volume up", "Heli üles"), + ("Volume down", "Heli alla"), + ("Power", "Toide"), + ("Telegram bot", "Telegrami bot"), + ("enable-bot-tip", "Kui lubad selle funktsiooni, saad 2FA koodi oma botilt. See võib töötada ka ühenduse teavitusena."), + ("enable-bot-desc", "1. Ava vestlus kasutajaga @BotFather.\n2. Saada käsklus \"/newbot\". Pärast selle sammu lõpetamist saad tokeni.\n3. Alusta vestlust oma uue loodud botiga. Saada sõnum, mis algab kaldkriipsuga (\"/\") nagu \"/hello\", et see aktiveerida.\n"), + ("cancel-2fa-confirm-tip", "Kas oled kindel, et soovid 2FA tühistada?"), + ("cancel-bot-confirm-tip", "Kas oled kindel, et soovid Telegrami boti tühistada?"), + ("About RustDesk", "RustDeski teave"), + ("Send clipboard keystrokes", "Saada lõikelaua klahvivajutused"), + ("network_error_tip", "Palun kontrolli oma võrguühendust ja seejärel klõpsa nuppu \"Proovi uuesti\"."), + ("Unlock with PIN", "Ava PIN-koodiga"), + ("Requires at least {} characters", "Nõuab vähemalt {} tähemärki"), + ("Wrong PIN", "Vale PIN"), + ("Set PIN", "Seadista PIN"), + ("Enable trusted devices", "Luba usaldusväärsed seadmed"), + ("Manage trusted devices", "Halda usaldusväärseid seadmeid"), + ("Platform", "Platvorm"), + ("Days remaining", "Päevi jäänud"), + ("enable-trusted-devices-tip", "Jäta usaldatud seadmetes 2FA kinnitamine vahele"), + ("Parent directory", "Ülemkaust"), + ("Resume", "Jätka"), + ("Invalid file name", "Kehtetu failinimi"), + ("one-way-file-transfer-tip", "Ühesuunaline failiedastus on lubatud juhitataval poolel."), + ("Authentication Required", "Autentimine nõutud"), + ("Authenticate", "Autendi"), + ("web_id_input_tip", "Saad sisestada sama serveri ID, otse IP-juurdepääs ei ole veebikliendis toetatud.\nKui soovid seadmele teises serveris ligi pääseda, palun lisa serveri aadress (@?key=), näiteks,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nKui soovid seadmele avalikus serveris ligi pääseda, palun sisesta \"@public\"; võti ei ole avaliku serveri jaoks vajalik."), + ("Download", "Laadi alla"), + ("Upload folder", "Laadi kaust üles"), + ("Upload files", "Laadi failid üles"), + ("Clipboard is synchronized", "Lõikelaud on sünkroonitud"), + ("Update client clipboard", "Uuenda kliendi lõikelauda"), + ("Untagged", "Sildistamata"), + ("new-version-of-{}-tip", "Saadaval on {} uus versioon"), + ("Accessible devices", "Ligipääsetavad seadmed"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Täiendage RustDeski klient kaugküljel versioonile {} või uuemale!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Vaata kaamerat"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eu.rs b/src/lang/eu.rs index efb281496df..769c3788f2a 100644 --- a/src/lang/eu.rs +++ b/src/lang/eu.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "%min%(e)tik %max% arteko luzera"), ("starts with a letter", "hizki batekin hasten da"), ("allowed characters", "onartutako karaktereak"), - ("id_change_tip", "Soilik a-z, A-Z, 0-9 eta _ (barra baxua) karaktereak daude onartuta. Lehen hizkia a-z, A-Z izan behar da. Luzera 6 eta 16 artekoa izan behar da."), + ("id_change_tip", "Soilik a-z, A-Z, 0-9, - (dash) eta _ (barra baxua) karaktereak daude onartuta. Lehen hizkia a-z, A-Z izan behar da. Luzera 6 eta 16 artekoa izan behar da."), ("Website", "Webgunea"), ("About", "Honi buruz"), ("Slogan_tip", "Bihotzez eginda mundu kaotiko honetan!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Sistema eragilearen pasahitza"), ("install_tip", "Erabiltzaile Kontuen Kontrolarengatik, RustDesk ezin du ondo funtzionatu urruneko mahaigainean. EKK saihesteko, mesedez, egin klik azpiko botoian RustDesk sistema mailan instalatzeko."), ("Click to upgrade", "Egin klik bertsio-berritzeko"), - ("Click to download", "Egin klik deskargatzeko"), - ("Click to update", "Egin klik eguneratzeko"), ("Configure", "Konfiguratu"), ("config_acc", "Zure mahaigaina urrunetik kontrolatzeko, RustDesk-i \"Irisgarritasuna\" baimenak eman behar dituzu."), ("config_screen", "Zure mahaigaina kanpotik kontrolatzeko, RustDesk-i \"Pantaila grabatu\" baimena eman behar duzu."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ez duzu baimenik fitxategiak transferitzeko"), ("Note", "Nota"), ("Connection", "Konexioa"), - ("Share Screen", "Partekatu pantaila"), + ("Share screen", "Partekatu pantaila"), ("Chat", "Txata"), ("Total", "Guztira"), ("items", "elementuak"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Pantaila-grabazioa"), ("Input Control", "Sarrera-kontrola"), ("Audio Capture", "Audio-grabazioa"), - ("File Connection", "Fitxategi-konexioa"), - ("Screen Connection", "Pantaila-konexioa"), ("Do you accept?", "Onartzen al duzu?"), ("Open System Setting", "Ireki sistemaren ezarpenak"), ("How to get Android input permission?", "Nola lortu dezaket Android sarrera-baimena?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Parekide gogokorik gabe oraindik?\nBilatu norbait konektatzeko eta gehitu zure gogokoetara!"), ("empty_lan_tip", "Ai ez, badirudi ez duzula parekiderik aurkitu oraindik."), ("empty_address_book_tip", "Badirudi ez dagoela parekiderik zure helbide-liburuan."), - ("eg: admin", "adib. admin"), ("Empty Username", "Erabiltzaile-izena hutsik"), ("Empty Password", "Pasahitza hutsik"), ("Me", "Ni"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Mesedez, eguneratu RustDesk bezeroa {} bertsiora edo berriagoa urruneko aldean!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Ikusi kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fa.rs b/src/lang/fa.rs index d1d3d47679a..548ad7e0e4c 100644 --- a/src/lang/fa.rs +++ b/src/lang/fa.rs @@ -44,7 +44,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_change_tip", "شناسه باید طبق این شرایط باشد : حرو٠کوچک Ùˆ بزرگ انگلیسی Ùˆ اعداد از 0 تا 9ØŒ _ Ùˆ همچنین حر٠اول آن Ùقط حرو٠بزرگ یا Ú©ÙˆÚ†Ú© انگلیسی Ùˆ طول آن بین 6 الی 16 کاراکتر باشد"), ("Website", "وب سایت"), ("About", "درباره"), - ("Slogan_tip", "ساخته شده با قلب(عشق) در این دنیای پر هرج Ùˆ مرج!"), + ("Slogan_tip", "ساخته شده با â¤ï¸â€(عشق) در این دنیای پر هرج Ùˆ مرج!"), ("Privacy Statement", "بیانیه حریم خصوصی"), ("Mute", "بستن صدا"), ("Build Date", "تاریخ ساخت"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "رمز عبور سیستم عامل"), ("install_tip", "Ù„Ø·ÙØ§ برنامه را نصب کنید UAC Ùˆ جلوگیری از خطای RustDesk برای راحتی در Ø§Ø³ØªÙØ§Ø¯Ù‡ از نرم Ø§ÙØ²Ø§Ø±"), ("Click to upgrade", "برای ارتقا کلیک کنید"), - ("Click to download", "برای دانلود کلیک کنید"), - ("Click to update", "برای به روز رسانی کلیک کنید"), ("Configure", "تنظیم"), ("config_acc", "بدهید \"access\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), ("config_screen", "بدهید \"screenshot\" مجوز RustDesk برای کنترل از راه دور دسکتاپ باید به"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "مجوز انتقال ÙØ§ÛŒÙ„ داده نشده"), ("Note", "یادداشت"), ("Connection", "ارتباط"), - ("Share Screen", "اشتراک گذاری ØµÙØ­Ù‡"), + ("Share screen", "اشتراک گذاری ØµÙØ­Ù‡"), ("Chat", "چت"), ("Total", "مجموع"), ("items", "آیتم ها"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "ضبط ØµÙØ­Ù‡"), ("Input Control", "کنترل ورودی"), ("Audio Capture", "ضبط صدا"), - ("File Connection", "ارتباط ÙØ§ÛŒÙ„"), - ("Screen Connection", "ارتباط ØµÙØ­Ù‡"), ("Do you accept?", "آیا Ù…ÛŒ پذیرید؟"), ("Open System Setting", "باز کردن تنظیمات سیستم"), ("How to get Android input permission?", "چگونه مجوز ورود به سیستم اندروید را Ø¯Ø±ÛŒØ§ÙØª کنیم؟"), @@ -364,7 +360,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "در حال ضبط"), ("Directory", "مسیر"), ("Automatically record incoming sessions", "ضبط خودکار جلسات ورودی"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "ضبط خودکار جلسات خروجی"), ("Change", "تغییر"), ("Start session recording", "شروع ضبط جلسه"), ("Stop session recording", "توق٠ضبط جلسه"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "هنوز همتای مورد علاقه‌ای ندارید؟\nبیایید ÙØ±Ø¯ÛŒ را برای ارتباط پیدا کنیم Ùˆ آن را به موارد دلخواه خود اضاÙÙ‡ کنیم!"), ("empty_lan_tip", "اوه نه، به نظر Ù…ÛŒ رسد Ú©Ù‡ ما هنوز همتای خود را پیدا نکرده ایم"), ("empty_address_book_tip", "اوه ØŒ به نظر Ù…ÛŒ رسد Ú©Ù‡ در حال حاضر هیچ همتایی در Ø¯ÙØªØ±Ú†Ù‡ آدرس شما وجود ندارد"), - ("eg: admin", "مثال : admin"), ("Empty Username", "نام کاربری خالی است"), ("Empty Password", "رمز عبور خالی است"), ("Me", "من"), @@ -617,8 +612,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("floating_window_tip", "Ú©Ù…Ú© Ù…ÛŒ کند RustDesk این به Ø­ÙØ¸ سرویس پس زمینه"), ("Keep screen on", "ØµÙØ­Ù‡ نمایش را روشن Ù†Ú¯Ù‡ دارید"), ("Never", "هرگز"), - ("During controlled", ""), - ("During service is on", ""), + ("During controlled", "در حین کنترل"), + ("During service is on", "در حین سرویس روشن است"), ("Capture screen using DirectX", "DirectX تصویربرداری از ØµÙØ­Ù‡ نمایش با Ø§Ø³ØªÙØ§Ø¯Ù‡ از"), ("Back", "برگشت"), ("Apps", "برنامه ها"), @@ -637,24 +632,82 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Requires at least {} characters", "حداقل به {} کاراکترها نیاز دارد"), ("Wrong PIN", "پین اشتباه است"), ("Set PIN", "پین را تنظیم کنید"), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Enable trusted devices", "ÙØ¹Ø§Ù„ کردن دستگاه‌های مورد اعتماد"), + ("Manage trusted devices", "مدیریت دستگاه‌های مورد اعتماد"), + ("Platform", "Ù¾Ù„ØªÙØ±Ù…"), + ("Days remaining", "روزهای باقی‌مانده"), + ("enable-trusted-devices-tip", "ÙØ¹Ø§Ù„ کردن این گزینه Ùقط به دستگاه‌های مورد اعتماد اجازه اتصال می‌دهد"), + ("Parent directory", "Ùهرست والد"), + ("Resume", "ادامه دادن"), + ("Invalid file name", "نام ÙØ§ÛŒÙ„ نامعتبر است"), + ("one-way-file-transfer-tip", "انتقال ÙØ§ÛŒÙ„ Ùقط در یک جهت انجام می‌شود"), + ("Authentication Required", "احراز هویت مورد نیاز است"), + ("Authenticate", "احراز هویت"), + ("web_id_input_tip", "Ù„Ø·ÙØ§Ù‹ شناسه وب را وارد کنید"), + ("Download", "دانلود"), + ("Upload folder", "آپلود پوشه"), + ("Upload files", "آپلود ÙØ§ÛŒÙ„‌ها"), + ("Clipboard is synchronized", "کلیپ‌بورد همگام‌سازی شده است"), + ("Update client clipboard", "به‌روزرسانی کلیپ‌بورد کاربر"), + ("Untagged", "بدون برچسب"), + ("new-version-of-{}-tip", "نسخه جدید {} در دسترس است"), + ("Accessible devices", "دستگاه‌های در دسترس"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Ù„Ø·ÙØ§Ù‹ RustDesk را به نسخه {} یا جدیدتر در سمت راه دور ارتقا دهید"), + ("d3d_render_tip", "ÙØ¹Ø§Ù„ کردن رندر D3D برای عملکرد بهتر"), + ("Use D3D rendering", "Ø§Ø³ØªÙØ§Ø¯Ù‡ از رندر D3D"), + ("Printer", "چاپگر"), + ("printer-os-requirement-tip", "سیستم‌عامل شما باید از چاپ از راه دور پشتیبانی کند"), + ("printer-requires-installed-{}-client-tip", "برای Ø§Ø³ØªÙØ§Ø¯Ù‡ از چاپگر، کلاینت {} باید نصب باشد"), + ("printer-{}-not-installed-tip", "چاپگر {} نصب نشده است"), + ("printer-{}-ready-tip", "چاپگر {} آماده است"), + ("Install {} Printer", "{} نصب چاپگر"), + ("Outgoing Print Jobs", "وظای٠چاپ خروجی"), + ("Incoming Print Jobs", "وظای٠چاپ ورودی"), + ("Incoming Print Job", "وظیÙÙ‡ چاپ ورودی"), + ("use-the-default-printer-tip", "از چاپگر Ù¾ÛŒØ´â€ŒÙØ±Ø¶ Ø§Ø³ØªÙØ§Ø¯Ù‡ کنید"), + ("use-the-selected-printer-tip", "از چاپگر انتخاب‌شده Ø§Ø³ØªÙØ§Ø¯Ù‡ کنید"), + ("auto-print-tip", "چاپ خودکار ÙØ¹Ø§Ù„ است"), + ("print-incoming-job-confirm-tip", "آیا می‌خواهید کار چاپ ورودی را تأیید کنید"), + ("remote-printing-disallowed-tile-tip", "چاپ از راه دور ØºÛŒØ±ÙØ¹Ø§Ù„ است"), + ("remote-printing-disallowed-text-tip", "شما مجوز لازم برای چاپ از راه دور را ندارید"), + ("save-settings-tip", "تنظیمات را ذخیره کنید"), + ("dont-show-again-tip", "دیگر نمایش داده نشود"), + ("Take screenshot", "عکس Ú¯Ø±ÙØªÙ†"), + ("Taking screenshot", "در حال Ú¯Ø±ÙØªÙ† عکس"), + ("screenshot-merged-screen-not-supported-tip", "ادغام تصاویر از نمایشگرهای متعدد در حال حاضر پشتیبانی نمی شود. Ù„Ø·ÙØ§Ù‹ به یک ØµÙØ­Ù‡ نمایش واحد تغییر دهید Ùˆ دوباره امتحان کنید."), + ("screenshot-action-tip", "Ù„Ø·ÙØ§Ù‹ نحوه ادامه با تصویر را انتخاب کنید."), + ("Save as", "ذخیره به عنوان"), + ("Copy to clipboard", "در کلیپ بورد Ú©Ù¾ÛŒ کنید"), + ("Enable remote printer", "چاپگر از راه دور را ÙØ¹Ø§Ù„ کنید"), + ("Downloading {}", "بارگیری {}"), + ("{} Update", "{} به روز رسانی"), + ("{}-to-update-tip", "{} اکنون بسته خواهد شد Ùˆ نسخه جدید را نصب Ù…ÛŒ کند."), + ("download-new-version-failed-tip", "بارگیری ناموÙÙ‚ بود. Ù…ÛŒ توانید دوباره امتحان کنید یا روی دکمه 'بارگیری' کلیک کنید تا از ØµÙØ­Ù‡ انتشار بارگیری کنید Ùˆ به صورت دستی ارتقا دهید."), + ("Auto update", "بروزرسانی خودکار"), + ("update-failed-check-msi-tip", "بررسی روش نصب انجام نشد. Ù„Ø·ÙØ§Ù‹ برای بارگیری از ØµÙØ­Ù‡ انتشار ØŒ روی دکمه 'بارگیری' کلیک کنید Ùˆ به صورت دستی ارتقا دهید."), + ("websocket_tip", "Ùقط اتصالات رله پشتیبانی Ù…ÛŒ شوند ØŒ WebSocket هنگام Ø§Ø³ØªÙØ§Ø¯Ù‡ از ."), + ("Use WebSocket", "Ø§Ø³ØªÙØ§Ø¯Ù‡ کنید WebSocket از"), + ("Trackpad speed", "سرعت ترک‌پد"), + ("Default trackpad speed", "سرعت Ù¾ÛŒØ´â€ŒÙØ±Ø¶ ترک‌پد"), + ("Numeric one-time password", "رمز عبور یک‌بار مصر٠عددی"), + ("Enable IPv6 P2P connection", "ÙØ¹Ø§Ù„‌سازی اتصال همتا‌به‌همتای IPv6"), + ("Enable UDP hole punching", "ÙØ¹Ø§Ù„‌سازی تکنیک UDP hole punching"), + ("View camera", "نمایش دوربین"), + ("Enable camera", "ÙØ¹Ø§Ù„ کردن دوربین"), + ("No cameras", "هیچ دوربینی ÛŒØ§ÙØª نشد"), + ("view_camera_unsupported_tip", "دوربین در این دستگاه پشتیبانی نمی‌شود"), + ("Terminal", "ترمینال"), + ("Enable terminal", "ÙØ¹Ø§Ù„‌سازی ترمینال"), + ("New tab", "زبانه جدید"), + ("Keep terminal sessions on disconnect", "Ø­ÙØ¸ جلسات ترمینال پس از قطع اتصال"), + ("Terminal (Run as administrator)", "ترمینال (اجرای به عنوان مدیر سیستم)"), + ("terminal-admin-login-tip", "برای اجرای ترمینال به‌عنوان مدیر، نام کاربری Ùˆ رمز عبور مدیر سیستم را وارد کنید."), + ("Failed to get user token.", "Ø¯Ø±ÛŒØ§ÙØª توکن کاربر ناموÙÙ‚ بود."), + ("Incorrect username or password.", "نام کاربری یا رمز عبور اشتباه است."), + ("The user is not an administrator.", "کاربر دارای دسترسی مدیر سیستم نیست."), + ("Failed to check if the user is an administrator.", "بررسی وضعیت مدیر سیستم برای کاربر ناموÙÙ‚ بود."), + ("Supported only in the installed version.", "Ùقط در نسخه نصب‌شده پشتیبانی می‌شود."), + ("elevation_username_tip", "Ù„Ø·ÙØ§Ù‹ نام کاربری مدیریتی را برای ارتقاء دسترسی وارد کنید."), + ("Preparing for installation ...", "در حال آماده‌سازی برای نصب..."), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 19b4e58a63b..384199cd795 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -1,50 +1,50 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", "Statut"), + ("Status", "État"), ("Your Desktop", "Votre bureau"), - ("desk_tip", "Votre bureau est accessible via l'identifiant et le mot de passe ci-dessous."), + ("desk_tip", "Votre bureau est accessible via l’identifiant et le mot de passe ci-dessous."), ("Password", "Mot de passe"), ("Ready", "Prêt"), - ("Established", "Établi"), - ("connecting_status", "Connexion au réseau RustDesk..."), - ("Enable service", "Autoriser le service"), + ("Established", "Établie"), + ("connecting_status", "Connexion au réseau RustDesk…"), + ("Enable service", "Activer le service"), ("Start service", "Démarrer le service"), - ("Service is running", "Le service est en cours d'exécution"), - ("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", "Transfert de fichiers"), + ("Service is running", "Le service est en cours d’exécution"), + ("Service is not running", "Le service est inactif"), + ("not_ready_status", "Pas prêt ; veuillez vérifier la connexion"), + ("Control Remote Desktop", "Contrôler un bureau à distance"), + ("Transfer file", "Transférer des fichiers"), ("Connect", "Se connecter"), ("Recent sessions", "Sessions récentes"), - ("Address book", "Carnet d'adresses"), + ("Address book", "Carnet d’adresses"), ("Confirmation", "Confirmation"), ("TCP tunneling", "Tunnel TCP"), - ("Remove", "Supprimer"), - ("Refresh random password", "Actualiser le mot de passe aléatoire"), + ("Remove", "Retirer"), + ("Refresh random password", "Générer un nouveau mot de passe aléatoire"), ("Set your own password", "Définir votre propre mot de passe"), ("Enable keyboard/mouse", "Activer le contrôle clavier/souris"), ("Enable clipboard", "Activer la synchronisation du presse-papier"), ("Enable file transfer", "Activer le transfert de fichiers"), ("Enable TCP tunneling", "Activer le tunnel TCP"), - ("IP Whitelisting", "Liste blanche IP"), - ("ID/Relay Server", "ID/Serveur Relais"), + ("IP Whitelisting", "Liste blanche d’adresses IP"), + ("ID/Relay Server", "Serveur ID/relais"), ("Import server config", "Importer la configuration du serveur"), ("Export Server Config", "Exporter la configuration du serveur"), ("Import server configuration successfully", "Configuration du serveur importée avec succès"), ("Export server configuration successfully", "Configuration du serveur exportée avec succès"), ("Invalid server configuration", "Configuration du serveur non valide"), - ("Clipboard is empty", "Presse-papier vide"), + ("Clipboard is empty", "Le presse-papier est vide"), ("Stop service", "Arrêter le service"), - ("Change ID", "Changer d'ID"), + ("Change ID", "Modifier l’ID"), ("Your new ID", "Votre nouvel ID"), ("length %min% to %max%", "longueur de %min% à %max%"), ("starts with a letter", "commence par une lettre"), ("allowed characters", "caractères autorisés"), - ("id_change_tip", "Seules les lettres a-z, A-Z, 0-9, _ (trait de soulignement) peuvent être utilisées. La première lettre doit être a-z, A-Z. La longueur doit être comprise entre 6 et 16."), - ("Website", "Site Web"), - ("About", "À propos de"), - ("Slogan_tip", "Fait avec cÅ“ur dans ce monde chaotique !"), + ("id_change_tip", "Seuls les caractères a-z, A-Z, 0-9, - (trait d’union) et _ (tiret bas) sont autorisés. La première lettre doit être a-z ou A-Z. La longueur doit être comprise entre 6 et 16."), + ("Website", "Site web"), + ("About", "À propos"), + ("Slogan_tip", "Fait avec cÅ“ur dans ce monde chaotique !"), ("Privacy Statement", "Déclaration de confidentialité"), ("Mute", "Muet"), ("Build Date", "Date de compilation"), @@ -58,10 +58,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Server", "Serveur relais"), ("API Server", "Serveur API"), ("invalid_http", "Doit commencer par http:// ou https://"), - ("Invalid IP", "IP invalide"), - ("Invalid format", "Format invalide"), - ("server_not_support", "Pas encore supporté par le serveur"), - ("Not available", "Indisponible"), + ("Invalid IP", "IP non valide"), + ("Invalid format", "Format non valide"), + ("server_not_support", "Non encore pris en charge par le serveur"), + ("Not available", "Non disponible"), ("Too frequent", "Modifié trop fréquemment, veuillez réessayer plus tard"), ("Cancel", "Annuler"), ("Skip", "Ignorer"), @@ -72,16 +72,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Please enter your password", "Veuillez saisir votre mot de passe"), ("Remember password", "Mémoriser le mot de passe"), ("Wrong Password", "Mauvais mot de passe"), - ("Do you want to enter again?", "Voulez-vous participer à nouveau ?"), + ("Do you want to enter again?", "Voulez-vous ressaisir le mot de passe ?"), ("Connection Error", "Erreur de connexion"), ("Error", "Erreur"), - ("Reset by the peer", "La connexion a été fermée par l'appareil distant"), - ("Connecting...", "Connexion..."), - ("Connection in progress. Please wait.", "Connexion en cours. Veuillez patienter."), - ("Please try 1 minute later", "Réessayez dans une minute"), + ("Reset by the peer", "Terminée par l’appareil distant"), + ("Connecting...", "Connexion…"), + ("Connection in progress. Please wait.", "Connexion en cours ; veuillez patienter."), + ("Please try 1 minute later", "Veuillez réessayer dans une minute"), ("Login Error", "Erreur de connexion"), ("Successful", "Succès"), - ("Connected, waiting for image...", "Connecté, en attente de transmission d'image..."), + ("Connected, waiting for image...", "Connecté ; en attente de l’image…"), ("Name", "Nom"), ("Type", "Type"), ("Modified", "Modifié le"), @@ -101,70 +101,68 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select All", "Tout sélectionner"), ("Unselect All", "Tout déselectionner"), ("Empty Directory", "Répertoire vide"), - ("Not an empty directory", "Pas un répertoire vide"), - ("Are you sure you want to delete this file?", "Voulez-vous vraiment supprimer ce fichier ?"), + ("Not an empty directory", "Répertoire non vide"), + ("Are you sure you want to delete this file?", "Voulez-vous vraiment supprimer ce fichier ?"), ("Are you sure you want to delete this empty directory?", "Voulez-vous vraiment supprimer ce répertoire vide ?"), ("Are you sure you want to delete the file of this directory?", "Voulez-vous vraiment supprimer le fichier de ce répertoire ?"), - ("Do this for all conflicts", "Appliquer à d'autres conflits"), - ("This is irreversible!", "C'est irréversible !"), + ("Do this for all conflicts", "Appliquer à tous les conflits"), + ("This is irreversible!", "Ceci est irréversible !"), ("Deleting", "Suppression"), - ("files", "fichier"), - ("Waiting", "En attente..."), + ("files", "fichiers"), + ("Waiting", "En attente"), ("Finished", "Terminé"), ("Speed", "Vitesse"), - ("Custom Image Quality", "Définir la qualité d'image"), - ("Privacy mode", "Mode privé"), - ("Block user input", "Bloquer la saisie de l'utilisateur"), - ("Unblock user input", "Débloquer l'entrée de l'utilisateur"), + ("Custom Image Quality", "Qualité d’image personnalisée"), + ("Privacy mode", "Mode de confidentialité"), + ("Block user input", "Bloquer la saisie de l’utilisateur"), + ("Unblock user input", "Débloquer la saisie de l’utilisateur"), ("Adjust Window", "Ajuster la fenêtre"), ("Original", "Ratio d'origine"), ("Shrink", "Rétrécir"), ("Stretch", "Étirer"), ("Scrollbar", "Barre de défilement"), ("ScrollAuto", "Défilement automatique"), - ("Good image quality", "Bonne qualité d'image"), - ("Balanced", "Qualité d'image normale"), + ("Good image quality", "Bonne qualité d’image"), + ("Balanced", "Équilibré"), ("Optimize reaction time", "Optimiser le temps de réaction"), ("Custom", "Personnalisé"), ("Show remote cursor", "Afficher le curseur distant"), ("Show quality monitor", "Afficher le moniteur de qualité"), ("Disable clipboard", "Désactiver le presse-papier"), - ("Lock after session end", "Verrouiller l'appareil distant après la déconnexion"), + ("Lock after session end", "Verrouiller l’appareil distant après la déconnexion"), ("Insert Ctrl + Alt + Del", "Envoyer Ctrl + Alt + Del"), - ("Insert Lock", "Verrouiller l'appareil distant"), - ("Refresh", "Rafraîchir l'écran"), - ("ID does not exist", "L'ID n'existe pas"), - ("Failed to connect to rendezvous server", "Échec de la connexion au serveur rendezvous"), + ("Insert Lock", "Verrouiller l’appareil distant"), + ("Refresh", "Rafraîchir l’écran"), + ("ID does not exist", "L’ID n’existe pas"), + ("Failed to connect to rendezvous server", "Échec de la connexion au serveur de rendez-vous"), ("Please try later", "Veuillez essayer plus tard"), - ("Remote desktop is offline", "Le bureau à distance est hors ligne"), - ("Key mismatch", "Discordance de clés"), + ("Remote desktop is offline", "Le bureau distant est hors ligne"), + ("Key mismatch", "Discordance des clés"), ("Timeout", "Connexion expirée"), ("Failed to connect to relay server", "Échec de la connexion au serveur relais"), - ("Failed to connect via rendezvous server", "Échec de l'établissement d'une connexion via le serveur rendezvous"), - ("Failed to connect via relay server", "Impossible d'établir une connexion via le serveur relais"), - ("Failed to make direct connection to remote desktop", "Impossible d'établir une connexion directe"), + ("Failed to connect via rendezvous server", "Échec de la connexion via le serveur de rendez-vous"), + ("Failed to connect via relay server", "Échec de la connexion via le serveur relais"), + ("Failed to make direct connection to remote desktop", "Échec de la connexion directe au bureau distant"), ("Set Password", "Définir le mot de passe"), - ("OS Password", "Mot de passe du système d'exploitation"), - ("install_tip", "RustDesk n'est pas installé, ce qui peut limiter son utilisation à cause de l'UAC. Cliquez ci-dessous pour l'installer."), - ("Click to upgrade", "Cliquer pour mettre à niveau"), - ("Click to download", "Cliquer pour télécharger"), - ("Click to update", "Cliquer pour mettre à jour"), + ("OS Password", "Mot de passe du système d’exploitation"), + ("install_tip", "RustDesk n’est pas installé, ce qui peut limiter son utilisation à cause de l’UAC. Cliquez ci-dessous pour l’installer."), + ("Click to upgrade", "Mettre à niveau"), ("Configure", "Configurer"), - ("config_acc", "Afin de pouvoir contrôler votre bureau à distance, veuillez donner l'autorisation \"accessibilité\" à RustDesk."), - ("config_screen", "Afin de pouvoir accéder à votre bureau à distance, veuillez donner à RustDesk l'autorisation \"enregistrement d'écran\"."), - ("Installing ...", "Installation..."), + ("config_acc", "L’autorisation « Accessibilité » est requise pour contrôler votre bureau à distance."), + ("config_screen", "L’autorisation « Enregistrement d’écran » est requise pour accéder à votre bureau à distance."), + ("Installing ...", "Installation…"), ("Install", "Installer"), ("Installation", "Installation"), - ("Installation Path", "Chemin d'installation"), + ("Installation Path", "Chemin d’installation"), ("Create start menu shortcuts", "Créer des raccourcis dans le menu démarrer"), ("Create desktop icon", "Créer une icône sur le bureau"), - ("agreement_tip", "Démarrer l'installation signifie accepter le contrat de licence."), + ("agreement_tip", "En lançant l’installation, vous acceptez le contrat de licence."), ("Accept and Install", "Accepter et installer"), - ("End-user license agreement", "Contrat d'utilisateur"), - ("Generating ...", "Génération..."), - ("Your installation is lower version.", "La version que vous avez installée est inférieure à la version en cours d'exécution."), - ("not_close_tcp_tip", "Veuillez ne pas fermer cette fenêtre lors de l'utilisation du tunnel"), - ("Listening ...", "En attente de connexion tunnel..."), + ("End-user license agreement", "Conditions générales d’utilisation"), + ("Generating ...", "Génération…"), + ("Your installation is lower version.", "La version installée est antérieure à la version en cours d’exécution."), + ("not_close_tcp_tip", "Veuillez ne pas fermer cette fenêtre lors de l’utilisation du tunnel"), + ("Listening ...", "En attente de connexion…"), ("Remote Host", "Hôte distant"), ("Remote Port", "Port distant"), ("Action", "Action"), @@ -172,90 +170,90 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Port local"), ("Local Address", "Adresse locale"), ("Change Local Port", "Changer le port local"), - ("setup_server_tip", "Si vous avez besoin d'une vitesse de connexion plus rapide, vous pouvez choisir de créer votre propre serveur"), - ("Too short, at least 6 characters.", "Trop court, au moins 6 caractères."), - ("The confirmation is not identical.", "Les deux entrées ne correspondent pas"), + ("setup_server_tip", "N’hésitez pas à mettre en place votre propre serveur afin d’améliorer la connexion"), + ("Too short, at least 6 characters.", "Trop court, 6 caractères minimum."), + ("The confirmation is not identical.", "Les deux entrées ne correspondent pas."), ("Permissions", "Autorisations"), ("Accept", "Accepter"), ("Dismiss", "Rejeter"), ("Disconnect", "Déconnecter"), - ("Enable file copy and paste", "Autoriser le copier-coller de fichiers"), + ("Enable file copy and paste", "Activer le copier-coller de fichiers"), ("Connected", "Connecté"), ("Direct and encrypted connection", "Connexion directe chiffrée"), - ("Relayed and encrypted connection", "Connexion relais chiffrée"), + ("Relayed and encrypted connection", "Connexion via relais chiffrée"), ("Direct and unencrypted connection", "Connexion directe non chiffrée"), - ("Relayed and unencrypted connection", "Connexion relais non chiffrée"), - ("Enter Remote ID", "Entrer l'ID de l'appareil distant"), - ("Enter your password", "Entrer votre mot de passe"), - ("Logging in...", "En cours de connexion ..."), + ("Relayed and unencrypted connection", "Connexion via relais non chiffrée"), + ("Enter Remote ID", "Saisissez l’ID de l’appareil distant"), + ("Enter your password", "Saisissez votre mot de passe"), + ("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"), + ("Auto Login", "Connexion automatique (Requiert l’activation de l’option « Verrouiller l’appareil distant après la déconnexion »)"), + ("Enable direct IP access", "Activer l’accès direct par adresse IP"), ("Rename", "Renommer"), ("Space", "Espace"), ("Create desktop shortcut", "Créer un raccourci sur le bureau"), - ("Change Path", "Changer de chemin"), + ("Change Path", "Modifier le chemin"), ("Create Folder", "Créer un dossier"), ("Please enter the folder name", "Veuillez saisir le nom du dossier"), ("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"), + ("Login screen using Wayland is not supported", "L’écran de connexion n’est pas pris en charge sous Wayland"), ("Reboot required", "Redémarrage requis"), - ("Unsupported display server", "Le serveur d'affichage actuel n'est pas pris en charge"), - ("x11 expected", "x11 requis"), + ("Unsupported display server", "Le serveur d’affichage n’est pas pris en charge"), + ("x11 expected", "x11 attendu"), ("Port", "Port"), ("Settings", "Paramètres"), - ("Username", " Nom d'utilisateur"), - ("Invalid port", "Port invalide"), - ("Closed manually by the peer", "Fermé manuellement par l'appareil distant"), - ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"), + ("Username", " Nom d’utilisateur"), + ("Invalid port", "Port non valide"), + ("Closed manually by the peer", "Terminée manuellement par l’appareil distant"), + ("Enable remote configuration modification", "Activer la modification de la configuration à distance"), ("Run without install", "Exécuter sans installer"), - ("Connect via relay", "Connexion via relais"), - ("Always connect via relay", "Forcer la connexion relais"), - ("whitelist_tip", "Seule une IP de la liste blanche peut accéder à mon appareil"), + ("Connect via relay", "Connecter via relais"), + ("Always connect via relay", "Forcer la connexion via relais"), + ("whitelist_tip", "Seules les adresses IP incluses dans la liste blanche pourront 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."), + ("verification_tip", "Un code de vérification a été envoyé à l’adresse électronique enregistrée ; saisissez le code de vérification afin de poursuivre la connexion."), ("Logout", "Déconnexion"), ("Tags", "Étiquettes"), ("Search ID", "Rechercher un ID"), ("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"), ("Add ID", "Ajouter un ID"), - ("Add Tag", "Ajout étiquette(s)"), + ("Add Tag", "Ajouter une étiquette"), ("Unselect all tags", "Désélectionner toutes les étiquettes"), ("Network error", "Erreur réseau"), - ("Username missed", "Nom d'utilisateur manquant"), + ("Username missed", "Nom d’utilisateur manquant"), ("Password missed", "Mot de passe manquant"), ("Wrong credentials", "Identifiant ou mot de passe erroné"), ("The verification code is incorrect or has expired", "Le code de vérification est incorrect ou a expiré"), - ("Edit Tag", "Gestion étiquettes"), - ("Forget Password", "Oublier le Mot de passe"), + ("Edit Tag", "Modifier l’étiquette"), + ("Forget Password", "Oublier le mot de passe"), ("Favorites", "Favoris"), - ("Add to Favorites", "Ajouter aux Favoris"), + ("Add to Favorites", "Ajouter aux favoris"), ("Remove from Favorites", "Retirer des favoris"), ("Empty", "Vide"), - ("Invalid folder name", "Nom de dossier invalide"), + ("Invalid folder name", "Nom de dossier non valide"), ("Socks5 Proxy", "Socks5 Agents"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) Agents"), - ("Discovered", "Découvert"), - ("install_daemon_tip", "Pour une exécution au démarrage du système, vous devez installer le service système."), - ("Remote ID", "ID de l'appareil distant"), + ("Socks5/Http(s) Proxy", "Proxy Socks5/Http(s)"), + ("Discovered", "Découverts"), + ("install_daemon_tip", "Le service système doit être installé avant de pouvoir activer l’exécution au démarrage du système."), + ("Remote ID", "ID de l’appareil distant"), ("Paste", "Coller"), - ("Paste here?", "Coller ici ?"), - ("Are you sure to close the connection?", "Êtes-vous sûr de fermer la connexion ?"), + ("Paste here?", "Coller ici ?"), + ("Are you sure to close the connection?", "Voulez-vous vraiment terminer la connexion ?"), ("Download new version", "Télécharger la nouvelle version"), ("Touch mode", "Mode tactile"), ("Mouse mode", "Mode souris"), - ("One-Finger Tap", "Tapez d'un doigt"), - ("Left Mouse", "Bouton gauche de la souris"), - ("One-Long Tap", "Un touché long"), - ("Two-Finger Tap", "Tapez à deux doigts"), - ("Right Mouse", "Bouton droit de la souris"), + ("One-Finger Tap", "Appui simple"), + ("Left Mouse", "Clic gauche"), + ("One-Long Tap", "Appui prolongé"), + ("Two-Finger Tap", "Appui à deux doigts"), + ("Right Mouse", "Clic droit"), ("One-Finger Move", "Mouvement à un doigt"), - ("Double Tap & Move", "Appuyez deux fois et déplacez"), + ("Double Tap & Move", "Mouvement après double appui"), ("Mouse Drag", "Glissement de la souris"), ("Three-Finger vertically", "Trois doigts verticalement"), ("Mouse Wheel", "Roulette de la souris"), @@ -264,80 +262,78 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Pinch to Zoom", "Pincer pour zoomer"), ("Canvas Zoom", "Zoom sur la vue"), ("Reset canvas", "Réinitialiser la vue"), - ("No permission of file transfer", "Aucune autorisation de transfert de fichiers"), - ("Note", "Noter"), + ("No permission of file transfer", "Absence de l’autorisation de transfert de fichiers"), + ("Note", "Note"), ("Connection", "Connexion"), - ("Share Screen", "Partager l'écran"), - ("Chat", "Discuter"), + ("Share screen", "Partage d’écran"), + ("Chat", "Discussion"), ("Total", "Total"), ("items", "éléments"), ("Selected", "Sélectionné(s)"), - ("Screen Capture", "Capture d'écran"), - ("Input Control", "Contrôle de saisie"), - ("Audio Capture", "Capture audio"), - ("File Connection", "Connexion de fichier"), - ("Screen Connection", "Connexion de l'écran"), - ("Do you accept?", "Acceptez-vous ?"), + ("Screen Capture", "Capture de l’écran"), + ("Input Control", "Contrôle de la saisie"), + ("Audio Capture", "Capture de l’audio"), + ("Do you accept?", "Acceptez-vous ?"), ("Open System Setting", "Ouvrir les paramètres système"), - ("How to get Android input permission?", "Comment obtenir l'autorisation d'entrée Android ?"), - ("android_input_permission_tip1", "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é\"."), - ("android_input_permission_tip2", "Veuillez accéder à la page suivante des paramètres système, recherchez et entrez [Services installés], activez le service [RustDesk Input]."), + ("How to get Android input permission?", "Comment obtenir l’autorisation de contrôle de la saisie sur Android ?"), + ("android_input_permission_tip1", "Pour qu’un appareil distant puisse contrôler votre appareil Android via la souris ou le toucher d’écran, vous devez autoriser RustDesk à utiliser le service « Accessibilité »."), + ("android_input_permission_tip2", "Veuillez accéder à la page suivante des paramètres système, puis recherchez et accédez à la section [Services installés] ; activez ensuite le service [RustDesk Input]."), ("android_new_connection_tip", "Une nouvelle demande de contrôle a été reçue, elle souhaite contrôler votre appareil actuel."), - ("android_service_will_start_tip", "L'activation de la capture d'écran démarrera automatiquement le service, permettant à d'autres appareils de demander une connexion à partir de cet appareil."), - ("android_stop_service_tip", "La fermeture du service fermera automatiquement toutes les connexions établies."), - ("android_version_audio_tip", "La version actuelle d'Android ne prend pas en charge la capture audio, veuillez passer à Android 10 ou supérieur."), - ("android_start_service_tip", "Appuyez sur [Démarrer le service] ou activez l'autorisation [Capture d'écran] pour démarrer le service de partage d'écran."), - ("android_permission_may_not_change_tip", "Les autorisations pour les connexions établies peuvent ne pas être prisent en compte instantanément ou avant la reconnection."), + ("android_service_will_start_tip", "L’activation de la capture de l’écran démarrera automatiquement le service, ce qui permettra aux appareils distants d’initier une connexion vers cet appareil."), + ("android_stop_service_tip", "L’arrêt du service terminera automatiquement toutes les connexions établies."), + ("android_version_audio_tip", "La version actuelle d’Android ne prend pas en charge la capture de l’audio, veuillez passer à Android 10 ou supérieur."), + ("android_start_service_tip", "Appuyez sur [Démarrer le service] ou activez l’autorisation [Capture de l’écran] pour démarrer le service de partage d’écran."), + ("android_permission_may_not_change_tip", "Les modifications des autorisations peuvent requérir une reconnexion avant d’être prises en compte par les connexions déjà établies."), ("Account", "Compte"), ("Overwrite", "Écraser"), - ("This file exists, skip or overwrite this file?", "Ce fichier existe, ignorer ou écraser ce fichier ?"), + ("This file exists, skip or overwrite this file?", "Ce fichier existe déjà, ignorer ou écraser ce fichier ?"), ("Quit", "Quitter"), - ("Help", "Aider"), - ("Failed", "échouer"), + ("Help", "Aide"), + ("Failed", "Échec"), ("Succeeded", "Succès"), - ("Someone turns on privacy mode, exit", "Quelqu'un active le mode de confidentialité, quittez"), + ("Someone turns on privacy mode, exit", "Quelqu’un active le mode de confidentialité, désactiver"), ("Unsupported", "Non pris en charge"), - ("Peer denied", "Appareil distant refusé"), + ("Peer denied", "Refusé par l’appareil distant"), ("Please install plugins", "Veuillez installer les plugins"), - ("Peer exit", "Appareil distant déconnecté"), + ("Peer exit", "Désactivé par l’appareil distant"), ("Failed to turn off", "Échec de la désactivation"), ("Turned off", "Désactivé"), ("Language", "Langue"), - ("Keep RustDesk background service", "Gardez le service RustDesk en arrière plan"), - ("Ignore Battery Optimizations", "Ignorer les optimisations batterie"), - ("android_open_battery_optimizations_tip", "Conseil android d'optimisation de batterie"), + ("Keep RustDesk background service", "Garder le service RustDesk en arrière plan"), + ("Ignore Battery Optimizations", "Ignorer les optimisations de la batterie"), + ("android_open_battery_optimizations_tip", "Pour désactiver cette fonctionnalité, veuillez accéder à la page suivante des paramètres de l’application RustDesk, puis recherchez et accédez à la section [Batterie] ; décochez ensuite l’option [Sans restriction]."), ("Start on boot", "Lancer au démarrage"), - ("Start the screen sharing service on boot, requires special permissions", "Lancer le service de partage d'écran au démarrage, nécessite des autorisations spéciales"), + ("Start the screen sharing service on boot, requires special permissions", "Lancer le service de partage d’écran au démarrage, nécessite des autorisations spéciales"), ("Connection not allowed", "Connexion non autorisée"), ("Legacy mode", "Mode hérité"), ("Map mode", "Mode correspondance"), ("Translate mode", "Mode traduction"), ("Use permanent password", "Utiliser un mot de passe permanent"), - ("Use both passwords", "Utiliser les mots de passe unique et permanent"), + ("Use both passwords", "Utiliser les deux mots de passe"), ("Set permanent password", "Définir le mot de passe permanent"), ("Enable remote restart", "Activer le redémarrage à distance"), - ("Restart remote device", "Redémarrer l'appareil à distance"), - ("Are you sure you want to restart", "Êtes-vous sûr de vouloir redémarrer l'appareil ?"), - ("Restarting remote device", "Redémarrage de l'appareil distant"), - ("remote_restarting_tip", "L'appareil distant redémarre, veuillez fermer cette boîte de message et vous reconnecter avec un mot de passe permanent après un certain temps"), + ("Restart remote device", "Redémarrer l’appareil distant"), + ("Are you sure you want to restart", "Voulez-vous vraiment redémarrer l’appareil ?"), + ("Restarting remote device", "Redémarrage de l’appareil distant"), + ("remote_restarting_tip", "L'appareil distant redémarre ; veuillez fermer cette boîte de dialogue et vous reconnecter en utilisant le mot de passe permanent dans quelques instants"), ("Copied", "Copié"), ("Exit Fullscreen", "Quitter le mode plein écran"), ("Fullscreen", "Plein écran"), ("Mobile Actions", "Actions mobiles"), - ("Select Monitor", "Sélection du Moniteur"), + ("Select Monitor", "Sélection du moniteur"), ("Control Actions", "Actions de contrôle"), - ("Display Settings", "Paramètres d'affichage"), + ("Display Settings", "Paramètres d’affichage"), ("Ratio", "Rapport"), - ("Image Quality", "Qualité d'image"), + ("Image Quality", "Qualité d’image"), ("Scroll Style", "Style de défilement"), - ("Show Toolbar", "Afficher la barre d'outils"), - ("Hide Toolbar", "Masquer la barre d'outils"), + ("Show Toolbar", "Afficher la barre d’outils"), + ("Hide Toolbar", "Cacher la barre d’outils"), ("Direct Connection", "Connexion directe"), - ("Relay Connection", "Connexion relais"), + ("Relay Connection", "Connexion via relais"), ("Secure Connection", "Connexion sécurisée"), ("Insecure Connection", "Connexion non sécurisée"), - ("Scale original", "Échelle 100%"), - ("Scale adaptive", "Mise à l'échelle Auto"), + ("Scale original", "Échelle 100 %"), + ("Scale adaptive", "Mise à l’échelle auto"), ("General", "Général"), ("Security", "Sécurité"), ("Theme", "Thème"), @@ -347,314 +343,371 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light", "Clair"), ("Follow System", "Suivi système"), ("Enable hardware codec", "Activer le transcodage matériel"), - ("Unlock Security Settings", "Déverrouiller les configurations de sécurité"), - ("Enable audio", "Activer l'audio"), - ("Unlock Network Settings", "Déverrouiller les configurations réseau"), + ("Unlock Security Settings", "Déverrouiller les paramètres de sécurité"), + ("Enable audio", "Activer l’audio"), + ("Unlock Network Settings", "Déverrouiller les paramètres réseau"), ("Server", "Serveur"), - ("Direct IP Access", "Accès IP direct"), + ("Direct IP Access", "Accès direct par adresse IP"), ("Proxy", "Proxy"), ("Apply", "Appliquer"), - ("Disconnect all devices?", "Déconnecter tous les appareils ?"), + ("Disconnect all devices?", "Déconnecter tous les appareils ?"), ("Clear", "Effacer"), ("Audio Input Device", "Périphérique source audio"), - ("Use IP Whitelisting", "Utiliser une liste blanche d'IP"), + ("Use IP Whitelisting", "Utiliser une liste blanche d’adresses IP"), ("Network", "Réseau"), - ("Pin Toolbar", "Épingler la barre d'outil"), - ("Unpin Toolbar", "Détacher la barre d'outil"), + ("Pin Toolbar", "Épingler la barre d’outils"), + ("Unpin Toolbar", "Détacher la barre d’outils"), ("Recording", "Enregistrement"), ("Directory", "Répertoire"), - ("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"), - ("Automatically record outgoing sessions", ""), + ("Automatically record incoming sessions", "Enregistrer automatiquement les sessions entrantes"), + ("Automatically record outgoing sessions", "Enregistrer automatiquement les sessions sortantes"), ("Change", "Modifier"), - ("Start session recording", "Commencer l'enregistrement"), - ("Stop session recording", "Stopper l'enregistrement"), - ("Enable recording session", "Activer l'enregistrement de session"), + ("Start session recording", "Commencer l’enregistrement"), + ("Stop session recording", "Stopper l’enregistrement"), + ("Enable recording session", "Activer l’enregistrement de session"), ("Enable LAN discovery", "Activer la découverte sur réseau local"), - ("Deny LAN discovery", "Interdir la découverte sur réseau local"), - ("Write a message", "Ecrire un message"), + ("Deny LAN discovery", "Interdire la découverte sur réseau local"), + ("Write a message", "Écrire un message"), ("Prompt", "Annonce"), - ("Please wait for confirmation of UAC...", "Veuillez attendre la confirmation de l'UAC..."), - ("elevated_foreground_window_tip", "La fenêtre actuelle de l'appareil distant nécessite des privilèges plus élevés pour fonctionner, elle ne peut donc pas être atteinte par la souris et le clavier. Vous pouvez demander à l'utilisateur distant de réduire la fenêtre actuelle ou de cliquer sur le bouton d'élévation dans la fenêtre de gestion des connexions. Pour éviter ce problème, il est recommandé d'installer le logiciel sur l'appareil distant."), + ("Please wait for confirmation of UAC...", "Veuillez attendre la confirmation de l’UAC…"), + ("elevated_foreground_window_tip", "La fenêtre active du bureau distant nécessite des privilèges plus élevés pour fonctionner, la souris et le clavier ne peuvent donc pas l’atteindre actuellement. Vous pouvez demander à l’utilisateur distant de réduire la fenêtre active ou de cliquer sur le bouton d’élévation dans la fenêtre de gestion de la connexion. Il est conseillé d’installer le logiciel sur l’appareil distant afin d’éviter ce problème."), ("Disconnected", "Déconnecté"), ("Other", "Divers"), ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"), - ("Keyboard Settings", "Configuration clavier"), + ("Keyboard Settings", "Paramètres du clavier"), ("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."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version supérieure de la distribution Linux. Veuillez essayer le bureau X11 ou changer votre système d'exploitation."), + ("Screen Share", "Partage d’écran"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version ultérieure."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version ultérieure de votre distribution Linux. Veuillez essayer le bureau X11 ou changer de système d’exploitation."), ("JumpLink", "Afficher"), - ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (côté appareil distant)."), + ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l’écran à partager (côté appareil distant)."), ("Show RustDesk", "Afficher RustDesk"), ("This PC", "Ce PC"), ("or", "ou"), ("Continue with", "Continuer avec"), - ("Elevate", "Autoriser l'accès"), + ("Elevate", "Élever les privilèges"), ("Zoom cursor", "Augmenter la taille du curseur"), ("Accept sessions via password", "Accepter les sessions via mot de passe"), - ("Accept sessions via click", "Accepter les sessions via clique de confirmation"), - ("Accept sessions via both", "Accepter les sessions via mot de passe ou clique de confirmation"), - ("Please wait for the remote side to accept your session request...", "Veuillez attendre que votre demande de session distante soit accepter ..."), - ("One-time Password", "Mot de passe unique"), - ("Use one-time password", "Utiliser un mot de passe unique"), - ("One-time password length", "Longueur du mot de passe unique"), - ("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"), + ("Accept sessions via click", "Accepter les sessions via clic de confirmation"), + ("Accept sessions via both", "Accepter les sessions via mot de passe ou clic de confirmation"), + ("Please wait for the remote side to accept your session request...", "Veuillez attendre que votre demande de session distante soit acceptée…"), + ("One-time Password", "Mot de passe à usage unique"), + ("Use one-time password", "Utiliser un mot de passe à usage unique"), + ("One-time password length", "Longueur du mot de passe à usage unique"), + ("Request access to your device", "Demande l’accès à votre appareil"), + ("Hide connection management window", "Cacher la fenêtre de gestion de la connexion"), + ("hide_cm_tip", "Requiert d’accepter les sessions via mot de passe avec un mot de passe permanent"), + ("wayland_experiment_tip", "La prise en charge de Wayland est en phase expérimentale, veuillez utiliser X11 si vous avez besoin d’un accès non assisté."), + ("Right click to select tabs", "Clic droit pour sélectionner les onglets"), ("Skipped", "Ignoré"), - ("Add to address book", "Ajouter au carnet d'adresses"), + ("Add to address book", "Ajouter au carnet d’adresses"), ("Group", "Groupe"), ("Search", "Rechercher"), - ("Closed manually by web console", "Fermé manuellement par la console Web"), + ("Closed manually by web console", "Terminée manuellement par la console web"), ("Local keyboard type", "Disposition du clavier local"), - ("Select local keyboard type", "Selectionner la disposition du clavier local"), - ("software_render_tip", "Si vous avez une carte graphique NVIDIA et que la fenêtre distante se ferme immédiatement après la connexion, l'installation du pilote Nouveau et le choix d'utiliser le rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), - ("Always use software rendering", "Utiliser toujours le rendu logiciel"), - ("config_input", "Afin de contrôler le bureau à distance avec le clavier, vous devez accorder à RustDesk l'autorisation \"Surveillance de l’entrée\"."), - ("config_microphone", "Pour discuter à distance, vous devez accorder à RustDesk les autorisations « Enregistrer l'audio »."), - ("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"), + ("Select local keyboard type", "Sélectionner la disposition du clavier local"), + ("software_render_tip", "Si vous utilisez une carte graphique Nvidia sous Linux et que la fenêtre distante se ferme immédiatement après la connexion, l’installation du pilote open-source Nouveau et l’utilisation du rendu du logiciel peuvent aider. Un redémarrage du logiciel est requis."), + ("Always use software rendering", "Toujours utiliser le rendu logiciel"), + ("config_input", "Vous devez accorder à RustDesk l’autorisation « Surveillance de l’entrée » pour contrôler le bureau distant avec le clavier."), + ("config_microphone", "Vous devez accorder à RustDesk l’autorisation « Enregistrer l’audio » pour discuter à distance."), + ("request_elevation_tip", "Vous pouvez également demander une élévation des privilèges si un utilisateur est présent côté distant."), + ("Wait", "Attendre"), + ("Elevation Error", "Erreur d’élévation des privilèges"), + ("Ask the remote user for authentication", "Demander à l’utilisateur distant de s’authentifier"), + ("Choose this if the remote account is administrator", "Sélectionnez cette option si le compte distant est administrateur"), + ("Transmit the username and password of administrator", "Transmettre le nom d’utilisateur et le mot de passe d’un compte administrateur"), + ("still_click_uac_tip", "L’utilisateur distant devra malgré tout confirmer l’UAC de l’instance RustDesk en cours d’éxécution."), + ("Request Elevation", "Demander l’élévation des privilèges"), + ("wait_accept_uac_tip", "Veuillez attendre l’acceptation de l’UAC par l’utilisateur distant."), + ("Elevate successfully", "Élévation des privilèges réussie"), ("uppercase", "majuscule"), ("lowercase", "minuscule"), ("digit", "chiffre"), ("special character", "caractère spécial"), - ("length>=8", "longueur>=8"), + ("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 ?"), + ("Please confirm if you want to share your desktop?", "Voulez-vous vraiment partager votre bureau ?"), ("Display", "Affichage"), ("Default View Style", "Style de vue par défaut"), ("Default Scroll Style", "Style de défilement par défaut"), - ("Default Image Quality", "Qualité d'image par défaut"), + ("Default Image Quality", "Qualité d’image par défaut"), ("Default Codec", "Codec par défaut"), ("Bitrate", "Débit"), ("FPS", "FPS"), ("Auto", "Auto"), ("Other Default Options", "Autres options par défaut"), - ("Voice call", "Appel voix"), + ("Voice call", "Appel vocal"), ("Text chat", "Conversation textuelle"), - ("Stop voice call", "Stopper l'appel voix"), - ("relay_hint_tip", "Il se peut qu'il ne doit pas possible de se connecter directement, vous pouvez essayer de vous connecter via un relais. \nEn outre, si vous souhaitez utiliser directement le relais, vous pouvez ajouter le suffixe \"/r\" à l'ID ou sélectionner l'option \"Toujours se connecter via le relais\" dans la fiche appareils distants."), + ("Stop voice call", "Terminer l’appel vocal"), + ("relay_hint_tip", "Il n’est pas toujours possible d’établir une connexion directe, mais une connexion via serveur relais est envisageable. En outre, si vous souhaitez utiliser un relais dès la première tentative, vous pouvez ajouter le suffixe « /r » à l’ID ou activer l’option « Forcer la connexion via relais » depuis la carte des sessions récentes, si elle s’y trouve."), ("Reconnect", "Se reconnecter"), ("Codec", "Codec"), ("Resolution", "Résolution"), - ("No transfers in progress", "Pas de transfert en cours"), + ("No transfers in progress", "Aucun transfert en cours"), ("Set one-time password length", "Définir la longueur du mot de passe à usage unique"), - ("RDP Settings", "Configuration RDP"), + ("RDP Settings", "Paramètres RDP"), ("Sort by", "Trier par"), ("New Connection", "Nouvelle connexion"), ("Restore", "Restaurer"), ("Minimize", "Minimiser"), ("Maximize", "Maximiser"), ("Your Device", "Votre appareil"), - ("empty_recent_tip", "Oups, pas de sessions récentes !\nIl est temps d'en prévoir une nouvelle."), - ("empty_favorite_tip", "Vous n'avez pas encore d'appareils distants favorits ?\nTrouvons quelqu'un avec qui vous connecter et ajoutez-les à vos favoris !"), - ("empty_lan_tip", "Oh non, il semble que nous n'ayons pas encore d'appareils réseau local découverts."), - ("empty_address_book_tip", "Ouh là là ! il semble qu'il n'y ait actuellement aucun appareil distant répertorié dans votre carnet d'adresses."), - ("eg: admin", "ex: admin"), - ("Empty Username", "Nom d'utilisation non spécifié"), - ("Empty Password", "Mot de passe non spécifié"), + ("empty_recent_tip", "Oups, aucune session récente !\nIl est l’heure d’en organiser une nouvelle."), + ("empty_favorite_tip", "Vous n’avez pas encore d’appareils distants favoris ?\nTrouvez quelqu’un avec qui vous connecter et ajoutez-le à vos favoris !"), + ("empty_lan_tip", "Oh non, il semble que nous n’avons pas encore découvert d’appareils sur le réseau local."), + ("empty_address_book_tip", "Mince, il n’y a actuellement aucun appareil distant répertorié dans votre carnet d’adresses."), + ("Empty Username", "Nom d’utilisation non renseigné"), + ("Empty Password", "Mot de passe non renseigné"), ("Me", "Moi"), - ("identical_file_tip", "Ce fichier est identique à celui de l'appareil distant."), - ("show_monitors_tip", "Afficher les moniteurs dans la barre d'outils"), + ("identical_file_tip", "Ce fichier est identique à celui sur l’appareil distant."), + ("show_monitors_tip", "Afficher les écrans dans la barre d’outils"), ("View Mode", "Mode vue"), - ("login_linux_tip", "Se connecter au compte Linux distant"), + ("login_linux_tip", "Vous devez vous connecter au compte Linux distant pour établir une session de bureau X"), ("verify_rustdesk_password_tip", "Vérifier le mot de passe RustDesk"), ("remember_account_tip", "Se souvenir de ce compte"), - ("os_account_desk_tip", "Ce compte est utilisé pour se connecter au système d'exploitation distant et activer la session de bureau en mode sans affichage"), - ("OS Account", "Compte système d'exploitation"), + ("os_account_desk_tip", "Ce compte est utilisé pour se connecter au système d’exploitation distant et activer la session de bureau en mode sans affichage"), + ("OS Account", "Compte du système d’exploitation"), ("another_user_login_title_tip", "Un autre utilisateur est déjà connecté"), - ("another_user_login_text_tip", "Déconnexion"), + ("another_user_login_text_tip", "Déconnecter"), ("xorg_not_found_title_tip", "Xorg introuvable"), ("xorg_not_found_text_tip", "Veuillez installer Xorg"), - ("no_desktop_title_tip", "Aucun gestionaire de bureau n'est disponible"), - ("no_desktop_text_tip", "Veuillez installer le gestionaire de bureau GNOME"), - ("No need to elevate", "Pas besoin de permissions administrateur"), + ("no_desktop_title_tip", "Aucun environnement de bureau n’est disponible"), + ("no_desktop_text_tip", "Veuillez installer l’environnement de bureau GNOME"), + ("No need to elevate", "Élever les privilèges n’est pas nécessaire"), ("System Sound", "Son système"), ("Default", "Défaut"), ("New RDP", "Nouvel RDP"), - ("Fingerprint", "Empreinte digitale"), - ("Copy Fingerprint", "Copier empreinte digitale"), - ("no fingerprints", "Pas d'empreintes digitales"), - ("Select a peer", "Sélectionnez l'appareil distant"), - ("Select peers", "Sélectionnez des appareils distants"), + ("Fingerprint", "Empreinte numérique"), + ("Copy Fingerprint", "Copier l’empreinte numérique"), + ("no fingerprints", "Aucune empreinte numérique"), + ("Select a peer", "Sélectionnez l’appareil distant"), + ("Select peers", "Sélectionnez les appareils distants"), ("Plugins", "Plugins"), ("Uninstall", "Désinstaller"), - ("Update", "Mise à jour"), - ("Enable", "Activé"), - ("Disable", "Desactivé"), + ("Update", "Mettre à jour"), + ("Enable", "Activer"), + ("Disable", "Désactiver"), ("Options", "Options"), - ("resolution_original_tip", "Résolution d'origine"), - ("resolution_fit_local_tip", "Adapter la résolution local"), + ("resolution_original_tip", "Résolution d’origine"), + ("resolution_fit_local_tip", "Adapter à la résolution locale"), ("resolution_custom_tip", "Résolution personnalisée"), - ("Collapse toolbar", "Réduire la barre d'outils"), - ("Accept and Elevate", "Accepter et autoriser l'augmentation des privilèges"), - ("accept_and_elevate_btn_tooltip", "Accepter la connexion l'augmentation des privilèges UAC."), - ("clipboard_wait_response_timeout_tip", "Expiration du délai d'attente presse-papiers."), + ("Collapse toolbar", "Réduire la barre d’outils"), + ("Accept and Elevate", "Accepter et élever les privilèges"), + ("accept_and_elevate_btn_tooltip", "Accepter la connexion et élever les privilèges UAC."), + ("clipboard_wait_response_timeout_tip", "Expiration du délai d’attente du presse-papier."), ("Incoming connection", "Connexion entrante"), ("Outgoing connection", "Connexion sortante"), ("Exit", "Quitter"), ("Open", "Ouvrir"), - ("logout_tip", "Êtes-vous sûr de vouloir vous déconnecter ?"), + ("logout_tip", "Voulez-vous vraiment vous déconnecter ?"), ("Service", "Service"), - ("Start", "Lancer"), - ("Stop", "Stopper"), - ("exceed_max_devices", "Vous avez atteint le nombre maximal d'appareils gérés."), + ("Start", "Démarrer"), + ("Stop", "Arrêter"), + ("exceed_max_devices", "Vous avez atteint le nombre maximal d’appareils gérés."), ("Sync with recent sessions", "Synchroniser avec les sessions récentes"), ("Sort tags", "Trier les étiquettes"), - ("Open connection in new tab", "Ouvrir la connexion dans un nouvel onglet"), - ("Move tab to new window", "Déplacer l'onglet vers une nouvelle fenêtre"), - ("Can not be empty", "Ne peux pas être vide"), + ("Open connection in new tab", "Ouvrir les connexions dans un nouvel onglet"), + ("Move tab to new window", "Déplacer l’onglet vers une nouvelle fenêtre"), + ("Can not be empty", "Ne peut pas être vide"), ("Already exists", "Existe déjà"), - ("Change Password", "Changer le mot de passe"), + ("Change Password", "Modifier le mot de passe"), ("Refresh Password", "Actualiser le mot de passe"), ("ID", "ID"), ("Grid View", "Vue Grille"), ("List View", "Vue Liste"), ("Select", "Sélectionner"), - ("Toggle Tags", "Basculer vers les étiquettes"), - ("pull_ab_failed_tip", "Impossible d'actualiser le carnet d'adresses"), - ("push_ab_failed_tip", "Échec de la synchronisation du carnet d'adresses"), - ("synced_peer_readded_tip", "Les appareils qui étaient présents dans les sessions récentes seront synchronisés avec le carnet d'adresses."), + ("Toggle Tags", "Basculer les étiquettes"), + ("pull_ab_failed_tip", "Échec de l’actualisation du carnet d’adresses"), + ("push_ab_failed_tip", "Échec de la synchronisation du carnet d’adresses avec le serveur"), + ("synced_peer_readded_tip", "Les appareils qui étaient présents dans les sessions récentes seront synchronisés vers le carnet d’adresses."), ("Change Color", "Modifier la couleur"), - ("Primary Color", "Couleur primaire"), - ("HSV Color", "Couleur TSL"), - ("Installation Successful!", "Installation réussie !"), - ("Installation failed!", "Échec de l'installation !"), + ("Primary Color", "Couleur principale"), + ("HSV Color", "Couleur TSV"), + ("Installation Successful!", "Installation réussie !"), + ("Installation failed!", "Échec de l’installation !"), ("Reverse mouse wheel", "Inverser le sens de la molette de la souris"), - ("{} sessions", "{} sessions"), - ("scam_title", "Vous êtes peut-être victime d'une ESCROQUERIE !"), - ("scam_text1", "Si vous êtes au téléphone avec quelqu'un QUE VOUS NE CONNAISSEZ PAS et en qui VOUS N'AVEZ PAS CONFIANCE et qui vous a demandé d'utiliser RustDesk et de démarrer le service, ne le faites pas et raccrochez immédiatement."), - ("scam_text2", "Il s'agit probablement d'un escroc qui tente de vous voler de l'argent ou d'autres informations personnelles."), - ("Don't show again", "Ne plus montrer"), - ("I Agree", "J'accepte"), + ("{} sessions", "{} sessions"), + ("scam_title", "Vous êtes peut-être victime d’une ESCROQUERIE !"), + ("scam_text1", "Si vous êtes au téléphone avec quelqu’un QUE VOUS NE CONNAISSEZ PAS et en qui VOUS N’AVEZ PAS CONFIANCE et qui vous a demandé d’utiliser RustDesk et de démarrer le service, ne le faites pas et raccrochez immédiatement."), + ("scam_text2", "Il s’agit probablement d’un escroc qui tente de vous voler de l’argent ou d’autres informations personnelles."), + ("Don't show again", "Ne plus afficher"), + ("I Agree", "J’accepte"), ("Decline", "Refuser"), - ("Timeout in minutes", "Délai d'expiration en minutes"), - ("auto_disconnect_option_tip", "Fermer automatiquement les sessions entrantes en cas d'inactivité de l'utilisateur"), - ("Connection failed due to inactivity", "Déconnecté automatiquement pour cause d'inactivité"), + ("Timeout in minutes", "Délai d’expiration en minutes"), + ("auto_disconnect_option_tip", "Terminer automatiquement les sessions entrantes en cas d’inactivité de l’utilisateur"), + ("Connection failed due to inactivity", "Déconnecté automatiquement pour cause d’inactivité"), ("Check for software update on startup", "Vérifier la disponibilité des mises à jour au démarrage"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Veuillez mettre à jour RustDesk Server Pro avec la version {} ou une version plus récente !"), - ("pull_group_failed_tip", "Échec de l'actualisation du groupe"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Veuillez mettre à jour RustDesk Server Pro vers la version {} ou une version ultérieure !"), + ("pull_group_failed_tip", "Échec de l’actualisation du groupe"), ("Filter by intersection", "Filtrer par intersection"), - ("Remove wallpaper during incoming sessions", "Supprimer le fond d'écran lors des sessions entrantes"), - ("Test", ""), - ("display_is_plugged_out_msg", "L'écran est débranché, passez au premier écran."), + ("Remove wallpaper during incoming sessions", "Cacher le fond d’écran lors des sessions entrantes"), + ("Test", "Test"), + ("display_is_plugged_out_msg", "L’affichage est débranché, passez sur le premier affichage."), ("No displays", "Aucun affichage"), ("Open in new window", "Ouvrir dans une nouvelle fenêtre"), ("Show displays as individual windows", "Montrer les affichages sous forme de fenêtres individuelles"), - ("Use all my displays for the remote session", "Utiliser tous mes écrans pour la session à distance"), - ("selinux_tip", "SELinux est activé sur votre appareil, ce qui peut empêcher RustDesk de fonctionner correctement en tant que machine contrôlé."), - ("Change view", "Disposition d'affichage"), + ("Use all my displays for the remote session", "Utiliser tous mes affichages pour la session à distance"), + ("selinux_tip", "SELinux est activé sur votre appareil, ce qui peut empêcher RustDesk de fonctionner correctement sur la machine contrôlée."), + ("Change view", "Disposition"), ("Big tiles", "Grandes tuiles"), ("Small tiles", "Petites tuiles"), ("List", "Liste"), ("Virtual display", "Affichage virtuel"), - ("Plug out all", "Déconnecter tout"), + ("Plug out all", "Tout débrancher"), ("True color (4:4:4)", "Couleur réelle (4:4:4)"), - ("Enable blocking user input", "Activer le blocage des entrées utilisateur"), - ("id_input_tip", "Vous pouvez saisir un ID, une adresse IP directe ou un nom de domaine avec un port (:).\nSi vous souhaitez accéder à un appareil sur un autre serveur, veuillez ajouter l'adresse du serveur (?key=), par exemple,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi vous souhaitez accéder à un appareil sur un serveur public, veuillez saisir \"@public\" , la clé n'est pas nécessaire pour le serveur public"), - ("privacy_mode_impl_mag_tip", "Mode 1"), - ("privacy_mode_impl_virtual_display_tip", "Mode 2"), - ("Enter privacy mode", "Passer en mode confidentialité"), - ("Exit privacy mode", "Quitter le mode confidentialité"), - ("idd_not_support_under_win10_2004_tip", "Le pilote d'affichage indirect n'est pas pris en charge. Windows 10, version 2004 ou plus récente est requise."), - ("input_source_1_tip", "Source entrée 1"), - ("input_source_2_tip", "Source entrée 2"), - ("Swap control-command key", "Échanger la touche de controle-commande"), - ("swap-left-right-mouse", "Intervertir le bouton gauche et droit de la souris"), - ("2FA code", "code 2FA"), + ("Enable blocking user input", "Activer le blocage des entrées de l’utilisateur"), + ("id_input_tip", "Vous pouvez saisir un ID, une adresse IP ou un nom de domaine suivi d’un port (:).\nSi vous souhaitez accéder à un appareil sur un autre serveur, veuillez ajouter l’adresse du serveur (@?key=), par exemple :\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi vous souhaitez accéder à un appareil sur un serveur public, veuillez saisir « @public » (la clé n’est pas nécessaire pour le serveur public).\n\nSi vous souhaitez forcer l’utilisation d’une connexion via relais dès la première tentative, ajoutez « /r » après l’ID, par exemple : « 9123456234/r »."), + ("privacy_mode_impl_mag_tip", "Mode 1"), + ("privacy_mode_impl_virtual_display_tip", "Mode 2"), + ("Enter privacy mode", "Entrer en mode de confidentialité"), + ("Exit privacy mode", "Quitter le mode de confidentialité"), + ("idd_not_support_under_win10_2004_tip", "Le pilote d’affichage indirect n’est pas pris en charge. Windows 10 version 2004 ou ultérieure est requis."), + ("input_source_1_tip", "Entrée source 1"), + ("input_source_2_tip", "Entrée source 2"), + ("Swap control-command key", "Intervertir la touche contrôle-commande"), + ("swap-left-right-mouse", "Intervertir les boutons gauche et droit de la souris"), + ("2FA code", "Code 2FA"), ("More", "Plus"), - ("enable-2fa-title", "Activer l'authentification à double facteur"), - ("enable-2fa-desc", "Veuillez configurer votre authentificateur maintenant. Vous pouvez utiliser une application d’authentification telle qu’Authy, Microsoft ou Google Authenticator sur votre téléphone ou votre ordinateur de bureau.nnScannez le code QR avec votre application et entrez le code affiché par votre application pour activer l’authentification à deux facteurs."), - ("wrong-2fa-code", "Impossible de vérifier le code. Vérifiez que le code et les paramètres d’heure locale sont corrects"), + ("enable-2fa-title", "Activer l’authentification à deux facteurs"), + ("enable-2fa-desc", "Veuillez maintenant configurer votre authentificateur. Vous pouvez utiliser une application d’authentification telle qu’Authy, Microsoft ou Google Authenticator sur votre téléphone ou votre ordinateur.\n\nScannez le code QR avec votre application puis saisissez le code affiché par votre application afin d’activer l’authentification à deux facteurs."), + ("wrong-2fa-code", "Impossible de vérifier le code. Vérifiez l’exactitude du code saisi ainsi que des paramètres d’heure locale"), ("enter-2fa-title", "Authentification à deux facteurs"), - ("Email verification code must be 6 characters.", "Le code de vérification email doit comporter 6 caractères"), - ("2FA code must be 6 digits.", "le code 2FA doit comporter 6 chiffres"), - ("Multiple Windows sessions found", "Plusieurs sessions Windows trouvées"), - ("Please select the session you want to connect to", "Merci de sélectionner la session Windows à laquelle vous voulez vous connecter"), - ("powered_by_me", ""), - ("outgoing_only_desk_tip", "Il s’agit d’une édition personnalisée.\nVous pouvez vous connecter à d’autres appareils, mais les autres appareils ne peuvent pas se connecter à votre appareil."), - ("preset_password_warning", "Cette édition personnalisée est livrée avec un mot de passe prédéfini. Toute personne connaissant ce mot de passe pourrait prendre le contrôle total de votre appareil. Si vous ne vous y attendiez pas, désinstallez immédiatement le logiciel."), + ("Email verification code must be 6 characters.", "Le code de vérification de l’adresse électronique doit être composé de 6 caractères."), + ("2FA code must be 6 digits.", "Le code 2FA doit être composé de 6 chiffres."), + ("Multiple Windows sessions found", "Plusieurs sessions Windows ont été trouvées"), + ("Please select the session you want to connect to", "Veuillez sélectionner la session à laquelle vous souhaitez vous connecter"), + ("powered_by_me", "Utilise la technologie RustDesk"), + ("outgoing_only_desk_tip", "Vous utilisez une version personnalisée.\nVous pouvez vous connecter à d’autres appareils, mais les autres appareils ne peuvent pas se connecter au vôtre."), + ("preset_password_warning", "Cette version personnalisée est livrée avec un mot de passe prédéfini. Toute personne connaissant ce mot de passe pourrait prendre le contrôle total de votre appareil. Si vous ne vous y attendiez pas, désinstallez immédiatement le logiciel."), ("Security Alert", "Alerte de sécurité"), - ("My address book", "Mon carnet d'adresse"), + ("My address book", "Mon carnet d’adresses"), ("Personal", "Personnel"), ("Owner", "Propriétaire"), ("Set shared password", "Définir le mot de passe partagé"), ("Exist in", "Existe dans"), - ("Read-only", "Lecture-seule"), + ("Read-only", "Lecture seule"), ("Read/Write", "Lecture/Écriture"), - ("Full Control", "Control Total"), + ("Full Control", "Contrôle complet"), ("share_warning_tip", "Les champs ci-dessus sont partagés et visibles par les autres."), ("Everyone", "Tout le monde"), - ("ab_web_console_tip", "Plus sur la console Web"), + ("ab_web_console_tip", "Plus sur la console web"), ("allow-only-conn-window-open-tip", "N’autoriser la connexion que si la fenêtre RustDesk est ouverte"), - ("no_need_privacy_mode_no_physical_displays_tip", "Pas d’affichage physique, pas besoin d’utiliser le mode confidentialité."), + ("no_need_privacy_mode_no_physical_displays_tip", "Aucun affichage physique ; l’utilisation du mode de confidentialité n’est pas nécessaire."), ("Follow remote cursor", "Suivre le curseur distant"), - ("Follow remote window focus", ""), + ("Follow remote window focus", "Suivre la focalisation de fenêtre distante"), ("default_proxy_tip", "Le protocole et le port par défaut sont Socks5 et 1080"), ("no_audio_input_device_tip", "Aucun périphérique d’entrée audio trouvé."), - ("Incoming", "Entrant"), - ("Outgoing", "Sortant"), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", "Une nouvelle demande d’appel vocal a été reçue. Si vous acceptez, l’audio passera à la communication vocale."), - ("texture_render_tip", "Utilisez le rendu des textures pour rendre les images plus fluides."), + ("Incoming", "Entrantes"), + ("Outgoing", "Sortantes"), + ("Clear Wayland screen selection", "Effacer la sélection d’écran Wayland"), + ("clear_Wayland_screen_selection_tip", "Une fois la sélection d’écran effacée, vous pourrez resélectionner l’écran à partager."), + ("confirm_clear_Wayland_screen_selection_tip", "Voulez-vous vraiment effacer la sélection d’écran Wayland ?"), + ("android_new_voice_call_tip", "Une nouvelle demande d’appel vocal a été reçue. Si vous acceptez, l’audio passera sur la communication vocale."), + ("texture_render_tip", "Utiliser le rendu de texture afin de lisser les images. Désactiver cette option permet de résoudre certains problèmes de rendu."), ("Use texture rendering", "Utiliser le rendu de texture"), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Floating window", "Fenêtre flottante"), + ("floating_window_tip", "Aide à maintenir le service en arrière-plan"), + ("Keep screen on", "Maintenir l’écran allumé"), + ("Never", "Jamais"), + ("During controlled", "Lorsque l’appareil est contrôlé"), + ("During service is on", "Lorsque le service est actif"), + ("Capture screen using DirectX", "Utiliser DirectX pour capturer l’écran"), + ("Back", "Retour"), + ("Apps", "Applis"), + ("Volume up", "Volume haut"), + ("Volume down", "Volume bas"), + ("Power", "Marche/Arrêt"), + ("Telegram bot", "Bot Telegram"), + ("enable-bot-tip", "Activer cette fonctionnalité vous permet de recevoir le code 2FA depuis votre bot. Peut également servir de notification de connexion."), + ("enable-bot-desc", "1. Entamez une discussion avec @BotFather.\n2. Envoyez-lui la commande « newbot ». Vous recevrez un jeton suite à cette étape.\n3. Entamez une discussion avec votre bot nouvellement créé. Envoyez-lui un message commençant par une barre oblique (« / ») tel que « /hello » afin de l’activer.\n"), + ("cancel-2fa-confirm-tip", "Voulez-vous vraiment désactiver l’authentication à deux facteurs ?"), + ("cancel-bot-confirm-tip", "Voulez-vous vraiment désactiver le bot Telegram ?"), + ("About RustDesk", "À propos de RustDesk"), + ("Send clipboard keystrokes", "Taper le contenu du presse-papier"), + ("network_error_tip", "Veuillez vérifier votre connexion réseau puis réessayer."), + ("Unlock with PIN", "Déverrouiller par code PIN"), + ("Requires at least {} characters", "Requiert un minimum de {} caractères"), + ("Wrong PIN", "Code PIN erroné"), + ("Set PIN", "Définir le code PIN"), + ("Enable trusted devices", "Activer les appareils de confiance"), + ("Manage trusted devices", "Gérer les appareils de confiance"), + ("Platform", "Plateforme"), + ("Days remaining", "Jours restants"), + ("enable-trusted-devices-tip", "Ne pas demander de code 2FA sur les appareils de confiance"), + ("Parent directory", "Répertoire parent"), + ("Resume", "Reprendre"), + ("Invalid file name", "Nom de fichier non valide"), + ("one-way-file-transfer-tip", "Le transfert de fichiers à sens unique est activé côté appareil contrôlé."), + ("Authentication Required", "Authentication requise"), + ("Authenticate", "Authentifier"), + ("web_id_input_tip", "Vous pouvez saisir un ID sur le même serveur ; le client web ne prend pas en charge l’accès par adresse IP.\nSi vous souhaitez accéder à un appareil sur un autre serveur, veuillez ajouter l’adresse du serveur (@?key=), par exemple :\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi vous souhaitez accéder à un appareil sur un serveur public, veuillez saisir « @public » (la clé n’est pas nécessaire pour le serveur public)."), + ("Download", "Télécharger"), + ("Upload folder", "Téléverser le dossier"), + ("Upload files", "Téléverser les fichiers"), + ("Clipboard is synchronized", "Le presse-papier est synchronisé"), + ("Update client clipboard", "Actualiser le presse-papier du client"), + ("Untagged", "Sans étiquette"), + ("new-version-of-{}-tip", "Une nouvelle version de {} est disponible"), + ("Accessible devices", "Appareils accessibles"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Veuillez mettre le client RustDesk distant à jour vers la version {} ou ultérieure !"), + ("d3d_render_tip", "Sur certaines machines, l’écran du contrôle à distance peut rester noir lors de l’utilisation du rendu D3D."), + ("Use D3D rendering", "Utiliser le rendu D3D"), + ("Printer", "Imprimante"), + ("printer-os-requirement-tip", "La fonction d’impression sortante nécessite Windows 10 ou une version ultérieure."), + ("printer-requires-installed-{}-client-tip", "{} doit être installé sur cet appareil avant de pouvoir utiliser l’impression à distance."), + ("printer-{}-not-installed-tip", "L’imprimante {} n’est pas installée."), + ("printer-{}-ready-tip", "L’imprimante {} est installée et opérationnelle."), + ("Install {} Printer", "Installer l’imprimante {}"), + ("Outgoing Print Jobs", "Impressions sortantes"), + ("Incoming Print Jobs", "Impressions entrantes"), + ("Incoming Print Job", "Impression entrante"), + ("use-the-default-printer-tip", "Utiliser l’imprimante par défaut"), + ("use-the-selected-printer-tip", "Utiliser l’imprimante sélectionnée"), + ("auto-print-tip", "Imprimer automatiquement en utilisant l’imprimante sélectionnée."), + ("print-incoming-job-confirm-tip", "L’appareil distant vous a envoyé une impression ; voulez-vous l’exécuter de votre côté ?"), + ("remote-printing-disallowed-tile-tip", "Impression à distance non autorisée"), + ("remote-printing-disallowed-text-tip", "Les paramètres de l’appareil contrôlé n’autorisent pas l’impression à distance."), + ("save-settings-tip", "Enregistrer les paramètres"), + ("dont-show-again-tip", "Ne plus afficher"), + ("Take screenshot", "Prendre une capture d’écran"), + ("Taking screenshot", "Prise de capture d’écran"), + ("screenshot-merged-screen-not-supported-tip", "Actuellement, la prise de capture d’écran ne prend pas en charge les affichages multiples. Veuillez réessayer après avoir sélectionné un seul affichage."), + ("screenshot-action-tip", "Veuillez choisir l’action à effectuer avec la capture d’écran."), + ("Save as", "Enregistrer sous"), + ("Copy to clipboard", "Copier dans le presse-papier"), + ("Enable remote printer", "Activer l’impression à distance"), + ("Downloading {}", "Téléchargement de {}"), + ("{} Update", "Mise à jour de {}"), + ("{}-to-update-tip", "{} va maintenant quitter afin d’installer la nouvelle version."), + ("download-new-version-failed-tip", "Le téléchargement a échoué. Vous pouvez réessayer, ou bien cliquer sur le bouton « Télécharger » pour vous rendre sur la page de publication afin de mettre à jour manuellement."), + ("Auto update", "Installer les mises à jour automatiquement"), + ("update-failed-check-msi-tip", "La vérification de la méthode d’installation a échoué. Veuillez cliquer sur le bouton « Télécharger » pour vous rendre sur la page de publication afin de mettre à jour manuellement."), + ("websocket_tip", "Seules les connexions via relais sont prises en charge lors de l’utilisation de WebSocket."), + ("Use WebSocket", "Utiliser WebSocket"), + ("Trackpad speed", "Vitesse du pavé tactile"), + ("Default trackpad speed", "Vitesse par défaut du pavé tactile"), + ("Numeric one-time password", "Mot de passe à usage unique numérique"), + ("Enable IPv6 P2P connection", "Activer la connexion P2P IPv6"), + ("Enable UDP hole punching", "Activer le « hole punching » UDP"), + ("View camera", "Afficher la caméra"), + ("Enable camera", "Activer la caméra"), + ("No cameras", "Aucune caméra"), + ("view_camera_unsupported_tip", "L’appareil distant ne prend pas en charge l’affichage de la caméra."), + ("Terminal", "Terminal"), + ("Enable terminal", "Activer le terminal"), + ("New tab", "Nouvel onglet"), + ("Keep terminal sessions on disconnect", "Maintenir les sessions du terminal lors de la déconnexion"), + ("Terminal (Run as administrator)", "Terminal (administrateur)"), + ("terminal-admin-login-tip", "Veuillez saisir le nom d’utilisateur et le mot de passe de l’administrateur de l’appareil contrôlé."), + ("Failed to get user token.", "Échec de l’obtention du jeton utilisateur."), + ("Incorrect username or password.", "Nom d’utilisateur ou mot de passe incorrect."), + ("The user is not an administrator.", "L’utilisateur n’est pas un administrateur."), + ("Failed to check if the user is an administrator.", "Échec de la vérification du statut d’administrateur de l’utilisateur."), + ("Supported only in the installed version.", "Uniquement pris en charge dans la version installée."), + ("elevation_username_tip", "Saisissez un nom d’utilisateur ou un domaine\\utilisateur"), + ("Preparing for installation ...", "Préparation de l’installation…"), ].iter().cloned().collect(); } diff --git a/src/lang/ge.rs b/src/lang/ge.rs new file mode 100644 index 00000000000..168752abc1c --- /dev/null +++ b/src/lang/ge.rs @@ -0,0 +1,713 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "სტáƒáƒ¢áƒ£áƒ¡áƒ˜"), + ("Your Desktop", "თქვენი სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდáƒ"), + ("desk_tip", "თქვენი სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდრხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜áƒ áƒáƒ› ID-ით დრპáƒáƒ áƒáƒšáƒ˜áƒ—."), + ("Password", "პáƒáƒ áƒáƒšáƒ˜"), + ("Ready", "მზáƒáƒ“áƒáƒ"), + ("Established", "დáƒáƒ›áƒ§áƒáƒ áƒ”ბულიáƒ"), + ("connecting_status", "RustDesk ქსელთáƒáƒœ დáƒáƒ™áƒáƒ•შირებáƒ..."), + ("Enable service", "სერვისის ჩáƒáƒ áƒ—ვáƒ"), + ("Start service", "სერვისის გáƒáƒ¨áƒ•ებáƒ"), + ("Service is running", "სერვისი გáƒáƒ¨áƒ•ებულიáƒ"), + ("Service is not running", "სერვისი áƒáƒ  áƒáƒ áƒ˜áƒ¡ გáƒáƒ¨áƒ•ებული"), + ("not_ready_status", "áƒáƒ  áƒáƒ áƒ˜áƒ¡ დáƒáƒ™áƒáƒ•შირებული. შეáƒáƒ›áƒáƒ¬áƒ›áƒ”თ კáƒáƒ•შირი."), + ("Control Remote Desktop", "áƒáƒ®áƒáƒšáƒ˜ კáƒáƒ•შირი"), + ("Transfer file", "ფáƒáƒ˜áƒšáƒ”ბის გáƒáƒ“áƒáƒªáƒ”მáƒ"), + ("Connect", "დáƒáƒ™áƒáƒ•შირებáƒ"), + ("Recent sessions", "ბáƒáƒšáƒ სესიები"), + ("Address book", "მისáƒáƒ›áƒáƒ áƒ—ების წიგნი"), + ("Confirmation", "დáƒáƒ“áƒáƒ¡áƒ¢áƒ£áƒ áƒ”ბáƒ"), + ("TCP tunneling", "TCP ტუნელირებáƒ"), + ("Remove", "წáƒáƒ¨áƒšáƒ"), + ("Refresh random password", "შემთხვევითი პáƒáƒ áƒáƒšáƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ"), + ("Set your own password", "სáƒáƒ™áƒ£áƒ—áƒáƒ áƒ˜ პáƒáƒ áƒáƒšáƒ˜áƒ¡ დáƒáƒ§áƒ”ნებáƒ"), + ("Enable keyboard/mouse", "კლáƒáƒ•იáƒáƒ¢áƒ£áƒ áƒ˜áƒ¡/თáƒáƒ’უნáƒáƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Enable clipboard", "გáƒáƒªáƒ•ლის ბუფერის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Enable file transfer", "ფáƒáƒ˜áƒšáƒ”ბის გáƒáƒ“áƒáƒªáƒ”მის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Enable TCP tunneling", "TCP ტუნელირების გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("IP Whitelisting", "დáƒáƒ¨áƒ•ებული IP მისáƒáƒ›áƒáƒ áƒ—ების სიáƒ"), + ("ID/Relay Server", "ID/რეტრáƒáƒœáƒ¡áƒšáƒáƒ¢áƒáƒ áƒ˜"), + ("Import server config", "სერვერის კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ იმპáƒáƒ áƒ¢áƒ˜"), + ("Export Server Config", "სერვერის კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ ექსპáƒáƒ áƒ¢áƒ˜"), + ("Import server configuration successfully", "სერვერის კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ წáƒáƒ áƒ›áƒáƒ¢áƒ”ბით იმპáƒáƒ áƒ¢áƒ˜áƒ áƒ”ბულიáƒ"), + ("Export server configuration successfully", "სერვერის კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ წáƒáƒ áƒ›áƒáƒ¢áƒ”ბით ექსპáƒáƒ áƒ¢áƒ˜áƒ áƒ”ბულიáƒ"), + ("Invalid server configuration", "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ სერვერის კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ"), + ("Clipboard is empty", "გáƒáƒªáƒ•ლის ბუფერი ცáƒáƒ áƒ˜áƒ”ლიáƒ"), + ("Stop service", "სერვისის გáƒáƒ©áƒ”რებáƒ"), + ("Change ID", "ID-ის შეცვლáƒ"), + ("Your new ID", "თქვენი áƒáƒ®áƒáƒšáƒ˜ ID"), + ("length %min% to %max%", "სიგრძე %min%...%max%"), + ("starts with a letter", "იწყებრáƒáƒ¡áƒáƒ—ი"), + ("allowed characters", "დáƒáƒ¨áƒ•ებული სიმბáƒáƒšáƒáƒ”ბი"), + ("id_change_tip", "დáƒáƒ¨áƒ•ებულირმხáƒáƒšáƒáƒ“ a-z, A-Z, 0-9, - (დეფისი) დრ_ (ქვედრტირე) სიმბáƒáƒšáƒáƒ”ბი. პირველი უნდრიყáƒáƒ¡ a-z, A-Z áƒáƒ¡áƒ. სიგრძე 6-დáƒáƒœ 16-მდე."), + ("Website", "ვებგვერდი"), + ("About", "პრáƒáƒ’რáƒáƒ›áƒ˜áƒ¡ შესáƒáƒ®áƒ”ბ"), + ("Slogan_tip", "შექმნილირგულით áƒáƒ› შეშლილ სáƒáƒ›áƒ§áƒáƒ áƒáƒ¨áƒ˜!"), + ("Privacy Statement", "კáƒáƒœáƒ¤áƒ˜áƒ“ენციáƒáƒšáƒ£áƒ áƒáƒ‘ის გáƒáƒœáƒáƒªáƒ®áƒáƒ“ი"), + ("Mute", "ხმის გáƒáƒ—იშვáƒ"), + ("Build Date", "áƒáƒ’ების თáƒáƒ áƒ˜áƒ¦áƒ˜"), + ("Version", "ვერსიáƒ"), + ("Home", "მთáƒáƒ•áƒáƒ áƒ˜"), + ("Audio Input", "áƒáƒ£áƒ“ირშესáƒáƒ•áƒáƒšáƒ˜"), + ("Enhancements", "გáƒáƒ£áƒ›áƒ¯áƒáƒ‘ესებები"), + ("Hardware Codec", "áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒšáƒ˜ კáƒáƒ“ეკი"), + ("Adaptive bitrate", "áƒáƒ“áƒáƒžáƒ¢áƒ£áƒ áƒ˜ ბიტრეიტი"), + ("ID Server", "ID სერვერი"), + ("Relay Server", "რეტრáƒáƒœáƒ¡áƒšáƒáƒ¢áƒáƒ áƒ˜"), + ("API Server", "API სერვერი"), + ("invalid_http", "მისáƒáƒ›áƒáƒ áƒ—ი უნდრიწყებáƒáƒ“ეს http:// áƒáƒœ https://-ით"), + ("Invalid IP", "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ IP მისáƒáƒ›áƒáƒ áƒ—ი"), + ("Invalid format", "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ ფáƒáƒ áƒ›áƒáƒ¢áƒ˜"), + ("server_not_support", "ჯერ სერვერით áƒáƒ  áƒáƒ áƒ˜áƒ¡ მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი"), + ("Not available", "მიუწვდáƒáƒ›áƒ”ლიáƒ"), + ("Too frequent", "ძáƒáƒšáƒ˜áƒáƒœ ხშირáƒáƒ“"), + ("Cancel", "გáƒáƒ£áƒ¥áƒ›áƒ”ბáƒ"), + ("Skip", "გáƒáƒ›áƒáƒ¢áƒáƒ•ებáƒ"), + ("Close", "დáƒáƒ®áƒ£áƒ áƒ•áƒ"), + ("Retry", "ხელáƒáƒ®áƒšáƒ ცდáƒ"), + ("OK", "დიáƒáƒ®"), + ("Password Required", "სáƒáƒ­áƒ˜áƒ áƒáƒ პáƒáƒ áƒáƒšáƒ˜"), + ("Please enter your password", "შეიყვáƒáƒœáƒ”თ თქვენი პáƒáƒ áƒáƒšáƒ˜"), + ("Remember password", "პáƒáƒ áƒáƒšáƒ˜áƒ¡ დáƒáƒ›áƒáƒ®áƒ¡áƒáƒ•რებáƒ"), + ("Wrong Password", "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ პáƒáƒ áƒáƒšáƒ˜"), + ("Do you want to enter again?", "გსურთ ხელáƒáƒ®áƒšáƒ შესვლáƒ?"), + ("Connection Error", "დáƒáƒ™áƒáƒ•შირების შეცდáƒáƒ›áƒ"), + ("Error", "შეცდáƒáƒ›áƒ"), + ("Reset by the peer", "გáƒáƒ“áƒáƒ¢áƒ•ირთულირდáƒáƒ¨áƒáƒ áƒ”ბული კვáƒáƒœáƒ«áƒ˜áƒ¡ მიერ"), + ("Connecting...", "დáƒáƒ™áƒáƒ•შირებáƒ..."), + ("Connection in progress. Please wait.", "მიმდინáƒáƒ áƒ”áƒáƒ‘ს დáƒáƒ™áƒáƒ•შირებáƒ. გთხáƒáƒ•თ, მáƒáƒ˜áƒªáƒáƒ“áƒáƒ—."), + ("Please try 1 minute later", "სცáƒáƒ“ეთ ერთი წუთის შემდეგ"), + ("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 Ctrl + Alt + Del", "Ctrl + Alt + Del ჩáƒáƒ¡áƒ›áƒ"), + ("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", "დáƒáƒáƒ­áƒ˜áƒ áƒ”თ გáƒáƒœáƒáƒ®áƒšáƒ”ბისთვის"), + ("Configure", "კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ"), + ("config_acc", "თქვენი სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდის დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ მáƒáƒ áƒ—ვისთვის უნდრმიáƒáƒœáƒ˜áƒ­áƒáƒ— RustDesk-ს \"წვდáƒáƒ›áƒ˜áƒ¡\" უფლებები"), + ("config_screen", "სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდáƒáƒ–ე დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ წვდáƒáƒ›áƒ˜áƒ¡áƒ—ვის უნდრმიáƒáƒœáƒ˜áƒ­áƒáƒ— RustDesk-ს \"ეკრáƒáƒœáƒ˜áƒ¡ áƒáƒœáƒáƒ‘ეჭდის\" უფლებები"), + ("Installing ...", "ინსტáƒáƒšáƒáƒªáƒ˜áƒ..."), + ("Install", "დáƒáƒ˜áƒœáƒ¡áƒ¢áƒáƒšáƒ˜áƒ áƒ”ბáƒ"), + ("Installation", "ინსტáƒáƒšáƒáƒªáƒ˜áƒ"), + ("Installation Path", "ინსტáƒáƒšáƒáƒªáƒ˜áƒ˜áƒ¡ გზáƒ"), + ("Create start menu shortcuts", "მენიუში მáƒáƒšáƒ¡áƒáƒ®áƒ›áƒáƒ‘ების შექმნáƒ"), + ("Create desktop icon", "სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდáƒáƒ–ე ხáƒáƒ¢áƒ£áƒšáƒ˜áƒ¡ შექმნáƒ"), + ("agreement_tip", "ინსტáƒáƒšáƒáƒªáƒ˜áƒ˜áƒ¡ დáƒáƒ¬áƒ§áƒ”ბით თქვენ ეთáƒáƒœáƒ®áƒ›áƒ”ბით სáƒáƒšáƒ˜áƒªáƒ”ნზირშეთáƒáƒœáƒ®áƒ›áƒ”ბის პირáƒáƒ‘ებს."), + ("Accept and Install", "დáƒáƒ—áƒáƒœáƒ®áƒ›áƒ”ბრდრინსტáƒáƒšáƒáƒªáƒ˜áƒ"), + ("End-user license agreement", "სáƒáƒ‘áƒáƒšáƒáƒ მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბლის სáƒáƒšáƒ˜áƒªáƒ”ნზირშეთáƒáƒœáƒ®áƒ›áƒ”ბáƒ"), + ("Generating ...", "გენერáƒáƒªáƒ˜áƒ..."), + ("Your installation is lower version.", "თქვენი ინსტáƒáƒšáƒáƒªáƒ˜áƒ უფრრáƒáƒ“რეული ვერსიáƒáƒ."), + ("not_close_tcp_tip", "ტუნელის გáƒáƒ›áƒáƒ§áƒ”ნებისáƒáƒ¡ áƒáƒ  დáƒáƒ®áƒ£áƒ áƒáƒ— ეს ფáƒáƒœáƒ¯áƒáƒ áƒ."), + ("Listening ...", "მáƒáƒ¡áƒ›áƒ”ნáƒ..."), + ("Remote Host", "დáƒáƒ¨áƒáƒ áƒ”ბული კვáƒáƒœáƒ«áƒ˜"), + ("Remote Port", "დáƒáƒ¨áƒáƒ áƒ”ბული პáƒáƒ áƒ¢áƒ˜"), + ("Action", "მáƒáƒ¥áƒ›áƒ”დებáƒ"), + ("Add", "დáƒáƒ›áƒáƒ¢áƒ”ბáƒ"), + ("Local Port", "ლáƒáƒ™áƒáƒšáƒ£áƒ áƒ˜ პáƒáƒ áƒ¢áƒ˜"), + ("Local Address", "ლáƒáƒ™áƒáƒšáƒ£áƒ áƒ˜ მისáƒáƒ›áƒáƒ áƒ—ი"), + ("Change Local Port", "ლáƒáƒ™áƒáƒšáƒ£áƒ áƒ˜ პáƒáƒ áƒ¢áƒ˜áƒ¡ შეცვლáƒ"), + ("setup_server_tip", "უფრრსწრáƒáƒ¤áƒ˜ დáƒáƒ™áƒáƒ•შირებისთვის დáƒáƒáƒ§áƒ”ნეთ სáƒáƒ™áƒ£áƒ—áƒáƒ áƒ˜ სერვერი."), + ("Too short, at least 6 characters.", "ძáƒáƒšáƒ˜áƒáƒœ მáƒáƒ™áƒšáƒ”áƒ, მინიმუმ 6 სიმბáƒáƒšáƒ."), + ("The confirmation is not identical.", "დáƒáƒ“áƒáƒ¡áƒ¢áƒ£áƒ áƒ”ბრáƒáƒ  ემთხვევáƒ"), + ("Permissions", "უფლებები"), + ("Accept", "მიღებáƒ"), + ("Dismiss", "უáƒáƒ áƒ§áƒáƒ¤áƒ"), + ("Disconnect", "გáƒáƒ—იშვáƒ"), + ("Enable file copy and paste", "ფáƒáƒ˜áƒšáƒ”ბის კáƒáƒžáƒ˜áƒ áƒ”ბის დრჩáƒáƒ¡áƒ›áƒ˜áƒ¡ დáƒáƒ¨áƒ•ებáƒ"), + ("Connected", "დáƒáƒ™áƒáƒ•შირებულიáƒ"), + ("Direct and encrypted connection", "პირდáƒáƒžáƒ˜áƒ áƒ˜ დრდáƒáƒ¨áƒ˜áƒ¤áƒ áƒ£áƒšáƒ˜ კáƒáƒ•შირი"), + ("Relayed and encrypted connection", "რეტრáƒáƒœáƒ¡áƒšáƒ˜áƒ áƒ”ბული დრდáƒáƒ¨áƒ˜áƒ¤áƒ áƒ£áƒšáƒ˜ კáƒáƒ•შირი"), + ("Direct and unencrypted connection", "პირდáƒáƒžáƒ˜áƒ áƒ˜ დრდáƒáƒ£áƒ¨áƒ˜áƒ¤áƒ áƒáƒ•ი კáƒáƒ•შირი"), + ("Relayed and unencrypted connection", "რეტრáƒáƒœáƒ¡áƒšáƒ˜áƒ áƒ”ბული დრდáƒáƒ£áƒ¨áƒ˜áƒ¤áƒ áƒáƒ•ი კáƒáƒ•შირი"), + ("Enter Remote ID", "შეიყვáƒáƒœáƒ”თ დáƒáƒ¨áƒáƒ áƒ”ბული ID"), + ("Enter your password", "შეიყვáƒáƒœáƒ”თ თქვენი პáƒáƒ áƒáƒšáƒ˜"), + ("Logging in...", "შესვლáƒ..."), + ("Enable RDP session sharing", "RDP სესიის გáƒáƒ–იáƒáƒ áƒ”ბის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Auto Login", "áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒ˜ შესვლრáƒáƒœáƒ’áƒáƒ áƒ˜áƒ¨áƒ¨áƒ˜"), + ("Enable direct IP access", "პირდáƒáƒžáƒ˜áƒ áƒ˜ IP წვდáƒáƒ›áƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Rename", "გáƒáƒ“áƒáƒ áƒ¥áƒ›áƒ”ვáƒ"), + ("Space", "სივრცე"), + ("Create desktop shortcut", "სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდáƒáƒ–ე მáƒáƒšáƒ¡áƒáƒ®áƒ›áƒáƒ‘ის შექმნáƒ"), + ("Change Path", "გზის შეცვლáƒ"), + ("Create Folder", "სáƒáƒ¥áƒáƒ¦áƒáƒšáƒ“ის შექმნáƒ"), + ("Please enter the folder name", "შეიყვáƒáƒœáƒ”თ სáƒáƒ¥áƒáƒ¦áƒáƒšáƒ“ის სáƒáƒ®áƒ”ლი"), + ("Fix it", "გáƒáƒ›áƒáƒ¡áƒ¬áƒáƒ áƒ”ბáƒ"), + ("Warning", "გáƒáƒ¤áƒ áƒ—ხილებáƒ"), + ("Login screen using Wayland is not supported", "Wayland-ის გáƒáƒ›áƒáƒ§áƒ”ნებით შესვლის ეკრáƒáƒœáƒ˜ áƒáƒ  áƒáƒ áƒ˜áƒ¡ მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი"), + ("Reboot required", "სáƒáƒ­áƒ˜áƒ áƒáƒ გáƒáƒ“áƒáƒ¢áƒ•ირთვáƒ"), + ("Unsupported display server", "áƒáƒ áƒáƒ›áƒ®áƒáƒ áƒ“áƒáƒ­áƒ”რილი ჩვენების სერვერი"), + ("x11 expected", "მáƒáƒ¡áƒáƒšáƒáƒ“ნელირX11"), + ("Port", "პáƒáƒ áƒ¢áƒ˜"), + ("Settings", "პáƒáƒ áƒáƒ›áƒ”ტრები"), + ("Username", "მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბლის სáƒáƒ®áƒ”ლი"), + ("Invalid port", "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ პáƒáƒ áƒ¢áƒ˜"), + ("Closed manually by the peer", "დáƒáƒ®áƒ£áƒ áƒ£áƒšáƒ˜áƒ დáƒáƒ¨áƒáƒ áƒ”ბული კვáƒáƒœáƒ«áƒ˜áƒ¡ მიერ ხელით"), + ("Enable remote configuration modification", "დáƒáƒ¨áƒáƒ áƒ”ბული კáƒáƒœáƒ¤áƒ˜áƒ’ურáƒáƒªáƒ˜áƒ˜áƒ¡ ცვლილების დáƒáƒ¨áƒ•ებáƒ"), + ("Run without install", "გáƒáƒ¨áƒ•ებრინსტáƒáƒšáƒáƒªáƒ˜áƒ˜áƒ¡ გáƒáƒ áƒ”შე"), + ("Connect via relay", "რეტრáƒáƒœáƒ¡áƒšáƒáƒ¢áƒáƒ áƒ˜áƒ¡ მეშვეáƒáƒ‘ით დáƒáƒ™áƒáƒ•შირებáƒ"), + ("Always connect via relay", "ყáƒáƒ•ელთვის დáƒáƒ™áƒáƒ•შირებრრეტრáƒáƒœáƒ¡áƒšáƒáƒ¢áƒáƒ áƒ˜áƒ¡ მეშვეáƒáƒ‘ით"), + ("whitelist_tip", "მხáƒáƒšáƒáƒ“ თეთრ სიáƒáƒ¨áƒ˜ áƒáƒ áƒ¡áƒ”ბულ IP მისáƒáƒ›áƒáƒ áƒ—ებს შეუძლიáƒáƒ— ჩემს მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე წვდáƒáƒ›áƒ."), + ("Login", "შესვლáƒ"), + ("Verify", "შემáƒáƒ¬áƒ›áƒ”ბáƒ"), + ("Remember me", "დáƒáƒ›áƒ˜áƒ›áƒáƒ®áƒ¡áƒáƒ•რე"), + ("Trust this device", "სáƒáƒœáƒ“რმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ"), + ("Verification code", "შემáƒáƒ¬áƒ›áƒ”ბის კáƒáƒ“ი"), + ("verification_tip", "áƒáƒ¦áƒ›áƒáƒ©áƒ”ნილირáƒáƒ®áƒáƒšáƒ˜ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ, რეგისტრირებულ ელფáƒáƒ¡áƒ¢áƒáƒ–ე გáƒáƒ’ზáƒáƒ•ნილირშემáƒáƒ¬áƒ›áƒ”ბის კáƒáƒ“ი. შეიყვáƒáƒœáƒ”თ ის სისტემáƒáƒ¨áƒ˜ შესვლის გáƒáƒ¡áƒáƒ’რძელებლáƒáƒ“."), + ("Logout", "გáƒáƒ›áƒáƒ¡áƒ•ლáƒ"), + ("Tags", "ჭდეები"), + ("Search ID", "ID-ით ძიებáƒ"), + ("whitelist_sep", "გáƒáƒ›áƒáƒ§áƒáƒ¤áƒ მძიმით, წერტილ-მძიმით, ჰáƒáƒ áƒ˜áƒ— áƒáƒœ áƒáƒ®áƒáƒšáƒ˜ ხáƒáƒ–ით."), + ("Add ID", "ID-ის დáƒáƒ›áƒáƒ¢áƒ”ბáƒ"), + ("Add Tag", "სáƒáƒ™áƒ•áƒáƒœáƒ«áƒ სიტყვის დáƒáƒ›áƒáƒ¢áƒ”ბáƒ"), + ("Unselect all tags", "ყველრჭდის მáƒáƒ®áƒ¡áƒœáƒ"), + ("Network error", "ქსელის შეცდáƒáƒ›áƒ"), + ("Username missed", "მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბლის სáƒáƒ®áƒ”ლი áƒáƒ™áƒšáƒ˜áƒ"), + ("Password missed", "პáƒáƒ áƒáƒšáƒ˜ დáƒáƒ’áƒáƒ•იწყდáƒáƒ—"), + ("Wrong credentials", "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ მáƒáƒœáƒáƒªáƒ”მები"), + ("The verification code is incorrect or has expired", "შემáƒáƒ¬áƒ›áƒ”ბის კáƒáƒ“ი áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜áƒ áƒáƒœ ვáƒáƒ“áƒáƒ’áƒáƒ¡áƒ£áƒšáƒ˜áƒ"), + ("Edit Tag", "ჭდის შეცვლáƒ"), + ("Forget Password", "პáƒáƒ áƒáƒšáƒ˜áƒ¡ დáƒáƒ•იწყებáƒ"), + ("Favorites", "რჩეულები"), + ("Add to Favorites", "რჩეულებში დáƒáƒ›áƒáƒ¢áƒ”ბáƒ"), + ("Remove from Favorites", "რჩეულებიდáƒáƒœ წáƒáƒ¨áƒšáƒ"), + ("Empty", "ცáƒáƒ áƒ˜áƒ”ლი"), + ("Invalid folder name", "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ სáƒáƒ¥áƒáƒ¦áƒáƒšáƒ“ის სáƒáƒ®áƒ”ლი"), + ("Socks5 Proxy", "SOCKS5-პრáƒáƒ¥áƒ¡áƒ˜"), + ("Socks5/Http(s) Proxy", ""), + ("Discovered", "ნáƒáƒžáƒáƒ•ნიáƒ"), + ("install_daemon_tip", "ჩáƒáƒ¢áƒ•ირთვისáƒáƒ¡ გáƒáƒ¡áƒáƒ¨áƒ•ებáƒáƒ“ სáƒáƒ­áƒ˜áƒ áƒáƒ სისტემური სერვისის დáƒáƒ§áƒ”ნებáƒ"), + ("Remote ID", "დáƒáƒ¨áƒáƒ áƒ”ბული ID"), + ("Paste", "ჩáƒáƒ¡áƒ›áƒ"), + ("Paste here?", "ჩáƒáƒ¡áƒ›áƒ áƒáƒ¥?"), + ("Are you sure to close the connection?", "ნáƒáƒ›áƒ“ვილáƒáƒ“ გსურთ კáƒáƒ•შირის დáƒáƒ¡áƒ áƒ£áƒšáƒ”ბáƒ?"), + ("Download new version", "áƒáƒ®áƒáƒšáƒ˜ ვერსიის ჩáƒáƒ›áƒáƒ¢áƒ•ირთვáƒ"), + ("Touch mode", "სენსáƒáƒ áƒ£áƒšáƒ˜ რეჟიმი"), + ("Mouse mode", "თáƒáƒ’უნáƒáƒ¡/ტáƒáƒ©áƒžáƒáƒ“ის რეჟიმი"), + ("One-Finger Tap", "ერთი თითით შეხებáƒ"), + ("Left Mouse", "თáƒáƒ’უნáƒáƒ¡ მáƒáƒ áƒªáƒ®áƒ”ნრღილáƒáƒ™áƒ˜"), + ("One-Long Tap", "ერთი თითით ხáƒáƒœáƒ’რძლივი შეხებáƒ"), + ("Two-Finger Tap", "áƒáƒ áƒ˜ თითით შეხებáƒ"), + ("Right Mouse", "თáƒáƒ’უნáƒáƒ¡ მáƒáƒ áƒ¯áƒ•ენრღილáƒáƒ™áƒ˜"), + ("One-Finger Move", "ერთი თითით გáƒáƒ“áƒáƒáƒ“გილებáƒ"), + ("Double Tap & Move", "áƒáƒ áƒ›áƒáƒ’ი შეხებრდრგáƒáƒ“áƒáƒáƒ“გილებáƒ"), + ("Mouse Drag", "თáƒáƒ’უნáƒáƒ—ი გáƒáƒ“áƒáƒ—რევáƒ"), + ("Three-Finger vertically", "სáƒáƒ›áƒ˜ თითით ვერტიკáƒáƒšáƒ£áƒ áƒáƒ“"), + ("Mouse Wheel", "თáƒáƒ’უნáƒáƒ¡ ბáƒáƒ áƒ‘áƒáƒšáƒ˜"), + ("Two-Finger Move", "áƒáƒ áƒ˜ თითით გáƒáƒ“áƒáƒáƒ“გილებáƒ"), + ("Canvas Move", "ტილáƒáƒ¡ გáƒáƒ“áƒáƒáƒ“გილებáƒ"), + ("Pinch to Zoom", "მáƒáƒ¡áƒ¨áƒ¢áƒáƒ‘ირებრთითებით"), + ("Canvas Zoom", "ტილáƒáƒ¡ მáƒáƒ¡áƒ¨áƒ¢áƒáƒ‘ი"), + ("Reset canvas", "ტილáƒáƒ¡ მáƒáƒ¡áƒ¨áƒ¢áƒáƒ‘ის გáƒáƒ“áƒáƒ¢áƒ•ირთვáƒ"), + ("No permission of file transfer", "ფáƒáƒ˜áƒšáƒ”ბის გáƒáƒ“áƒáƒªáƒ”მის უფლებრáƒáƒ  áƒáƒ áƒ˜áƒ¡"), + ("Note", "შენიშვნáƒ"), + ("Connection", "კáƒáƒ•შირი"), + ("Share screen", "ეკრáƒáƒœáƒ˜áƒ¡ დემáƒáƒœáƒ¡áƒ¢áƒ áƒáƒªáƒ˜áƒ"), + ("Chat", "ჩáƒáƒ¢áƒ˜"), + ("Total", "სულ"), + ("items", "ელემენტები"), + ("Selected", "áƒáƒ áƒ©áƒ”ულიáƒ"), + ("Screen Capture", "ეკრáƒáƒœáƒ˜áƒ¡ ჩáƒáƒ¬áƒ”რáƒ"), + ("Input Control", "შეყვáƒáƒœáƒ˜áƒ¡ კáƒáƒœáƒ¢áƒ áƒáƒšáƒ˜"), + ("Audio Capture", "áƒáƒ£áƒ“იáƒáƒ¡ ჩáƒáƒ¬áƒ”რáƒ"), + ("Do you accept?", "თáƒáƒœáƒáƒ®áƒ›áƒ ხáƒáƒ áƒ—?"), + ("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_service_will_start_tip", "ეკრáƒáƒœáƒ˜áƒ¡ ჩáƒáƒ¬áƒ”რის ჩáƒáƒ áƒ—ვრáƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒáƒ“ გáƒáƒ£áƒ¨áƒ•ებს სერვისს, რáƒáƒª სხვრმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ებს სáƒáƒ¨áƒ£áƒáƒšáƒ”ბáƒáƒ¡ áƒáƒ«áƒšáƒ”ვს მáƒáƒ˜áƒ—ხáƒáƒ•áƒáƒœ áƒáƒ› მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ¡áƒ—áƒáƒœ დáƒáƒ™áƒáƒ•შირებáƒ."), + ("android_stop_service_tip", "სერვისის დáƒáƒ®áƒ£áƒ áƒ•რáƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒáƒ“ დáƒáƒ®áƒ£áƒ áƒáƒ•ს ყველრდáƒáƒ›áƒ§áƒáƒ áƒ”ბულ კáƒáƒ•შირს."), + ("android_version_audio_tip", "Android-ის მიმდინáƒáƒ áƒ” ვერსირáƒáƒ  უჭერს მხáƒáƒ áƒ¡ ხმის ჩáƒáƒ¬áƒ”რáƒáƒ¡, გáƒáƒœáƒáƒáƒ®áƒšáƒ”თ Android 10-მდე áƒáƒœ უფრრáƒáƒ®áƒáƒš ვერსიáƒáƒ›áƒ“ე."), + ("android_start_service_tip", "დáƒáƒáƒ­áƒ˜áƒ áƒ”თ [სერვისის გáƒáƒ¨áƒ•ებáƒ] áƒáƒœ დáƒáƒ£áƒ¨áƒ•ით [ეკრáƒáƒœáƒ˜áƒ¡ ჩáƒáƒ¬áƒ”რáƒ] ეკრáƒáƒœáƒ˜áƒ¡ დემáƒáƒœáƒ¡áƒ¢áƒ áƒáƒªáƒ˜áƒ˜áƒ¡ სერვისის გáƒáƒ¡áƒáƒ¨áƒ•ებáƒáƒ“."), + ("android_permission_may_not_change_tip", "დáƒáƒ›áƒ§áƒáƒ áƒ”ბული კáƒáƒ•შირების უფლებები ვერ შეიცვლებáƒ, სáƒáƒ­áƒ˜áƒ áƒáƒ ხელáƒáƒ®áƒáƒšáƒ˜ დáƒáƒ™áƒáƒ•შირებáƒ."), + ("Account", "áƒáƒœáƒ’áƒáƒ áƒ˜áƒ¨áƒ˜"), + ("Overwrite", "გáƒáƒ“áƒáƒ¬áƒ”რáƒ"), + ("This file exists, skip or overwrite this file?", "ფáƒáƒ˜áƒšáƒ˜ უკვე áƒáƒ áƒ¡áƒ”ბáƒáƒ‘ს, გáƒáƒ›áƒáƒ¢áƒáƒ•áƒáƒ— თუ გáƒáƒ“áƒáƒ•წერáƒáƒ—?"), + ("Quit", "გáƒáƒ¡áƒ•ლáƒ"), + ("Help", "დáƒáƒ®áƒ›áƒáƒ áƒ”ბáƒ"), + ("Failed", "ვერ შესრულდáƒ"), + ("Succeeded", "შესრულდáƒ"), + ("Someone turns on privacy mode, exit", "ვიღáƒáƒªáƒáƒ› ჩáƒáƒ áƒ—რკáƒáƒœáƒ¤áƒ˜áƒ“ენციáƒáƒšáƒ£áƒ áƒáƒ‘ის რეჟიმი, გáƒáƒ¡áƒ•ლáƒ"), + ("Unsupported", "áƒáƒ  áƒáƒ áƒ˜áƒ¡ მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი"), + ("Peer denied", "უáƒáƒ áƒ§áƒáƒ¤áƒ˜áƒšáƒ˜áƒ დáƒáƒ¨áƒáƒ áƒ”ბული კვáƒáƒœáƒ«áƒ˜áƒ¡ მიერ"), + ("Please install plugins", "დáƒáƒáƒ˜áƒœáƒ¡áƒ¢áƒáƒšáƒ˜áƒ áƒ”თ პლáƒáƒ’ინები"), + ("Peer exit", "გáƒáƒ—იშულირმáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბლის მიერ"), + ("Failed to turn off", "გáƒáƒ›áƒáƒ áƒ—ვრშეუძლებელიáƒ"), + ("Turned off", "გáƒáƒ›áƒáƒ áƒ—ული"), + ("Language", "ენáƒ"), + ("Keep RustDesk background service", "RustDesk-ის ფáƒáƒœáƒ£áƒ áƒ˜ სერვისის შენáƒáƒ áƒ©áƒ£áƒœáƒ”ბáƒ"), + ("Ignore Battery Optimizations", "ბáƒáƒ¢áƒáƒ áƒ”ის áƒáƒžáƒ¢áƒ˜áƒ›áƒ˜áƒ–áƒáƒªáƒ˜áƒ˜áƒ¡ იგნáƒáƒ áƒ˜áƒ áƒ”ბáƒ"), + ("android_open_battery_optimizations_tip", "გáƒáƒ“áƒáƒ“ით პáƒáƒ áƒáƒ›áƒ”ტრების შემდეგ გვერდზე"), + ("Start on boot", "ჩáƒáƒ áƒ—ვისáƒáƒ¡ გáƒáƒ¨áƒ•ებáƒ"), + ("Start the screen sharing service on boot, requires special permissions", "ეკრáƒáƒœáƒ˜áƒ¡ გáƒáƒ–იáƒáƒ áƒ”ბის სერვისის გáƒáƒ¨áƒ•ებრჩáƒáƒ áƒ—ვისáƒáƒ¡ (სáƒáƒ­áƒ˜áƒ áƒáƒ”ბს სპეციáƒáƒšáƒ£áƒ  უფლებებს)"), + ("Connection not allowed", "კáƒáƒ•შირი áƒáƒ  áƒáƒ áƒ˜áƒ¡ დáƒáƒ¨áƒ•ებული"), + ("Legacy mode", "ძველი რეჟიმი"), + ("Map mode", "რუკის რეჟიმი"), + ("Translate mode", "თáƒáƒ áƒ’მნის რეჟიმი"), + ("Use permanent password", "მუდმივი პáƒáƒ áƒáƒšáƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Use both passwords", "áƒáƒ áƒ˜áƒ•ე პáƒáƒ áƒáƒšáƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Set permanent password", "მუდმივი პáƒáƒ áƒáƒšáƒ˜áƒ¡ დáƒáƒ§áƒ”ნებáƒ"), + ("Enable 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 Toolbar", "ხელსáƒáƒ¬áƒ§áƒáƒ—რპáƒáƒœáƒ”ლის ჩვენებáƒ"), + ("Hide Toolbar", "ხელსáƒáƒ¬áƒ§áƒáƒ—რპáƒáƒœáƒ”ლის დáƒáƒ›áƒáƒšáƒ•áƒ"), + ("Direct Connection", "პირდáƒáƒžáƒ˜áƒ áƒ˜ კáƒáƒ•შირი"), + ("Relay Connection", "რეტრáƒáƒœáƒ¡áƒšáƒ˜áƒ áƒ”ბული კáƒáƒ•შირი"), + ("Secure Connection", "უსáƒáƒ¤áƒ áƒ—ხრკáƒáƒ•შირი"), + ("Insecure Connection", "áƒáƒ áƒáƒ£áƒ¡áƒáƒ¤áƒ áƒ—ხრკáƒáƒ•შირი"), + ("Scale original", "áƒáƒ áƒ˜áƒ’ინáƒáƒšáƒ£áƒ áƒ˜ მáƒáƒ¡áƒ¨áƒ¢áƒáƒ‘ი"), + ("Scale adaptive", "áƒáƒ“áƒáƒžáƒ¢áƒ˜áƒ áƒ”ბáƒáƒ“ი მáƒáƒ¡áƒ¨áƒ¢áƒáƒ‘ი"), + ("General", "ზáƒáƒ’áƒáƒ“ი"), + ("Security", "უსáƒáƒ¤áƒ áƒ—ხáƒáƒ”ბáƒ"), + ("Theme", "თემáƒ"), + ("Dark Theme", "მუქი თემáƒ"), + ("Light Theme", "ნáƒáƒ—ელი თემáƒ"), + ("Dark", "მუქი"), + ("Light", "ნáƒáƒ—ელი"), + ("Follow System", "სისტემური"), + ("Enable hardware codec", "áƒáƒžáƒáƒ áƒáƒ¢áƒ£áƒ áƒ£áƒšáƒ˜ კáƒáƒ“ეკის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Unlock Security Settings", "უსáƒáƒ¤áƒ áƒ—ხáƒáƒ”ბის პáƒáƒ áƒáƒ›áƒ”ტრების გáƒáƒœáƒ‘ლáƒáƒ™áƒ•áƒ"), + ("Enable audio", "áƒáƒ£áƒ“იáƒáƒ¡ ჩáƒáƒ áƒ—ვáƒ"), + ("Unlock Network Settings", "ქსელის პáƒáƒ áƒáƒ›áƒ”ტრების გáƒáƒœáƒ‘ლáƒáƒ™áƒ•áƒ"), + ("Server", "სერვერი"), + ("Direct IP Access", "პირდáƒáƒžáƒ˜áƒ áƒ˜ IP წვდáƒáƒ›áƒ"), + ("Proxy", "პრáƒáƒ¥áƒ¡áƒ˜"), + ("Apply", "გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Disconnect all devices?", "გáƒáƒ•თიშáƒáƒ— ყველრმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ?"), + ("Clear", "გáƒáƒ¡áƒ£áƒ¤áƒ—áƒáƒ•ებáƒ"), + ("Audio Input Device", "áƒáƒ£áƒ“იáƒáƒ¡ შეყვáƒáƒœáƒ˜áƒ¡ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ"), + ("Use IP Whitelisting", "IP თეთრი სიის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Network", "ქსელი"), + ("Pin Toolbar", "ხელსáƒáƒ¬áƒ§áƒáƒ—რპáƒáƒœáƒ”ლის მიმáƒáƒ’რებáƒ"), + ("Unpin Toolbar", "ხელსáƒáƒ¬áƒ§áƒáƒ—რპáƒáƒœáƒ”ლის მáƒáƒ®áƒ¡áƒœáƒ"), + ("Recording", "ჩáƒáƒ¬áƒ”რáƒ"), + ("Directory", "სáƒáƒ¥áƒáƒ¦áƒáƒšáƒ“ე"), + ("Automatically record incoming sessions", "შემáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ სესიების áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒ˜ ჩáƒáƒ¬áƒ”რáƒ"), + ("Automatically record outgoing sessions", "გáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ სესიების áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒ˜ ჩáƒáƒ¬áƒ”რáƒ"), + ("Change", "შეცვლáƒ"), + ("Start session recording", "სესიის ჩáƒáƒ¬áƒ”რის დáƒáƒ¬áƒ§áƒ”ბáƒ"), + ("Stop session recording", "სესიის ჩáƒáƒ¬áƒ”რის შეწყვეტáƒ"), + ("Enable 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-ის დისტრიბუტივის უფრრáƒáƒ®áƒáƒšáƒ˜ ვერსიáƒ. გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნეთ X11 სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდრáƒáƒœ შეცვáƒáƒšáƒ”თ áƒáƒžáƒ”რáƒáƒªáƒ˜áƒ£áƒšáƒ˜ სისტემáƒ."), + ("JumpLink", "ნáƒáƒ®áƒ•áƒ"), + ("Please Select the screen to be shared(Operate on the peer side).", "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ ეკრáƒáƒœáƒ˜ გáƒáƒ¡áƒáƒ–იáƒáƒ áƒ”ბლáƒáƒ“ (იმუშáƒáƒ•ეთ პáƒáƒ áƒ¢áƒœáƒ˜áƒáƒ áƒ˜áƒ¡ მხáƒáƒ áƒ”ს)."), + ("Show RustDesk", "RustDesk-ის ჩვენებáƒ"), + ("This PC", "ეს კáƒáƒ›áƒžáƒ˜áƒ£áƒ¢áƒ”რი"), + ("or", "áƒáƒœ"), + ("Continue with", "გáƒáƒ’რძელებáƒ"), + ("Elevate", "უფლებების áƒáƒ¬áƒ”ვáƒ"), + ("Zoom cursor", "კურსáƒáƒ áƒ˜áƒ¡ მáƒáƒ¡áƒ¨áƒ¢áƒáƒ‘ირებáƒ"), + ("Accept sessions via password", "სესიების მიღებრპáƒáƒ áƒáƒšáƒ˜áƒ—"), + ("Accept sessions via click", "სესიების მიღებრღილáƒáƒ™áƒ–ე დáƒáƒ­áƒ”რით"), + ("Accept sessions via both", "სესიების მიღებრპáƒáƒ áƒáƒšáƒ˜áƒ— დრღილáƒáƒ™áƒ–ე დáƒáƒ­áƒ”რით"), + ("Please wait for the remote side to accept your session request...", "გთხáƒáƒ•თ, დáƒáƒ”ლáƒáƒ“áƒáƒ—, სáƒáƒœáƒáƒ› დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ მხáƒáƒ áƒ” მიიღებს თქვენს სესიის მáƒáƒ—ხáƒáƒ•ნáƒáƒ¡..."), + ("One-time Password", "ერთჯერáƒáƒ“ი პáƒáƒ áƒáƒšáƒ˜"), + ("Use one-time password", "ერთჯერáƒáƒ“ი პáƒáƒ áƒáƒšáƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("One-time password length", "ერთჯერáƒáƒ“ი პáƒáƒ áƒáƒšáƒ˜áƒ¡ სიგრძე"), + ("Request access to your device", "თქვენს მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე წვდáƒáƒ›áƒ˜áƒ¡ მáƒáƒ—ხáƒáƒ•ნáƒ"), + ("Hide connection management window", "კáƒáƒ•შირის მáƒáƒ áƒ—ვის ფáƒáƒœáƒ¯áƒ áƒ˜áƒ¡ დáƒáƒ›áƒáƒšáƒ•áƒ"), + ("hide_cm_tip", "დáƒáƒ›áƒáƒšáƒ•ის დáƒáƒ¨áƒ•ებáƒ, თუ სესიები მიიღებრპáƒáƒ áƒáƒšáƒ˜áƒ— áƒáƒœ გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნებრმუდმივი პáƒáƒ áƒáƒšáƒ˜"), + ("wayland_experiment_tip", "Wayland-ის მხáƒáƒ áƒ“áƒáƒ­áƒ”რრექსპერიმენტულ ეტáƒáƒžáƒ–ეáƒ, გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნეთ X11, თუ გჭირდებáƒáƒ— áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒ˜ წვდáƒáƒ›áƒ."), + ("Right click to select tabs", "ჩáƒáƒœáƒáƒ áƒ—ების áƒáƒ áƒ©áƒ”ვრმáƒáƒ áƒ¯áƒ•ენრღილáƒáƒ™áƒ˜áƒ—"), + ("Skipped", "გáƒáƒ›áƒáƒ¢áƒáƒ•ებულიáƒ"), + ("Add to address book", "მისáƒáƒ›áƒáƒ áƒ—ების წიგნში დáƒáƒ›áƒáƒ¢áƒ”ბáƒ"), + ("Group", "ჯგუფი"), + ("Search", "ძიებáƒ"), + ("Closed manually by web console", "ხელით დáƒáƒ˜áƒ®áƒ£áƒ áƒ ვებ-კáƒáƒœáƒ¡áƒáƒšáƒ˜áƒ¡ სáƒáƒ¨áƒ£áƒáƒšáƒ”ბით"), + ("Local keyboard type", "ლáƒáƒ™áƒáƒšáƒ£áƒ áƒ˜ კლáƒáƒ•იáƒáƒ¢áƒ£áƒ áƒ˜áƒ¡ ტიპი"), + ("Select local keyboard type", "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ ლáƒáƒ™áƒáƒšáƒ£áƒ áƒ˜ კლáƒáƒ•იáƒáƒ¢áƒ£áƒ áƒ˜áƒ¡ ტიპი"), + ("software_render_tip", "თუ გáƒáƒ¥áƒ•თ Nvidia ვიდეáƒáƒ‘áƒáƒ áƒáƒ—ი დრდისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ ფáƒáƒœáƒ¯áƒáƒ áƒ იხურებრდáƒáƒ™áƒáƒ•შირებისთáƒáƒœáƒáƒ•ე, შეიძლებრდáƒáƒ’ეხმáƒáƒ áƒáƒ— Nouveau დრáƒáƒ˜áƒ•ერის დáƒáƒ§áƒ”ნებრდრპრáƒáƒ’რáƒáƒ›áƒ£áƒšáƒ˜ ვიზუáƒáƒšáƒ˜áƒ–áƒáƒªáƒ˜áƒ˜áƒ¡ áƒáƒ áƒ©áƒ”ვáƒ. სáƒáƒ­áƒ˜áƒ áƒ იქნებრგáƒáƒ“áƒáƒ¢áƒ•ირთვáƒ."), + ("Always use software rendering", "ყáƒáƒ•ელთვის გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნეთ პრáƒáƒ’რáƒáƒ›áƒ£áƒšáƒ˜ ვიზუáƒáƒšáƒ˜áƒ–áƒáƒªáƒ˜áƒ"), + ("config_input", "დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდის კლáƒáƒ•იáƒáƒ¢áƒ£áƒ áƒ˜áƒ— სáƒáƒ›áƒáƒ áƒ—áƒáƒ•áƒáƒ“, სáƒáƒ­áƒ˜áƒ áƒáƒ RustDesk-ისთვის \"შეყვáƒáƒœáƒ˜áƒ¡ მáƒáƒœáƒ˜áƒ¢áƒáƒ áƒ˜áƒœáƒ’ის\" უფლების მინიჭებáƒ."), + ("config_microphone", "დისტáƒáƒœáƒªáƒ˜áƒ£áƒ  მხáƒáƒ áƒ”სთáƒáƒœ სáƒáƒ¡áƒáƒ£áƒ‘რáƒáƒ“, სáƒáƒ­áƒ˜áƒ áƒáƒ RustDesk-ისთვის \"áƒáƒ£áƒ“იáƒáƒ¡ ჩáƒáƒ¬áƒ”რის\" უფლების მინიჭებáƒ."), + ("request_elevation_tip", "áƒáƒ¡áƒ”ვე შეგიძლიáƒáƒ— მáƒáƒ˜áƒ—ხáƒáƒ•áƒáƒ— უფლებების áƒáƒ¬áƒ”ვáƒ, თუ ვინმე áƒáƒ áƒ˜áƒ¡ დისტáƒáƒœáƒªáƒ˜áƒ£áƒ  მხáƒáƒ áƒ”ს."), + ("Wait", "დáƒáƒ”ლáƒáƒ“ეთ"), + ("Elevation Error", "უფლებების áƒáƒ¬áƒ”ვის შეცდáƒáƒ›áƒ"), + ("Ask the remote user for authentication", "მáƒáƒ˜áƒ—ხáƒáƒ•ეთ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბლისგáƒáƒœ"), + ("Choose this if the remote account is administrator", "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ ეს, თუ დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ áƒáƒœáƒ’áƒáƒ áƒ˜áƒ¨áƒ˜ áƒáƒ“მინისტრáƒáƒ¢áƒáƒ áƒ˜áƒ"), + ("Transmit the username and password of administrator", "áƒáƒ“მინისტრáƒáƒ¢áƒáƒ áƒ˜áƒ¡ სáƒáƒ®áƒ”ლის დრპáƒáƒ áƒáƒšáƒ˜áƒ¡ გáƒáƒ“áƒáƒªáƒ”მáƒ"), + ("still_click_uac_tip", "კვლáƒáƒ• სáƒáƒ­áƒ˜áƒ áƒáƒ, რáƒáƒ› დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ›áƒ მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბელმრდáƒáƒáƒ­áƒ˜áƒ áƒáƒ¡ \"OK\"-ს UAC ფáƒáƒœáƒ¯áƒáƒ áƒáƒ¨áƒ˜ RustDesk-ის გáƒáƒ¨áƒ•ებისáƒáƒ¡."), + ("Request Elevation", "უფლებების áƒáƒ¬áƒ”ვის მáƒáƒ—ხáƒáƒ•ნáƒ"), + ("wait_accept_uac_tip", "დáƒáƒ”ლáƒáƒ“ეთ, სáƒáƒœáƒáƒ› დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბელი დáƒáƒáƒ“áƒáƒ¡áƒ¢áƒ£áƒ áƒ”ბს UAC მáƒáƒ—ხáƒáƒ•ნáƒáƒ¡."), + ("Elevate successfully", "უფლებები წáƒáƒ áƒ›áƒáƒ¢áƒ”ბით áƒáƒ˜áƒ¬áƒ˜áƒ"), + ("uppercase", "დიდი áƒáƒ¡áƒáƒ”ბი"), + ("lowercase", "პáƒáƒ¢áƒáƒ áƒ áƒáƒ¡áƒáƒ”ბი"), + ("digit", "ციფრები"), + ("special character", "სპეციáƒáƒšáƒ£áƒ áƒ˜ სიმბáƒáƒšáƒáƒ”ბი"), + ("length>=8", "8+ სიმბáƒáƒšáƒ"), + ("Weak", "სუსტი"), + ("Medium", "სáƒáƒ¨áƒ£áƒáƒšáƒ"), + ("Strong", "ძლიერი"), + ("Switch Sides", "მხáƒáƒ áƒ”ების გáƒáƒ“áƒáƒ áƒ—ვáƒ"), + ("Please confirm if you want to share your desktop?", "áƒáƒ“áƒáƒ¡áƒ¢áƒ£áƒ áƒ”ბთ, რáƒáƒ› გსურთ სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდის გáƒáƒ–იáƒáƒ áƒ”ბáƒ?"), + ("Display", "ეკრáƒáƒœáƒ˜"), + ("Default View Style", "ნáƒáƒ’ულისხმევი ჩვენების სტილი"), + ("Default Scroll Style", "ნáƒáƒ’ულისხმევი გáƒáƒ“áƒáƒáƒ“გილების სტილი"), + ("Default Image Quality", "ნáƒáƒ’ულისხმევი გáƒáƒ›áƒáƒ¡áƒáƒ®áƒ£áƒšáƒ”ბის ხáƒáƒ áƒ˜áƒ¡áƒ®áƒ˜"), + ("Default Codec", "ნáƒáƒ’ულისხმევი კáƒáƒ“ეკი"), + ("Bitrate", "ბიტრეიტი"), + ("FPS", "კáƒáƒ“რების სიხშირე"), + ("Auto", "áƒáƒ•ტáƒ"), + ("Other Default Options", "სხვრნáƒáƒ’ულისხმევი პáƒáƒ áƒáƒ›áƒ”ტრები"), + ("Voice call", "ხმáƒáƒ•áƒáƒœáƒ˜ ზáƒáƒ áƒ˜"), + ("Text chat", "ტექსტური ჩáƒáƒ¢áƒ˜"), + ("Stop voice call", "ხმáƒáƒ•áƒáƒœáƒ˜ ზáƒáƒ áƒ˜áƒ¡ დáƒáƒ¡áƒ áƒ£áƒšáƒ”ბáƒ"), + ("relay_hint_tip", "პირდáƒáƒžáƒ˜áƒ áƒ˜ კáƒáƒ•შირი შეიძლებრშეუძლებელი იყáƒáƒ¡. áƒáƒ› შემთხვევáƒáƒ¨áƒ˜ შეგიძლიáƒáƒ— სცáƒáƒ“áƒáƒ— რეტრáƒáƒœáƒ¡áƒšáƒáƒ¢áƒáƒ áƒ˜áƒ¡ გáƒáƒ•ლით დáƒáƒ™áƒáƒ•შირებáƒ.\náƒáƒ¡áƒ”ვე, თუ გსურთ პირდáƒáƒžáƒ˜áƒ  რეტრáƒáƒœáƒ¡áƒšáƒáƒ¢áƒáƒ áƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ, შეგიძლიáƒáƒ— დáƒáƒáƒ›áƒáƒ¢áƒáƒ— ID-ს სუფიქსი \"/r\" áƒáƒœ ჩáƒáƒ áƒ—áƒáƒ— \"ყáƒáƒ•ელთვის დáƒáƒ£áƒ™áƒáƒ•შირდით რეტრáƒáƒœáƒ¡áƒšáƒáƒ¢áƒáƒ áƒ˜áƒ¡ გáƒáƒ•ლით\" დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ კვáƒáƒœáƒ«áƒ˜áƒ¡ პáƒáƒ áƒáƒ›áƒ”ტრებში."), + ("Reconnect", "ხელáƒáƒ®áƒšáƒ დáƒáƒ™áƒáƒ•შირებáƒ"), + ("Codec", "კáƒáƒ“ეკი"), + ("Resolution", "გáƒáƒ áƒ©áƒ”ვáƒáƒ“áƒáƒ‘áƒ"), + ("No transfers in progress", "გáƒáƒ“áƒáƒªáƒ”მრáƒáƒ  მიმდინáƒáƒ áƒ”áƒáƒ‘ს"), + ("Set one-time password length", "ერთჯერáƒáƒ“ი პáƒáƒ áƒáƒšáƒ˜áƒ¡ სიგრძის დáƒáƒ§áƒ”ნებáƒ"), + ("RDP Settings", "RDP პáƒáƒ áƒáƒ›áƒ”ტრები"), + ("Sort by", "სáƒáƒ áƒ¢áƒ˜áƒ áƒ”ბáƒ"), + ("New Connection", "áƒáƒ®áƒáƒšáƒ˜ კáƒáƒ•შირი"), + ("Restore", "áƒáƒ¦áƒ“გენáƒ"), + ("Minimize", "ჩáƒáƒ™áƒ”ცვáƒ"), + ("Maximize", "გáƒáƒ¨áƒšáƒ"), + ("Your Device", "თქვენი მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒ"), + ("empty_recent_tip", "áƒáƒ  áƒáƒ áƒ˜áƒ¡ ბáƒáƒšáƒ სესიები!\nდრáƒáƒ დáƒáƒ’ეგმáƒáƒ— áƒáƒ®áƒáƒšáƒ˜."), + ("empty_favorite_tip", "ჯერ áƒáƒ  გáƒáƒ¥áƒ•თ რჩეული დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ კვáƒáƒœáƒ«áƒ”ბი?\nმáƒáƒ“ით, ვნáƒáƒ®áƒáƒ—, ვის შეიძლებრდáƒáƒ•áƒáƒ›áƒáƒ¢áƒáƒ— რჩეულებში!"), + ("empty_lan_tip", "დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ კვáƒáƒœáƒ«áƒ”ბი ვერ მáƒáƒ˜áƒ«áƒ”ბნáƒ."), + ("empty_address_book_tip", "მისáƒáƒ›áƒáƒ áƒ—ების წიგნში áƒáƒ  áƒáƒ áƒ˜áƒ¡ დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ კვáƒáƒœáƒ«áƒ”ბი."), + ("Empty Username", "ცáƒáƒ áƒ˜áƒ”ლი მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბლის სáƒáƒ®áƒ”ლი"), + ("Empty Password", "ცáƒáƒ áƒ˜áƒ”ლი პáƒáƒ áƒáƒšáƒ˜"), + ("Me", "მე"), + ("identical_file_tip", "ფáƒáƒ˜áƒšáƒ˜ იდენტურირდისტáƒáƒœáƒªáƒ˜áƒ£áƒ  კვáƒáƒœáƒ«áƒ–ე áƒáƒ áƒ¡áƒ”ბული ფáƒáƒ˜áƒšáƒ˜áƒ¡"), + ("show_monitors_tip", "მáƒáƒœáƒ˜áƒ¢áƒáƒ áƒ”ბის ჩვენებრხელსáƒáƒ¬áƒ§áƒáƒ—რპáƒáƒœáƒ”ლზე"), + ("View Mode", "ნáƒáƒ®áƒ•ის რეჟიმი"), + ("login_linux_tip", "X სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდის სესიის ჩáƒáƒ¡áƒáƒ áƒ—áƒáƒ•áƒáƒ“, სáƒáƒ­áƒ˜áƒ áƒáƒ დისტáƒáƒœáƒªáƒ˜áƒ£áƒ  Linux áƒáƒœáƒ’áƒáƒ áƒ˜áƒ¨áƒ¨áƒ˜ შესვლáƒ."), + ("verify_rustdesk_password_tip", "დáƒáƒáƒ“áƒáƒ¡áƒ¢áƒ£áƒ áƒ”თ RustDesk-ის პáƒáƒ áƒáƒšáƒ˜"), + ("remember_account_tip", "დáƒáƒ˜áƒ›áƒáƒ®áƒ¡áƒáƒ•რეთ ეს áƒáƒœáƒ’áƒáƒ áƒ˜áƒ¨áƒ˜"), + ("os_account_desk_tip", "ეს áƒáƒœáƒ’áƒáƒ áƒ˜áƒ¨áƒ˜ გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნებრდისტáƒáƒœáƒªáƒ˜áƒ£áƒ  áƒáƒžáƒ”რáƒáƒªáƒ˜áƒ£áƒš სისტემáƒáƒ¨áƒ˜ შესáƒáƒ¡áƒ•ლელáƒáƒ“ დრheadless რეჟიმში სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდის სესიის ჩáƒáƒ¡áƒáƒ áƒ—áƒáƒ•áƒáƒ“."), + ("OS Account", "áƒáƒžáƒ”რáƒáƒªáƒ˜áƒ£áƒšáƒ˜ სისტემის áƒáƒœáƒ’áƒáƒ áƒ˜áƒ¨áƒ˜"), + ("another_user_login_title_tip", "სხვრმáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბელი უკვე შესულირსისტემáƒáƒ¨áƒ˜"), + ("another_user_login_text_tip", "გáƒáƒ—იშვáƒ"), + ("xorg_not_found_title_tip", "Xorg ვერ მáƒáƒ˜áƒ«áƒ”ბნáƒ"), + ("xorg_not_found_text_tip", "დáƒáƒáƒ˜áƒœáƒ¡áƒ¢áƒáƒšáƒ˜áƒ áƒ”თ Xorg"), + ("no_desktop_title_tip", "სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდრáƒáƒ  áƒáƒ áƒ˜áƒ¡ ხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜"), + ("no_desktop_text_tip", "დáƒáƒáƒ˜áƒœáƒ¡áƒ¢áƒáƒšáƒ˜áƒ áƒ”თ GNOME Desktop"), + ("No need to elevate", "უფლებების áƒáƒ¬áƒ”ვრáƒáƒ  áƒáƒ áƒ˜áƒ¡ სáƒáƒ­áƒ˜áƒ áƒ"), + ("System Sound", "სისტემური ხმáƒ"), + ("Default", "ნáƒáƒ’ულისხმევი"), + ("New RDP", "áƒáƒ®áƒáƒšáƒ˜ RDP"), + ("Fingerprint", "áƒáƒœáƒáƒ‘ეჭდი"), + ("Copy Fingerprint", "áƒáƒœáƒáƒ‘ეჭდის კáƒáƒžáƒ˜áƒ áƒ”ბáƒ"), + ("no fingerprints", "áƒáƒœáƒáƒ‘ეჭდები áƒáƒ  áƒáƒ áƒ˜áƒ¡"), + ("Select a peer", "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ კვáƒáƒœáƒ«áƒ˜"), + ("Select peers", "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ კვáƒáƒœáƒ«áƒ”ბი"), + ("Plugins", "დáƒáƒœáƒáƒ›áƒáƒ¢áƒ”ბი"), + ("Uninstall", "წáƒáƒ¨áƒšáƒ"), + ("Update", "გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ"), + ("Enable", "ჩáƒáƒ áƒ—ვáƒ"), + ("Disable", "გáƒáƒ›áƒáƒ áƒ—ვáƒ"), + ("Options", "პáƒáƒ áƒáƒ›áƒ”ტრები"), + ("resolution_original_tip", "სáƒáƒ¬áƒ§áƒ˜áƒ¡áƒ˜ გáƒáƒ áƒ©áƒ”ვáƒáƒ“áƒáƒ‘áƒ"), + ("resolution_fit_local_tip", "ლáƒáƒ™áƒáƒšáƒ£áƒ áƒ˜ გáƒáƒ áƒ©áƒ”ვáƒáƒ“áƒáƒ‘ის შესáƒáƒ‘áƒáƒ›áƒ˜áƒ¡áƒ˜"), + ("resolution_custom_tip", "მáƒáƒ áƒ’ებული გáƒáƒ áƒ©áƒ”ვáƒáƒ“áƒáƒ‘áƒ"), + ("Collapse toolbar", "ხელსáƒáƒ¬áƒ§áƒáƒ—რპáƒáƒœáƒ”ლის ჩáƒáƒ™áƒ”ცვáƒ"), + ("Accept and Elevate", "მიღებრდრუფლებების áƒáƒ¬áƒ”ვáƒ"), + ("accept_and_elevate_btn_tooltip", "კáƒáƒ•შირის დáƒáƒ¨áƒ•ებრდრUAC უფლებების áƒáƒ¬áƒ”ვáƒ."), + ("clipboard_wait_response_timeout_tip", "გáƒáƒªáƒ•ლის ბუფერის კáƒáƒžáƒ˜áƒ áƒ”ბის ლáƒáƒ“ინის დრრáƒáƒ›áƒáƒ˜áƒ¬áƒ£áƒ áƒ"), + ("Incoming connection", "შემáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ კáƒáƒ•შირი"), + ("Outgoing connection", "გáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ კáƒáƒ•შირი"), + ("Exit", "გáƒáƒ¡áƒ•ლáƒ"), + ("Open", "გáƒáƒ®áƒ¡áƒœáƒ"), + ("logout_tip", "ნáƒáƒ›áƒ“ვილáƒáƒ“ გსურთ გáƒáƒ¡áƒ•ლáƒ?"), + ("Service", "სერვისი"), + ("Start", "გáƒáƒ¨áƒ•ებáƒ"), + ("Stop", "შეჩერებáƒ"), + ("exceed_max_devices", "მიღწეულირსáƒáƒ›áƒáƒ áƒ—áƒáƒ•ი მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების მáƒáƒ¥áƒ¡áƒ˜áƒ›áƒáƒšáƒ£áƒ áƒ˜ რáƒáƒáƒ“ენáƒáƒ‘áƒ."), + ("Sync with recent sessions", "ბáƒáƒšáƒ სესიების სინქრáƒáƒœáƒ˜áƒ–áƒáƒªáƒ˜áƒ"), + ("Sort tags", "ტეგების სáƒáƒ áƒ¢áƒ˜áƒ áƒ”ბáƒ"), + ("Open connection in new tab", "კáƒáƒ•შირის გáƒáƒ®áƒ¡áƒœáƒ áƒáƒ®áƒáƒš ჩáƒáƒœáƒáƒ áƒ—ში"), + ("Move tab to new window", "ჩáƒáƒœáƒáƒ áƒ—ის გáƒáƒ“áƒáƒ¢áƒáƒœáƒ áƒáƒ®áƒáƒš ფáƒáƒœáƒ¯áƒáƒ áƒáƒ¨áƒ˜"), + ("Can not be empty", "áƒáƒ  შეიძლებრიყáƒáƒ¡ ცáƒáƒ áƒ˜áƒ”ლი"), + ("Already exists", "უკვე áƒáƒ áƒ¡áƒ”ბáƒáƒ‘ს"), + ("Change Password", "პáƒáƒ áƒáƒšáƒ˜áƒ¡ შეცვლáƒ"), + ("Refresh Password", "პáƒáƒ áƒáƒšáƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ"), + ("ID", "ID"), + ("Grid View", "ბáƒáƒ“ე"), + ("List View", "სიáƒ"), + ("Select", "áƒáƒ áƒ©áƒ”ვáƒ"), + ("Toggle Tags", "ტეგების გáƒáƒ“áƒáƒ áƒ—ვáƒ"), + ("pull_ab_failed_tip", "მისáƒáƒ›áƒáƒ áƒ—ების წიგნის გáƒáƒœáƒáƒ®áƒšáƒ”ბრშეუძლებელიáƒ"), + ("push_ab_failed_tip", "მისáƒáƒ›áƒáƒ áƒ—ების წიგნის სერვერთáƒáƒœ სინქრáƒáƒœáƒ˜áƒ–áƒáƒªáƒ˜áƒ შეუძლებელიáƒ"), + ("synced_peer_readded_tip", "ბáƒáƒšáƒ სესიებში áƒáƒ áƒ¡áƒ”ბული მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ები დáƒáƒ¡áƒ˜áƒœáƒ¥áƒ áƒáƒœáƒ˜áƒ–დებრმისáƒáƒ›áƒáƒ áƒ—ების წიგნში."), + ("Change Color", "ფერის შეცვლáƒ"), + ("Primary Color", "ძირითáƒáƒ“ი ფერი"), + ("HSV Color", "HSV ფერი"), + ("Installation Successful!", "ინსტáƒáƒšáƒáƒªáƒ˜áƒ წáƒáƒ áƒ›áƒáƒ¢áƒ”ბით დáƒáƒ¡áƒ áƒ£áƒšáƒ“áƒ!"), + ("Installation failed!", "ინსტáƒáƒšáƒáƒªáƒ˜áƒ ვერ გáƒáƒœáƒ®áƒáƒ áƒªáƒ˜áƒ”ლდáƒ!"), + ("Reverse mouse wheel", "მáƒáƒ£áƒ¡áƒ˜áƒ¡ ბáƒáƒ áƒ‘ლის რევერსირებáƒ"), + ("{} sessions", "{} სესიáƒ"), + ("scam_title", "თქვენ შეიძლებრგáƒáƒªáƒ£áƒ áƒáƒœ!"), + ("scam_text1", "თუ ტელეფáƒáƒœáƒ˜áƒ— ესáƒáƒ£áƒ‘რებით ვინმეს, ვისáƒáƒª áƒáƒ  იცნáƒáƒ‘თ დრáƒáƒ  ენდáƒáƒ‘ით, დრის გთხáƒáƒ•თ გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნáƒáƒ— RustDesk დრგáƒáƒ£áƒ¨áƒ•áƒáƒ— მისი სერვისი, áƒáƒ  გáƒáƒáƒ’რძელáƒáƒ— დრდáƒáƒ£áƒ§áƒáƒ•ნებლივ შეწყვიტეთ სáƒáƒ£áƒ‘áƒáƒ áƒ˜."), + ("scam_text2", "სáƒáƒ•áƒáƒ áƒáƒ£áƒ“áƒáƒ“, ეს áƒáƒ áƒ˜áƒ¡ თáƒáƒ¦áƒšáƒ˜áƒ—ი, რáƒáƒ›áƒ”ლიც ცდილáƒáƒ‘ს მáƒáƒ˜áƒžáƒáƒ áƒáƒ¡ თქვენი ფული áƒáƒœ სხვრპირáƒáƒ“ი ინფáƒáƒ áƒ›áƒáƒªáƒ˜áƒ."), + ("Don't show again", "áƒáƒ¦áƒáƒ  áƒáƒ©áƒ•ენáƒáƒ—"), + ("I Agree", "ვეთáƒáƒœáƒ®áƒ›áƒ”ბი"), + ("Decline", "უáƒáƒ áƒ§áƒáƒ¤áƒ"), + ("Timeout in minutes", "ლáƒáƒ“ინის დრრ(წუთები)"), + ("auto_disconnect_option_tip", "áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒáƒ“ დáƒáƒ®áƒ£áƒ áƒáƒ¡ შემáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ სესიები მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბლის áƒáƒ áƒáƒáƒ¥áƒ¢áƒ˜áƒ£áƒ áƒáƒ‘ისáƒáƒ¡"), + ("Connection failed due to inactivity", "კáƒáƒ•შირი ვერ გáƒáƒœáƒ®áƒáƒ áƒªáƒ˜áƒ”ლდრáƒáƒ áƒáƒáƒ¥áƒ¢áƒ˜áƒ£áƒ áƒáƒ‘ის გáƒáƒ›áƒ"), + ("Check for software update on startup", "პრáƒáƒ’რáƒáƒ›áƒ˜áƒ¡ გáƒáƒœáƒáƒ®áƒšáƒ”ბის შემáƒáƒ¬áƒ›áƒ”ბრგáƒáƒ¨áƒ•ებისáƒáƒ¡"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "გáƒáƒœáƒáƒáƒ®áƒšáƒ”თ RustDesk Server Pro ვერსიáƒáƒ›áƒ“ე {} áƒáƒœ უფრრáƒáƒ®áƒáƒšáƒ˜!"), + ("pull_group_failed_tip", "ჯგუფის გáƒáƒœáƒáƒ®áƒšáƒ”ბრშეუძლებელიáƒ"), + ("Filter by intersection", "ფილტრáƒáƒªáƒ˜áƒ გáƒáƒ“áƒáƒ™áƒ•ეთით"), + ("Remove wallpaper during incoming sessions", "სáƒáƒ›áƒ£áƒ¨áƒáƒ მáƒáƒ’იდის ფáƒáƒœáƒ˜áƒ¡ დáƒáƒ›áƒáƒšáƒ•რშემáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ სესიის დრáƒáƒ¡"), + ("Test", "ტესტი"), + ("display_is_plugged_out_msg", "ეკრáƒáƒœáƒ˜ გáƒáƒ›áƒáƒ áƒ—ულიáƒ, გáƒáƒ“áƒáƒ áƒ—ეთ პირველ ეკრáƒáƒœáƒ–ე."), + ("No displays", "ეკრáƒáƒœáƒ”ბი áƒáƒ  áƒáƒ áƒ˜áƒ¡"), + ("Open in new window", "áƒáƒ®áƒáƒš ფáƒáƒœáƒ¯áƒáƒ áƒáƒ¨áƒ˜ გáƒáƒ®áƒ¡áƒœáƒ"), + ("Show displays as individual windows", "ეკრáƒáƒœáƒ”ბის ცáƒáƒšáƒ™áƒ”ულ ფáƒáƒœáƒ¯áƒ áƒ”ბში ჩვენებáƒ"), + ("Use all my displays for the remote session", "ყველრჩემი ეკრáƒáƒœáƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებრდისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ სესიისთვის"), + ("selinux_tip", "თქვენს მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე ჩáƒáƒ áƒ—ულირSELinux, რáƒáƒ›áƒáƒª შეიძლებრხელი შეუშáƒáƒšáƒáƒ¡ RustDesk-ის სწáƒáƒ  მუშáƒáƒáƒ‘áƒáƒ¡ მáƒáƒ áƒ—ულ მხáƒáƒ áƒ”ზე."), + ("Change view", "ხედი"), + ("Big tiles", "დიდი ხáƒáƒ¢áƒ£áƒšáƒ”ბი"), + ("Small tiles", "პáƒáƒ¢áƒáƒ áƒ ხáƒáƒ¢áƒ£áƒšáƒ”ბი"), + ("List", "სიáƒ"), + ("Virtual display", "ვირტუáƒáƒšáƒ£áƒ áƒ˜ ეკრáƒáƒœáƒ˜"), + ("Plug out all", "ყველáƒáƒ¡ გáƒáƒ›áƒáƒ áƒ—ვáƒ"), + ("True color (4:4:4)", "True color (4:4:4)"), + ("Enable blocking user input", "მáƒáƒ›áƒ®áƒ›áƒáƒ áƒ”ბლის შეყვáƒáƒœáƒ˜áƒ¡ დáƒáƒ‘ლáƒáƒ™áƒ•ის დáƒáƒ¨áƒ•ებáƒ"), + ("id_input_tip", "შეგიძლიáƒáƒ— შეიყვáƒáƒœáƒáƒ— იდენტიფიკáƒáƒ¢áƒáƒ áƒ˜, პირდáƒáƒžáƒ˜áƒ áƒ˜ IP მისáƒáƒ›áƒáƒ áƒ—ი áƒáƒœ დáƒáƒ›áƒ”ნი პáƒáƒ áƒ¢áƒ˜áƒ— (<დáƒáƒ›áƒ”ნი>:<პáƒáƒ áƒ¢áƒ˜>).\nთუ გჭირდებáƒáƒ— წვდáƒáƒ›áƒ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე სხვრსერვერზე, დáƒáƒáƒ›áƒáƒ¢áƒ”თ სერვერის მისáƒáƒ›áƒáƒ áƒ—ი (@<სერვერის_მისáƒáƒ›áƒáƒ áƒ—ი>?key=<გáƒáƒ¡áƒáƒ¦áƒ”ბის_მნიშვნელáƒáƒ‘áƒ>), მáƒáƒ’áƒáƒšáƒ˜áƒ—áƒáƒ“:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nთუ გჭირდებáƒáƒ— წვდáƒáƒ›áƒ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე სáƒáƒ¯áƒáƒ áƒ სერვერზე, შეიყვáƒáƒœáƒ”თ \"@public\", გáƒáƒ¡áƒáƒ¦áƒ”ბი სáƒáƒ¯áƒáƒ áƒ სერვერისთვის áƒáƒ  áƒáƒ áƒ˜áƒ¡ სáƒáƒ­áƒ˜áƒ áƒ."), + ("privacy_mode_impl_mag_tip", "რეჟიმი 1"), + ("privacy_mode_impl_virtual_display_tip", "რეჟიმი 2"), + ("Enter privacy mode", "კáƒáƒœáƒ¤áƒ˜áƒ“ენციáƒáƒšáƒ£áƒ áƒáƒ‘ის რეჟიმის ჩáƒáƒ áƒ—ვáƒ"), + ("Exit privacy mode", "კáƒáƒœáƒ¤áƒ˜áƒ“ენციáƒáƒšáƒ£áƒ áƒáƒ‘ის რეჟიმის გáƒáƒ›áƒáƒ áƒ—ვáƒ"), + ("idd_not_support_under_win10_2004_tip", "áƒáƒ áƒáƒžáƒ˜áƒ áƒ“áƒáƒžáƒ˜áƒ áƒ˜ ჩვენების დრáƒáƒ˜áƒ•ერი áƒáƒ  áƒáƒ áƒ˜áƒ¡ მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი. სáƒáƒ­áƒ˜áƒ áƒáƒ Windows 10 ვერსირ2004 áƒáƒœ უფრრáƒáƒ®áƒáƒšáƒ˜."), + ("input_source_1_tip", "შეყვáƒáƒœáƒ˜áƒ¡ წყáƒáƒ áƒ 1"), + ("input_source_2_tip", "შეყვáƒáƒœáƒ˜áƒ¡ წყáƒáƒ áƒ 2"), + ("Swap control-command key", "Ctrl დრCommand ღილáƒáƒ™áƒ”ბის მნიშვნელáƒáƒ‘ების გáƒáƒªáƒ•ლáƒ"), + ("swap-left-right-mouse", "მáƒáƒ£áƒ¡áƒ˜áƒ¡ მáƒáƒ áƒªáƒ®áƒ”ნრდრმáƒáƒ áƒ¯áƒ•ენრღილáƒáƒ™áƒ”ბის მნიშვნელáƒáƒ‘ების გáƒáƒªáƒ•ლáƒ"), + ("2FA code", "áƒáƒ áƒ¤áƒáƒ¥áƒ¢áƒáƒ áƒ˜áƒáƒœáƒ˜ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ˜áƒ¡ კáƒáƒ“ი"), + ("More", "მეტი"), + ("enable-2fa-title", "áƒáƒ áƒ¤áƒáƒ¥áƒ¢áƒáƒ áƒ˜áƒáƒœáƒ˜ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("enable-2fa-desc", "მáƒáƒáƒ¬áƒ§áƒ•ეთ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ˜áƒ¡ áƒáƒžáƒšáƒ˜áƒ™áƒáƒªáƒ˜áƒ. გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნეთ, მáƒáƒ’áƒáƒšáƒ˜áƒ—áƒáƒ“, Authy, Microsoft áƒáƒœ Google Authenticator ტელეფáƒáƒœáƒ–ე áƒáƒœ კáƒáƒ›áƒžáƒ˜áƒ£áƒ¢áƒ”რზე.\n\nდáƒáƒáƒ¡áƒ™áƒáƒœáƒ”რეთ QR კáƒáƒ“ი áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ˜áƒ¡ áƒáƒžáƒšáƒ˜áƒ™áƒáƒªáƒ˜áƒ˜áƒ— დრშეიყვáƒáƒœáƒ”თ კáƒáƒ“ი, რáƒáƒ›áƒ”ლიც გáƒáƒ›áƒáƒ©áƒœáƒ“ებრáƒáƒ› áƒáƒžáƒšáƒ˜áƒ™áƒáƒªáƒ˜áƒáƒ¨áƒ˜, áƒáƒ áƒ¤áƒáƒ¥áƒ¢áƒáƒ áƒ˜áƒáƒœáƒ˜ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ˜áƒ¡ ჩáƒáƒ¡áƒáƒ áƒ—áƒáƒ•áƒáƒ“."), + ("wrong-2fa-code", "კáƒáƒ“ის დáƒáƒ“áƒáƒ¡áƒ¢áƒ£áƒ áƒ”ბრშეუძლებელიáƒ. შეáƒáƒ›áƒáƒ¬áƒ›áƒ”თ კáƒáƒ“ი დრáƒáƒ“გილáƒáƒ‘რივი დრáƒáƒ˜áƒ¡ პáƒáƒ áƒáƒ›áƒ”ტრები."), + ("enter-2fa-title", "áƒáƒ áƒ¤áƒáƒ¥áƒ¢áƒáƒ áƒ˜áƒáƒœáƒ˜ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ"), + ("Email verification code must be 6 characters.", "ელ-ფáƒáƒ¡áƒ¢áƒ˜áƒ¡ დáƒáƒ“áƒáƒ¡áƒ¢áƒ£áƒ áƒ”ბის კáƒáƒ“ი უნდრშედგებáƒáƒ“ეს 6 სიმბáƒáƒšáƒáƒ¡áƒ’áƒáƒœ."), + ("2FA code must be 6 digits.", "áƒáƒ áƒ¤áƒáƒ¥áƒ¢áƒáƒ áƒ˜áƒáƒœáƒ˜ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ˜áƒ¡ კáƒáƒ“ი უნდრშედგებáƒáƒ“ეს 6 ციფრისგáƒáƒœ."), + ("Multiple Windows sessions found", "áƒáƒ¦áƒ›áƒáƒ©áƒ”ნილირWindows-ის რáƒáƒ›áƒ“ენიმე სესიáƒ"), + ("Please select the session you want to connect to", "áƒáƒ˜áƒ áƒ©áƒ˜áƒ”თ სესიáƒ, რáƒáƒ›áƒ”ლთáƒáƒœáƒáƒª გსურთ დáƒáƒ™áƒáƒ•შირებáƒ"), + ("powered_by_me", "RustDesk-ზე დáƒáƒ¤áƒ£áƒ«áƒœáƒ”ბული"), + ("outgoing_only_desk_tip", "ეს სპეციáƒáƒšáƒ˜áƒ–ებული ვერსიáƒáƒ.\nშეგიძლიáƒáƒ— დáƒáƒ£áƒ™áƒáƒ•შირდეთ სხვრმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ებს, მáƒáƒ’რáƒáƒ› სხვრმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ებს áƒáƒ  შეუძლიáƒáƒ— დáƒáƒ£áƒ™áƒáƒ•შირდნენ თქვენსáƒáƒ¡."), + ("preset_password_warning", "ეს სპეციáƒáƒšáƒ˜áƒ–ებული ვერსიáƒáƒ წინáƒáƒ¡áƒ¬áƒáƒ  დáƒáƒ§áƒ”ნებული პáƒáƒ áƒáƒšáƒ˜áƒ—. ნებისმიერს, ვინც იცის ეს პáƒáƒ áƒáƒšáƒ˜, შეუძლირმიიღáƒáƒ¡ სრული კáƒáƒœáƒ¢áƒ áƒáƒšáƒ˜ თქვენს მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე. თუ ეს თქვენთვის მáƒáƒ£áƒšáƒáƒ“ნელიáƒ, დáƒáƒ£áƒ§áƒáƒ•ნებლივ წáƒáƒ¨áƒáƒšáƒ”თ ეს პრáƒáƒ’რáƒáƒ›áƒ£áƒšáƒ˜ უზრუნველყáƒáƒ¤áƒ."), + ("Security Alert", "უსáƒáƒ¤áƒ áƒ—ხáƒáƒ”ბის გáƒáƒ¤áƒ áƒ—ხილებáƒ"), + ("My address book", "ჩემი მისáƒáƒ›áƒáƒ áƒ—ების წიგნი"), + ("Personal", "პირáƒáƒ“ი"), + ("Owner", "მფლáƒáƒ‘ელი"), + ("Set shared password", "სáƒáƒ–იáƒáƒ áƒ პáƒáƒ áƒáƒšáƒ˜áƒ¡ დáƒáƒ§áƒ”ნებáƒ"), + ("Exist in", "áƒáƒ áƒ¡áƒ”ბáƒáƒ‘ს"), + ("Read-only", "მხáƒáƒšáƒáƒ“ წáƒáƒ™áƒ˜áƒ—ხვáƒ"), + ("Read/Write", "წáƒáƒ™áƒ˜áƒ—ხვრდრჩáƒáƒ¬áƒ”რáƒ"), + ("Full Control", "სრული კáƒáƒœáƒ¢áƒ áƒáƒšáƒ˜"), + ("share_warning_tip", "ზემáƒáƒ— მáƒáƒªáƒ”მული ველები სáƒáƒ–იáƒáƒ áƒáƒ დრხილულირსხვებისთვის."), + ("Everyone", "ყველáƒ"), + ("ab_web_console_tip", "მეტი ვებ-კáƒáƒœáƒ¡áƒáƒšáƒ¨áƒ˜"), + ("allow-only-conn-window-open-tip", "დáƒáƒ¨áƒ•ებრმხáƒáƒšáƒáƒ“ მáƒáƒ¨áƒ˜áƒœ, რáƒáƒªáƒ RustDesk-ის ფáƒáƒœáƒ¯áƒáƒ áƒ გáƒáƒ®áƒ¡áƒœáƒ˜áƒšáƒ˜áƒ"), + ("no_need_privacy_mode_no_physical_displays_tip", "ფიზიკური ეკრáƒáƒœáƒ”ბი áƒáƒ  áƒáƒ áƒ˜áƒ¡, áƒáƒ  áƒáƒ áƒ˜áƒ¡ სáƒáƒ­áƒ˜áƒ áƒ კáƒáƒœáƒ¤áƒ˜áƒ“ენციáƒáƒšáƒ£áƒ áƒáƒ‘ის რეჟიმის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ."), + ("Follow remote cursor", "დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ კურსáƒáƒ áƒ˜áƒ¡ მიყáƒáƒšáƒ"), + ("Follow remote window focus", "დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ ფáƒáƒœáƒ¯áƒ áƒ˜áƒ¡ ფáƒáƒ™áƒ£áƒ¡áƒ˜áƒ¡ მიყáƒáƒšáƒ"), + ("default_proxy_tip", "ნáƒáƒ’ულისხმევი პრáƒáƒ¢áƒáƒ™áƒáƒšáƒ˜ დრპáƒáƒ áƒ¢áƒ˜: Socks5 დრ1080"), + ("no_audio_input_device_tip", "áƒáƒ£áƒ“ირშეყვáƒáƒœáƒ˜áƒ¡ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რვერ მáƒáƒ˜áƒ«áƒ”ბნáƒ."), + ("Incoming", "შემáƒáƒ›áƒáƒ•áƒáƒšáƒ˜"), + ("Outgoing", "გáƒáƒ›áƒáƒ•áƒáƒšáƒ˜"), + ("Clear Wayland screen selection", "Wayland ეკრáƒáƒœáƒ˜áƒ¡ áƒáƒ áƒ©áƒ”ვáƒáƒœáƒ˜áƒ¡ გáƒáƒ£áƒ¥áƒ›áƒ”ბáƒ"), + ("clear_Wayland_screen_selection_tip", "გáƒáƒ£áƒ¥áƒ›áƒ”ბის შემდეგ შეგიძლიáƒáƒ— ხელáƒáƒ®áƒšáƒ áƒáƒ˜áƒ áƒ©áƒ˜áƒáƒ— ეკრáƒáƒœáƒ˜ გáƒáƒ¡áƒáƒ–იáƒáƒ áƒ”ბლáƒáƒ“."), + ("confirm_clear_Wayland_screen_selection_tip", "გáƒáƒ•áƒáƒ£áƒ¥áƒ›áƒáƒ— Wayland ეკრáƒáƒœáƒ˜áƒ¡ áƒáƒ áƒ©áƒ”ვáƒáƒœáƒ˜?"), + ("android_new_voice_call_tip", "მიღებულირáƒáƒ®áƒáƒšáƒ˜ ხმáƒáƒ•áƒáƒœáƒ˜ ზáƒáƒ áƒ˜áƒ¡ მáƒáƒ—ხáƒáƒ•ნáƒ. თუ მიიღებთ, ხმრგáƒáƒ“áƒáƒ˜áƒ áƒ—ვებრხმáƒáƒ•áƒáƒœ კáƒáƒ•შირზე."), + ("texture_render_tip", "გáƒáƒ›áƒáƒ˜áƒ§áƒ”ნეთ ტექსტურების ვიზუáƒáƒšáƒ˜áƒ–áƒáƒªáƒ˜áƒ გáƒáƒ›áƒáƒ¡áƒáƒ®áƒ£áƒšáƒ”ბების უფრრგლუვáƒáƒ“ გáƒáƒ¡áƒáƒ™áƒ”თებლáƒáƒ“."), + ("Use texture rendering", "ტექსტურების ვიზუáƒáƒšáƒ˜áƒ–áƒáƒªáƒ˜áƒ"), + ("Floating window", "მáƒáƒ¢áƒ˜áƒ•ტივე ფáƒáƒœáƒ¯áƒáƒ áƒ"), + ("floating_window_tip", "ეხმáƒáƒ áƒ”ბრRustDesk-ის ფáƒáƒœáƒ£áƒ áƒ˜ სერვისის შენáƒáƒ áƒ©áƒ£áƒœáƒ”ბáƒáƒ¡"), + ("Keep screen on", "ეკრáƒáƒœáƒ˜áƒ¡ ჩáƒáƒ áƒ—ულáƒáƒ“ შენáƒáƒ áƒ©áƒ£áƒœáƒ”ბáƒ"), + ("Never", "áƒáƒ áƒáƒ¡áƒ“რáƒáƒ¡"), + ("During controlled", "მáƒáƒ áƒ—ვისáƒáƒ¡"), + ("During service is on", "სერვისის მუშáƒáƒáƒ‘ისáƒáƒ¡"), + ("Capture screen using DirectX", "ეკრáƒáƒœáƒ˜áƒ¡ გáƒáƒ“áƒáƒ¦áƒ”ბრDirectX-ის გáƒáƒ›áƒáƒ§áƒ”ნებით"), + ("Back", "უკáƒáƒœ"), + ("Apps", "áƒáƒžáƒšáƒ˜áƒ™áƒáƒªáƒ˜áƒ”ბი"), + ("Volume up", "ხმის გáƒáƒ–რდáƒ"), + ("Volume down", "ხმის შემცირებáƒ"), + ("Power", "კვებáƒ"), + ("Telegram bot", "Telegram ბáƒáƒ¢áƒ˜"), + ("enable-bot-tip", "თუ ჩáƒáƒ áƒ—ულიáƒ, შეგიძლიáƒáƒ— მიიღáƒáƒ— áƒáƒ áƒ¤áƒáƒ¥áƒ¢áƒáƒ áƒ˜áƒáƒœáƒ˜ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ˜áƒ¡ კáƒáƒ“ი ბáƒáƒ¢áƒ˜áƒ¡áƒ’áƒáƒœ. მáƒáƒ¡ áƒáƒ¡áƒ”ვე შეუძლირშეáƒáƒ¡áƒ áƒ£áƒšáƒáƒ¡ დáƒáƒ™áƒáƒ•შირების შეტყáƒáƒ‘ინების ფუნქციáƒ."), + ("enable-bot-desc", "1) გáƒáƒ®áƒ¡áƒ”ნით ჩáƒáƒ¢áƒ˜ @BotFather-თáƒáƒœ.\n2) გáƒáƒ’ზáƒáƒ•ნეთ ბრძáƒáƒœáƒ”ბრ\"/newbot\". áƒáƒ› ნáƒáƒ‘იჯის შესრულების შემდეგ მიიღებთ ტáƒáƒ™áƒ”ნს.\n3) დáƒáƒ˜áƒ¬áƒ§áƒ”თ ჩáƒáƒ¢áƒ˜ თქვენს áƒáƒ®áƒšáƒáƒ“ შექმნილ ბáƒáƒ¢áƒ—áƒáƒœ. გáƒáƒ’ზáƒáƒ•ნეთ შეტყáƒáƒ‘ინებáƒ, რáƒáƒ›áƒ”ლიც იწყებრდáƒáƒ®áƒ áƒ˜áƒšáƒ˜ ხáƒáƒ–ით (\"/\"), მáƒáƒ’áƒáƒšáƒ˜áƒ—áƒáƒ“, \"/hello\", მის გáƒáƒ¡áƒáƒáƒ¥áƒ¢áƒ˜áƒ£áƒ áƒ”ბლáƒáƒ“.\n"), + ("cancel-2fa-confirm-tip", "გáƒáƒ›áƒáƒ•რთáƒáƒ— áƒáƒ áƒ¤áƒáƒ¥áƒ¢áƒáƒ áƒ˜áƒáƒœáƒ˜ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ?"), + ("cancel-bot-confirm-tip", "გáƒáƒ›áƒáƒ•რთáƒáƒ— Telegram ბáƒáƒ¢áƒ˜?"), + ("About RustDesk", "RustDesk-ის შესáƒáƒ®áƒ”ბ"), + ("Send clipboard keystrokes", "გáƒáƒªáƒ•ლის ბუფერიდáƒáƒœ კლáƒáƒ•იშების დáƒáƒ­áƒ”რის გáƒáƒ’ზáƒáƒ•ნáƒ"), + ("network_error_tip", "შეáƒáƒ›áƒáƒ¬áƒ›áƒ”თ ქსელთáƒáƒœ კáƒáƒ•შირი, შემდეგ დáƒáƒáƒ­áƒ˜áƒ áƒ”თ \"გáƒáƒœáƒ›áƒ”áƒáƒ áƒ”ბáƒ\"."), + ("Unlock with PIN", "PIN-კáƒáƒ“ით გáƒáƒœáƒ‘ლáƒáƒ™áƒ•áƒ"), + ("Requires at least {} characters", "სáƒáƒ­áƒ˜áƒ áƒáƒ მინიმუმ {} სიმბáƒáƒšáƒ"), + ("Wrong PIN", "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ PIN-კáƒáƒ“ი"), + ("Set PIN", "PIN-კáƒáƒ“ის დáƒáƒ§áƒ”ნებáƒ"), + ("Enable trusted devices", "სáƒáƒœáƒ“რმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების ჩáƒáƒ áƒ—ვáƒ"), + ("Manage trusted devices", "სáƒáƒœáƒ“რმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ების მáƒáƒ áƒ—ვáƒ"), + ("Platform", "პლáƒáƒ¢áƒ¤áƒáƒ áƒ›áƒ"), + ("Days remaining", "დáƒáƒ áƒ©áƒ”ნილი დღეები"), + ("enable-trusted-devices-tip", "სáƒáƒœáƒ“რმáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ებს შეუძლიáƒáƒ— გáƒáƒ›áƒáƒ¢áƒáƒ•áƒáƒœ 2FA áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ˜áƒ¡ შემáƒáƒ¬áƒ›áƒ”ბáƒ"), + ("Parent directory", "მშáƒáƒ‘ელი სáƒáƒ¥áƒáƒ¦áƒáƒšáƒ“ე"), + ("Resume", "გáƒáƒ’რძელებáƒ"), + ("Invalid file name", "áƒáƒ áƒáƒ¡áƒ¬áƒáƒ áƒ˜ ფáƒáƒ˜áƒšáƒ˜áƒ¡ სáƒáƒ®áƒ”ლი"), + ("one-way-file-transfer-tip", "მáƒáƒ áƒ—ულ მხáƒáƒ áƒ”ზე ჩáƒáƒ áƒ—ულირცáƒáƒšáƒ›áƒ®áƒ áƒ˜áƒ•ი ფáƒáƒ˜áƒšáƒ”ბის გáƒáƒ“áƒáƒªáƒ”მáƒ."), + ("Authentication Required", "სáƒáƒ­áƒ˜áƒ áƒáƒ áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ"), + ("Authenticate", "áƒáƒ•თენტიფიკáƒáƒªáƒ˜áƒ"), + ("web_id_input_tip", "შეგიძლიáƒáƒ— შეიყვáƒáƒœáƒáƒ— ID იმáƒáƒ•ე სერვერზე, პირდáƒáƒžáƒ˜áƒ áƒ˜ IP წვდáƒáƒ›áƒ ვებ-კლიენტში áƒáƒ  áƒáƒ áƒ˜áƒ¡ მხáƒáƒ áƒ“áƒáƒ­áƒ”რილი.\nთუ გსურთ წვდáƒáƒ›áƒ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე სხვრსერვერზე, დáƒáƒáƒ›áƒáƒ¢áƒ”თ სერვერის მისáƒáƒ›áƒáƒ áƒ—ი (@<სერვერის_მისáƒáƒ›áƒáƒ áƒ—ი>?key=<გáƒáƒ¡áƒáƒ¦áƒ”ბი>), მáƒáƒ’áƒáƒšáƒ˜áƒ—áƒáƒ“,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nთუ გსურთ წვდáƒáƒ›áƒ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე სáƒáƒ¯áƒáƒ áƒ სერვერზე, შეიყვáƒáƒœáƒ”თ \"@public\", სáƒáƒ¯áƒáƒ áƒ სერვერისთვის გáƒáƒ¡áƒáƒ¦áƒ”ბი áƒáƒ  áƒáƒ áƒ˜áƒ¡ სáƒáƒ­áƒ˜áƒ áƒ."), + ("Download", "ჩáƒáƒ›áƒáƒ¢áƒ•ირთვáƒ"), + ("Upload folder", "სáƒáƒ¥áƒáƒ¦áƒáƒšáƒ“ის áƒáƒ¢áƒ•ირთვáƒ"), + ("Upload files", "ფáƒáƒ˜áƒšáƒ”ბის áƒáƒ¢áƒ•ირთვáƒ"), + ("Clipboard is synchronized", "გáƒáƒªáƒ•ლის ბუფერი სინქრáƒáƒœáƒ˜áƒ–ებულიáƒ"), + ("Update client clipboard", "კლიენტის გáƒáƒªáƒ•ლის ბუფერის გáƒáƒœáƒáƒ®áƒšáƒ”ბáƒ"), + ("Untagged", "უტეგáƒ"), + ("new-version-of-{}-tip", "ხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜áƒ áƒáƒ®áƒáƒšáƒ˜ ვერსირ{}"), + ("Accessible devices", "ხელმისáƒáƒ¬áƒ•დáƒáƒ›áƒ˜ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘ები"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "გáƒáƒœáƒáƒáƒ®áƒšáƒ”თ RustDesk კლიენტი ვერსიáƒáƒ›áƒ“ე {} áƒáƒœ უფრრáƒáƒ®áƒáƒšáƒ˜ დისტáƒáƒœáƒªáƒ˜áƒ£áƒ  მხáƒáƒ áƒ”ზე!"), + ("d3d_render_tip", "D3D ვიზუáƒáƒšáƒ˜áƒ–áƒáƒªáƒ˜áƒ˜áƒ¡ ჩáƒáƒ áƒ—ვისáƒáƒ¡ ზáƒáƒ’იერთ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ ეკრáƒáƒœáƒ˜ შეიძლებრიყáƒáƒ¡ შáƒáƒ•ი."), + ("Use D3D rendering", "D3D ვიზუáƒáƒšáƒ˜áƒ–áƒáƒªáƒ˜áƒ˜áƒ¡ გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("Printer", "პრინტერი"), + ("printer-os-requirement-tip", "პრინტერთáƒáƒœ გáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ კáƒáƒ•შირის ფუნქციისთვის სáƒáƒ­áƒ˜áƒ áƒáƒ Windows 10 áƒáƒœ უფრრáƒáƒ®áƒáƒšáƒ˜ ვერსიáƒ."), + ("printer-requires-installed-{}-client-tip", "დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ ბეჭდვის გáƒáƒ›áƒáƒ¡áƒáƒ§áƒ”ნებლáƒáƒ“, {} უნდრიყáƒáƒ¡ დáƒáƒ˜áƒœáƒ¡áƒ¢áƒáƒšáƒ˜áƒ áƒ”ბული áƒáƒ› მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘áƒáƒ–ე."), + ("printer-{}-not-installed-tip", "პრინტერი {} áƒáƒ  áƒáƒ áƒ˜áƒ¡ დáƒáƒ˜áƒœáƒ¡áƒ¢áƒáƒšáƒ˜áƒ áƒ”ბული."), + ("printer-{}-ready-tip", "პრინტერი {} დáƒáƒ˜áƒœáƒ¡áƒ¢áƒáƒšáƒ˜áƒ áƒ”ბულირდრმზáƒáƒ“ áƒáƒ áƒ˜áƒ¡ გáƒáƒ›áƒáƒ¡áƒáƒ§áƒ”ნებლáƒáƒ“."), + ("Install {} Printer", "დáƒáƒáƒ˜áƒœáƒ¡áƒ¢áƒáƒšáƒ˜áƒ áƒ”თ პრინტერი {}"), + ("Outgoing Print Jobs", "გáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ ბეჭდვის დáƒáƒ•áƒáƒšáƒ”ბáƒ"), + ("Incoming Print Jobs", "შემáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ ბეჭდვის დáƒáƒ•áƒáƒšáƒ”ბáƒ"), + ("Incoming Print Job", "შემáƒáƒ›áƒáƒ•áƒáƒšáƒ˜ ბეჭდვის დáƒáƒ•áƒáƒšáƒ”ბáƒ"), + ("use-the-default-printer-tip", "ნáƒáƒ’ულისხმევი პრინტერის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("use-the-selected-printer-tip", "áƒáƒ áƒ©áƒ”ული პრინტერის გáƒáƒ›áƒáƒ§áƒ”ნებáƒ"), + ("auto-print-tip", "áƒáƒ•ტáƒáƒ›áƒáƒ¢áƒ£áƒ áƒáƒ“ დáƒáƒ‘ეჭდეთ áƒáƒ áƒ©áƒ”ულ პრინტერზე."), + ("print-incoming-job-confirm-tip", "დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘იდáƒáƒœ მიღებულირბეჭდვის დáƒáƒ•áƒáƒšáƒ”ბáƒ. გáƒáƒ•უშვáƒáƒ— ლáƒáƒ™áƒáƒšáƒ£áƒ áƒáƒ“?"), + ("remote-printing-disallowed-tile-tip", "დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ ბეჭდვრáƒáƒ™áƒ áƒ«áƒáƒšáƒ£áƒšáƒ˜áƒ"), + ("remote-printing-disallowed-text-tip", "მáƒáƒ áƒ—ულ მხáƒáƒ áƒ”ზე უფლებების პáƒáƒ áƒáƒ›áƒ”ტრები კრძáƒáƒšáƒáƒ•ს დისტáƒáƒœáƒªáƒ˜áƒ£áƒ  ბეჭდვáƒáƒ¡."), + ("save-settings-tip", "პáƒáƒ áƒáƒ›áƒ”ტრების შენáƒáƒ®áƒ•áƒ"), + ("dont-show-again-tip", "áƒáƒ¦áƒáƒ  áƒáƒ©áƒ•ენáƒáƒ—"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "კáƒáƒ›áƒ”რის ნáƒáƒ®áƒ•áƒ"), + ("Enable camera", "კáƒáƒ›áƒ”რის ჩáƒáƒ áƒ—ვáƒ"), + ("No cameras", "კáƒáƒ›áƒ”რრáƒáƒ  áƒáƒ áƒ˜áƒ¡"), + ("view_camera_unsupported_tip", "დისტáƒáƒœáƒªáƒ˜áƒ£áƒ áƒ˜ მáƒáƒ¬áƒ§áƒáƒ‘ილáƒáƒ‘რáƒáƒ  უჭერს მხáƒáƒ áƒ¡ კáƒáƒ›áƒ”რის ნáƒáƒ®áƒ•áƒáƒ¡."), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/he.rs b/src/lang/he.rs index d877f022682..54d44f6c576 100644 --- a/src/lang/he.rs +++ b/src/lang/he.rs @@ -1,254 +1,252 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ - ("Status", ""), - ("Your Desktop", ""), + ("Status", "מצב"), + ("Your Desktop", "שולחן העבודה שלך"), ("desk_tip", "ניתן לגשת לשולחן העבודה שלך ×¢× ×ž×–×”×” וסיסמה זו."), - ("Password", ""), - ("Ready", ""), - ("Established", ""), + ("Password", "סיסמה"), + ("Ready", "מוכן"), + ("Established", "מחובר"), ("connecting_status", "מתחבר לרשת RustDesk..."), - ("Enable service", ""), - ("Start service", ""), - ("Service is running", ""), - ("Service is not running", ""), + ("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", ""), - ("Remove", ""), - ("Refresh random password", ""), - ("Set your own password", ""), - ("Enable keyboard/mouse", ""), - ("Enable clipboard", ""), - ("Enable file transfer", ""), - ("Enable TCP tunneling", ""), - ("IP Whitelisting", ""), - ("ID/Relay Server", "שרת מזהה/ריליי"), - ("Import server config", ""), - ("Export Server Config", ""), - ("Import server configuration successfully", ""), - ("Export server configuration successfully", ""), - ("Invalid server configuration", ""), - ("Clipboard is empty", ""), - ("Stop service", ""), - ("Change ID", ""), - ("Your new ID", ""), - ("length %min% to %max%", ""), - ("starts with a letter", ""), - ("allowed characters", ""), - ("id_change_tip", "×ž×•×ª×¨×™× ×¨×§ ×ª×•×•×™× a-z, A-Z, 0-9 ו_ (קו תחתון). ×”×ות הר×שונה חייבת להיות a-z, A-Z. ×ורך בין 6 ל-16."), - ("Website", ""), - ("About", ""), - ("Slogan_tip", "נוצר בלב ×‘×¢×•×œ× ×”×–×” ×”×›×וטי!"), - ("Privacy Statement", ""), - ("Mute", ""), + ("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/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", "שנה מזהה"), + ("Your new ID", "המזהה החדש שלך"), + ("length %min% to %max%", "×ורך בין %min% ל %max%"), + ("starts with a letter", "מתחיל ב×ות"), + ("allowed characters", "×ª×•×•×™× ×ž×•×ª×¨×™×"), + ("id_change_tip", "×ž×•×ª×¨×™× ×¨×§ ×ª×•×•×™× a-z, A-Z, 0-9, מקף (-) וקו תחתון (_). התו הר×שון חייב להיות ×ות (a-z, A-Z). ×ורך בין 6 ל-16 תווי×."), + ("Website", "דף הבית"), + ("About", "×ודות"), + ("Slogan_tip", "נוצר ב×הבה ×‘×¢×•×œ× ×”×›×וטי ×”×–×”!"), + ("Privacy Statement", "הצהרת פרטיות"), + ("Mute", "השתק"), ("Build Date", "ת×ריך בנייה"), - ("Version", ""), - ("Home", ""), + ("Version", "גרסה"), + ("Home", "בית"), ("Audio Input", "קלט שמע"), - ("Enhancements", ""), - ("Hardware Codec", "קודק חומרה"), - ("Adaptive bitrate", ""), - ("ID Server", "שרת מזהה"), - ("Relay Server", "שרת ריליי"), + ("Enhancements", "שיפורי×"), + ("Hardware Codec", "Hardware Codec"), + ("Adaptive bitrate", "Adaptive bitrate"), + ("ID Server", "שרת ID"), + ("Relay Server", "שרת Relay"), ("API Server", "שרת API"), ("invalid_http", "חייב להתחיל ×¢× http:// ×ו https://"), - ("Invalid IP", ""), - ("Invalid format", ""), - ("server_not_support", "עדיין ×œ× × ×ª×ž×š על ידי השרת"), - ("Not available", ""), - ("Too frequent", ""), - ("Cancel", ""), - ("Skip", ""), - ("Close", ""), - ("Retry", ""), - ("OK", ""), + ("Invalid IP", "IP ×œ× ×ª×§×™×Ÿ"), + ("Invalid format", "פורמט ×œ× ×ª×§×™×Ÿ"), + ("server_not_support", "×œ× × ×ª×ž×š על-ידי השרת כרגע"), + ("Not available", "×œ× ×–×ž×™×Ÿ"), + ("Too frequent", "תדיר מדי"), + ("Cancel", "ביטול"), + ("Skip", "דלג"), + ("Close", "סגור"), + ("Retry", "נסה שוב"), + ("OK", "×ישור"), ("Password Required", "נדרשת סיסמה"), - ("Please enter your password", ""), - ("Remember password", ""), + ("Please enter your password", "×× × ×”×›× ×¡ סיסמה"), + ("Remember password", "זכור סיסמה"), ("Wrong Password", "סיסמה שגויה"), - ("Do you want to enter again?", ""), + ("Do you want to enter again?", "×”×× ×תה רוצה לנסות שוב?"), ("Connection Error", "שגי×ת חיבור"), - ("Error", ""), - ("Reset by the peer", ""), - ("Connecting...", ""), - ("Connection in progress. Please wait.", ""), - ("Please try 1 minute later", ""), + ("Error", "שגי××”"), + ("Reset by the peer", "×ופס על-ידי הצד השני"), + ("Connecting...", "מתחבר..."), + ("Connection in progress. Please wait.", "מתחבר. ×× × ×”×ž×ª×Ÿ."), + ("Please try 1 minute later", "×× × ×”×ž×ª×Ÿ דקה ונסה שוב"), ("Login Error", "שגי×ת התחברות"), - ("Successful", ""), - ("Connected, waiting for image...", ""), - ("Name", ""), - ("Type", ""), - ("Modified", ""), - ("Size", ""), - ("Show Hidden Files", "הצג ×§×‘×¦×™× × ×¡×ª×¨×™×"), - ("Receive", ""), - ("Send", ""), + ("Successful", "הצלחה"), + ("Connected, waiting for image...", "מחובר, מחכה לתמונה..."), + ("Name", "ש×"), + ("Type", "סוג"), + ("Modified", "שונה"), + ("Size", "גודל"), + ("Show Hidden Files", "הצג ×§×‘×¦×™× ×ž×•×¡×ª×¨×™×"), + ("Receive", "קבל"), + ("Send", "שלח"), ("Refresh File", "רענן קובץ"), - ("Local", ""), - ("Remote", ""), + ("Local", "מקומי"), + ("Remote", "מרוחק"), ("Remote Computer", "מחשב מרוחק"), ("Local Computer", "מחשב מקומי"), ("Confirm Delete", "×שר מחיקה"), - ("Delete", ""), - ("Properties", ""), + ("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", ""), + ("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", ""), + ("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 Ctrl + Alt + Del", ""), + ("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 Ctrl + Alt + Del", "לחץ Ctrl + Alt + Delete"), ("Insert Lock", "הוסף נעילה"), - ("Refresh", ""), - ("ID does not exist", ""), - ("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", ""), + ("Refresh", "רענן"), + ("ID does not exist", "מזהה ×ינו ×§×™×™×"), + ("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", ""), + ("Click to upgrade", "לחץ כדי לשדרג"), + ("Configure", "הגדר"), ("config_acc", "כדי לשלוט מרחוק בשולחן העבודה שלך, עליך להעניק ל-RustDesk הרש×ות \"נגישות\"."), ("config_screen", "כדי לגשת מרחוק לשולחן העבודה שלך, עליך להעניק ל-RustDesk הרש×ות \"הקלטת מסך\"."), - ("Installing ...", ""), - ("Install", ""), - ("Installation", ""), + ("Installing ...", "מתקין ..."), + ("Install", "התקן"), + ("Installation", "התקנה"), ("Installation Path", "נתיב התקנה"), - ("Create start menu shortcuts", ""), - ("Create desktop icon", ""), + ("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 ...", ""), + ("End-user license agreement", "×”×¡×›× ×¨×™×©×™×•×Ÿ משתמש קצה"), + ("Generating ...", "יוצר ..."), + ("Your installation is lower version.", "מותקנת ×צלך בגרסה ישנה יותר"), + ("not_close_tcp_tip", "×ל תסגור חלון ×–×” בזמן ש×תה משתמש בtcp"), + ("Listening ...", "מ×זין ..."), ("Remote Host", "מ×רח מרוחק"), ("Remote Port", "פורט מרוחק"), - ("Action", ""), - ("Add", ""), + ("Action", "פעולה"), + ("Add", "הוסף"), ("Local Port", "פורט מקומי"), ("Local Address", "כתובת מקומית"), ("Change Local Port", "שנה פורט מקומי"), - ("setup_server_tip", "לחיבור מהיר יותר, ×× × ×”×’×“×¨ שרת משלך"), - ("Too short, at least 6 characters.", ""), - ("The confirmation is not identical.", ""), - ("Permissions", ""), - ("Accept", ""), - ("Dismiss", ""), - ("Disconnect", ""), - ("Enable file copy and paste", ""), - ("Connected", ""), - ("Direct and encrypted connection", ""), - ("Relayed and encrypted connection", ""), - ("Direct and unencrypted connection", ""), - ("Relayed and unencrypted connection", ""), + ("setup_server_tip", "לחיבור מהיר יותר, מומלץ להגדיר שרת משלך"), + ("Too short, at least 6 characters.", "קצר מידי, לפחות 6 תווי×."), + ("The confirmation is not identical.", "×”×ימות ×ינו ×–×”×”."), + ("Permissions", "הרש×ות"), + ("Accept", "קבל"), + ("Dismiss", "התעל×"), + ("Disconnect", "נתק"), + ("Enable file copy and paste", "×פשר העתקה והדבקה עבור קבצי×"), + ("Connected", "מחובר"), + ("Direct and encrypted connection", "חיבור ישיר ומוצפן"), + ("Relayed and encrypted connection", "חיבור ב×מצעות ממסר ומוצפן"), + ("Direct and unencrypted connection", "חיבור ישיר ×•×œ× ×ž×•×¦×¤×Ÿ"), + ("Relayed and unencrypted connection", "חיבור ב×מצעות ממסר ×•×œ× ×ž×•×¦×¤×Ÿ"), ("Enter Remote ID", "הזן מזהה מרוחק"), - ("Enter your password", ""), - ("Logging in...", ""), - ("Enable RDP session sharing", ""), + ("Enter your password", "הכנס סיסמה"), + ("Logging in...", "מתחבר..."), + ("Enable RDP session sharing", "×פשר שיתוף סשן RDP"), ("Auto Login", "התחברות ×וטומטית (תקפה רק ×× ×”×’×“×¨×ª \"נעל ל×חר ×¡×™×•× ×”×¡×©×Ÿ\")"), - ("Enable direct IP access", ""), - ("Rename", ""), - ("Space", ""), - ("Create desktop shortcut", ""), + ("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", ""), - ("Reboot required", ""), - ("Unsupported display server", ""), - ("x11 expected", ""), - ("Port", ""), - ("Settings", ""), - ("Username", ""), - ("Invalid port", ""), - ("Closed manually by the peer", ""), - ("Enable remote configuration modification", ""), - ("Run without install", ""), - ("Connect via relay", ""), - ("Always connect via relay", ""), - ("whitelist_tip", "רק IP ברשימה הלבנה יכול לגשת ×לי"), - ("Login", ""), - ("Verify", ""), - ("Remember me", ""), - ("Trust this device", ""), - ("Verification code", ""), - ("verification_tip", "קוד ×ימות נשלח לכתובת הדו×\"ל הרשומה, הזן ×ת קוד ×”×ימות כדי להמשיך בהתחברות."), - ("Logout", ""), - ("Tags", ""), - ("Search ID", ""), + ("Please enter the folder name", "×× × ×”×›× ×¡ ×©× ×ª×™×§×™×™×”"), + ("Fix it", "תקן ×ת ×–×”"), + ("Warning", "×זהרה"), + ("Login screen using Wayland is not supported", "מסך התחברות המשתמש ב-Wayland ×ינו נתמך"), + ("Reboot required", "נדרש ×תחול מחדש"), + ("Unsupported display server", "שרת תצוגה ×œ× × ×ª×ž×š"), + ("x11 expected", "נדרש X11"), + ("Port", "יצי××”"), + ("Settings", "הגדרות"), + ("Username", "×©× ×ž×©×ª×ž×©"), + ("Invalid port", "פורט ×œ× ×—×•×§×™"), + ("Closed manually by the peer", "נסגר ידנית על ידי הצד השני"), + ("Enable remote configuration modification", "×פשר שינוי הגדרות מרחוק"), + ("Run without install", "הרץ ×œ×œ× ×”×ª×§× ×”"), + ("Connect via relay", "התחבר ב×מצעות ממסר"), + ("Always connect via relay", "התחבר תמיד דרך ממסר"), + ("whitelist_tip", "רק כתובות IP מהרשימה הלבנה יכולות לגשת ×לי"), + ("Login", "התחברות"), + ("Verify", "×מת"), + ("Remember me", "זכור ×ותי"), + ("Trust this device", "סמוך על מכשיר ×–×”"), + ("Verification code", "קוד ×ימות"), + ("verification_tip", "קוד ×ימות נשלח לכתובת ×”×ימייל הרשומה, הזן ×ת קוד ×”×ימות כדי להמשיך בהתחברות."), + ("Logout", "התנתק"), + ("Tags", "תגי×"), + ("Search ID", "חפש מזהה"), ("whitelist_sep", "מופרד על ידי פסיק, נקודה פסיק, ×¨×•×•×—×™× ×ו שורה חדשה"), - ("Add ID", ""), + ("Add ID", "הוסף מזהה"), ("Add Tag", "הוסף תג"), - ("Unselect all tags", ""), - ("Network error", ""), - ("Username missed", ""), - ("Password missed", ""), - ("Wrong credentials", "×©× ×ž×©×ª×ž×© ×ו סיסמה שגויי×"), - ("The verification code is incorrect or has expired", ""), + ("Unselect all tags", "בטל בחירת כל התגי×"), + ("Network error", "שגי×ת רשת"), + ("Username missed", "חסר ×©× ×ž×©×ª×ž×©"), + ("Password missed", "חסרה סיסמה"), + ("Wrong credentials", "פרטי התחברות שגויי×"), + ("The verification code is incorrect or has expired", "קוד ×”×ימות שגוי ×ו שפג תוקפו"), ("Edit Tag", "ערוך תג"), ("Forget Password", "שכחת סיסמה"), - ("Favorites", ""), + ("Favorites", "מועדפי×"), ("Add to Favorites", "הוסף למועדפי×"), ("Remove from Favorites", "הסר מהמועדפי×"), - ("Empty", ""), - ("Invalid folder name", ""), + ("Empty", "ריק"), + ("Invalid folder name", "×©× ×ª×™×§×™×™×” ×ינו תקין"), ("Socks5 Proxy", "פרוקסי Socks5"), ("Socks5/Http(s) Proxy", "פרוקסי Socks5/Http(s)"), - ("Discovered", ""), + ("Discovered", "נמצ×"), ("install_daemon_tip", "לצורך הפעלה בעת הפעלת המחשב, עליך להתקין שירות מערכת."), - ("Remote ID", ""), - ("Paste", ""), - ("Paste here?", ""), + ("Remote ID", "מזהה מרוחק"), + ("Paste", "הדבק"), + ("Paste here?", "להדביק ×›×ן?"), ("Are you sure to close the connection?", "×”×× ×תה בטוח שברצונך לסגור ×ת החיבור?"), - ("Download new version", ""), - ("Touch mode", ""), - ("Mouse mode", ""), + ("Download new version", "הורד גרסה חדשה"), + ("Touch mode", "מצב מגע"), + ("Mouse mode", "מצב עכבר"), ("One-Finger Tap", "הקשה ב×צבע ×חת"), ("Left Mouse", "עכבר שמ×לי"), ("One-Long Tap", "הקשה ×רוכה ב×צבע ×חת"), @@ -257,223 +255,220 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("One-Finger Move", "×”×–×–×” ב×צבע ×חת"), ("Double Tap & Move", "הקשה כפולה והזזה"), ("Mouse Drag", "גרירת עכבר"), - ("Three-Finger vertically", "שלוש ×צבעות ×נכית"), + ("Three-Finger vertically", "תנועה ×נכית בשלוש ×צבעות"), ("Mouse Wheel", "גלגלת עכבר"), ("Two-Finger Move", "×”×–×–×” בשתי ×צבעות"), ("Canvas Move", "הזזת בד"), ("Pinch to Zoom", "צביטה לזו×"), ("Canvas Zoom", "×–×•× ×‘×“"), - ("Reset canvas", ""), - ("No permission of file transfer", ""), - ("Note", ""), - ("Connection", ""), - ("Share Screen", "שיתוף מסך"), - ("Chat", ""), - ("Total", ""), - ("items", ""), - ("Selected", ""), + ("Reset canvas", "×פס לוח ציור"), + ("No permission of file transfer", "×ין הרש×ת העברת קבצי×"), + ("Note", "הערה"), + ("Connection", "התחברות"), + ("Share screen", "שיתוף מסך"), + ("Chat", "צ'×ט"), + ("Total", "הכל"), + ("items", "פריטי×"), + ("Selected", "נבחר"), ("Screen Capture", "לכידת מסך"), ("Input Control", "בקרת קלט"), ("Audio Capture", "לכידת שמע"), - ("File Connection", "חיבור קובץ"), - ("Screen Connection", "חיבור מסך"), - ("Do you accept?", ""), - ("Open System Setting", "פתח הגדרת מערכת"), - ("How to get Android input permission?", ""), + ("Do you accept?", "×”×× ×תה מקבל?"), + ("Open System Setting", "פתח הגדרות מערכת"), + ("How to get Android input permission?", "כיצד לקבל הרש×ת קלט ב×נדרו×יד?"), ("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_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", "הקש על [התחל שירות] ×ו ×פשר הרש×ת [לכידת מסך] כדי להתחיל ×ת שירות שיתוף המסך."), - ("android_permission_may_not_change_tip", "הרש×ות עבור ×—×™×‘×•×¨×™× ×©× ×•×¦×¨×• עשויות ×œ× ×œ×”×©×ª× ×•×ª מייד עד להתחברות מחדש."), - ("Account", ""), - ("Overwrite", ""), - ("This file exists, skip or overwrite this file?", ""), - ("Quit", ""), - ("Help", ""), - ("Failed", ""), - ("Succeeded", ""), - ("Someone turns on privacy mode, exit", ""), - ("Unsupported", ""), - ("Peer denied", ""), - ("Please install plugins", ""), - ("Peer exit", ""), - ("Failed to turn off", ""), - ("Turned off", ""), - ("Language", ""), - ("Keep RustDesk background service", ""), + ("android_permission_may_not_change_tip", "הרש×ות עבור ×—×™×‘×•×¨×™× ×§×™×™×ž×™× ×¢×©×•×™×•×ª ×œ× ×œ×”×©×ª× ×•×ª ב×ופן מיידי עד להתחברות מחדש."), + ("Account", "חשבון"), + ("Overwrite", "דרוס"), + ("This file exists, skip or overwrite this file?", "הקובץ כבר ×§×™×™×, לדלג ×ו לדרוס ×ותו?"), + ("Quit", "צ×"), + ("Help", "עזרה"), + ("Failed", "נכשל"), + ("Succeeded", "הצליח"), + ("Someone turns on privacy mode, exit", "מישהו הפעיל מצב פרטיות, מתבצעת יצי××”"), + ("Unsupported", "×œ× × ×ª×ž×š"), + ("Peer denied", "הצד השני סירב"), + ("Please install plugins", "×× × ×”×ª×§×Ÿ תוספי×"), + ("Peer exit", "הצד השני התנתק"), + ("Failed to turn off", "הכיבוי נכשל"), + ("Turned off", "מכובה"), + ("Language", "שפה"), + ("Keep RustDesk background service", "הש×ר ×ת שירות הרקע של RustDesk פעיל"), ("Ignore Battery Optimizations", "×”×ª×¢×œ× ×ž×ופטימיזציות סוללה"), - ("android_open_battery_optimizations_tip", "×× ×‘×¨×¦×•× ×š לבטל תכונה זו, ×× × ×¢×‘×•×¨ לדף ההגדרות של ×™×™×©×•× RustDesk הב×, ×ž×¦× ×•×”×›× ×¡ ל[סוללה], הסר ×ת הסימון מ-[×œ× ×ž×•×’×‘×œ]"), - ("Start on boot", ""), - ("Start the screen sharing service on boot, requires special permissions", ""), - ("Connection not allowed", ""), - ("Legacy mode", ""), - ("Map mode", ""), - ("Translate mode", ""), - ("Use permanent password", ""), - ("Use both passwords", ""), - ("Set permanent password", ""), - ("Enable remote restart", ""), - ("Restart remote device", ""), - ("Are you sure you want to restart", ""), - ("Restarting remote device", ""), - ("remote_restarting_tip", "המכשיר המרוחק מתחיל מחדש, ×× × ×¡×’×•×¨ ×ת תיבת ההודעה הזו והתחבר מחדש ×¢× ×¡×™×¡×ž×” קבועה ל×חר זמן מה"), - ("Copied", ""), + ("android_open_battery_optimizations_tip", "×× ×‘×¨×¦×•× ×š לבטל תכונה זו, ×× × ×¢×‘×•×¨ לדף ההגדרות של ×™×™×©×•× RustDesk , ×ž×¦× ×•×”×™×›× ×¡ ל[סוללה], ובטל ×ת הסימון מ-[×œ× ×ž×•×’×‘×œ]"), + ("Start on boot", "התחל בהפעלה"), + ("Start the screen sharing service on boot, requires special permissions", "הפעל ×ת שירות שיתוף המסך בעת ×תחול המכשיר (דורש הרש×ות מיוחדות)"), + ("Connection not allowed", "חיבור ×œ× ×ž×•×¨×©×”"), + ("Legacy mode", "מצב ישן"), + ("Map mode", "מצב מיפוי מקשי×"), + ("Translate mode", "מצב תרגו×"), + ("Use permanent password", "השתמש בסיסמה קבועה"), + ("Use both passwords", "השתמש בשתי הסיסמ×ות"), + ("Set permanent password", "הגדר סיסמה קבועה"), + ("Enable remote restart", "×פשר ×תחול מרחוק"), + ("Restart remote device", "×תחל ×ת המכשיר המרוחק"), + ("Are you sure you want to restart", "×”×× ×תה בטוח שברצונך ל×תחל"), + ("Restarting remote device", "מ×תחל ×ת המכשיר המרוחק"), + ("remote_restarting_tip", "המכשיר המרוחק מ×תחל ×ת עצמו, ×× × ×¡×’×•×¨ ×ת תיבת ההודעה הזו והתחבר מחדש ×¢× ×¡×™×¡×ž×” קבועה בעוד זמן מה"), + ("Copied", "הועתק"), ("Exit Fullscreen", "יצי××” ממסך מל×"), - ("Fullscreen", ""), + ("Fullscreen", "מסך מל×"), ("Mobile Actions", "פעולות ניידות"), ("Select Monitor", "בחר מסך"), ("Control Actions", "פעולות בקרה"), ("Display Settings", "הגדרות תצוגה"), - ("Ratio", ""), + ("Ratio", "יחס"), ("Image Quality", "×יכות תמונה"), ("Scroll Style", "סגנון גלילה"), ("Show Toolbar", "הצג סרגל כלי×"), ("Hide Toolbar", "הסתר סרגל כלי×"), ("Direct Connection", "חיבור ישיר"), - ("Relay Connection", "חיבור ריליי"), + ("Relay Connection", "חיבור ב×מצעות ממסר"), ("Secure Connection", "חיבור מ×ובטח"), ("Insecure Connection", "חיבור ×œ× ×ž×ובטח"), - ("Scale original", ""), - ("Scale adaptive", ""), - ("General", ""), - ("Security", ""), - ("Theme", ""), + ("Scale original", "×§× ×” מידה מקורי"), + ("Scale adaptive", "×§× ×” מידה מות××"), + ("General", "כללי"), + ("Security", "×בטחה"), + ("Theme", "ערכת נוש×"), ("Dark Theme", "ערכת × ×•×©× ×›×”×”"), ("Light Theme", "ערכת × ×•×©× ×‘×”×™×¨×”"), - ("Dark", ""), - ("Light", ""), - ("Follow System", "עקוב ×חר המערכת"), - ("Enable hardware codec", ""), + ("Dark", "×›×”×”"), + ("Light", "בהיר"), + ("Follow System", "×–×”×” למערכת"), + ("Enable hardware codec", "×פשר מקודד חומרה"), ("Unlock Security Settings", "פתח הגדרות ×בטחה"), - ("Enable audio", ""), + ("Enable audio", "הפעל שמע"), ("Unlock Network Settings", "פתח הגדרות רשת"), - ("Server", ""), + ("Server", "שרת"), ("Direct IP Access", "גישה ישירה ל-IP"), - ("Proxy", ""), - ("Apply", ""), - ("Disconnect all devices?", ""), - ("Clear", ""), + ("Proxy", "פרוקסי"), + ("Apply", "החל"), + ("Disconnect all devices?", "נתק ×ת כל המכשירי×?"), + ("Clear", "× ×§×”"), ("Audio Input Device", "מכשיר קלט שמע"), ("Use IP Whitelisting", "השתמש ברשימת לבנה של IP"), - ("Network", ""), + ("Network", "רשת"), ("Pin Toolbar", "× ×¢×¥ סרגל כלי×"), ("Unpin Toolbar", "הסר נעיצת סרגל כלי×"), - ("Recording", ""), - ("Directory", ""), - ("Automatically record incoming sessions", ""), - ("Automatically record outgoing sessions", ""), - ("Change", ""), - ("Start session recording", ""), - ("Stop session recording", ""), - ("Enable recording session", ""), - ("Enable LAN discovery", ""), - ("Deny LAN discovery", ""), - ("Write a message", ""), - ("Prompt", ""), - ("Please wait for confirmation of UAC...", ""), - ("elevated_foreground_window_tip", "החלון הנוכחי של שולחן העבודה המרוחק דורש הרש××” גבוהה יותר לפעולה, לכן ××™ ×פשר להשתמש בעכבר ובמקלדת ב×ופן זמני. תוכל לבקש מהמשתמש המרוחק למזער ×ת החלון הנוכחי, ×ו ללחוץ על כפתור ההגבהה בחלון ניהול החיבור. כדי להימנע מבעיה זו, מומלץ להתקין ×ת התוכנה במכשיר המרוחק."), - ("Disconnected", ""), - ("Other", ""), - ("Confirm before closing multiple tabs", ""), + ("Recording", "הקלטה"), + ("Directory", "תיקיה"), + ("Automatically record incoming sessions", "הקלט הפעלות נכנסות ב×ופן ×וטומטי"), + ("Automatically record outgoing sessions", "הקלט הפעלות יוצ×ות ב×ופן ×וטומטי"), + ("Change", "שנה"), + ("Start session recording", "התחל הקלטת הפעלה"), + ("Stop session recording", "הפסק הקלטת הפעלה"), + ("Enable recording session", "×פשר הקלטת הפעלה"), + ("Enable LAN discovery", "×פשר זיהוי ברשת מקומית"), + ("Deny LAN discovery", "×—×¡×•× ×–×™×”×•×™ ברשת מקומית"), + ("Write a message", "כתוב הודעה"), + ("Prompt", "×”× ×—×™×”"), + ("Please wait for confirmation of UAC...", "×× × ×”×ž×ª×Ÿ ל×ישור בקרת חשבון משתמש (UAC)..."), + ("elevated_foreground_window_tip", "החלון הנוכחי של שולחן העבודה המרוחק דורש הרש××” גבוהה יותר לפעולה, לכן ××™ ×פשר להשתמש בעכבר ובמקלדת ב×ופן זמני. תוכל לבקש מהמשתמש המרוחק למזער ×ת החלון הנוכחי, ×ו ללחוץ על כפתור העל×ת הרש×ות בחלון ניהול החיבור. כדי להימנע מבעיה זו, מומלץ להתקין ×ת התוכנה במכשיר המרוחק."), + ("Disconnected", "מנותק"), + ("Other", "×חר"), + ("Confirm before closing multiple tabs", "×שר לפני סגירת מספר לשוניות"), ("Keyboard Settings", "הגדרות מקלדת"), ("Full Access", "גישה מל××”"), ("Screen Share", "שיתוף מסך"), - ("Wayland requires Ubuntu 21.04 or higher version.", ""), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", ""), - ("JumpLink", "הצג"), + ("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", "קישור מהיר"), ("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...", ""), + ("Show RustDesk", "הצג ×ת RustDesk"), + ("This PC", "מחשב ×–×”"), + ("or", "×ו"), + ("Continue with", "המשך ×¢×"), + ("Elevate", "הפעל הרש×ות מורחבות"), + ("Zoom cursor", "הגדל סמן"), + ("Accept sessions via password", "קבל הפעלות ב×מצעות סיסמה"), + ("Accept sessions via click", "קבל הפעלות ב×מצעות לחיצה"), + ("Accept sessions via both", "קבל הפעלות ב×מצעות סיסמה ×ו לחיצה"), + ("Please wait for the remote side to accept your session request...", "×× × ×”×ž×ª×Ÿ שהצד המרוחק ×™×שר ×ת בקשת ההפעלה שלך..."), ("One-time Password", "סיסמה חד-פעמית"), - ("Use one-time password", ""), - ("One-time password length", ""), - ("Request access to your device", ""), - ("Hide connection management window", ""), - ("hide_cm_tip", "×פשר הסתרה רק ×× ×ž×§×‘×œ×™× ×¡×©× ×™× ×“×¨×š סיסמה ×•×ž×©×ª×ž×©×™× ×‘×¡×™×¡×ž×” קבועה"), - ("wayland_experiment_tip", "תמיכה ב-Wayland נמצ×ת בשלב ניסיוני, ×× × ×”×©×ª×ž×© ב-X11 ×× ×תה זקוק לגישה ×œ× ×ž×œ×•×•×”."), - ("Right click to select tabs", ""), - ("Skipped", ""), - ("Add to address book", ""), - ("Group", ""), - ("Search", ""), - ("Closed manually by web console", ""), - ("Local keyboard type", ""), - ("Select local keyboard type", ""), - ("software_render_tip", "×× ×תה משתמש בכרטיס גרפיקה של Nvidia תחת Linux וחלון המרחוק נסגר מיד ל×חר החיבור, החלפה למנהל ההתקן הפתוח Nouveau ובחירה בשימוש בעיבוד תוכנה עשויה לעזור. נדרשת הפעלה מחדש של התוכנה."), - ("Always use software rendering", ""), - ("config_input", "כדי לשלוט בשולחן העבודה המרוחק ב×מצעות מקלדת, עליך להעניק ל-RustDesk הרש×ות \"מעקב ×חרי קלט\"."), + ("Use one-time password", "השתמש בסיסמה חד-פעמית"), + ("One-time password length", "×ורך סיסמה חד-פעמית"), + ("Request access to your device", "בקשת גישה למכשיר שלך"), + ("Hide connection management window", "הסתר חלון ניהול חיבורי×"), + ("hide_cm_tip", "×פשר הסתרה רק ×× ×ž×§×‘×œ×™× ×”×¤×¢×œ×•×ª דרך סיסמה ×•×ž×©×ª×ž×©×™× ×‘×¡×™×¡×ž×” קבועה"), + ("wayland_experiment_tip", "תמיכה ב-Wayland נמצ×ת בשלב ניסיוני, ×× × ×”×©×ª×ž×© ב-X11 ×× ×תה זקוק לגישה ×œ×œ× ×œ×™×•×•×™ מהצד המרוחק"), + ("Right click to select tabs", "לחץ לחיצה ימנית כדי לבחור לשוניות"), + ("Skipped", "דולג"), + ("Add to address book", "הוסף לספר הכתובות"), + ("Group", "קבוצה"), + ("Search", "חפש"), + ("Closed manually by web console", "נסגר ידנית דרך מסוף ×”×ינטרנט"), + ("Local keyboard type", "סוג מקלדת מקומי"), + ("Select local keyboard type", "בחר סוג מקלדת מקומי"), + ("software_render_tip", "×× ×תה משתמש בכרטיס גרפיקה של Nvidia תחת Linux וחלון המרוחק נסגר מיד ל×חר החיבור, החלפה למנהל ההתקן הפתוח Nouveau ובחירה בשימוש בעיבוד תוכנה עשויה לעזור. נדרשת הפעלה מחדש של התוכנה."), + ("Always use software rendering", "השתמש תמיד בעיבוד תוכנה"), + ("config_input", "כדי לשלוט בשולחן העבודה המרוחק ב×מצעות מקלדת, עליך להעניק ל-RustDesk הרש×ות \"מעקב קלט\"."), ("config_microphone", "כדי לדבר מרחוק, עליך להעניק ל-RustDesk הרש×ות \"הקלטת שמע\"."), - ("request_elevation_tip", "ניתן ×’× ×œ×‘×§×© הגבהה ×× ×™×© מישהו בצד המרוחק."), - ("Wait", ""), - ("Elevation Error", "שגי×ת הגבהה"), - ("Ask the remote user for authentication", ""), - ("Choose this if the remote account is administrator", ""), - ("Transmit the username and password of administrator", ""), - ("still_click_uac_tip", "עדיין דורש מהמשתמש המרוחק ללחוץ OK בחלון ×”-UAC של הרצת RustDesk."), - ("Request Elevation", "בקש הגבהה"), - ("wait_accept_uac_tip", "×× × ×”×ž×ª×Ÿ למשתמש המרוחק לקבל ×ת די×לוג ×”-UAC."), - ("Elevate successfully", ""), - ("uppercase", ""), - ("lowercase", ""), - ("digit", ""), - ("special character", ""), - ("length>=8", ""), - ("Weak", ""), - ("Medium", ""), - ("Strong", ""), + ("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 של RustDesk"), + ("Request Elevation", "בקש העל×ת הרש×ות"), + ("wait_accept_uac_tip", "×× × ×”×ž×ª×Ÿ שהמשתמש המרוחק ×™×שר ×ת חלון ×”-UAC"), + ("Elevate successfully", "ההרש×ות הורחבו בהצלחה"), + ("uppercase", "×ותיות גדולות"), + ("lowercase", "×ותיות קטנות"), + ("digit", "ספרה"), + ("special character", "תו מיוחד"), + ("length>=8", "לפחות ב×ורך 8"), + ("Weak", "חלש"), + ("Medium", "בינוני"), + ("Strong", "×—×–×§"), ("Switch Sides", "החלף צדדי×"), - ("Please confirm if you want to share your desktop?", ""), - ("Display", ""), + ("Please confirm if you want to share your desktop?", "×”×× ×œ×©×ª×£ ×ת שולחן העבודה שלך?"), + ("Display", "תצוגה"), ("Default View Style", "סגנון תצוגה ברירת מחדל"), ("Default Scroll Style", "סגנון גלילה ברירת מחדל"), ("Default Image Quality", "×יכות תמונה ברירת מחדל"), ("Default Codec", "קודק ברירת מחדל"), - ("Bitrate", ""), - ("FPS", ""), - ("Auto", ""), + ("Bitrate", "קצב סיביות"), + ("FPS", "FPS"), + ("Auto", "×וטומטי"), ("Other Default Options", "×פשרויות ברירת מחדל ×חרות"), - ("Voice call", ""), - ("Text chat", ""), - ("Stop voice call", ""), - ("relay_hint_tip", "ייתכן ×©×œ× × ×™×ª×Ÿ להתחבר ישירות; ניתן לנסות להתחבר דרך ריליי. בנוסף, ×× ×‘×¨×¦×•× ×š להשתמש בריליי בניסיון הר×שון שלך, תוכל להוסיף ×ת הסיומת \"/r\" למזהה ×ו לבחור ב×פשרות \"התחבר תמיד דרך ריליי\" בכרטיס של ×”×¡×©× ×™× ×”××—×¨×•× ×™× ×× ×§×™×™×."), - ("Reconnect", ""), - ("Codec", ""), - ("Resolution", ""), - ("No transfers in progress", ""), - ("Set one-time password length", ""), + ("Voice call", "שיחה קולית"), + ("Text chat", "שיחת טקסט"), + ("Stop voice call", "הפסק שיחה קולית"), + ("relay_hint_tip", "ייתכן ×©×œ× × ×™×ª×Ÿ להתחבר ישירות. נסה להתחבר דרך ממסר. כדי להשתמש בממסר כבר מהניסיון הר×שון, הוסף ×ת הסיומת /r למזהה ×ו בחר \"התחבר תמיד דרך ממסר\" בכרטיס ההפעלות ×”×חרונות, ×× ×§×™×™×."), + ("Reconnect", "התחברות מחדש"), + ("Codec", "קודק"), + ("Resolution", "רזולוציה"), + ("No transfers in progress", "×ין העברות בתהליך"), + ("Set one-time password length", "הגדר ×ורך סיסמה חד-פעמית"), ("RDP Settings", "הגדרות RDP"), - ("Sort by", ""), + ("Sort by", "מיין לפי"), ("New Connection", "חיבור חדש"), - ("Restore", ""), - ("Minimize", ""), - ("Maximize", ""), + ("Restore", "שחזור"), + ("Minimize", "מזער"), + ("Maximize", "הגדל"), ("Your Device", "המכשיר שלך"), - ("empty_recent_tip", "×ופס, ×ין ×¡×©× ×™× ×חרוני×!\n×”×’×™×¢ הזמן לתכנן חדש."), - ("empty_favorite_tip", "עדיין ×ין ×¢×ž×™×ª×™× ×ž×•×¢×“×¤×™×?\n×‘×•× × ×ž×¦× ×ž×™×©×”×• להתחבר ×ליו ונוסיף ×ותו למועדפי×!"), + ("empty_recent_tip", "×ופס, ×ין הפעלות ×חרונות!\n×”×’×™×¢ הזמן להתחבר למישהו חדש."), + ("empty_favorite_tip", "עדיין ×ין ×¢×ž×™×ª×™× ×ž×•×¢×“×¤×™×?\n×‘× × ×ž×¦× ×ž×™×©×”×• להתחבר ×ליו ונוסיף ×ותו למועדפי×!"), ("empty_lan_tip", "×וי ל×, נר××” שעדיין ×œ× ×’×™×œ×™× ×• עמיתי×."), - ("empty_address_book_tip", "×וי ו×בוי, נר××” שכרגע ×ין ×¢×ž×™×ª×™× ×‘×¡×¤×¨ הכתובות שלך."), - ("eg: admin", ""), + ("empty_address_book_tip", "×בוי, נר××” שכרגע ×ין ×¢×ž×™×ª×™× ×‘×¡×¤×¨ הכתובות שלך."), ("Empty Username", "×©× ×ž×©×ª×ž×© ריק"), ("Empty Password", "סיסמה ריקה"), - ("Me", ""), - ("identical_file_tip", "קובץ ×–×” ×–×”×” לקובץ של העמית."), + ("Me", "×× ×™"), + ("identical_file_tip", "קובץ ×–×” ×–×”×” לקובץ שבצד העמית."), ("show_monitors_tip", "הצג ×ž×¡×›×™× ×‘×¡×¨×’×œ כלי×"), ("View Mode", "מצב תצוגה"), ("login_linux_tip", "עליך להתחבר לחשבון Linux מרוחק כדי ל×פשר פעילות שולחן עבודה X"), ("verify_rustdesk_password_tip", "×מת סיסמת RustDesk"), ("remember_account_tip", "זכור חשבון ×–×”"), - ("os_account_desk_tip", "חשבון ×–×” משמש להתחברות למערכת ההפעלה המרוחקת ול×פשר פעילות שולחן עבודה במצב ×œ× ×ž×§×•×•×Ÿ"), + ("os_account_desk_tip", "חשבון ×–×” משמש להתחברות למערכת ההפעלה המרוחקת ולהפעלת שולחן עבודה במצב ×œ× ×ž×§×•×•×Ÿ"), ("OS Account", "חשבון מערכת הפעלה"), ("another_user_login_title_tip", "משתמש ×חר כבר התחבר"), ("another_user_login_text_tip", "נתק"), @@ -481,180 +476,238 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("xorg_not_found_text_tip", "×× × ×”×ª×§×Ÿ Xorg"), ("no_desktop_title_tip", "×ין שולחן עבודה זמין"), ("no_desktop_text_tip", "×× × ×”×ª×§×Ÿ שולחן עבודה GNOME"), - ("No need to elevate", ""), + ("No need to elevate", "×ין צורך בהעל×ת הרש×ות"), ("System Sound", "צליל מערכת"), - ("Default", ""), - ("New RDP", ""), - ("Fingerprint", ""), + ("Default", "ברירת מחדל"), + ("New RDP", "RDP חדש"), + ("Fingerprint", "טביעת ×צבע"), ("Copy Fingerprint", "העתק טביעת ×צבע"), ("no fingerprints", "×ין טביעות ×צבע"), - ("Select a peer", ""), - ("Select peers", ""), - ("Plugins", ""), - ("Uninstall", ""), - ("Update", ""), - ("Enable", ""), - ("Disable", ""), - ("Options", ""), + ("Select a peer", "בחר עמית"), + ("Select peers", "בחר עמיתי×"), + ("Plugins", "תוספי×"), + ("Uninstall", "הסר"), + ("Update", "עדכן"), + ("Enable", "פועל"), + ("Disable", "כבוי"), + ("Options", "×פשרויות"), ("resolution_original_tip", "רזולוציה מקורית"), ("resolution_fit_local_tip", "הת×× ×œ×¨×–×•×œ×•×¦×™×” מקומית"), ("resolution_custom_tip", "רזולוציה מות×מת ×ישית"), - ("Collapse toolbar", ""), - ("Accept and Elevate", "קבל והגבה"), - ("accept_and_elevate_btn_tooltip", "קבל ×ת החיבור והגבה הרש×ות UAC."), + ("Collapse toolbar", "מזער סרגל כלי×"), + ("Accept and Elevate", "×שר והפעל הרש×ות מורחבות"), + ("accept_and_elevate_btn_tooltip", "קבל ×ת החיבור והפעל הרש×ות מורחבות (UAC)"), ("clipboard_wait_response_timeout_tip", "המתנה לתגובת העתקה הסתיימה בזמן."), - ("Incoming connection", ""), - ("Outgoing connection", ""), - ("Exit", ""), - ("Open", ""), + ("Incoming connection", "חיבור נכנס"), + ("Outgoing connection", "חיבור יוצ×"), + ("Exit", "צ×"), + ("Open", "פתח"), ("logout_tip", "×”×× ×תה בטוח שברצונך להתנתק?"), - ("Service", ""), - ("Start", ""), - ("Stop", ""), + ("Service", "שירות"), + ("Start", "התחל"), + ("Stop", "עצור"), ("exceed_max_devices", "הגעת למספר המקסימלי של ×ž×›×©×™×¨×™× ×©× ×™×ª×Ÿ לנהל."), - ("Sync with recent sessions", ""), - ("Sort tags", ""), - ("Open connection in new tab", ""), - ("Move tab to new window", ""), - ("Can not be empty", ""), - ("Already exists", ""), + ("Sync with recent sessions", "סנכרן ×¢× ×”×¤×¢×œ×•×ª ×חרונות"), + ("Sort tags", "מיין תגי×"), + ("Open connection in new tab", "פתח חיבור בלשונית חדשה"), + ("Move tab to new window", "העבר לשונית לחלון חדש"), + ("Can not be empty", "×œ× ×™×›×•×œ להיות ריק"), + ("Already exists", "כבר ×§×™×™×"), ("Change Password", "שנה סיסמה"), ("Refresh Password", "רענן סיסמה"), - ("ID", ""), + ("ID", "מזהה"), ("Grid View", "תצוגת רשת"), ("List View", "תצוגת רשימה"), - ("Select", ""), + ("Select", "בחר"), ("Toggle Tags", "החלף תגיות"), ("pull_ab_failed_tip", "נכשל ברענון ספר הכתובות"), - ("push_ab_failed_tip", "נכשל בסנכרון ספר הכתובות לשרת"), - ("synced_peer_readded_tip", "×”×ž×›×©×™×¨×™× ×©×”×™×• × ×•×›×—×™× ×‘×¡×©× ×™× ×”××—×¨×•× ×™× ×™×¡×•× ×›×¨× ×• בחזרה לספר הכתובות."), + ("push_ab_failed_tip", "נכשל סנכרון ספר הכתובות ×¢× ×”×©×¨×ª"), + ("synced_peer_readded_tip", "×”×ž×›×©×™×¨×™× ×©×”×™×• × ×•×›×—×™× ×‘×”×¤×¢×œ×•×ª ×”×חרונות יסונכרנו בחזרה לספר הכתובות."), ("Change Color", "שנה צבע"), ("Primary Color", "צבע עיקרי"), ("HSV Color", "צבע HSV"), ("Installation Successful!", "ההתקנה הצליחה!"), - ("Installation failed!", ""), - ("Reverse mouse wheel", ""), - ("{} sessions", ""), - ("scam_title", "ייתכן ש×תה נפלת להונ××”!"), + ("Installation failed!", "התקנה נכשלה!"), + ("Reverse mouse wheel", "הפוך כיוון גלגלת העכבר"), + ("{} sessions", "{} הפעלות"), + ("scam_title", "ייתכן שנפלת להונ××”!"), ("scam_text1", "×× ×תה בשיחת טלפון ×¢× ×ž×™×©×”×• ש×ינך מכיר ו×ינך סומך עליו שביקש ממך להשתמש ב-RustDesk ולהתחיל ×ת השירות, ×ל תמשיך ונתק מיד."), ("scam_text2", "סביר להניח שמדובר בהונ××” שמנסה לגנוב ממך כסף ×ו מידע פרטי ×חר."), - ("Don't show again", ""), - ("I Agree", ""), - ("Decline", ""), - ("Timeout in minutes", ""), - ("auto_disconnect_option_tip", "סגור ב×ופן ×וטומטי ×¡×©× ×™× × ×›× ×¡×™× ×‘×ž×§×¨×” של חוסר פעילות של המשתמש"), + ("Don't show again", "×ל תר××” שוב"), + ("I Agree", "×× ×™ מסכי×"), + ("Decline", "דחה"), + ("Timeout in minutes", "משך זמן עד התנתקות (בדקות)"), + ("auto_disconnect_option_tip", "סגור ב×ופן ×וטומטי הפעלות נכנסות במקרה של חוסר פעילות של המשתמש"), ("Connection failed due to inactivity", "התנתקות ×וטומטית בגלל חוסר פעילות"), - ("Check for software update on startup", ""), + ("Check for software update on startup", "בדוק ×¢×“×›×•× ×™× ×¢× ×”×”×¤×¢×œ×”"), ("upgrade_rustdesk_server_pro_to_{}_tip", "×× × ×©×“×¨×’ ×ת RustDesk Server Pro לגרסה {} ×ו חדשה יותר!"), - ("pull_group_failed_tip", "נכשל ברענון קבוצה"), - ("Filter by intersection", ""), - ("Remove wallpaper during incoming sessions", ""), - ("Test", ""), + ("pull_group_failed_tip", "נכשל ברענון הקבוצה"), + ("Filter by intersection", "סנן לפי חיתוך"), + ("Remove wallpaper during incoming sessions", "הסר רקע שולחן עבודה במהלך הפעלות נכנסות"), + ("Test", "בדיקה"), ("display_is_plugged_out_msg", "המסך הופסק, החלף למסך הר×שון."), - ("No displays", ""), - ("Open in new window", ""), - ("Show displays as individual windows", ""), - ("Use all my displays for the remote session", ""), + ("No displays", "×ין מסכי×"), + ("Open in new window", "פתח בחלון חדש"), + ("Show displays as individual windows", "הצג ×ž×¡×›×™× ×›×—×œ×•× ×•×ª נפרדי×"), + ("Use all my displays for the remote session", "השתמש בכל ×”×ž×¡×›×™× ×©×œ×™ עבור ההפעלה המרוחקת"), ("selinux_tip", "SELinux מופעל במכשיר שלך, מה שעלול למנוע מ-RustDesk לפעול כר×וי כצד הנשלט."), - ("Change view", ""), - ("Big tiles", ""), - ("Small tiles", ""), - ("List", ""), - ("Virtual display", ""), - ("Plug out all", ""), - ("True color (4:4:4)", ""), - ("Enable blocking user input", ""), + ("Change view", "שנה תצוגה"), + ("Big tiles", "××¨×™×—×™× ×’×“×•×œ×™×"), + ("Small tiles", "××¨×™×—×™× ×§×˜× ×™×"), + ("List", "רשימה"), + ("Virtual display", "מסך וירטו×לי"), + ("Plug out all", "נתק הכל"), + ("True color (4:4:4)", "צבע מדויק (4:4:4)"), + ("Enable blocking user input", "×פשר חסימת קלט משתמש"), ("id_input_tip", "ניתן להזין מזהה, IP ישיר, ×ו דומיין ×¢× ×¤×•×¨×˜ (:).\n×× ×‘×¨×¦×•× ×š לגשת למכשיר בשרת ×חר, ×× × ×”×•×¡×£ ×ת כתובת השרת (@?key=), לדוגמה,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\n×× ×‘×¨×¦×•× ×š לגשת למכשיר בשרת ציבורי, ×× × ×”×–×Ÿ \"@public\", המפתח ×ינו נדרש לשרת ציבורי"), ("privacy_mode_impl_mag_tip", "מצב 1"), ("privacy_mode_impl_virtual_display_tip", "מצב 2"), - ("Enter privacy mode", ""), - ("Exit privacy mode", ""), - ("idd_not_support_under_win10_2004_tip", "× ×”×’ התצוגה ×”×¢×§×™×£ ×ינו נתמך. נדרשת גרסת Windows 10, גרסה 2004 ×ו חדשה יותר."), + ("Enter privacy mode", "הכנס למצב פרטיות"), + ("Exit privacy mode", "×¦× ×ž×ž×¦×‘ פרטיות"), + ("idd_not_support_under_win10_2004_tip", "מנהל התצוגה ×”×¢×§×™×£ ×ינו נתמך. נדרשת גרסת Windows 10, גרסה 2004 ×ו חדשה יותר."), ("input_source_1_tip", "מקור קלט 1"), ("input_source_2_tip", "מקור קלט 2"), - ("Swap control-command key", ""), - ("swap-left-right-mouse", "החלף בין כפתור העכבר השמ×לי לימני"), + ("Swap control-command key", "החלף בין ×”×ž×§×©×™× Control ו־Command"), + ("swap-left-right-mouse", "החלף בין לחצן שמ×לי וימני בעכבר"), ("2FA code", "קוד ×ימות דו-שלבי"), - ("More", ""), + ("More", "עוד"), ("enable-2fa-title", "הפעל ×ימות דו-שלבי"), ("enable-2fa-desc", "×× × ×”×’×“×¨ כעת ×ת ×”×פליקציה שלך ל×ימות. תוכל להשתמש ב×פליקציית ×ימות כגון Authy, Microsoft ×ו Google Authenticator בטלפון ×ו במחשב שלך.\n\nסרוק ×ת קוד ×”-QR ×¢× ×”×פליקציה שלך והזן ×ת הקוד שה×פליקציה מציגה כדי להפעיל ×ת ×ימות הדו-שלבי."), - ("wrong-2fa-code", "×œ× × ×™×ª×Ÿ ל×מת ×ת הקוד. בדוק שהקוד והגדרות הזמן המקומיות נכונות"), + ("wrong-2fa-code", "קוד שגוי. בדוק ×ת הקוד ו×ת הגדרות השעה במכשיר"), ("enter-2fa-title", "×ימות דו-שלבי"), - ("Email verification code must be 6 characters.", ""), - ("2FA code must be 6 digits.", ""), - ("Multiple Windows sessions found", ""), - ("Please select the session you want to connect to", ""), - ("powered_by_me", ""), + ("Email verification code must be 6 characters.", "קוד ×ימות במייל חייב להיות ב×ורך של 6 תווי×."), + ("2FA code must be 6 digits.", "קוד ×ימות דו-שלבי חייב להיות ב×ורך של 6 מספרי×."), + ("Multiple Windows sessions found", "נמצ×ו מספר הפעלות Windows"), + ("Please select the session you want to connect to", "×× × ×‘×—×¨ ×ת ההפעלה שברצונך להתחבר ×ליה"), + ("powered_by_me", "מופעל דרכי"), ("outgoing_only_desk_tip", "זוהי מהדורה מות×מת ×ישית.\nניתן להתחבר ×œ×ž×›×©×™×¨×™× ×חרי×, ×ך ×ž×›×©×™×¨×™× ××—×¨×™× ×œ× ×™×›×•×œ×™× ×œ×”×ª×—×‘×¨ ×ליך."), - ("preset_password_warning", ""), - ("Security Alert", ""), - ("My address book", ""), - ("Personal", ""), - ("Owner", ""), - ("Set shared password", ""), - ("Exist in", ""), - ("Read-only", ""), - ("Read/Write", ""), - ("Full Control", ""), - ("share_warning_tip", ""), - ("Everyone", ""), - ("ab_web_console_tip", ""), - ("allow-only-conn-window-open-tip", ""), - ("no_need_privacy_mode_no_physical_displays_tip", ""), - ("Follow remote cursor", ""), - ("Follow remote window focus", ""), - ("default_proxy_tip", ""), - ("no_audio_input_device_tip", ""), - ("Incoming", ""), - ("Outgoing", ""), - ("Clear Wayland screen selection", ""), - ("clear_Wayland_screen_selection_tip", ""), - ("confirm_clear_Wayland_screen_selection_tip", ""), - ("android_new_voice_call_tip", ""), - ("texture_render_tip", ""), - ("Use texture rendering", ""), - ("Floating window", ""), - ("floating_window_tip", ""), - ("Keep screen on", ""), - ("Never", ""), - ("During controlled", ""), - ("During service is on", ""), - ("Capture screen using DirectX", ""), - ("Back", ""), - ("Apps", ""), - ("Volume up", ""), - ("Volume down", ""), - ("Power", ""), - ("Telegram bot", ""), - ("enable-bot-tip", ""), - ("enable-bot-desc", ""), - ("cancel-2fa-confirm-tip", ""), - ("cancel-bot-confirm-tip", ""), - ("About RustDesk", ""), - ("Send clipboard keystrokes", ""), - ("network_error_tip", ""), - ("Unlock with PIN", ""), - ("Requires at least {} characters", ""), - ("Wrong PIN", ""), - ("Set PIN", ""), - ("Enable trusted devices", ""), - ("Manage trusted devices", ""), - ("Platform", ""), - ("Days remaining", ""), - ("enable-trusted-devices-tip", ""), - ("Parent directory", ""), - ("Resume", ""), - ("Invalid file name", ""), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("preset_password_warning", "שימו לב: שימוש בסיסמה קבועה עלול להפחית ×ת רמת ×”×בטחה"), + ("Security Alert", "התר×ת ×בטחה"), + ("My address book", "ספר הכתובות שלי"), + ("Personal", "×ישי"), + ("Owner", "בעלי×"), + ("Set shared password", "הגדר סיסמה שיתופית"), + ("Exist in", "×§×™×™× ×‘"), + ("Read-only", "קרי××” בלבד"), + ("Read/Write", "קרי××”/כתיבה"), + ("Full Control", "שליטה מל××”"), + ("share_warning_tip", "זהירות: כל מי שברשימה יקבל ×ת ההרש×ות שנבחרו"), + ("Everyone", "כול×"), + ("ab_web_console_tip", "ספר הכתובות מסונכרן ×¢× ×ž×ž×©×§ ניהול ×ינטרנטי"), + ("allow-only-conn-window-open-tip", "×פשר ×—×™×‘×•×¨×™× ×¨×§ ×›×שר חלון הניהול פתוח"), + ("no_need_privacy_mode_no_physical_displays_tip", "×ין צורך במצב פרטיות ×›×שר ×ין תצוגות פיזיות"), + ("Follow remote cursor", "עקוב ×חר מצביע מרוחק"), + ("Follow remote window focus", "עקוב ×חר פוקוס בחלון מרוחק"), + ("default_proxy_tip", "ברירת מחדל זו תשתמש בהגדרות ×”proxy הכלליות של המערכת"), + ("no_audio_input_device_tip", "×œ× × ×ž×¦× ×ž×›×©×™×¨ קלט שמע"), + ("Incoming", "נכנס"), + ("Outgoing", "יוצ×"), + ("Clear Wayland screen selection", "× ×§×” ×ת בחירת המסך ב־Wayland"), + ("clear_Wayland_screen_selection_tip", "× ×§×” ×ת בחירת המסך שנשמרה בעת שימוש ב־Wayland"), + ("confirm_clear_Wayland_screen_selection_tip", "×”×× ×תה בטוח שברצונך לנקות ×ת בחירת המסך עבור Wayland?"), + ("android_new_voice_call_tip", "בקשת שיחת קול חדשה התקבלה"), + ("texture_render_tip", "השתמש בטכניקת עיבוד מבוססת טקסטורות (ייתכן שיגדיל ×ת הביצועי×)"), + ("Use texture rendering", "השתמש בעיבוד טקסטורה"), + ("Floating window", "חלון צף"), + ("floating_window_tip", "הפעלת חלון צף ת×פשר גישה נוחה מעל ×פליקציות ×חרות"), + ("Keep screen on", "הש×ר מסך דולק"), + ("Never", "××£ פע×"), + ("During controlled", "בזמן שליטה"), + ("During service is on", "×›×שר השירות פעיל"), + ("Capture screen using DirectX", "לכוד מסך ב×מצעות DirectX"), + ("Back", "חזור"), + ("Apps", "×פליקציות"), + ("Volume up", "הגבר"), + ("Volume down", "הנמך"), + ("Power", "הפעלה"), + ("Telegram bot", "בוט טלגר×"), + ("enable-bot-tip", "×פשר שליטה ×ו קבלת התר×ות דרך בוט טלגר×"), + ("enable-bot-desc", "×פשרות זו ת×פשר לבוט ×˜×œ×’×¨× ×œ×‘×¦×¢ פעולות ×ו לשלוח התר×ות עבור חשבונך"), + ("cancel-2fa-confirm-tip", "×”×× ×תה בטוח שברצונך לבטל ×ת ×”×ימות הדו-שלבי?"), + ("cancel-bot-confirm-tip", "×”×× ×תה בטוח שברצונך לבטל ×ת קישור הבוט?"), + ("About RustDesk", "×ודות RustDesk"), + ("Send clipboard keystrokes", "שלח הקשות לוח גזירי×"), + ("network_error_tip", "×ירעה שגי×ת רשת. ×× × ×‘×“×•×§ ×ת החיבור שלך ונסה שוב."), + ("Unlock with PIN", "פתח ב×מצעות קוד PIN"), + ("Requires at least {} characters", "× ×“×¨×©×™× ×œ×¤×—×•×ª {} תווי×"), + ("Wrong PIN", "PIN שגוי"), + ("Set PIN", "הגדר PIN"), + ("Enable trusted devices", "×פשר גישה ×ž×ž×›×©×™×¨×™× ×ž×”×™×ž× ×™×"), + ("Manage trusted devices", "נהל ×ž×›×©×™×¨×™× ×ž×”×™×ž× ×™×"), + ("Platform", "פלטפורמה"), + ("Days remaining", "×™×ž×™× ×©× ×©×רו"), + ("enable-trusted-devices-tip", "×פשר גישה ×וטומטית ×ž×ž×›×©×™×¨×™× ×©×¡×•×ž× ×• כמהימני×"), + ("Parent directory", "תיקיית ×ב"), + ("Resume", "המשך"), + ("Invalid file name", "×©× ×§×•×‘×¥ ×ינו תקין"), + ("one-way-file-transfer-tip", "תכונה זו מ×פשרת העברת ×§×‘×¦×™× ×‘×›×™×•×•×Ÿ ×חד בלבד – מהמכשיר שלך למרוחק ×ו להפך"), + ("Authentication Required", "הזדהות נדרשת"), + ("Authenticate", "הזדהה"), + ("web_id_input_tip", "הזן ×ת מזהה ההתחברות ×ו כתובת דומיין להתחברות דרך הדפדפן"), + ("Download", "הורדה"), + ("Upload folder", "העלה תיקיה"), + ("Upload files", "העלה קבצי×"), + ("Clipboard is synchronized", "לוח ×”×’×–×™×¨×™× ×¡×•× ×›×¨×Ÿ"), + ("Update client clipboard", "עדכן ×ת לוח ×”×’×–×™×¨×™× ×©×œ הלקוח"), + ("Untagged", "×œ× ×ž×ª×•×™×™×’"), + ("new-version-of-{}-tip", "גרסה חדשה של {} זמינה"), + ("Accessible devices", "×ž×›×©×™×¨×™× × ×’×™×©×™×"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "×× × ×©×“×¨×’ ×ת לקוח RustDesk לגרסה {} ×ו חדשה יותר בצד המרוחק!"), + ("d3d_render_tip", "שימוש בעיבוד Direct3D עשוי לשפר ×‘×™×¦×•×¢×™× ×‘×—×œ×§ מהמקרי×"), + ("Use D3D rendering", "השתמש בעיבוד D3D"), + ("Printer", "מדפסת"), + ("printer-os-requirement-tip", "להפעלת מדפסת נדרש מערכת הפעלה תו×מת"), + ("printer-requires-installed-{}-client-tip", "נדרש לקוח {} מותקן כדי להשתמש במדפסת"), + ("printer-{}-not-installed-tip", "המדפסת {} ××™× ×” מותקנת"), + ("printer-{}-ready-tip", "המדפסת {} מוכנה לשימוש"), + ("Install {} Printer", "התקן מדפסת {}"), + ("Outgoing Print Jobs", "עבודות הדפסה יוצ×ות"), + ("Incoming Print Jobs", "עבודות הדפסה נכנסות"), + ("Incoming Print Job", "עבודת הדפסה נכנסת"), + ("use-the-default-printer-tip", "השתמש במדפסת ברירת המחדל של המערכת"), + ("use-the-selected-printer-tip", "השתמש במדפסת שנבחרה מתוך הרשימה"), + ("auto-print-tip", "הדפס ×וטומטית עבודות הדפסה נכנסות ×œ×œ× ×ישור נוסף"), + ("print-incoming-job-confirm-tip", "×”×× ×‘×¨×¦×•× ×š להדפיס ×ת עבודת ההדפסה הנכנסת?"), + ("remote-printing-disallowed-tile-tip", "×œ× × ×™×ª×Ÿ להדפיס מרחוק"), + ("remote-printing-disallowed-text-tip", "המכשיר המרוחק ×ינו מ×פשר הדפסה מרחוק"), + ("save-settings-tip", "שמור ×ת ההגדרות להב×"), + ("dont-show-again-tip", "×ל תציג שוב"), + ("Take screenshot", "×¦×œ× ×¦×™×œ×•× ×ž×¡×š"), + ("Taking screenshot", "×ž×¦×œ× ×¦×™×œ×•× ×ž×¡×š"), + ("screenshot-merged-screen-not-supported-tip", "×¦×™×œ×•× ×ž×¡×š משולב מכל ×”×ž×¡×›×™× ×ינו נתמך"), + ("screenshot-action-tip", "בחר פעולה ל×חר ×¦×™×œ×•× ×”×ž×¡×š"), + ("Save as", "שמור בש×"), + ("Copy to clipboard", "העתק ללוח"), + ("Enable remote printer", "×פשר מדפסת מרוחקת"), + ("Downloading {}", "מוריד ×ת {}"), + ("{} Update", "עדכון {}"), + ("{}-to-update-tip", "קיימת גרסה חדשה של {} – מומלץ לעדכן"), + ("download-new-version-failed-tip", "נכשל בהורדת הגרסה החדשה"), + ("Auto update", "עדכון ×וטומטי"), + ("update-failed-check-msi-tip", "העדכון נכשל – בדוק ×× ×§×•×‘×¥ ×”Ö¾MSI מותקן ×ו הוסר"), + ("websocket_tip", "×פשר שימוש בפרוטוקול WebSocket לחיבורי×"), + ("Use WebSocket", "השתמש ב־WebSocket"), + ("Trackpad speed", "מהירות משטח מגע"), + ("Default trackpad speed", "מהירות ברירת מחדל של משטח מגע"), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "הצג מצלמה"), + ("Enable camera", "הפעל מצלמה"), + ("No cameras", "×ין מצלמות"), + ("view_camera_unsupported_tip", "הצגת מצלמה ××™× ×” נתמכת במכשיר המרוחק"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hr.rs b/src/lang/hr.rs index ba4723b8a2f..8339b16f2d0 100644 --- a/src/lang/hr.rs +++ b/src/lang/hr.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "duljina %min% do %max%"), ("starts with a letter", "PoÄinje slovom"), ("allowed characters", "DopuÅ¡teni znakovi"), - ("id_change_tip", "DopuÅ¡teni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Duljina je od 6 do 16."), + ("id_change_tip", "DopuÅ¡teni su samo a-z, A-Z, 0-9, - (dash) i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Duljina je od 6 do 16."), ("Website", "Web stranica"), ("About", "O programu"), ("Slogan_tip", "Stvoren srcem u ovom kaotiÄnom svijetu!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Lozinka OS-a"), ("install_tip", "Zbog UAC-a RustDesk ne može u nekim sluÄajevima raditi pravilno. Da biste prevaziÅ¡li UAC, kliknite na tipku ispod da instalirate RustDesk na sustav."), ("Click to upgrade", "Klik za nadogradnju"), - ("Click to download", "Klik za preuzimanje"), - ("Click to update", "Klik za ažuriranje"), ("Configure", "Konfiguracija"), ("config_acc", "Da biste daljinski kontrolirali radnu povrÅ¡inu, RustDesk-u trebate dodijeliti prava za \"PristupaÄnost\"."), ("config_screen", "Da biste daljinski pristupili radnoj povrÅ¡ini, RustDesk-u trebate dodijeliti prava za \"Snimanje zaslona\"."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nemate pravo prijenosa datoteka"), ("Note", "BiljeÅ¡ka"), ("Connection", "Povezivanje"), - ("Share Screen", "Podijeli zaslon"), + ("Share screen", "Podijeli zaslon"), ("Chat", "Dopisivanje"), ("Total", "Ukupno"), ("items", "stavki"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Snimanje zaslona"), ("Input Control", "Kontrola unosa"), ("Audio Capture", "Snimanje zvuka"), - ("File Connection", "Spajanje preko datoteke"), - ("Screen Connection", "Podijelite vezu"), ("Do you accept?", "Prihvaćate li?"), ("Open System Setting", "Postavke otvorenog sustava"), ("How to get Android input permission?", "Kako dobiti pristup za unos na Androidu?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "JoÅ¡ nemate nijednog omiljenog partnera?\nPronaÄ‘ite nekoga s kim se možete povezati i dodajte ga u svoje favorite!"), ("empty_lan_tip", "Ali ne, izgleda da joÅ¡ nismo otkrili niti jednu drugu stranu."), ("empty_address_book_tip", "Izgleda da trenutno nemate nijednog kolege navedenog u svom imeniku."), - ("eg: admin", "napr. admin"), ("Empty Username", "Prazno korisniÄko ime"), ("Empty Password", "Prazna lozinka"), ("Me", "Ja"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Molimo ažurirajte RustDesk klijent na verziju {} ili noviju na udaljenoj strani!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Pregled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 8180b9f5f30..df0f716f6e3 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -2,93 +2,93 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Ãllapot"), - ("Your Desktop", "Saját asztal"), + ("Your Desktop", "Saját számítógép"), ("desk_tip", "A számítógép ezzel a jelszóval és azonosítóval érhetÅ‘ el távolról."), ("Password", "Jelszó"), ("Ready", "Kész"), ("Established", "Létrejött"), - ("connecting_status", "Csatlakozás folyamatban..."), + ("connecting_status", "Kapcsolódás folyamatban…"), ("Enable service", "Szolgáltatás engedélyezése"), ("Start service", "Szolgáltatás indítása"), ("Service is running", "Szolgáltatás aktív"), ("Service is not running", "Szolgáltatás inaktív"), - ("not_ready_status", "Kapcsolódási hiba. Kérlek ellenÅ‘rizze a hálózati beállításokat."), + ("not_ready_status", "Kapcsolódási hiba. EllenÅ‘rizze a hálózati beállításokat."), ("Control Remote Desktop", "Távoli számítógép vezérlése"), ("Transfer file", "Fájlátvitel"), - ("Connect", "Csatlakozás"), - ("Recent sessions", "Legutóbbi munkamanetek"), + ("Connect", "Kapcsolódás"), + ("Recent sessions", "Legutóbbi munkamenetek"), ("Address book", "Címjegyzék"), ("Confirmation", "MegerÅ‘sítés"), - ("TCP tunneling", "TCP alagútépítés"), - ("Remove", "Eltávolít"), + ("TCP tunneling", "TCP-alagút"), + ("Remove", "Eltávolítás"), ("Refresh random password", "Új véletlenszerű jelszó"), ("Set your own password", "Saját jelszó beállítása"), ("Enable keyboard/mouse", "Billentyűzet/egér engedélyezése"), ("Enable clipboard", "Megosztott vágólap engedélyezése"), ("Enable file transfer", "Fájlátvitel engedélyezése"), - ("Enable TCP tunneling", "TCP alagútépítés engedélyezése"), + ("Enable TCP tunneling", "TCP-alagút engedélyezése"), ("IP Whitelisting", "IP engedélyezési lista"), - ("ID/Relay Server", "ID/Továbbító szerver"), - ("Import server config", "Szerver konfiguráció importálása"), - ("Export Server Config", "Szerver konfiguráció exportálása"), - ("Import server configuration successfully", "Szerver konfiguráció sikeresen importálva"), - ("Export server configuration successfully", "Szerver konfiguráció sikeresen exportálva"), - ("Invalid server configuration", "Érvénytelen szerver konfiguráció"), + ("ID/Relay Server", "ID/Továbbító-kiszolgáló"), + ("Import server config", "Kiszolgáló-konfiguráció importálása"), + ("Export Server Config", "Kiszolgáló-konfiguráció exportálása"), + ("Import server configuration successfully", "Kiszolgáló-konfiguráció sikeresen importálva"), + ("Export server configuration successfully", "Kiszolgáló-konfiguráció sikeresen exportálva"), + ("Invalid server configuration", "Érvénytelen kiszolgáló-konfiguráció"), ("Clipboard is empty", "A vágólap üres"), ("Stop service", "Szolgáltatás leállítása"), ("Change ID", "Azonosító megváltoztatása"), - ("Your new ID", "Az új azonosítód"), + ("Your new ID", "Az új azonosítója"), ("length %min% to %max%", "hossz %min% és %max% között"), ("starts with a letter", "betűvel kezdÅ‘dik"), ("allowed characters", "engedélyezett karakterek"), - ("id_change_tip", "Csak a-z, A-Z, 0-9 csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az elsÅ‘ karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), + ("id_change_tip", "Csak a-z, A-Z, 0-9, - (kötÅ‘jel) csoportokba tartozó karakterek, illetve a _ karakter van engedélyezve. Az elsÅ‘ karakternek mindenképpen a-z, A-Z csoportokba kell esnie. Az azonosító hosszúsága 6-tól, 16 karakter."), ("Website", "Webhely"), - ("About", "Rólunk"), + ("About", "Névjegy"), ("Slogan_tip", "Szenvedéllyel programozva - egy káoszba süllyedÅ‘ világban!"), ("Privacy Statement", "Adatvédelmi nyilatkozat"), ("Mute", "Némítás"), - ("Build Date", "Build ideje"), + ("Build Date", "Összeállítás ideje"), ("Version", "Verzió"), ("Home", "KezdÅ‘képernyÅ‘"), ("Audio Input", "Hangátvitel"), ("Enhancements", "Fejlesztések"), - ("Hardware Codec", "Hardveres kódek"), + ("Hardware Codec", "Hardveres kodek"), ("Adaptive bitrate", "Adaptív bitráta"), - ("ID Server", "ID szerver"), - ("Relay Server", "Továbbító szerver"), - ("API Server", "API szerver"), + ("ID Server", "ID kiszolgáló"), + ("Relay Server", "Továbbító-kiszolgáló"), + ("API Server", "API kiszolgáló"), ("invalid_http", "A címnek mindenképpen http(s)://-el kell kezdÅ‘dnie."), - ("Invalid IP", "A megadott IP cím helytelen."), + ("Invalid IP", "A megadott IP-cím érvénytelen"), ("Invalid format", "Érvénytelen formátum"), - ("server_not_support", "Nem támogatott a szerver által"), - ("Not available", "Nem elérhetÅ‘"), + ("server_not_support", "A kiszolgáló nem támogatja"), + ("Not available", "Nem érhetÅ‘ el"), ("Too frequent", "Túl gyakori"), - ("Cancel", "Mégsem"), + ("Cancel", "Mégse"), ("Skip", "Kihagyás"), ("Close", "Bezárás"), ("Retry", "Újra"), ("OK", "OK"), - ("Password Required", "Jelszó megadása kötelezÅ‘"), - ("Please enter your password", "Kérem írja be a jelszavát"), + ("Password Required", "A jelszó megadása kötelezÅ‘"), + ("Please enter your password", "Adja meg a jelszavát"), ("Remember password", "Jelszó megjegyzése"), ("Wrong Password", "Hibás jelszó"), ("Do you want to enter again?", "Szeretne újra belépni?"), - ("Connection Error", "Csatlakozási hiba"), + ("Connection Error", "Kapcsolódási hiba"), ("Error", "Hiba"), - ("Reset by the peer", "A kapcsolatot alaphelyzetbe állt"), - ("Connecting...", "Csatlakozás..."), - ("Connection in progress. Please wait.", "Csatlakozás folyamatban. Kérem várjon."), - ("Please try 1 minute later", "Kérem próbálja meg 1 perc múlva"), + ("Reset by the peer", "A kapcsolatot a másik fél lezárta."), + ("Connecting...", "Kapcsolódás…"), + ("Connection in progress. Please wait.", "A kapcsolódás folyamatban van. Kis türelmet…"), + ("Please try 1 minute later", "Próbálja meg 1 perc múlva"), ("Login Error", "Bejelentkezési hiba"), ("Successful", "Sikeres"), - ("Connected, waiting for image...", "Csatlakozva, várakozás a kép adatokra..."), + ("Connected, waiting for image...", "Kapcsolódva, várakozás a képadatokra…"), ("Name", "Név"), ("Type", "Típus"), ("Modified", "Módosított"), ("Size", "Méret"), - ("Show Hidden Files", "Rejtett fájlok mutatása"), - ("Receive", "Fogad"), - ("Send", "Küld"), + ("Show Hidden Files", "Rejtett fájlok megjelenítése"), + ("Receive", "Fogadás"), + ("Send", "Küldés"), ("Refresh File", "Fájl frissítése"), ("Local", "Helyi"), ("Remote", "Távoli"), @@ -99,21 +99,21 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Properties", "Tulajdonságok"), ("Multi Select", "Többszörös kijelölés"), ("Select All", "Összes kijelölése"), - ("Unselect All", "Kijelölések megszűntetése"), + ("Unselect All", "Kijelölések megszüntetése"), ("Empty Directory", "Üres könyvtár"), ("Not an empty directory", "Nem egy üres könyvtár"), ("Are you sure you want to delete this file?", "Biztosan törli ezt a fájlt?"), ("Are you sure you want to delete this empty directory?", "Biztosan törli ezt az üres könyvtárat?"), - ("Are you sure you want to delete the file of this directory?", "Biztos benne, hogy törölni szeretné a könyvtár tartalmát?"), + ("Are you sure you want to delete the file of this directory?", "Biztosan törli a könyvtár tartalmát?"), ("Do this for all conflicts", "Tegye ezt minden ütközéskor"), - ("This is irreversible!", "Ez a folyamat visszafordíthatatlan!"), + ("This is irreversible!", "Ez a művelet nem vonható vissza!"), ("Deleting", "Törlés folyamatban"), ("files", "fájlok"), ("Waiting", "Várakozás"), ("Finished", "Befejezve"), ("Speed", "Sebesség"), - ("Custom Image Quality", "Egyedi képminÅ‘ség"), - ("Privacy mode", "Inkognító mód"), + ("Custom Image Quality", "Egyéni képminÅ‘ség"), + ("Privacy mode", "Inkognitó mód"), ("Block user input", "Felhasználói bevitel letiltása"), ("Unblock user input", "Felhasználói bevitel engedélyezése"), ("Adjust Window", "Ablakméret beállítása"), @@ -125,46 +125,44 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Good image quality", "Eredetihez hű"), ("Balanced", "Kiegyensúlyozott"), ("Optimize reaction time", "Gyorsan reagáló"), - ("Custom", "Egyedi"), + ("Custom", "Egyéni"), ("Show remote cursor", "Távoli kurzor megjelenítése"), - ("Show quality monitor", "MinÅ‘ségi monitor megjelenítése"), + ("Show quality monitor", "KijelzÅ‘ minÅ‘ségének ellenÅ‘rzése"), ("Disable clipboard", "Közös vágólap kikapcsolása"), ("Lock after session end", "Távoli fiók zárolása a munkamenet végén"), ("Insert Ctrl + Alt + Del", "Illessze be a Ctrl + Alt + Del"), ("Insert Lock", "Távoli fiók zárolása"), ("Refresh", "Frissítés"), ("ID does not exist", "Az azonosító nem létezik"), - ("Failed to connect to rendezvous server", "Nem sikerült csatlakozni a kiszolgáló szerverhez"), - ("Please try later", "Kérjük, próbálja késÅ‘bb"), + ("Failed to connect to rendezvous server", "Nem sikerült kapcsolódni a kiszolgálóhoz"), + ("Please try later", "Próbálja meg késÅ‘bb"), ("Remote desktop is offline", "A távoli számítógép offline állapotban van"), ("Key mismatch", "Eltérés a kulcsokban"), ("Timeout", "IdÅ‘túllépés"), - ("Failed to connect to relay server", "Nem sikerült csatlakozni a közvetítÅ‘ szerverhez"), - ("Failed to connect via rendezvous server", "Nem sikerült csatlakozni a kiszolgáló szerveren keresztül"), - ("Failed to connect via relay server", "Nem sikerült csatlakozni a közvetítÅ‘ szerveren keresztül"), + ("Failed to connect to relay server", "Nem sikerült kapcsolódni a továbbító-kiszolgálóhoz"), + ("Failed to connect via rendezvous server", "Nem sikerült kapcsolódni a kiszolgálón keresztül"), + ("Failed to connect via relay server", "Nem sikerült kapcsolódni a továbbító-kiszolgálón keresztül"), ("Failed to make direct connection to remote desktop", "Nem sikerült közvetlen kapcsolatot létesíteni a távoli számítógéppel"), ("Set Password", "Jelszó beállítása"), ("OS Password", "Operációs rendszer jelszavának beállítása"), - ("install_tip", "ElÅ‘fordul, hogy bizonyos esetekben hiba léphet fel a Portable verzió használata során. A megfelelÅ‘ működés érdekében, kérem telepítse a RustDesk alkalmazást a számítógépre."), + ("install_tip", "ElÅ‘fordul, hogy bizonyos esetekben hiba léphet fel a Portable verzió használatakor. A megfelelÅ‘ működés érdekében, telepítse a RustDesk alkalmazást a számítógépére."), ("Click to upgrade", "Kattintson ide a frissítés telepítéséhez"), - ("Click to download", "Kattintson ide a letöltéshez"), - ("Click to update", "Kattintson ide a frissítés letöltéséhez"), ("Configure", "Beállítás"), - ("config_acc", "A távoli vezérléshez a RustDesk-nek \"KisegítÅ‘ lehetÅ‘ség\" engedélyre van szüksége"), - ("config_screen", "A távoli vezérléshez szükséges a \"KépernyÅ‘felvétel\" engedély megadása"), - ("Installing ...", "Telepítés..."), - ("Install", "Telepítsd"), + ("config_acc", "A számítógép távoli vezérléséhez a RustDesknek hozzáférési jogokat kell biztosítania."), + ("config_screen", "Ahhoz, hogy távolról hozzáférhessen számítógépéhez, meg kell adnia a RustDesknek a \"KépernyÅ‘felvétel\" jogosultságot."), + ("Installing ...", "Telepítés…"), + ("Install", "Telepítés"), ("Installation", "Telepítés"), ("Installation Path", "Telepítési útvonal"), ("Create start menu shortcuts", "Start menü parancsikonok létrehozása"), ("Create desktop icon", "Ikon létrehozása az asztalon"), ("agreement_tip", "A telepítés folytatásával automatikusan elfogadásra kerül a licensz szerzÅ‘dés."), ("Accept and Install", "Elfogadás és telepítés"), - ("End-user license agreement", "Felhasználói licensz szerzÅ‘dés"), - ("Generating ...", "Létrehozás..."), + ("End-user license agreement", "Végfelhasználói licensz szerzÅ‘dés"), + ("Generating ...", "Létrehozás…"), ("Your installation is lower version.", "A telepített verzió alacsonyabb."), - ("not_close_tcp_tip", "Ne zárja be ezt az ablakot miközben a tunnelt használja"), - ("Listening ...", "Figyelés..."), + ("not_close_tcp_tip", "Ne zárja be ezt az ablakot, amíg TCP-alagutat használ"), + ("Listening ...", "Figyelés…"), ("Remote Host", "Távoli kiszolgáló"), ("Remote Port", "Távoli port"), ("Action", "Indítás"), @@ -172,7 +170,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Helyi port"), ("Local Address", "Helyi cím"), ("Change Local Port", "Helyi port megváltoztatása"), - ("setup_server_tip", "Gyorsabb kapcsolat érdekében, hozzon létre saját szervert"), + ("setup_server_tip", "Gyorsabb kapcsolat érdekében, hozzon létre saját kiszolgálót"), ("Too short, at least 6 characters.", "Túl rövid, legalább 6 karakter."), ("The confirmation is not identical.", "A megerÅ‘sítés nem volt azonos"), ("Permissions", "Engedélyek"), @@ -180,14 +178,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Dismiss", "Elutasítás"), ("Disconnect", "Kapcsolat bontása"), ("Enable file copy and paste", "Fájlok másolásának és beillesztésének engedélyezése"), - ("Connected", "Csatlakozva"), + ("Connected", "Kapcsolódva"), ("Direct and encrypted connection", "Közvetlen, és titkosított kapcsolat"), ("Relayed and encrypted connection", "Továbbított, és titkosított kapcsolat"), ("Direct and unencrypted connection", "Közvetlen, és nem titkosított kapcsolat"), ("Relayed and unencrypted connection", "Továbbított, és nem titkosított kapcsolat"), ("Enter Remote ID", "Távoli számítógép azonosítója"), - ("Enter your password", "Ãrja be a jelszavát"), - ("Logging in...", "A belépés folyamatban..."), + ("Enter your password", "Adja meg a jelszavát"), + ("Logging in...", "Belépés folyamatban…"), ("Enable RDP session sharing", "RDP-munkamenet-megosztás engedélyezése"), ("Auto Login", "Automatikus bejelentkezés"), ("Enable direct IP access", "Közvetlen IP-elérés engedélyezése"), @@ -196,32 +194,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Create desktop shortcut", "Asztali parancsikon létrehozása"), ("Change Path", "Elérési út módosítása"), ("Create Folder", "Mappa létrehozás"), - ("Please enter the folder name", "Kérjük, adja meg a mappa nevét"), + ("Please enter the folder name", "Adja meg a mappa nevét"), ("Fix it", "Javítás"), ("Warning", "Figyelmeztetés"), ("Login screen using Wayland is not supported", "Bejelentkezéskori Wayland használata nem támogatott"), ("Reboot required", "Újraindítás szükséges"), - ("Unsupported display server", "Nem támogatott megjelenítÅ‘ szerver"), + ("Unsupported display server", "Nem támogatott megjelenítÅ‘ kiszolgáló"), ("x11 expected", "x11-re számítottt"), ("Port", "Port"), ("Settings", "Beállítások"), ("Username", "Felhasználónév"), ("Invalid port", "Érvénytelen port"), - ("Closed manually by the peer", "A kapcsolatot a másik fél manuálisan bezárta"), - ("Enable remote configuration modification", "Távoli konfiguráció módosítás engedélyezése"), - ("Run without install", "Futtatás feltelepítés nélkül"), - ("Connect via relay", "Csatlakozás közvetítÅ‘n keresztül"), - ("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"), + ("Closed manually by the peer", "A kapcsolatot a másik fél kézileg bezárta"), + ("Enable remote configuration modification", "Távoli konfiguráció-módosítás engedélyezése"), + ("Run without install", "Futtatás telepítés nélkül"), + ("Connect via relay", "Kapcsolódás továbbító-kiszolgálón keresztül"), + ("Always connect via relay", "Kapcsolódás mindig továbbító-kiszolgálón keresztül"), + ("whitelist_tip", "Csak az engedélyezési listán szereplÅ‘ címek kapcsolódhatnak"), ("Login", "Belépés"), ("Verify", "EllenÅ‘rzés"), - ("Remember me", "Emlékezz rám"), - ("Trust this device", "Bízzon ebben az eszközben"), + ("Remember me", "Emlékezzen rám"), + ("Trust this device", "Megbízom ebben az eszközben"), ("Verification code", "EllenÅ‘rzÅ‘ kód"), - ("verification_tip", "A regisztrált e-mail címre egy ellenÅ‘rzÅ‘ kódot küldtek. Adja meg az ellenÅ‘rzÅ‘ kódot az újbóli bejelentkezéshez."), + ("verification_tip", "A regisztrált e-mail-címre egy ellenÅ‘rzÅ‘ kód lesz elküldve. Adja meg az ellenÅ‘rzÅ‘ kódot az újbóli bejelentkezéshez."), ("Logout", "Kilépés"), ("Tags", "Címkék"), - ("Search ID", "Azonosító keresése..."), + ("Search ID", "Azonosító keresése…"), ("whitelist_sep", "A címeket veszÅ‘vel, pontosvesszÅ‘vel, szóközzel, vagy új sorral válassza el"), ("Add ID", "Azonosító hozzáadása"), ("Add Tag", "Címke hozzáadása"), @@ -230,9 +228,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Üres felhasználónév"), ("Password missed", "Üres jelszó"), ("Wrong credentials", "Hibás felhasználónév vagy jelszó"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "A hitelesítÅ‘kód érvénytelen vagy lejárt"), ("Edit Tag", "Címke szerkesztése"), - ("Forget Password", "A jelszó megjegyzésének törlése"), + ("Forget Password", "A jelszó megjegyzésének megszüntetése"), ("Favorites", "Kedvencek"), ("Add to Favorites", "Hozzáadás a kedvencekhez"), ("Remove from Favorites", "Eltávolítás a kedvencekbÅ‘l"), @@ -244,9 +242,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("install_daemon_tip", "Az automatikus indításhoz szükséges a szolgáltatás telepítése"), ("Remote ID", "Távoli azonosító"), ("Paste", "Beillesztés"), - ("Paste here?", "Beilleszti ide?"), - ("Are you sure to close the connection?", "Biztos, hogy bezárja a kapcsolatot?"), - ("Download new version", "Új verzó letöltése"), + ("Paste here?", "Beillesztés ide?"), + ("Are you sure to close the connection?", "Biztosan bezárja a kapcsolatot?"), + ("Download new version", "Új verzió letöltése"), ("Touch mode", "Érintési mód bekapcsolása"), ("Mouse mode", "Egérhasználati mód bekapcsolása"), ("One-Finger Tap", "Egyujjas érintés"), @@ -259,56 +257,54 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Mouse Drag", "Mozgatás egérrel"), ("Three-Finger vertically", "Három ujj függÅ‘legesen"), ("Mouse Wheel", "EgérgörgÅ‘"), - ("Two-Finger Move", "Kátujjas mozgatás"), + ("Two-Finger Move", "Kétujjas mozgatás"), ("Canvas Move", "Nézet mozgatása"), ("Pinch to Zoom", "Kétujjas nagyítás"), ("Canvas Zoom", "Nézet nagyítása"), ("Reset canvas", "Nézet visszaállítása"), ("No permission of file transfer", "Nincs engedély a fájlátvitelre"), - ("Note", "Megyjegyzés"), + ("Note", "Megjegyzés"), ("Connection", "Kapcsolat"), - ("Share Screen", "KépernyÅ‘megosztás"), + ("Share screen", "KépernyÅ‘megosztás"), ("Chat", "Csevegés"), ("Total", "Összes"), ("items", "elemek"), - ("Selected", "Kijelölt"), + ("Selected", "Kijelölve"), ("Screen Capture", "KépernyÅ‘rögzítés"), ("Input Control", "Távoli vezérlés"), ("Audio Capture", "Hangrögzítés"), - ("File Connection", "Fájlátvitel"), - ("Screen Connection", "Képátvitel"), - ("Do you accept?", "Elfogadja?"), + ("Do you accept?", "Elfogadás?"), ("Open System Setting", "Rendszerbeállítások megnyitása"), - ("How to get Android input permission?", "Hogyan állíthatok be Android beviteli engedélyt?"), - ("android_input_permission_tip1", "A távoli vezérléshez kérjük engedélyezze a \"KisegítÅ‘ lehetÅ‘ség\" lehetÅ‘séget."), + ("How to get Android input permission?", "Hogyan állítható be az Androidos beviteli engedély?"), + ("android_input_permission_tip1", "Ahhoz, hogy egy távoli eszköz vezérelhesse Android készülékét, engedélyeznie kell a RustDesk számára a \"HozzáférhetÅ‘ség\" szolgáltatás használatát."), ("android_input_permission_tip2", "A következÅ‘ rendszerbeállítások oldalon a letöltött alkalmazások menüponton belül, kapcsolja be a [RustDesk Input] szolgáltatást."), - ("android_new_connection_tip", "Új kérés érkezett mely vezérelni szeretné az eszközét"), - ("android_service_will_start_tip", "A \"KépernyÅ‘rögzítés\" bekapcsolásával automatikus elindul a szolgáltatás, lehetÅ‘vé téve, hogy más eszközök csatlakozási kérelmet küldhessenek"), + ("android_new_connection_tip", "Új kérés érkezett, mely vezérelni szeretné az eszközét"), + ("android_service_will_start_tip", "A képernyÅ‘megosztás aktiválása automatikusan elindítja a szolgáltatást, így más eszközök is vezérelhetik ezt az Android-eszközt."), ("android_stop_service_tip", "A szolgáltatás leállítása automatikusan szétkapcsol minden létezÅ‘ kapcsolatot."), ("android_version_audio_tip", "A jelenlegi Android verzió nem támogatja a hangrögzítést, frissítsen legalább Android 10-re, vagy egy újabb verzióra."), - ("android_start_service_tip", "A képernyÅ‘megosztó szolgáltatás elindításához koppintson a \" KözvetítÅ‘ szolgáltatás indítása\" gombra, vagy aktiválja a \"KépernyÅ‘felvétel\" engedélyt."), - ("android_permission_may_not_change_tip", "A meglévÅ‘ kapcsolatok engedélyei csak új csatlakozás után módosulnak."), + ("android_start_service_tip", "A képernyÅ‘megosztó szolgáltatás elindításához koppintson a \"Kapcsolási szolgáltatás indítása\" gombra, vagy aktiválja a \"KépernyÅ‘felvétel\" engedélyt."), + ("android_permission_may_not_change_tip", "A meglévÅ‘ kapcsolatok engedélyei csak új kapcsolódás után módosulnak."), ("Account", "Fiók"), ("Overwrite", "Felülírás"), ("This file exists, skip or overwrite this file?", "Ez a fájl már létezik, kihagyja vagy felülírja ezt a fájlt?"), ("Quit", "Kilépés"), - ("Help", "Segítség"), + ("Help", "Súgó"), ("Failed", "Sikertelen"), ("Succeeded", "Sikeres"), ("Someone turns on privacy mode, exit", "Valaki bekacsolta az inkognitó módot, lépjen ki"), ("Unsupported", "Nem támogatott"), - ("Peer denied", "Elutasítva a távoli fél álltal"), - ("Please install plugins", "Kérem telepítse a bÅ‘vítményeket"), + ("Peer denied", "Elutasítva a távoli fél által"), + ("Please install plugins", "Telepítse a bÅ‘vítményeket"), ("Peer exit", "A távoli fél kilépett"), ("Failed to turn off", "Nem sikerült kikapcsolni"), ("Turned off", "Kikapcsolva"), ("Language", "Nyelv"), ("Keep RustDesk background service", "RustDesk futtatása a háttérben"), - ("Ignore Battery Optimizations", "AkkumulátorkímélÅ‘ figyelmen kívűl hagyása"), - ("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállítási oldalára, keresse meg az [AkkumulátorkímélÅ‘] lehetÅ‘séget és válassza a nincs korlátozás lehetÅ‘séget."), + ("Ignore Battery Optimizations", "AkkumulátorkímélÅ‘ figyelmen kívül hagyása"), + ("android_open_battery_optimizations_tip", "Ha le szeretné tiltani ezt a funkciót, lépjen a RustDesk alkalmazás beállításaiba, keresse meg az [AkkumulátorkímélÅ‘] lehetÅ‘séget és válassza a nincs korlátozás lehetÅ‘séget."), ("Start on boot", "Indítás bekapcsoláskor"), ("Start the screen sharing service on boot, requires special permissions", "Indítsa el a képernyÅ‘megosztó szolgáltatást rendszerindításkor, speciális engedélyeket igényel"), - ("Connection not allowed", "A csatlakozás nem engedélyezett"), + ("Connection not allowed", "A kapcsolódás nem engedélyezett"), ("Legacy mode", "Kompatibilitási mód"), ("Map mode", "Hozzárendelési mód"), ("Translate mode", "Fordító mód"), @@ -317,9 +313,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set permanent password", "Ãllandó jelszó beállítása"), ("Enable remote restart", "Távoli újraindítás engedélyezése"), ("Restart remote device", "Távoli eszköz újraindítása"), - ("Are you sure you want to restart", "Biztos szeretné újraindítani?"), - ("Restarting remote device", "Távoli eszköz újraindítása..."), - ("remote_restarting_tip", "A távoli eszköz újraindul, zárja be ezt az üzenetet, csatlakozzon újra, állandó jelszavával"), + ("Are you sure you want to restart", "Biztosan újra szeretné indítani?"), + ("Restarting remote device", "Távoli eszköz újraindítása…"), + ("remote_restarting_tip", "A távoli eszköz újraindul, zárja be ezt az üzenetet, kapcsolódjon újra az állandó jelszavával"), ("Copied", "Másolva"), ("Exit Fullscreen", "Kilépés teljes képernyÅ‘s módból"), ("Fullscreen", "Teljes képernyÅ‘"), @@ -331,9 +327,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Image Quality", "KépminÅ‘ség"), ("Scroll Style", "Görgetési stílus"), ("Show Toolbar", "Eszköztár megjelenítése"), - ("Hide Toolbar", "Eszköztár eljertése"), - ("Direct Connection", "Közvetlen kapcsolat"), - ("Relay Connection", "Közvetett csatlakozás"), + ("Hide Toolbar", "Eszköztár elrejtése"), + ("Direct Connection", "Kapcsolódás közvetlenül"), + ("Relay Connection", "Kapcsolódás továbbító-kiszolgálón keresztül"), ("Secure Connection", "Biztonságos kapcsolat"), ("Insecure Connection", "Nem biztonságos kapcsolat"), ("Scale original", "Eredeti méretarány"), @@ -345,13 +341,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light Theme", "Világos téma"), ("Dark", "Sötét"), ("Light", "Világos"), - ("Follow System", "Rendszer téma követése"), + ("Follow System", "Rendszer beállításainak követése"), ("Enable hardware codec", "Hardveres kodek engedélyezése"), ("Unlock Security Settings", "Biztonsági beállítások feloldása"), ("Enable audio", "Hang engedélyezése"), ("Unlock Network Settings", "Hálózati beállítások feloldása"), - ("Server", "Szerver"), - ("Direct IP Access", "Közvetlen IP hozzáférés"), + ("Server", "Kiszolgáló"), + ("Direct IP Access", "Közvetlen IP-hozzáférés"), ("Proxy", "Proxy"), ("Apply", "Alkalmaz"), ("Disconnect all devices?", "Leválasztja az összes eszközt?"), @@ -369,22 +365,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Start session recording", "Munkamenet rögzítés indítása"), ("Stop session recording", "Munkamenet rögzítés leállítása"), ("Enable recording session", "Munkamenet rögzítés engedélyezése"), - ("Enable LAN discovery", "Felfedezés enegedélyezése"), + ("Enable LAN discovery", "Felfedezés engedélyezése"), ("Deny LAN discovery", "Felfedezés tiltása"), ("Write a message", "Üzenet írása"), ("Prompt", "Kérés"), - ("Please wait for confirmation of UAC...", "Kérjük, várjon az UAC megerÅ‘sítésére..."), + ("Please wait for confirmation of UAC...", "Várjon az UAC megerÅ‘sítésére…"), ("elevated_foreground_window_tip", "A távvezérelt számítógép jelenleg nyitott ablakához magasabb szintű jogok szükségesek. Ezért jelenleg nem lehetséges az egér és a billentyűzet használata. Kérje meg azt a felhasználót, akinek a számítógépét távolról vezérli, hogy minimalizálja az ablakot, vagy növelje a jogokat. A jövÅ‘beni probléma elkerülése érdekében ajánlott a szoftvert a távvezérelt számítógépre telepíteni."), - ("Disconnected", "Szétkapcsolva"), + ("Disconnected", "Kapcsolat bontva"), ("Other", "Egyéb"), - ("Confirm before closing multiple tabs", "Biztos, hogy bezárja az összes lapot?"), + ("Confirm before closing multiple tabs", "Biztosan bezárja az összes lapot?"), ("Keyboard Settings", "Billentyűzet beállítások"), ("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."), - ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "A Wayland a Linux disztró magasabb verzióját igényli. Próbálja ki az X11 desktopot, vagy változtassa meg az operációs rendszert."), + ("Wayland requires Ubuntu 21.04 or higher version.", "A Waylandhez Ubuntu 21.04 vagy újabb verzió szükséges."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "A Wayland a Linux disztribúció magasabb verzióját igényli. Próbálja ki az X11 desktopot, vagy változtassa meg az operációs rendszert."), ("JumpLink", "Hiperhivatkozás"), - ("Please Select the screen to be shared(Operate on the peer side).", "Kérjük, válassza ki a megosztani kívánt képernyÅ‘t."), + ("Please Select the screen to be shared(Operate on the peer side).", "Válassza ki a megosztani kívánt képernyÅ‘t."), ("Show RustDesk", "A RustDesk megjelenítése"), ("This PC", "Ez a számítógép"), ("or", "vagy"), @@ -394,35 +390,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Accept sessions via password", "Munkamenetek elfogadása jelszóval"), ("Accept sessions via click", "Munkamenetek elfogadása kattintással"), ("Accept sessions via both", "Munkamenetek fogadása mindkettÅ‘n keresztül"), - ("Please wait for the remote side to accept your session request...", "Kérjük, várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét..."), + ("Please wait for the remote side to accept your session request...", "Várjon, amíg a távoli oldal elfogadja a munkamenet-kérelmét…"), ("One-time Password", "Egyszer használatos jelszó"), ("Use one-time password", "Használjon ideiglenes jelszót"), ("One-time password length", "Egyszer használatos jelszó hossza"), ("Request access to your device", "Hozzáférés kérése az eszközéhez"), ("Hide connection management window", "KapcsolatkezelÅ‘ ablak elrejtése"), ("hide_cm_tip", "Ez csak akkor lehetséges, ha a hozzáférés állandó jelszóval történik."), - ("wayland_experiment_tip", "A Wayland-támogatás csak kísérleti jellegű. Kérjük, használja az X11-et, ha felügyelet nélküli hozzáférésre van szüksége."), + ("wayland_experiment_tip", "A Wayland-támogatás csak kísérleti jellegű. Használja az X11-et, ha felügyelet nélküli hozzáférésre van szüksége."), ("Right click to select tabs", "Jobb klikk a lapok kiválasztásához"), ("Skipped", "Kihagyott"), ("Add to address book", "Hozzáadás a címjegyzékhez"), ("Group", "Csoport"), ("Search", "Keresés"), - ("Closed manually by web console", "Kézzel zárva a webkonzolon keresztül"), + ("Closed manually by web console", "Kézzel bezárva a webkonzolon keresztül"), ("Local keyboard type", "Helyi billentyűzet típusa"), ("Select local keyboard type", "Helyi billentyűzet típusának kiválasztása"), ("software_render_tip", "Ha Nvidia grafikus kártyát használ Linux alatt, és a távoli ablak a kapcsolat létrehozása után azonnal bezáródik, akkor a Nouveau nyílt forráskódú illesztÅ‘programra való váltás és a szoftveres renderelés használata segíthet. A szoftvert újra kell indítani."), ("Always use software rendering", "Mindig szoftveres renderelést használjon"), - ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \" Bemenet figyelése\" jogosultságot."), + ("config_input", "Ahhoz, hogy a távoli asztalt a billentyűzettel vezérelhesse, a RustDesknek meg kell adnia a \"Bemenet figyelése\" jogosultságot."), ("config_microphone", "Ahhoz, hogy távolról beszélhessen, meg kell adnia a RustDesknek a \"Hangfelvétel\" jogosultságot."), ("request_elevation_tip", "Akkor is kérhet megnövelt jogokat, ha valaki a partneroldalon van."), ("Wait", "Várjon"), - ("Elevation Error", "Emeltszintű hozzáférési hiba"), + ("Elevation Error", "Emelt szintű hozzáférési hiba"), ("Ask the remote user for authentication", "Hitelesítés kérése a távoli felhasználótól"), - ("Choose this if the remote account is administrator", "Válassza ezt, ha a távoli fiók rendszergazda"), + ("Choose this if the remote account is administrator", "Akkor válassza ezt, ha a távoli fiók rendszergazda"), ("Transmit the username and password of administrator", "Küldje el a rendszergazda felhasználónevét és jelszavát"), - ("still_click_uac_tip", "A távoli felhasználónak továbbra is a \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), + ("still_click_uac_tip", "A távoli felhasználónak továbbra is az \"Igen\" gombra kell kattintania a RustDesk UAC ablakában. Kattintson!"), ("Request Elevation", "Emelt szintű jogok igénylése"), - ("wait_accept_uac_tip", "Kérjük, várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), + ("wait_accept_uac_tip", "Várjon, amíg a távoli felhasználó elfogadja az UAC párbeszédet."), ("Elevate successfully", "Emelt szintű jogok megadva"), ("uppercase", "NAGYBETŰS"), ("lowercase", "kisbetűs"), @@ -433,12 +429,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Közepes"), ("Strong", "ErÅ‘s"), ("Switch Sides", "Oldalváltás"), - ("Please confirm if you want to share your desktop?", "Kérjük, erÅ‘sítse meg, hogy meg akarja-e osztani az asztalát?"), + ("Please confirm if you want to share your desktop?", "ErÅ‘sítse meg, hogy meg akarja-e osztani az asztalát?"), ("Display", "KépernyÅ‘"), ("Default View Style", "Alapértelmezett megjelenítés"), ("Default Scroll Style", "Alapértelmezett görgetés"), ("Default Image Quality", "Alapértelmezett képminÅ‘ség"), - ("Default Codec", "Alapértelmezett kódek"), + ("Default Codec", "Alapértelmezett kodek"), ("Bitrate", "Bitsebesség"), ("FPS", "FPS"), ("Auto", "Automatikus"), @@ -446,9 +442,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Hanghívás"), ("Text chat", "Szöveges csevegés"), ("Stop voice call", "Hanghívás leállítása"), - ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy relé-kiszolgálón keresztül.\nHa az elsÅ‘ próbálkozáskor relé-kapcsolatot szeretne létrehozni, használhatja a \"/r\" utótagot. az azonosítóhoz vagy a \"Mindig relé-kiszolgálón keresztül csatlakozom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), - ("Reconnect", "Újracsatlakoztatás"), - ("Codec", "Kódek"), + ("relay_hint_tip", "Ha a közvetlen kapcsolat nem lehetséges, megpróbálhat kapcsolatot létesíteni egy továbbító-kiszolgálón keresztül.\nHa az elsÅ‘ próbálkozáskor továbbító-kiszolgálón keresztüli kapcsolatot szeretne létrehozni, használhatja az \"/r\" utótagot. az azonosítóhoz vagy a \"Mindig továbbító-kiszolgálón keresztül kapcsolódom\" opcióhoz a legutóbbi munkamenetek listájában, ha van ilyen."), + ("Reconnect", "Újrakapcsolódás"), + ("Codec", "Kodek"), ("Resolution", "Felbontás"), ("No transfers in progress", "Nincs folyamatban átvitel"), ("Set one-time password length", "Ãllítsa be az egyszeri jelszó hosszát"), @@ -459,14 +455,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Minimalizálás"), ("Maximize", "Maximalizálás"), ("Your Device", "Az Ön eszköze"), - ("empty_recent_tip", "Hoppá, nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."), - ("empty_favorite_tip", "Még nincs kedvenc távoli állomás?\nHagyd, hogy találjunk valakit, akivel kapcsolatba tudunk lépni, és add hozzá a kedvenceidhez!"), - ("empty_lan_tip", "Ó, nem, úgy tűnik, még nem fedeztünk fel egy távoli helyszínt."), - ("empty_address_book_tip", "Ó, kedvesem, úgy tűnik, hogy jelenleg nincsenek távoli állomások a címjegyzékében."), - ("eg: admin", "pl: admin"), + ("empty_recent_tip", "Nincsenek aktuális munkamenetek!\nIdeje ütemezni egy újat."), + ("empty_favorite_tip", "Még nincs kedvenc távoli állomása?\nHagyja, hogy találjunk valakit, akivel kapcsolatba tud lépni, és add hozzá a kedvenceidhez!"), + ("empty_lan_tip", "Úgy tűnik, még nem adott hozzá egyetlen távoli helyszínt sem."), + ("empty_address_book_tip", "Úgy tűnik, hogy jelenleg nincsenek távoli állomások a címjegyzékében."), ("Empty Username", "Üres felhasználónév"), ("Empty Password", "Üres jelszó"), - ("Me", "Én"), + ("Me", "Ön"), ("identical_file_tip", "Ez a fájl megegyezik a távoli állomás fájljával."), ("show_monitors_tip", "KépernyÅ‘k megjelenítése az eszköztáron"), ("View Mode", "Nézet mód"), @@ -478,11 +473,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("another_user_login_title_tip", "Egy másik felhasználó már bejelentkezett."), ("another_user_login_text_tip", "Különálló"), ("xorg_not_found_title_tip", "Xorg nem található."), - ("xorg_not_found_text_tip", "Kérjük, telepítse az Xorg-ot."), + ("xorg_not_found_text_tip", "Telepítse az Xorgot."), ("no_desktop_title_tip", "Nem áll rendelkezésre asztali környezet."), - ("no_desktop_text_tip", "Kérjük, telepítse a GNOME asztalt."), - ("No need to elevate", ""), - ("System Sound", "A jogok növelése nem szükséges"), + ("no_desktop_text_tip", "Telepítse a GNOME asztali környezetet."), + ("No need to elevate", "Nem szükséges megemelni"), + ("System Sound", "Rendszer hangok"), ("Default", "Alapértelmezett"), ("New RDP", "Új RDP"), ("Fingerprint", "Ujjlenyomat"), @@ -498,16 +493,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Options", "Beállítások"), ("resolution_original_tip", "Eredeti felbontás"), ("resolution_fit_local_tip", "Helyi felbontás beállítása"), - ("resolution_custom_tip", "Testreszabható felbontás"), + ("resolution_custom_tip", "Testre szabható felbontás"), ("Collapse toolbar", "Eszköztár összecsukása"), - ("Accept and Elevate", "Elfogadás és magasabb szintű jogrosultságra emelés"), + ("Accept and Elevate", "Elfogadás és magasabb szintű jogosultságra emelés"), ("accept_and_elevate_btn_tooltip", "Fogadja el a kapcsolatot, és növelje az UAC-engedélyeket."), ("clipboard_wait_response_timeout_tip", "IdÅ‘túllépés, amíg a másolat válaszára vár."), ("Incoming connection", "BejövÅ‘ kapcsolat"), ("Outgoing connection", "KimenÅ‘ kapcsolat"), ("Exit", "Kilépés"), ("Open", "Megnyitás"), - ("logout_tip", "Biztosan le szeretne iratkozni?"), + ("logout_tip", "Biztosan ki szeretne lépni?"), ("Service", "Szolgáltatás"), ("Start", "Indítás"), ("Stop", "Leállítás"), @@ -524,9 +519,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Grid View", "Mozaik nézet"), ("List View", "Lista nézet"), ("Select", "Kiválasztás"), - ("Toggle Tags", "Címke kapcsoló"), + ("Toggle Tags", "Címkekapcsoló"), ("pull_ab_failed_tip", "A címjegyzék frissítése nem sikerült"), - ("push_ab_failed_tip", "A címjegyzék szinkronizálása a szerverrel nem sikerült"), + ("push_ab_failed_tip", "A címjegyzék szinkronizálása a kiszolgálóval nem sikerült"), ("synced_peer_readded_tip", "A legutóbbi munkamenetekben jelen lévÅ‘ eszközök ismét felkerülnek a címjegyzékbe."), ("Change Color", "Szín módosítása"), ("Primary Color", "ElsÅ‘dleges szín"), @@ -538,17 +533,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("scam_title", "Lehet, hogy átverték!"), ("scam_text1", "Ha olyan valakivel beszél telefonon, akit NEM ISMER, akiben NEM BÃZIK MEG, és aki arra kéri, hogy használja a RustDesket és indítsa el a szolgáltatást, ne folytassa, és azonnal tegye le a telefont."), ("scam_text2", "Valószínűleg egy csaló próbálja ellopni a pénzét vagy más személyes adatait."), - ("Don't show again", "Ne mutasd újra"), - ("I Agree", "Elfogadom"), - ("Decline", "Elutasítom"), + ("Don't show again", "Ne jelenítse meg újra"), + ("I Agree", "Elfogadás"), + ("Decline", "Elutasítás"), ("Timeout in minutes", "IdÅ‘túllépés percekben"), ("auto_disconnect_option_tip", "A bejövÅ‘ munkamenetek automatikus bezárása, ha a felhasználó inaktív"), ("Connection failed due to inactivity", "A kapcsolat inaktivitás miatt megszakadt"), ("Check for software update on startup", "Szoftverfrissítés keresése indításkor"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "Kérjük, frissítse a RustDesk Server Pro-t a(z) {} vagy újabb verzióra!"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Frissítse a RustDesk Server Prot a(z) {} vagy újabb verzióra!"), ("pull_group_failed_tip", "A csoport frissítése nem sikerült"), ("Filter by intersection", "Szűrés metszéspontok szerint"), - ("Remove wallpaper during incoming sessions", "Távolítsa el a háttérképet a bejövÅ‘ munkamenetek során"), + ("Remove wallpaper during incoming sessions", "Távolítsa el a háttérképet a bejövÅ‘ munkamenetek közben"), ("Test", "Teszt"), ("display_is_plugged_out_msg", "A képernyÅ‘ nincs csatlakoztatva, váltson az elsÅ‘ képernyÅ‘re."), ("No displays", "Nincsenek kijelzÅ‘k"), @@ -564,7 +559,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Kapcsolja ki az összeset"), ("True color (4:4:4)", "Valódi szín (4:4:4)"), ("Enable blocking user input", "Engedélyezze a felhasználói bevitel blokkolását"), - ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP címet vagy egy tartományt egy porttal (:).\nHa egy másik szerveren lévÅ‘ eszközhöz szeretne hozzáférni, adja meg a szerver címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános szerveren lévÅ‘ eszközhöz szeretne hozzáférni, adja meg a \"@public\" lehetÅ‘séget. in. A kulcsra nincs szükség nyilvános szerverek esetén.\n\nHa az elsÅ‘ kapcsolathoz relé-kapcsolatot akar kényszeríteni, adjon hozzá \"/r\" az azonosító végén, például \"9123456234/r\"."), + ("id_input_tip", "Megadhat egy azonosítót, egy közvetlen IP-címet vagy egy tartományt egy porttal (:).\nHa egy másik kiszolgálón lévÅ‘ eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévÅ‘ eszközhöz szeretne hozzáférni, adja meg a \"@public\" lehetÅ‘séget. in. A kulcsra nincs szükség nyilvános kiszolgálók esetén.\n\nHa az elsÅ‘ kapcsolathoz továbbító-kiszolgálón keresztüli kapcsolatot akar kényszeríteni, adja hozzá az \"/r\" az azonosítót a végén, például \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "1. mód"), ("privacy_mode_impl_virtual_display_tip", "2. mód"), ("Enter privacy mode", "Lépjen be az adatvédelmi módba"), @@ -575,20 +570,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Swap control-command key", "VezérlÅ‘- és parancsgombok cseréje"), ("swap-left-right-mouse", "Bal és jobb egérgomb felcserélése"), ("2FA code", "2FA kód"), - ("More", "További"), + ("More", "Továbbiak"), ("enable-2fa-title", "Kétfaktoros hitelesítés aktiválása"), - ("enable-2fa-desc", "Kérjük, most állítsa be a hitelesítÅ‘t. Használhat egy hitelesítési alkalmazást, például az Authy, a Microsoft vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nScannelje be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."), + ("enable-2fa-desc", "Ãllítsa be a hitelesítÅ‘t. Használhat egy hitelesítÅ‘ alkalmazást, például az Aegis, Authy, a Microsoft- vagy a Google Authenticator alkalmazást a telefonján vagy az asztali számítógépén.\n\nOlvassa be a QR-kódot az alkalmazással, és adja meg az alkalmazás által megjelenített kódot a kétfaktoros hitelesítés aktiválásához."), ("wrong-2fa-code", "A kód nem ellenÅ‘rizhetÅ‘. EllenÅ‘rizze, hogy a kód és a helyi idÅ‘ beállításai helyesek-e."), ("enter-2fa-title", "Kétfaktoros hitelesítés"), - ("Email verification code must be 6 characters.", "Az e-mail ellenÅ‘rzÅ‘ kódnak 6 karakterbÅ‘l kell állnia."), + ("Email verification code must be 6 characters.", "Az e-mailben kapott ellenÅ‘rzÅ‘-kódnak 6 karakterbÅ‘l kell állnia."), ("2FA code must be 6 digits.", "A 2FA-kódnak 6 számjegyűnek kell lennie."), ("Multiple Windows sessions found", "Több Windows munkamenet található"), - ("Please select the session you want to connect to", "Kérjük, válassza ki a munkamenetet, amelyhez csatlakozni szeretne"), + ("Please select the session you want to connect to", "Válassza ki a munkamenetet, amelyhez kapcsolódni szeretne"), ("powered_by_me", "ÜzemeltetÅ‘: RustDesk"), - ("outgoing_only_desk_tip", "Ez a RustDesk testreszabott kimenete.\nMás eszközökhöz csatlakozhat, de más eszközök nem csatlakozhatnak az Ön eszközéhez."), - ("preset_password_warning", "Ez egy testreszabott kimenet a RustDeskbÅ‘l egy elÅ‘re beállított jelszóval. Bárki, aki ismeri ezt a jelszót, teljes irányítást szerezhet a készülék felett. Ha nem kívánja ezt megtenni, kérjük, azonnal távolítsa el ezt a szoftvert."), + ("outgoing_only_desk_tip", "Ez a RustDesk testre szabott kimenete.\nMás eszközökhöz kapcsolódhat, de más eszközök nem kapcsolódhatnak az Ön eszközéhez."), + ("preset_password_warning", "Ez egy testre szabott kimenet a RustDeskbÅ‘l egy elÅ‘re beállított jelszóval. Bárki, aki ismeri ezt a jelszót, teljes irányítást szerezhet a készülék felett. Ha nem kívánja ezt megtenni, azonnal távolítsa el ezt a szoftvert."), ("Security Alert", "Biztonsági riasztás"), - ("My address book", "Címjegyzékem"), + ("My address book", "Saját címjegyzék"), ("Personal", "Személyes"), ("Owner", "Tulajdonos"), ("Set shared password", "Megosztott jelszó beállítása"), @@ -599,7 +594,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("share_warning_tip", "A fenti mezÅ‘k megosztottak és mások számára is láthatóak."), ("Everyone", "Mindenki"), ("ab_web_console_tip", "További információk a webes konzolról"), - ("allow-only-conn-window-open-tip", "Csak akkor engedélyezze a csatlakozást, ha a RustDesk ablak nyitva van."), + ("allow-only-conn-window-open-tip", "Csak akkor engedélyezze a kapcsolódást, ha a RustDesk ablak nyitva van."), ("no_need_privacy_mode_no_physical_displays_tip", "Nincsenek fizikai képernyÅ‘k; Nincs szükség az adatvédelmi üzemmód használatára."), ("Follow remote cursor", "Kövesse a távoli kurzort"), ("Follow remote window focus", "Kövesse a távoli ablak fókuszt"), @@ -627,12 +622,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "Teljesítmény"), ("Telegram bot", "Telegram bot"), ("enable-bot-tip", "Ha aktiválja ezt a funkciót, akkor a 2FA-kódot a botjától kaphatja meg. Kapcsolati értesítésként is használható."), - ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjelrel kezdÅ‘dik (\"/\"), pl. B. \"/hello\" az aktiváláshoz.\n"), - ("cancel-2fa-confirm-tip", "Biztos, hogy le akarja mondani a 2FA-t?"), - ("cancel-bot-confirm-tip", "Biztos, hogy le akarod mondani a Telegram botot?"), - ("About RustDesk", "A RustDeskrÅ‘l"), - ("Send clipboard keystrokes", "Vágólap billentyűleütések küldése"), - ("network_error_tip", "Kérjük, ellenÅ‘rizze a hálózati kapcsolatot, majd próbálja meg újra."), + ("enable-bot-desc", "1. Nyisson csevegést @BotFather.\n2. Küldje el a \"/newbot\" parancsot. Miután ezt a lépést elvégezte, kap egy tokent.\n3. Indítson csevegést az újonnan létrehozott botjával. Küldjön egy olyan üzenetet, amely egy perjel (\"/\") kezdetű, pl. \"/hello\" az aktiváláshoz.\n"), + ("cancel-2fa-confirm-tip", "Biztosan le akarja mondani a 2FA-t?"), + ("cancel-bot-confirm-tip", "Biztosan le akarja mondani a Telegram botot?"), + ("About RustDesk", "RustDesk névjegye"), + ("Send clipboard keystrokes", "Billentyűleütések küldése a vágólapra"), + ("network_error_tip", "EllenÅ‘rizze a hálózati kapcsolatot, majd próbálja meg újra."), ("Unlock with PIN", "Feloldás PIN-kóddal"), ("Requires at least {} characters", "Legalább {} karakter szükséges"), ("Wrong PIN", "Hibás PIN"), @@ -648,13 +643,71 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Az egyirányú fájlátvitel engedélyezve van a vezérelt oldalon."), ("Authentication Required", "Hitelesítés szükséges"), ("Authenticate", "Hitelesítés"), - ("web_id_input_tip", "Azonos szerveren lévÅ‘ azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik szerveren lévÅ‘ eszközhöz szeretne hozzáférni, kérjük, adja meg a szerver címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános szerveren lévÅ‘ eszközhöz szeretne hozzáférni, kérjük, adja meg a \"@public\" betűt. in. A kulcsra nincs szükség a nyilvános szerverek esetében."), + ("web_id_input_tip", "Azonos kiszolgálón lévÅ‘ azonosítót adhat meg, a közvetlen IP elérés nem támogatott a webkliensben.\nHa egy másik kiszolgálón lévÅ‘ eszközhöz szeretne hozzáférni, adja meg a kiszolgáló címét (@?key=), például\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nHa egy nyilvános kiszolgálón lévÅ‘ eszközhöz szeretne hozzáférni, adja meg a \"@public\" betűt. in. A kulcsra nincs szükség a nyilvános kiszolgálók esetében."), ("Download", "Letöltés"), ("Upload folder", "Mappa feltöltése"), ("Upload files", "Fájlok feltöltése"), ("Clipboard is synchronized", "A vágólap szinkronizálva van"), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Update client clipboard", "Az ügyfél vágólapjának frissítése"), + ("Untagged", "Címkézetlen"), + ("new-version-of-{}-tip", "A(z) {} új verziója"), + ("Accessible devices", "HozzáférhetÅ‘ eszközök"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Frissítse a RustDesk klienst {} vagy újabb verziójára a távoli oldalon!"), + ("d3d_render_tip", "D3D renderelés"), + ("Use D3D rendering", "D3D renderelés használata"), + ("Printer", "Nyomtató"), + ("printer-os-requirement-tip", "Nyomtató operációs rendszerének minimális rendszerkövetelménye"), + ("printer-requires-installed-{}-client-tip", "A nyomtatóhoz szükséges a(z) {} kliens telepítése"), + ("printer-{}-not-installed-tip", "A(z) {} nyomtató nincs telepítve"), + ("printer-{}-ready-tip", "A(z) {} nyomtató készen áll"), + ("Install {} Printer", "A(z) {} nyomtató nyomtató telepítése"), + ("Outgoing Print Jobs", "KimenÅ‘ nyomtatási feladatok"), + ("Incoming Print Jobs", "BejövÅ‘ nyomtatási feladatok"), + ("Incoming Print Job", "BejövÅ‘ nyomtatási feladat"), + ("use-the-default-printer-tip", "Alapértelmezett nyomtató használata"), + ("use-the-selected-printer-tip", "Kiválasztott nyomtató használata"), + ("auto-print-tip", "Automatikus nyomtatás"), + ("print-incoming-job-confirm-tip", "BejövÅ‘ nyomtatási feladat megerÅ‘sítése"), + ("remote-printing-disallowed-tile-tip", "A távoli nyomtatás nincs engedélyezve"), + ("remote-printing-disallowed-text-tip", "A távoli nyomtatás nincs engedélyezve"), + ("save-settings-tip", "Beállítások mentése"), + ("dont-show-again-tip", "Ne jelenítse meg újra"), + ("Take screenshot", "KépernyÅ‘kép készítése"), + ("Taking screenshot", "KépernyÅ‘kép készítése…"), + ("screenshot-merged-screen-not-supported-tip", "Egyesített képernyÅ‘rÅ‘l nem támogatott a képernyÅ‘kép készítése"), + ("screenshot-action-tip", "KépernyÅ‘kép-művelet"), + ("Save as", "Mentés másként"), + ("Copy to clipboard", "Másolás a vágólapra"), + ("Enable remote printer", "Távoli nyomtatók engedélyezése"), + ("Downloading {}", "Letöltés {}"), + ("{} Update", "{} Frissítés"), + ("{}-to-update-tip", "A {} bezárása és az új verzió telepítése."), + ("download-new-version-failed-tip", "Ha a letöltés sikertelen, akkor vagy újrapróbálkozhat, vagy a \"Letöltés\" gombra kattintva letöltheti a kiadási oldalról, és manuálisan frissíthet."), + ("Auto update", "Automatikus frissítés"), + ("update-failed-check-msi-tip", "A telepítési módszer felismerése nem sikerült. Kérjük, kattintson a \"Letöltés\" gombra, hogy letöltse a kiadási oldalról, és manuálisan frissítse."), + ("websocket_tip", "WebSocket használatakor csak a relé-kapcsolatok támogatottak."), + ("Use WebSocket", "WebSocket használata"), + ("Trackpad speed", "ÉrintÅ‘pad sebessége"), + ("Default trackpad speed", "Alapértelmezett érintÅ‘pad sebessége"), + ("Numeric one-time password", "Numerikus, egyszer használatos jelszó"), + ("Enable IPv6 P2P connection", "IPv6 P2P kapcsolat engedélyezése"), + ("Enable UDP hole punching", "UDP résszűrés engedélyezése"), + ("View camera", "Kamera nézet"), + ("Enable camera", "Kamera engedélyezése"), + ("No cameras", "Nincs kamera"), + ("view_camera_unsupported_tip", "A kameranézet nem támogatott"), + ("Terminal", "Terminál"), + ("Enable terminal", "Terminál engedélyezése"), + ("New tab", "Új lap"), + ("Keep terminal sessions on disconnect", "Terminál munkamenetek megtartása leválasztáskor"), + ("Terminal (Run as administrator)", "Terminál (rendszergazdaként futtatva)"), + ("terminal-admin-login-tip", "Kérjük, adja meg a felügyelt terminál rendszergazdai fiókjának jelszavát."), + ("Failed to get user token.", "Hiba a felhasználói token lekérdezésekor."), + ("Incorrect username or password.", "A felhasználónév vagy a jelszó helytelen."), + ("The user is not an administrator.", "A felhasználó nem rendszergazda."), + ("Failed to check if the user is an administrator.", "Hiba merült fel annak ellenÅ‘rzése során, hogy a felhasználó rendszergazda-e."), + ("Supported only in the installed version.", "Csak a telepített változatban támogatott."), + ("elevation_username_tip", "Felhasználónév vagy tartománynév megadása\\felhasználónév"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 7d90a3ea438..ed179729e78 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -2,18 +2,18 @@ lazy_static::lazy_static! { pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), - ("Your Desktop", "Layar Utama"), - ("desk_tip", "Layar kamu dapat diakses dengan ID dan kata sandi ini."), + ("Your Desktop", "Desktop Anda"), + ("desk_tip", "Akses desktop anda dengan ID & Kata sandi ini"), ("Password", "Kata sandi"), ("Ready", "Sudah siap"), ("Established", "Didirikan"), - ("connecting_status", "Menghubungkan ke jaringan RustDesk..."), + ("connecting_status", "Menghubungkan ke RustDesk..."), ("Enable service", "Aktifkan Layanan"), ("Start service", "Mulai Layanan"), ("Service is running", "Layanan berjalan"), ("Service is not running", "Layanan tidak berjalan"), ("not_ready_status", "Belum siap digunakan. Silakan periksa koneksi"), - ("Control Remote Desktop", "Kontrol PC dari jarak jauh"), + ("Control Remote Desktop", "Lakukan Kontrol PC dari jarak jauh"), ("Transfer file", "Transfer File"), ("Connect", "Sambungkan"), ("Recent sessions", "Sesi Terkini"), @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "panjang %min% s/d %max%"), ("starts with a letter", "Dimulai dengan huruf"), ("allowed characters", "Karakter yang dapat digunakan"), - ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9 dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), + ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9, - (dash) dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."), ("Website", "Situs Web"), ("About", "Tentang"), ("Slogan_tip", "Dibuat dengan penuh kasih sayang dalam dunia yang penuh kekacauan ini"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Kata Sandi OS"), ("install_tip", "Karena UAC, RustDesk tidak dapat bekerja dengan baik sebagai sisi remote dalam beberapa kasus. Untuk menghindari UAC, silakan klik tombol di bawah ini untuk menginstal RustDesk ke sistem."), ("Click to upgrade", "Klik untuk upgrade"), - ("Click to download", "Klik untuk unduh"), - ("Click to update", "Klik untuk memperbarui"), ("Configure", "Konfigurasi"), ("config_acc", "Agar bisa mengontrol Desktopmu dari jarak jauh, Kamu harus memberikan izin \"Aksesibilitas\" untuk RustDesk."), ("config_screen", "Agar bisa mengakses Desktopmu dari jarak jauh, kamu harus memberikan izin \"Perekaman Layar\" untuk RustDesk."), @@ -167,7 +165,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Listening ...", "Menghubungkan..."), ("Remote Host", "Host Remote"), ("Remote Port", "Port Remote"), - ("Action", "Aksi"), + ("Action", "Tindakan"), ("Add", "Tambah"), ("Local Port", "Port Lokal"), ("Local Address", "Alamat lokal"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Tidak ada izin untuk mengirim file"), ("Note", "Catatan"), ("Connection", "Koneksi"), - ("Share Screen", "Bagikan Layar"), + ("Share screen", "Bagikan Layar"), ("Chat", "Obrolan"), ("Total", "Total"), ("items", "item"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Tangkapan Layar"), ("Input Control", "Kontrol input"), ("Audio Capture", "Rekam Suara"), - ("File Connection", "Koneksi File"), - ("Screen Connection", "Koneksi layar"), ("Do you accept?", "Apakah anda setuju?"), ("Open System Setting", "Buka Pengaturan Sistem"), ("How to get Android input permission?", "Bagaimana cara mendapatkan izin input dari Android?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Belum ada rekan favorit?\nTemukan seseorang untuk terhubung dan tambahkan ke favorit!"), ("empty_lan_tip", "Sepertinya kami belum memiliki rekan"), ("empty_address_book_tip", "Tampaknya saat ini tidak ada rekan yang terdaftar dalam buku alamat Anda"), - ("eg: admin", "contoh: admin"), ("Empty Username", "Nama pengguna kosong"), ("Empty Password", "Kata sandi kosong"), ("Me", "Saya"), @@ -649,12 +644,70 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Diperlukan autentikasi"), ("Authenticate", "Autentikasi"), ("web_id_input_tip", "Kamu bisa memasukkan ID pada server yang sama, akses IP langsung tidak didukung di klien web.\nJika Anda ingin mengakses perangkat di server lain, silakan tambahkan alamat server (@?key=), contohnya:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nUntuk mengakses perangkat di server publik, cukup masukkan \"@public\", tanpa kunci/key."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), + ("Download", "Download"), + ("Upload folder", "Upload folder"), + ("Upload files", "Upload file"), ("Clipboard is synchronized", ""), ("Update client clipboard", ""), ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("new-version-of-{}-tip", "Versi {} sudah tersedia."), + ("Accessible devices", "Perangkat yang tersedia"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Silahkan perbarui aplikasi RustDesk ke versi {} atau yang lebih baru pada komputer yang akan terhubung!"), + ("d3d_render_tip", "Ketika rendering D3D diaktifkan, layar kontrol jarak jauh bisa tampak hitam di beberapa komputer"), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", "Printer {} tidak terinstal"), + ("printer-{}-ready-tip", "Printer {} sudah terinstal dan siap digunakan."), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Lihat Kamera"), + ("Enable camera", "Aktifkan kamera"), + ("No cameras", "Tidak ada kamera"), + ("view_camera_unsupported_tip", "Perangkat yang terhubung tidak mendukung tampilan kamera."), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 46d4c937dc5..613c4ce16ae 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -31,8 +31,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("ID/Relay Server", "Server ID/Relay"), ("Import server config", "Importa configurazione server dagli appunti"), ("Export Server Config", "Esporta configurazione server negli appunti"), - ("Import server configuration successfully", "Configurazione server importata completata"), - ("Export server configuration successfully", "Configurazione Server esportata completata"), + ("Import server configuration successfully", "Configurazione server importata con successo"), + ("Export server configuration successfully", "Configurazione Server esportata con successo"), ("Invalid server configuration", "Configurazione server non valida"), ("Clipboard is empty", "Gli appunti sono vuoti"), ("Stop service", "Arresta servizio"), @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "lunghezza da %min% a %max%"), ("starts with a letter", "inizia con una lettera"), ("allowed characters", "caratteri consentiti"), - ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9 e _ (sottolineato).\nIl primo carattere deve essere a-z o A-Z.\nLa lunghezza deve essere fra 6 e 16 caratteri."), + ("id_change_tip", "Puoi usare solo i caratteri a-z, A-Z, 0-9, - (dash) e _ (sottolineato).\nIl primo carattere deve essere a-z o A-Z.\nLa lunghezza deve essere fra 6 e 16 caratteri."), ("Website", "Sito web programma"), ("About", "Info programma"), ("Slogan_tip", "Realizzato con il cuore in questo mondo caotico!"), @@ -105,7 +105,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to delete this file?", "Sei sicuro di voler eliminare questo file?"), ("Are you sure you want to delete this empty directory?", "Sei sicuro di voler eliminare questa cartella vuota?"), ("Are you sure you want to delete the file of this directory?", "Sei sicuro di voler eliminare il file di questa cartella?"), - ("Do this for all conflicts", "Ricorca questa scelta per tutti i conflitti"), + ("Do this for all conflicts", "Ricorda questa scelta per tutti i conflitti"), ("This is irreversible!", "Questo è irreversibile!"), ("Deleting", "Eliminazione di"), ("files", "file"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Password sistema operativo"), ("install_tip", "A causa del Controllo Account Utente (UAC), RustDesk potrebbe non funzionare correttamente come desktop remoto.\nPer evitare questo problema, fai clic sul tasto qui sotto per installare RustDesk a livello di sistema."), ("Click to upgrade", "Aggiorna"), - ("Click to download", "Download"), - ("Click to update", "Aggiorna"), ("Configure", "Configura"), ("config_acc", "Per controllare il desktop dall'esterno, devi fornire a RustDesk il permesso 'Accessibilità'."), ("config_screen", "Per controllare il desktop dall'esterno, devi fornire a RustDesk il permesso 'Registrazione schermo'."), @@ -226,7 +224,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Add ID", "Aggiungi ID"), ("Add Tag", "Aggiungi etichetta"), ("Unselect all tags", "Deseleziona tutte le etichette"), - ("Network error", "Errore rete"), + ("Network error", "Errore di rete"), ("Username missed", "Nome utente mancante"), ("Password missed", "Password mancante"), ("Wrong credentials", "Credenziali errate"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nessun permesso per il trasferimento file"), ("Note", "Nota"), ("Connection", "Connessione"), - ("Share Screen", "Condividi schermo"), + ("Share screen", "Condividi schermo"), ("Chat", "Chat"), ("Total", "Totale"), ("items", "Oggetti"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Cattura schermo"), ("Input Control", "Controllo input"), ("Audio Capture", "Acquisizione audio"), - ("File Connection", "Connessione file"), - ("Screen Connection", "Connessione schermo"), ("Do you accept?", "Accetti?"), ("Open System Setting", "Apri impostazioni di sistema"), ("How to get Android input permission?", "Come ottenere l'autorizzazione input in Android?"), @@ -297,7 +293,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Succeeded", "Completato"), ("Someone turns on privacy mode, exit", "Qualcuno ha attivato la modalità privacy, uscita"), ("Unsupported", "Non supportato"), - ("Peer denied", "Acvesso negato al dispositivo remoto"), + ("Peer denied", "Accesso negato al dispositivo remoto"), ("Please install plugins", "Installa i plugin"), ("Peer exit", "Uscita dal dispostivo remoto"), ("Failed to turn off", "Impossibile spegnere"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ancora nessuna connessione?\nTrova qualcuno con cui connetterti e aggiungilo ai preferiti!"), ("empty_lan_tip", "Sembra proprio che non sia stata rilevata nessuna connessione."), ("empty_address_book_tip", "Sembra che per ora nella rubrica non ci siano connessioni."), - ("eg: admin", "es: admin"), ("Empty Username", "Nome utente vuoto"), ("Empty Password", "Password vuota"), ("Me", "Io"), @@ -479,7 +474,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("another_user_login_text_tip", "Separato"), ("xorg_not_found_title_tip", "Xorg non trovato."), ("xorg_not_found_text_tip", "Installa Xorg."), - ("no_desktop_title_tip", "Non c'è nessun envorinment desktop disponibile."), + ("no_desktop_title_tip", "Non è presente alcun ambiente desktop disponibile."), ("no_desktop_text_tip", "Installa il desktop GNOME."), ("No need to elevate", "Elevazione dei privilegi non richiesta"), ("System Sound", "Dispositivo audio sistema"), @@ -614,7 +609,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("texture_render_tip", "Usa il rendering texture per rendere le immagini più fluide. Se riscontri problemi di rendering prova a disabilitare questa opzione."), ("Use texture rendering", "Usa rendering texture"), ("Floating window", "Finestra galleggiante"), - ("floating_window_tip", "It helps to keep RustDesk background service"), + ("floating_window_tip", "Aiuta a mantenere il servizio Rustdesk in background."), ("Keep screen on", "Mantieni schermo acceso"), ("Never", "Mai"), ("During controlled", "Durante il controllo"), @@ -625,7 +620,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Volume up", "Volume +"), ("Volume down", "Volume -"), ("Power", "Alimentazione"), - ("Telegram bot", "Bot Telgram"), + ("Telegram bot", "Bot Telegram"), ("enable-bot-tip", "Se abiliti questa funzione, puoi ricevere il codice 2FA dal tuo bot.\nPuò anche funzionare come notifica di connessione."), ("enable-bot-desc", "1. apri una chat con @BotFather.\n2. Invia il comando \"/newbot\", dopo aver completato questo passaggio riceverai un token.\n3. Avvia una chat con il tuo bot appena creato. Per attivarlo Invia un messaggio che inizia con una barra (\"/\") tipo \"/hello\".\n"), ("cancel-2fa-confirm-tip", "Sei sicuro di voler annullare 2FA?"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Aggiorna appunti client"), ("Untagged", "Senza tag"), ("new-version-of-{}-tip", "È disponibile una nuova versione di {}"), + ("Accessible devices", "Dispositivi accessibili"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Aggiorna il client RustDesk remoto alla versione {} o successiva!"), + ("d3d_render_tip", "Quando è abilitato il rendering D3D, in alcuni computer la schermata del telecomando potrebbe essere nera."), + ("Use D3D rendering", "Usa rendering D3D"), + ("Printer", "Stampante"), + ("printer-os-requirement-tip", "La funzione della stampante richiede Windows 10 o superiore."), + ("printer-requires-installed-{}-client-tip", "Per usare la stampa remota, {} è necessario installare il programma nel dispositivo."), + ("printer-{}-not-installed-tip", "La stampante {} non è installata."), + ("printer-{}-ready-tip", "La stampante {} è installata e pronta all'uso."), + ("Install {} Printer", "Installa la stampante {}"), + ("Outgoing Print Jobs", "Lavori di stampa in uscita"), + ("Incoming Print Jobs", "Lavori di stampa in entrata"), + ("Incoming Print Job", "Lavoro di stampa in entrata"), + ("use-the-default-printer-tip", "Usa la stampante predefinita"), + ("use-the-selected-printer-tip", "Usa la stampante selezionata"), + ("auto-print-tip", "Stampa usando automaticamente la stampante selezionata."), + ("print-incoming-job-confirm-tip", "Hai ricevuto un lavoro di stampa da remoto. Vuoi eseguirlo sul desktop?"), + ("remote-printing-disallowed-tile-tip", "Stampa remota disabilitata"), + ("remote-printing-disallowed-text-tip", "Le impostazioni di autorizzazione del lato controllato negano la stampa remota."), + ("save-settings-tip", "Salva impostazioni"), + ("dont-show-again-tip", "Non visualizzare più questo messaggio"), + ("Take screenshot", "Cattura schermata"), + ("Taking screenshot", "Cattura schermata"), + ("screenshot-merged-screen-not-supported-tip", "L'unione della cattura di schermate di più display non è attualmente supportata.\nPassa ad un singolo display e riprova."), + ("screenshot-action-tip", "Seleziona come continuare con la schermata."), + ("Save as", "Salva come"), + ("Copy to clipboard", "Copia negli appunti"), + ("Enable remote printer", "Abilita stampante remota"), + ("Downloading {}", "Download {}"), + ("{} Update", "Aggiorna {}"), + ("{}-to-update-tip", "{} si chiuderà e installerà la nuova versione"), + ("download-new-version-failed-tip", "Download non riuscito.\nÈ possibile riprovare o selezionare 'Download' per scaricare e aggiornarlo manualmente."), + ("Auto update", "Aggiornamento automatico"), + ("update-failed-check-msi-tip", "Controllo metodo installazione non riuscito.\nSeleziona 'Download' per scaricare il programma e aggiornarlo manualmente."), + ("websocket_tip", "Quando usi WebSocket, sono supportate solo le connessioni relay."), + ("Use WebSocket", "Usa WebSocket"), + ("Trackpad speed", "Velocità trackpad"), + ("Default trackpad speed", "Velocità predefinita trackpad"), + ("Numeric one-time password", "Password numerica monouso"), + ("Enable IPv6 P2P connection", "Abilita connessione P2P IPv6"), + ("Enable UDP hole punching", "Abilita hole punching UDP"), + ("View camera", "Visualizza telecamera"), + ("Enable camera", "Abilita camera"), + ("No cameras", "Nessuna camera"), + ("view_camera_unsupported_tip", "Il dispositivo remoto non supporta la visualizzazione della camera."), + ("Terminal", "Terminale"), + ("Enable terminal", "Abilita terminale"), + ("New tab", "Nuova scheda"), + ("Keep terminal sessions on disconnect", "Quando disconetti mantieni attiva sessione terminale"), + ("Terminal (Run as administrator)", "Terminale (esegui come amministratore)"), + ("terminal-admin-login-tip", "Inserisci il nome utente e la password dell'amministratore del lato controllato."), + ("Failed to get user token.", "Impossibile ottenere il token utente."), + ("Incorrect username or password.", "Nome utente o password non corretti."), + ("The user is not an administrator.", "L'utente non è un amministratore."), + ("Failed to check if the user is an administrator.", "Impossibile verificare se l'utente è un amministratore."), + ("Supported only in the installed version.", "Supportato solo nella versione installata."), + ("elevation_username_tip", "Inserisci Nome utente o dominio sorgente\\nome Utente"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ja.rs b/src/lang/ja.rs index 3a967afc6c9..eeedf0c6183 100644 --- a/src/lang/ja.rs +++ b/src/lang/ja.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OSã®ãƒ‘スワード"), ("install_tip", "UACã®å½±éŸ¿ã«ã‚ˆã‚Šã€RustDeskãŒãƒªãƒ¢ãƒ¼ãƒˆã‚³ãƒ³ãƒ”ãƒ¥ãƒ¼ã‚¿ãƒ¼ä¸Šã§æ­£å¸¸ã«å‹•作ã—ãªã„å ´åˆãŒã‚りã¾ã™ã€‚UACを回é¿ã™ã‚‹ã«ã¯ã€ä¸‹ã®ãƒœã‚¿ãƒ³ã‚’クリックã—ã¦ã‚·ã‚¹ãƒ†ãƒ ã«RustDeskをインストールã—ã¦ãã ã•ã„。"), ("Click to upgrade", "アップグレード"), - ("Click to download", "ダウンロード"), - ("Click to update", "アップデート"), ("Configure", "設定"), ("config_acc", "リモートã‹ã‚‰ã‚ãªãŸã®ã‚³ãƒ³ãƒ”ューターをæ“作ã™ã‚‹ã«ã¯ã€RustDeskã«ã€Œã‚¢ã‚¯ã‚»ã‚·ãƒ“ãƒªãƒ†ã‚£ã€æ¨©é™ã‚’与ãˆã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚"), ("config_screen", "リモートã‹ã‚‰ã‚ãªãŸã®ã‚³ãƒ³ãƒ”ューターã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹ã«ã¯ã€RustDeskã«ã€Œç”»é¢éŒ²ç”»ã€ã®æ¨©é™ã‚’与ãˆã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "ファイル転é€ã®æ¨©é™ãŒã‚りã¾ã›ã‚“"), ("Note", "ノート"), ("Connection", "接続"), - ("Share Screen", "ç”»é¢ã‚’共有"), + ("Share screen", "ç”»é¢ã‚’共有"), ("Chat", "ãƒãƒ£ãƒƒãƒˆ"), ("Total", "計"), ("items", "個ã®ã‚¢ã‚¤ãƒ†ãƒ "), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "ç”»é¢ã‚­ãƒ£ãƒ—ãƒãƒ£"), ("Input Control", "入力æ“作"), ("Audio Capture", "音声キャプãƒãƒ£"), - ("File Connection", "ãƒ•ã‚¡ã‚¤ãƒ«ã®æŽ¥ç¶š"), - ("Screen Connection", "ç”»é¢ã®æŽ¥ç¶š"), ("Do you accept?", "許å¯ã—ã¾ã™ã‹ï¼Ÿ"), ("Open System Setting", "システム設定を開ã"), ("How to get Android input permission?", "Androidã®å…¥åŠ›æ¨©é™ã‚’å–å¾—ã™ã‚‹ã«ã¯ï¼Ÿ"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "ãŠæ°—ã«å…¥ã‚Šã®ãƒªãƒ¢ãƒ¼ãƒˆã‚³ãƒ³ãƒ”ュータãŒãªã„よã†ã§ã™ã­ï¼Ÿã‚ãªãŸã®æŽ¥ç¶šå…ˆã‚’登録ã—ã¾ã—ょã†ï¼"), ("empty_lan_tip", "ã‚ららã€ã¾ã è¿‘ãã®ã‚³ãƒ³ãƒ”ューターã¯ç™ºè¦‹ã§ãã¦ã„ãªã„よã†ã§ã™ã€‚"), ("empty_address_book_tip", "驚ãã¹ãã“ã¨ã«ã€ã‚ãªãŸã®ã‚¢ãƒ‰ãƒ¬ã‚¹å¸³ã«ã¯ç¾åœ¨ã‚³ãƒ³ãƒ”ューターãŒç™»éŒ²ã•れã¦ã„ã¾ã›ã‚“。"), - ("eg: admin", "例: 管ç†è€…"), ("Empty Username", "空ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼å"), ("Empty Password", "空ã®ãƒ‘スワード"), ("Me", "ã‚ãªãŸ"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "リモートå´ã®RustDeskクライアントをãƒãƒ¼ã‚¸ãƒ§ãƒ³{}以上ã«ã‚¢ãƒƒãƒ—グレードã—ã¦ãã ã•ã„ï¼"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "カメラを表示"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ko.rs b/src/lang/ko.rs index fa35507f849..2f60302b10e 100644 --- a/src/lang/ko.rs +++ b/src/lang/ko.rs @@ -3,49 +3,49 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "ìƒíƒœ"), ("Your Desktop", "ë‚´ ë°ìФí¬íƒ‘"), - ("desk_tip", "ì•„ëž˜ì˜ ID와 비밀번호로 연결할수 있습니다"), + ("desk_tip", "ì´ ID와 비밀번호로 ë°ìФí¬í†±ì— 액세스할 수 있습니다."), ("Password", "비밀번호"), - ("Ready", "준비"), + ("Ready", "준비 완료"), ("Established", "ì—°ê²°ë¨"), - ("connecting_status", "RustDesk 네트워í¬ë¡œ 연결중입니다..."), + ("connecting_status", "RustDesk 네트워í¬ì— ì—°ê²° 중..."), ("Enable service", "서비스 활성화"), ("Start service", "서비스 시작"), - ("Service is running", "서비스가 실행ë˜ì—ˆìŠµë‹ˆë‹¤"), + ("Service is running", "서비스가 실행 중 입니다"), ("Service is not running", "서비스가 실행ë˜ì§€ 않았습니다"), - ("not_ready_status", "준비ë˜ì§€ 않았습니다. ì—°ê²°ì„ í™•ì¸í•´ì£¼ì„¸ìš”."), + ("not_ready_status", "준비ë˜ì§€ 않았습니다. ì—°ê²°ì„ í™•ì¸í•´ 주세요"), ("Control Remote Desktop", "ì›ê²© ë°ìФí¬íƒ‘ 제어"), ("Transfer file", "íŒŒì¼ ì „ì†¡"), - ("Connect", "연결하기"), + ("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 í„°ë„ë§ í™œì„±í™”"), + ("Refresh random password", "ìž„ì˜ì˜ 비밀번호 새로 고침"), + ("Set your own password", "ìžì‹ ë§Œì˜ 비밀번호 설정"), + ("Enable keyboard/mouse", "키보드/마우스 사용함"), + ("Enable clipboard", "í´ë¦½ë³´ë“œ 사용함"), + ("Enable file transfer", "íŒŒì¼ ì „ì†¡ 사용함"), + ("Enable TCP tunneling", "TCP í„°ë„ë§ ì‚¬ìš©í•¨"), ("IP Whitelisting", "IP í™”ì´íŠ¸ë¦¬ìŠ¤íŠ¸"), ("ID/Relay Server", "ID/ë¦´ë ˆì´ ì„œë²„"), - ("Import server config", "서버 설정 가져오기"), - ("Export Server Config", "서버 설정 내보내기"), - ("Import server configuration successfully", "서버 설정 가져오기 성공"), - ("Export server configuration successfully", "서버 설정 내보내기 성공"), - ("Invalid server configuration", "ìž˜ëª»ëœ ì„œë²„ 설정"), + ("Import server config", "서버 구성 가져오기"), + ("Export Server Config", "서버 구성 내보내기"), + ("Import server configuration successfully", "서버 구성 ê°€ì ¸ì˜¤ê¸°ì— ì„±ê³µí–ˆìŠµë‹ˆë‹¤"), + ("Export server configuration successfully", "서버 구성 내보내기가 성공했습니다"), + ("Invalid server configuration", "ìž˜ëª»ëœ ì„œë²„ 구성입니다"), ("Clipboard is empty", "í´ë¦½ë³´ë“œê°€ 비어있습니다"), ("Stop service", "서비스 중지"), ("Change ID", "ID 변경"), - ("Your new ID", "ë‹¹ì‹ ì˜ ìƒˆë¡œìš´ ID"), + ("Your new ID", "새 ID"), ("length %min% to %max%", "ê¸¸ì´ %min% ~ %max%"), ("starts with a letter", "문ìžë¡œ 시작해야 합니다"), ("allowed characters", "허용ë˜ëŠ” 문ìž"), - ("id_change_tip", "a-z, A-Z, 0-9, _(ì–¸ë”ë°”)ë§Œ ìž…ë ¥ 가능합니다. 첫 문ìžëŠ” a-z í˜¹ì€ A-Z로 시작해야 합니다. 길ì´ëŠ” 6~16글ìžê°€ 요구ë©ë‹ˆë‹¤."), + ("id_change_tip", "a-z, A-Z, 0-9, -(대시) ë° _(밑줄) 문ìžë§Œ 허용ë©ë‹ˆë‹¤. 첫 글ìžëŠ” a-z, A-Z여야 합니다. 길ì´ëŠ” 6ì—서 16 사ì´ì—¬ì•¼ 합니다."), ("Website", "웹사ì´íЏ"), ("About", "ì •ë³´"), ("Slogan_tip", "ì´ í˜¼ëž€ìŠ¤ëŸ¬ìš´ 세ìƒì—서 마ìŒì„ ë‹´ì•„ 만들었습니다!"), - ("Privacy Statement", "ê°œì¸ ì •ë³´ 보호 ì •ì±…"), + ("Privacy Statement", "ê°œì¸ì •ë³´ 보호정책"), ("Mute", "ìŒì†Œê±°"), ("Build Date", "빌드 ë‚ ì§œ"), ("Version", "버전"), @@ -53,43 +53,43 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input", "오디오 ìž…ë ¥"), ("Enhancements", "í–¥ìƒëœ 기능"), ("Hardware Codec", "하드웨어 ì½”ë±"), - ("Adaptive bitrate", "가변 비트레ì´íЏ"), + ("Adaptive bitrate", "ì ì‘형 비트레ì´íЏ"), ("ID Server", "ID 서버"), ("Relay Server", "ë¦´ë ˆì´ ì„œë²„"), ("API Server", "API 서버"), - ("invalid_http", "http:// ë˜ëŠ” https:// 로 시작해야합니다"), - ("Invalid IP", "유효하지 ì•Šì€ IP"), - ("Invalid format", "유효하지 ì•Šì€ í˜•ì‹"), + ("invalid_http", "http:// ë˜ëŠ” https://로 시작해야 합니다"), + ("Invalid IP", "유효하지 ì•Šì€ IP 주소입니다"), + ("Invalid format", "유효하지 ì•Šì€ í˜•ì‹ìž…니다"), ("server_not_support", "ì•„ì§ ì„œë²„ì—서 ì§€ì›ë˜ì§€ 않습니다"), - ("Not available", "불가능"), - ("Too frequent", "ìˆ˜ì •ì´ ë„ˆë¬´ ìžì£¼ ë°œìƒí•©ë‹ˆë‹¤. ë‚˜ì¤‘ì— ìž¬ì‹œë„í•´ 주세요."), + ("Not available", "사용할 수 ì—†ìŒ"), + ("Too frequent", "너무 빈번합니다"), ("Cancel", "취소"), - ("Skip", "넘기기"), + ("Skip", "건너뛰기"), ("Close", "닫기"), ("Retry", "재시ë„"), ("OK", "확ì¸"), - ("Password Required", "비밀번호 ìž…ë ¥"), - ("Please enter your password", "비밀번호를 입력해주세요"), - ("Remember password", "비밀번호 기억하기"), - ("Wrong Password", "비밀번호가 다릅니다"), - ("Do you want to enter again?", "다시 연결하시겠습니까?"), + ("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ë¶„ í›„ì— ìž¬ì‹œë„하세요"), + ("Reset by the peer", "í”¼ì–´ì— ì˜í•´ 초기화"), + ("Connecting...", "ì—°ê²° 중..."), + ("Connection in progress. Please wait.", "ì—°ê²°ì´ ì§„í–‰ 중입니다. 기다려 주세요."), + ("Please try 1 minute later", "1ë¶„ í›„ì— ë‹¤ì‹œ 시ë„하세요"), ("Login Error", "ë¡œê·¸ì¸ ì˜¤ë¥˜"), ("Successful", "성공"), - ("Connected, waiting for image...", "ì—°ê²°ë¨. 화면 전송 대기 중..."), + ("Connected, waiting for image...", "ì—°ê²°ë˜ì—ˆìŠµë‹ˆë‹¤, ì´ë¯¸ì§€ë¥¼ 기다리는 중..."), ("Name", "ì´ë¦„"), ("Type", "유형"), - ("Modified", "수정ë¨"), + ("Modified", "수정 ë‚ ì§œ"), ("Size", "í¬ê¸°"), - ("Show Hidden Files", "숨겨진 íŒŒì¼ í‘œì‹œ"), + ("Show Hidden Files", "숨김 íŒŒì¼ í‘œì‹œ"), ("Receive", "받기"), ("Send", "보내기"), - ("Refresh File", "íŒŒì¼ ìƒˆë¡œê³ ì¹¨"), + ("Refresh File", "íŒŒì¼ ìƒˆë¡œ 고침"), ("Local", "로컬"), ("Remote", "ì›ê²©"), ("Remote Computer", "ì›ê²© 컴퓨터"), @@ -98,341 +98,337 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Delete", "ì‚­ì œ"), ("Properties", "ì†ì„±"), ("Multi Select", "다중 ì„ íƒ"), - ("Select All", "ì „ì²´ ì„ íƒ"), - ("Unselect All", "ì „ì²´ ì„ íƒ í•´ì œ"), - ("Empty Directory", "í´ë”ê°€ 비어있습니다"), - ("Not an empty directory", "í´ë”ê°€ 비어있지 않습니다"), + ("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", "삭제중"), + ("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", "완료ë¨"), + ("Waiting", "대기 중"), + ("Finished", "완료ë˜ì—ˆìŠµë‹ˆë‹¤"), ("Speed", "ì†ë„"), - ("Custom Image Quality", "화질 설정"), + ("Custom Image Quality", "ì‚¬ìš©ìž ì§€ì • ì´ë¯¸ì§€ 품질"), ("Privacy mode", "ê°œì¸ì •ë³´ 보호 모드"), ("Block user input", "ì‚¬ìš©ìž ìž…ë ¥ 차단"), ("Unblock user input", "ì‚¬ìš©ìž ìž…ë ¥ 차단 í•´ì œ"), - ("Adjust Window", "화면 ì¡°ì •"), + ("Adjust Window", "ì°½ í¬ê¸° ì¡°ì •"), ("Original", "ì›ë³¸"), ("Shrink", "축소"), - ("Stretch", "확대"), - ("Scrollbar", "스í¬ë¡¤ë°”"), - ("ScrollAuto", "ìžë™ìФí¬ë¡¤"), - ("Good image quality", "ì´ë¯¸ì§€ 품질 최ì í™”"), - ("Balanced", "균형"), + ("Stretch", "늘ì´ê¸°"), + ("Scrollbar", "스í¬ë¡¤ 막대"), + ("ScrollAuto", "ìžë™ 스í¬ë¡¤"), + ("Good image quality", "ì¢‹ì€ ì´ë¯¸ì§€ 품질"), + ("Balanced", "균형 잡힌"), ("Optimize reaction time", "ë°˜ì‘ ì‹œê°„ 최ì í™”"), - ("Custom", "ì‚¬ìš©ìž ì •ì˜"), - ("Show remote cursor", "ì›ê²© 커서 ë³´ì´ê¸°"), - ("Show quality monitor", "품질 모니터 보기"), - ("Disable clipboard", "í´ë¦½ë³´ë“œ 비활성화"), - ("Lock after session end", "세션 종료 후 화면 잠금"), - ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del ìž…ë ¥"), - ("Insert Lock", "ì›ê²© ìž…ë ¥ 잠금"), - ("Refresh", "새로고침"), + ("Custom", "ì‚¬ìš©ìž ì§€ì •"), + ("Show remote cursor", "ì›ê²© 커서 표시"), + ("Show quality monitor", "품질 모니터 표시"), + ("Disable clipboard", "í´ë¦½ë³´ë“œ 사용 안 함"), + ("Lock after session end", "세션 종료 후 잠금"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del 삽입"), + ("Insert Lock", "삽입 잠금"), + ("Refresh", "새로 고침"), ("ID does not exist", "IDê°€ 존재하지 않습니다"), - ("Failed to connect to rendezvous server", "ë“±ë¡ ì„œë²„ì— ì—°ê²°í•˜ì§€ 못했습니다."), - ("Please try later", "재시ë„해주세요"), - ("Remote desktop is offline", "ì›ê²© ë°ìФí¬íƒ‘ì´ ì—°ê²°ë˜ì–´ 있지 않습니다"), - ("Key mismatch", "키가 ì¼ì¹˜í•˜ì§€ 않습니다."), + ("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", "ì›ê²© ë°ìФí¬íƒ‘ìœ¼ë¡œì˜ ì§ì ‘ ì—°ê²° ìƒì„±ì— 실패하였습니다"), + ("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", "OS 비밀번호"), - ("install_tip", "UAC로 ì¸í•´, RustDeskê°€ ì›ê²©ì§€ì¼ 때 ì¼ë¶€ ê¸°ëŠ¥ì´ ë™ìž‘하지 ì•Šì„ ìˆ˜ 있습니다. UAC 문제를 방지하려면, 아래 ë²„íŠ¼ì„ í´ë¦­í•˜ì—¬ RustDesk를 ì‹œìŠ¤í…œì— ì„¤ì¹˜í•´ì£¼ì„¸ìš”."), + ("install_tip", "UAC로 ì¸í•´ ê²½ìš°ì— ë”°ë¼ RustDeskê°€ ì›ê²© 쪽ì—서 제대로 ìž‘ë™í•˜ì§€ ì•Šì„ ìˆ˜ 있습니다. UAC를 피하려면 아래 ë²„íŠ¼ì„ í´ë¦­í•˜ì—¬ ì‹œìŠ¤í…œì— RustDesk를 설치하세요."), ("Click to upgrade", "업그레ì´ë“œ"), - ("Click to download", "다운로드"), - ("Click to update", "ì—…ë°ì´íЏ"), ("Configure", "구성"), - ("config_acc", "ë‚´ ë°ìФí¬íƒ‘ì„ ì›ê²©ì œì–´í•˜ê¸° ì „ì—, RustDeskì—게 \"Accessibility (접근성)\" ê¶Œí•œì„ ë¶€ì—¬í•´ì•¼ 합니다."), - ("config_screen", "ë‚´ ë°ìФí¬íƒ‘ì„ ì›ê²©ì œì–´í•˜ê¸° ì „ì—, RustDeskì—게 \"Screen Recording (화면 녹화)\" ê¶Œí•œì„ ë¶€ì—¬í•´ì•¼ 합니다."), - ("Installing ...", "설치중 ..."), + ("config_acc", "ë°ìФí¬í†±ì„ ì›ê²©ìœ¼ë¡œ 제어하려면 RustDeskì— \"접근성\" ê¶Œí•œì„ ë¶€ì—¬í•´ì•¼ 합니다."), + ("config_screen", "ë°ìФí¬í†±ì— ì›ê²©ìœ¼ë¡œ 액세스하려면 RustDeskì— \"화면 녹화\" ê¶Œí•œì„ ë¶€ì—¬í•´ì•¼ 합니다."), + ("Installing ...", "설치 중..."), ("Install", "설치하기"), ("Installation", "설치"), ("Installation Path", "설치 경로"), - ("Create start menu shortcuts", "시작 ë©”ë‰´ì— ë°”ë¡œê°€ê¸° ìƒì„±"), - ("Create desktop icon", "ë°ìФí¬íƒ‘ ì•„ì´ì½˜ ìƒì„±"), - ("agreement_tip", "설치를 시작하기 ì „ì—, ë¼ì´ì„ ìФ ì•½ê´€ì— ë™ì˜ë¥¼ 해야합니다."), - ("Accept and Install", "ë™ì˜ ë° ì„¤ì¹˜"), - ("End-user license agreement", "최종 ì‚¬ìš©ìž ë¼ì´ì„ ìФ 약관 ë™ì˜"), - ("Generating ...", "ìƒì„±ì¤‘ ..."), - ("Your installation is lower version.", "설치한 ë²„ì „ì´ í˜„ìž¬ 실행 ì¤‘ì¸ ë²„ì „ë³´ë‹¤ 낮습니다."), - ("not_close_tcp_tip", "연결중ì—는 ì´ ì°½ì„ ë„ì§€ 마세요"), - ("Listening ...", "ì—°ê²° 대기중 ..."), + ("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", "ì•¡ì…˜"), + ("Action", "ë™ìž‘"), ("Add", "추가"), ("Local Port", "로컬 í¬íЏ"), - ("Local Address", "현재 주소"), + ("Local Address", "로컬 주소"), ("Change Local Port", "로컬 í¬íЏ 변경"), - ("setup_server_tip", "ìžì²´ 서버를 구축하면 ë” ë¹ ë¥¸ ì†ë„로 사용할수 있습니다"), - ("Too short, at least 6 characters.", "너무 짧습니다, 최소 6ê¸€ìž ì´ìƒ 입력해주세요."), - ("The confirmation is not identical.", "ë‘ ìž…ë ¥ì´ ì¼ì¹˜í•˜ì§€ 않습니다."), + ("setup_server_tip", "ë” ë¹ ë¥¸ ì—°ê²°ì„ ìœ„í•´, ìžì‹ ë§Œì˜ 서버를 설정해 주세요."), + ("Too short, at least 6 characters.", "너무 짧습니다. 최소 6ìž ì´ìƒìž…니다."), + ("The confirmation is not identical.", "확ì¸ì´ ë™ì¼í•˜ì§€ 않습니다."), ("Permissions", "권한"), ("Accept", "수ë½"), ("Dismiss", "ê±°ë¶€"), - ("Disconnect", "ì—°ê²° 종료"), - ("Enable file copy and paste", "íŒŒì¼ ë³µì‚¬ ë° ë¶™ì—¬ë„£ê¸° 허용"), + ("Disconnect", "ì—°ê²° í•´ì œ"), + ("Enable 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", "비밀번호를 입력하세요"), + ("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 세션 공유 활성화"), + ("Enable RDP session sharing", "RDP 세션 공유 사용함"), ("Auto Login", "ìžë™ 로그ì¸"), - ("Enable direct IP access", "다ì´ë ‰íЏ IP ì—°ê²° 활성화"), - ("Rename", "ì´ë¦„ 변경"), - ("Space", "공간"), - ("Create desktop shortcut", "ë°ìФí¬íƒ‘ 바로가기 ìƒì„±"), + ("Enable direct IP access", "ì§ì ‘ IP 액세스 사용함"), + ("Rename", "ì´ë¦„ 바꾸기"), + ("Space", "공백"), + ("Create desktop shortcut", "바탕 화면 바로가기 만들기"), ("Change Path", "경로 변경"), - ("Create Folder", "í´ë” ìƒì„±"), - ("Please enter the folder name", "í´ë”ëª…ì„ ìž…ë ¥í•´ì£¼ì„¸ìš”"), + ("Create Folder", "í´ë” 만들기"), + ("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", "x11로 전환해주세요"), + ("x11 expected", "x11 예ìƒ"), ("Port", "í¬íЏ"), ("Settings", "설정"), - ("Username", "사용ìžëª…"), - ("Invalid port", "í¬íŠ¸ê°€ 유효하지않습니다"), - ("Closed manually by the peer", "다른 사용ìžì— ì˜í•´ 종료ë¨"), - ("Enable remote configuration modification", "ì›ê²© 구성 변경 활성화"), + ("Username", "ì‚¬ìš©ìž ì´ë¦„"), + ("Invalid port", "유효하지 ì•Šì€ í¬íŠ¸ìž…ë‹ˆë‹¤"), + ("Closed manually by the peer", "피어가 수ë™ìœ¼ë¡œ 닫았습니다"), + ("Enable remote configuration modification", "ì›ê²© 구성 수정 사용함"), ("Run without install", "설치 ì—†ì´ ì‹¤í–‰"), ("Connect via relay", "릴레ì´ë¥¼ 통해 ì—°ê²°"), ("Always connect via relay", "í•­ìƒ ë¦´ë ˆì´ë¥¼ 통해 ì—°ê²°"), - ("whitelist_tip", "í™”ì´íŠ¸ë¦¬ìŠ¤íŠ¸ì— ìžˆëŠ” IPë§Œ 나ì—게 연결할수 있습니다"), + ("whitelist_tip", "í™”ì´íŠ¸ë¦¬ìŠ¤íŠ¸ì— ìžˆëŠ” IPë§Œ 나ì—게 액세스할 수 있ìŒ"), ("Login", "로그ì¸"), ("Verify", "확ì¸"), ("Remember me", "기억하기"), ("Trust this device", "ì´ ìž¥ì¹˜ 신뢰"), - ("Verification code", "í™•ì¸ ì½”ë“œ"), - ("verification_tip", "등ë¡ëœ ì´ë©”ì¼ ì£¼ì†Œë¡œ ì¸ì¦ë²ˆí˜¸ê°€ 발송ë˜ì—ˆìŠµë‹ˆë‹¤. ì¸ì¦ë²ˆí˜¸ë¥¼ 입력하시면 ê³„ì† ë¡œê·¸ì¸í•˜ì‹¤ 수 있습니다"), + ("Verification code", "ì¸ì¦ 코드"), + ("verification_tip", "등ë¡í•œ ì´ë©”ì¼ ì£¼ì†Œë¡œ ì¸ì¦ 코드가 전송ë˜ì—ˆìœ¼ë‹ˆ ì¸ì¦ 코드를 입력하여 로그ì¸ì„ 계ì†í•˜ì„¸ìš”."), ("Logout", "로그아웃"), ("Tags", "태그"), ("Search ID", "ID 검색"), - ("whitelist_sep", "ë‹¤ìŒ ê¸€ìžë¡œ 구분합니다. ',(콤마) ;(세미콜론) ë„어쓰기 í˜¹ì€ ì¤„ë°”ê¿ˆ'"), + ("whitelist_sep", "쉼표, 세미콜론, 공백 ë˜ëŠ” 새 줄로 구분합니다."), ("Add ID", "ID 추가"), ("Add Tag", "태그 추가"), ("Unselect all tags", "모든 태그 ì„ íƒ í•´ì œ"), ("Network error", "ë„¤íŠ¸ì›Œí¬ ì˜¤ë¥˜"), - ("Username missed", "사용ìžëª…ì´ ìž…ë ¥ë˜ì§€ì•Šì•˜ìŠµë‹ˆë‹¤"), - ("Password missed", "비밀번호가 ìž…ë ¥ë˜ì§€ì•Šì•˜ìŠµë‹ˆë‹¤"), - ("Wrong credentials", "ë¡œê·¸ì¸ ì •ë³´ê°€ 다릅니다"), - ("The verification code is incorrect or has expired", "ì¸ì¦ 코드가 잘못ë˜ì—ˆê±°ë‚˜ ì‹œê°„ì´ ì´ˆê³¼ë˜ì—ˆìŠµë‹ˆë‹¤."), - ("Edit Tag", "태그 수정"), - ("Forget Password", "패스워드 기억하지 않기"), + ("Username missed", "ì‚¬ìš©ìž ì´ë¦„ì´ ëˆ„ë½ë˜ì—ˆìŠµë‹ˆë‹¤"), + ("Password missed", "비밀번호가 누ë½ë˜ì—ˆìŠµë‹ˆë‹¤"), + ("Wrong credentials", "ìž˜ëª»ëœ ìžê²© ì¦ëª…"), + ("The verification code is incorrect or has expired", "ì¸ì¦ 코드가 올바르지 않거나 만료ë˜ì—ˆìŠµë‹ˆë‹¤."), + ("Edit Tag", "태그 편집"), + ("Forget Password", "비밀번호 분실"), ("Favorites", "ì¦ê²¨ì°¾ê¸°"), ("Add to Favorites", "ì¦ê²¨ì°¾ê¸°ì— 추가"), ("Remove from Favorites", "ì¦ê²¨ì°¾ê¸°ì—서 ì‚­ì œ"), ("Empty", "비어 있ìŒ"), - ("Invalid folder name", "유효하지 ì•Šì€ í´ë”명"), + ("Invalid folder name", "유효하지 ì•Šì€ í´ë” ì´ë¦„"), ("Socks5 Proxy", "Socks5 프ë¡ì‹œ"), ("Socks5/Http(s) Proxy", "Socks5/Http(s) 프ë¡ì‹œ"), - ("Discovered", "ì°¾ìŒ"), - ("install_daemon_tip", "ë¶€íŒ…ëœ ì´í›„ 시스템 ì„œë¹„ìŠ¤ì— ì„¤ì¹˜í•´ì•¼ 합니다."), + ("Discovered", "발견ë¨"), + ("install_daemon_tip", "부팅할 때 시작하려면 시스템 서비스를 설치해야 합니다."), ("Remote ID", "ì›ê²© ID"), ("Paste", "붙여넣기"), - ("Paste here?", "ì—¬ê¸°ì— ë¶™ì—¬ë„£ê¸°ë¥¼ 실핼할까요?"), + ("Paste here?", "ì—¬ê¸°ì— ë¶™ì—¬ë„£ìœ¼ì‹œê² ìŠµë‹ˆê¹Œ?"), ("Are you sure to close the connection?", "ì—°ê²°ì„ ì¢…ë£Œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"), - ("Download new version", "최신 버전 다운로드"), + ("Download new version", "새 버전 다운로드"), ("Touch mode", "터치 모드"), ("Mouse mode", "마우스 모드"), ("One-Finger Tap", "한 ì†ê°€ë½ 탭"), ("Left Mouse", "왼쪽 마우스"), - ("One-Long Tap", "길게 누르기"), + ("One-Long Tap", "한 번 길게 탭"), ("Two-Finger Tap", "ë‘ ì†ê°€ë½ 탭"), ("Right Mouse", "오른쪽 마우스"), - ("One-Finger Move", "한 ì†ê°€ë½ ì´ë™"), - ("Double Tap & Move", "ë‘ ë²ˆ 탭 하고 ì´ë™"), - ("Mouse Drag", "마우스 드래그"), - ("Three-Finger vertically", "세 ì†ê°€ë½ 세로로"), + ("One-Finger Move", "한 ì†ê°€ë½ìœ¼ë¡œ ì´ë™"), + ("Double Tap & Move", "ë‘ ë²ˆ 탭하고 ì´ë™"), + ("Mouse Drag", "마우스 ëŒê¸°"), + ("Three-Finger vertically", "세 ì†ê°€ë½ìœ¼ë¡œ 수ì§"), ("Mouse Wheel", "마우스 휠"), - ("Two-Finger Move", "ë‘ ì†ê°€ë½ ì´ë™"), + ("Two-Finger Move", "ë‘ ì†ê°€ë½ìœ¼ë¡œ ì´ë™"), ("Canvas Move", "캔버스 ì´ë™"), - ("Pinch to Zoom", "확대/축소"), - ("Canvas Zoom", "캔버스 확대"), + ("Pinch to Zoom", "ì°ì–´ì„œ 확대/축소"), + ("Canvas Zoom", "캔버스 확대/축소"), ("Reset canvas", "캔버스 초기화"), ("No permission of file transfer", "íŒŒì¼ ì „ì†¡ ê¶Œí•œì´ ì—†ìŠµë‹ˆë‹¤"), ("Note", "노트"), ("Connection", "ì—°ê²°"), - ("Share Screen", "화면 공유"), + ("Share screen", "화면 공유"), ("Chat", "채팅"), - ("Total", "ì´í•©"), - ("items", "개체"), + ("Total", "ì „ì²´"), + ("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 (접근성)\" 서비스 ì‚¬ìš©ì„ í—ˆìš©í•´ì•¼ 합니다."), - ("android_input_permission_tip2", "시스템 설정 페ì´ì§€ë¡œ ì´ë™í•˜ì—¬ [ì„¤ì¹˜ëœ ì„œë¹„ìŠ¤]ì—서 [RustDesk Input] 서비스를 켜십시오."), - ("android_new_connection_tip", "현재 ìž¥ì¹˜ì˜ ìƒˆë¡œìš´ 제어 ìš”ì²­ì´ ìˆ˜ì‹ ë˜ì—ˆìŠµë‹ˆë‹¤."), - ("android_service_will_start_tip", "\"화면 캡처\"를 켜면 서비스가 ìžë™ìœ¼ë¡œ 시작ë˜ì–´ 다른 장치ì—서 ì‚¬ìš©ìž ìž¥ì¹˜ì— ëŒ€í•œ ì—°ê²°ì„ ìš”ì²­í•  수 있습니다."), - ("android_stop_service_tip", "서비스를 종료하면 모든 ì—°ê²°ì´ ìžë™ìœ¼ë¡œ 닫힙니다."), - ("android_version_audio_tip", "현재 Android ë²„ì „ì€ ì˜¤ë””ì˜¤ 캡처를 ì§€ì›í•˜ì§€ 않습니다. Android 10 ì´ìƒìœ¼ë¡œ 업그레ì´ë“œí•˜ì‹­ì‹œì˜¤."), - ("android_start_service_tip", "서비스 ì‹œìž‘ì„ í´ë¦­í•˜ê±°ë‚˜ 화면 캡처 ê¶Œí•œì„ í™œì„±í™”í•˜ì—¬ 화면 공유 서비스를 시작하세요."), - ("android_permission_may_not_change_tip", "ì„¤ì •ëœ ì—°ê²°ì˜ ê²½ìš° ì—°ê²°ì´ ìž¬ì„¤ì •ë˜ì§€ 않는 한 ê¶Œí•œì´ ì¦‰ì‹œ 변경ë˜ì§€ ì•Šì„ ìˆ˜ 있습니다."), + ("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_stop_service_tip", "서비스를 닫으면 ì„¤ì •ëœ ëª¨ë“  ì—°ê²°ì´ ìžë™ìœ¼ë¡œ 닫힙니다."), + ("android_version_audio_tip", "현재 Android ë²„ì „ì€ ì˜¤ë””ì˜¤ 캡처를 ì§€ì›í•˜ì§€ 않으므로 Android 10 ì´ìƒìœ¼ë¡œ 업그레ì´ë“œí•˜ì„¸ìš”."), + ("android_start_service_tip", "[서비스 시작]ì„ íƒ­í•˜ê±°ë‚˜ [화면 캡처] ê¶Œí•œì„ í™œì„±í™”í•˜ì—¬ 화면 공유 서비스를 시작합니다."), + ("android_permission_may_not_change_tip", "ì„¤ì •ëœ ì—°ê²°ì— ëŒ€í•œ ê¶Œí•œì€ ë‹¤ì‹œ ì—°ê²°í•  때까지 즉시 변경ë˜ì§€ ì•Šì„ ìˆ˜ 있습니다."), ("Account", "계정"), ("Overwrite", "ë®ì–´ì“°ê¸°"), - ("This file exists, skip or overwrite this file?", "해당 파ì¼ì´ ì´ë¯¸ 존재합니다, 건너뛰거나 ë®ì–´ì“°ì‹œê² ìŠµë‹ˆê¹Œ?"), + ("This file exists, skip or overwrite this file?", "ì´ íŒŒì¼ì´ ì´ë¯¸ 존재합니다, 건너뛰거나 ë®ì–´ì“°ì‹œê² ìŠµë‹ˆê¹Œ?"), ("Quit", "종료"), - ("Help", "ì§€ì›"), + ("Help", "ë„움ë§"), ("Failed", "실패"), ("Succeeded", "성공"), - ("Someone turns on privacy mode, exit", "ê°œì¸ì •ë³´ 보호 모드가 활성화ë˜ì–´ 종료ë©ë‹ˆë‹¤"), + ("Someone turns on privacy mode, exit", "누군가가 ê°œì¸ì •ë³´ 보호 모드를 켭니다, 종료합니다"), ("Unsupported", "ì§€ì›ë˜ì§€ 않ìŒ"), ("Peer denied", "ì—°ê²° ê±°ë¶€ë¨"), ("Please install plugins", "플러그ì¸ì„ 설치해주세요"), - ("Peer exit", "다른 사용ìžê°€ 종료함"), - ("Failed to turn off", "종료 실패"), - ("Turned off", "종료ë¨"), + ("Peer exit", "피어 종료"), + ("Failed to turn off", "ë„기 실패"), + ("Turned off", "꺼ì§"), ("Language", "언어"), - ("Keep RustDesk background service", "RustDesk 백그ë¼ìš´ë“œ 서비스로 유지하기"), - ("Ignore Battery Optimizations", "배터리 최ì í™” 무시하기"), - ("android_open_battery_optimizations_tip", "해당 ê¸°ëŠ¥ì„ ë¹„í™œì„±í™”í•˜ë ¤ë©´ RustDesk ì‘ìš© 프로그램 설정 페ì´ì§€ë¡œ ì´ë™í•˜ì—¬ [배터리]ì—서 [제한 ì—†ìŒ] ì„ íƒì„ 해제하십시오."), - ("Start on boot", "부팅시 시작"), - ("Start the screen sharing service on boot, requires special permissions", "부팅 시 화면 공유 서비스를 시작하려면 특별한 ê¶Œí•œì´ í•„ìš”í•©ë‹ˆë‹¤."), + ("Keep RustDesk background service", "RustDesk 백그ë¼ìš´ë“œ 서비스 유지"), + ("Ignore Battery Optimizations", "배터리 최ì í™” 무시"), + ("android_open_battery_optimizations_tip", "ì´ ê¸°ëŠ¥ì„ ë¹„í™œì„±í™”í•˜ë ¤ë©´ ë‹¤ìŒ RustDesk ì‘ìš© 프로그램 설정 페ì´ì§€ë¡œ ì´ë™í•˜ì—¬ [배터리]를 찾아서 입력하고 [제한 ì—†ìŒ]ì„ ì„ íƒ ì·¨ì†Œí•˜ì„¸ìš”"), + ("Start on boot", "부팅 시 시작"), + ("Start the screen sharing service on boot, requires special permissions", "부팅 시 화면 공유 서비스를 시작하려면 특별 ê¶Œí•œì´ í•„ìš”í•©ë‹ˆë‹¤"), ("Connection not allowed", "ì—°ê²°ì´ í—ˆìš©ë˜ì§€ 않았습니다"), ("Legacy mode", "레거시 모드"), ("Map mode", "ë§µ 모드"), ("Translate mode", "번역 모드"), ("Use permanent password", "ì˜êµ¬ 비밀번호 사용"), - ("Use both passwords", "(임시/ì˜êµ¬) 비밀번호 ëª¨ë‘ ì‚¬ìš©"), + ("Use both passwords", "ë‘ ê°€ì§€ 비밀번호 ëª¨ë‘ ì‚¬ìš©"), ("Set permanent password", "ì˜êµ¬ 비밀번호 설정"), - ("Enable remote restart", "ì›ê²© 재시작 활성화"), - ("Restart remote device", "ì›ê²© 장치 재시작"), - ("Are you sure you want to restart", "ì •ë§ë¡œ 재시작 하시겠습니까"), - ("Restarting remote device", "ì›ê²© 장치를 재시작하는중"), - ("remote_restarting_tip", "ì›ê²© 장치를 재시작하는 중입니다. ì´ ë©”ì‹œì§€ ìƒìžë¥¼ ë‹«ê³  잠시 후 ì˜êµ¬ 비밀번호로 다시 연결하십시오."), - ("Copied", "복사ë¨"), + ("Enable 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", "화면 설정"), + ("Display Settings", "ë””ìŠ¤í”Œë ˆì´ ì„¤ì •"), ("Ratio", "비율"), ("Image Quality", "ì´ë¯¸ì§€ 품질"), ("Scroll Style", "스í¬ë¡¤ 스타ì¼"), - ("Show Toolbar", "툴바 보기"), - ("Hide Toolbar", "툴바 숨기기"), - ("Direct Connection", "다ì´ë ‰íЏ ì—°ê²°"), + ("Show Toolbar", "ë„구 ëª¨ìŒ í‘œì‹œ"), + ("Hide Toolbar", "ë„구 ëª¨ìŒ ìˆ¨ê¸°ê¸°"), + ("Direct Connection", "ì§ì ‘ ì—°ê²°"), ("Relay Connection", "ë¦´ë ˆì´ ì—°ê²°"), ("Secure Connection", "보안 ì—°ê²°"), ("Insecure Connection", "보안ë˜ì§€ ì•Šì€ ì—°ê²°"), - ("Scale original", "ì›ëž˜ í¬ê¸°"), - ("Scale adaptive", "ì°½ì— ë§žê²Œ"), + ("Scale original", "ì›ë³¸ í¬ê¸° ì¡°ì •"), + ("Scale adaptive", "í¬ê¸° ì¡°ì • 가능"), ("General", "ì¼ë°˜"), ("Security", "보안"), ("Theme", "테마"), ("Dark Theme", "ì–´ë‘ìš´ 테마"), ("Light Theme", "ë°ì€ 테마"), - ("Dark", "어둡게"), - ("Light", "ë°ê²Œ"), - ("Follow System", "시스템 기본값"), + ("Dark", "ì–´ë‘ìš´"), + ("Light", "ë°ì€"), + ("Follow System", "시스템 설정 따름"), ("Enable hardware codec", "하드웨어 ì½”ë± í™œì„±í™”"), ("Unlock Security Settings", "보안 설정 잠금 í•´ì œ"), - ("Enable audio", "오디오 활성화"), + ("Enable audio", "오디오 사용함"), ("Unlock Network Settings", "ë„¤íŠ¸ì›Œí¬ ì„¤ì • 잠금 í•´ì œ"), ("Server", "서버"), - ("Direct IP Access", "다ì´ë ‰íЏ IP ì—°ê²°"), + ("Direct IP Access", "ì§ì ‘ IP ì—°ê²°"), ("Proxy", "프ë¡ì‹œ"), ("Apply", "ì ìš©"), - ("Disconnect all devices?", "모든 ê¸°ê¸°ì˜ ì—°ê²°ì„ í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"), + ("Disconnect all devices?", "모든 ìž¥ì¹˜ì˜ ì—°ê²°ì„ í•´ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"), ("Clear", "지우기"), ("Audio Input Device", "오디오 ìž…ë ¥ 장치"), ("Use IP Whitelisting", "IP í™”ì´íŠ¸ë¦¬ìŠ¤íŠ¸ 사용"), ("Network", "네트워í¬"), - ("Pin Toolbar", "툴바 ê³ ì •"), - ("Unpin Toolbar", "툴바 ê³ ì • í•´ì œ"), + ("Pin Toolbar", "ë„구 ëª¨ìŒ ê³ ì •"), + ("Unpin Toolbar", "ë„구 ëª¨ìŒ ê³ ì • í•´ì œ"), ("Recording", "녹화"), - ("Directory", "경로"), - ("Automatically record incoming sessions", "들어오는 ì„¸ì…˜ì„ ìžë™ìœ¼ë¡œ 녹화"), - ("Automatically record outgoing sessions", "나가는 ì„¸ì…˜ì„ ìžë™ìœ¼ë¡œ 녹화"), + ("Directory", "디렉터리"), + ("Automatically record incoming sessions", "수신 세션 ìžë™ 녹화"), + ("Automatically record outgoing sessions", "발신 세션 ìžë™ 녹화"), ("Change", "변경"), ("Start session recording", "세션 녹화 시작"), ("Stop session recording", "세션 녹화 중지"), - ("Enable recording session", "세션 녹화 활성화"), - ("Enable LAN discovery", "LAN 검색 활성화"), + ("Enable 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", "ì—°ê²°ì´ ëŠê¹€"), + ("Please wait for confirmation of UAC...", "UAC 확ì¸ì„ 기다려주세요..."), + ("elevated_foreground_window_tip", "ì›ê²© ë°ìФí¬í†±ì˜ 현재 ì°½ì„ ìž‘ë™í•˜ë ¤ë©´ ë” ë†’ì€ ê¶Œí•œì´ í•„ìš”í•˜ë¯€ë¡œ ì¼ì‹œì ìœ¼ë¡œ 마우스와 키보드를 사용할 수 없습니다. ì›ê²© 사용ìžì—게 현재 ì°½ì„ ìµœì†Œí™”í•˜ë„ë¡ ìš”ì²­í•˜ê±°ë‚˜ ì—°ê²° 관리 ì°½ì—서 권한 ìƒìй ë²„íŠ¼ì„ í´ë¦­í•  수 있습니다. ì´ ë¬¸ì œë¥¼ 방지하려면 ì›ê²© ìž¥ì¹˜ì— ì†Œí”„íŠ¸ì›¨ì–´ë¥¼ 설치하는 ê²ƒì´ ì¢‹ìŠµë‹ˆë‹¤."), + ("Disconnected", "ì—°ê²° ëŠê¹€"), ("Other", "기타"), ("Confirm before closing multiple tabs", "여러 íƒ­ì„ ë‹«ê¸° ì „ì— í™•ì¸"), ("Keyboard Settings", "키보드 설정"), - ("Full Access", "ì „ì²´ 권한"), + ("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 ë°ìФí¬íƒ‘ì„ ì‹œë„하거나 OS를 변경하십시오."), - ("JumpLink", "ë§í¬ì—°ê²°"), - ("Please Select the screen to be shared(Operate on the peer side).", "공유할 í™”ë©´ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤(피어 측ì—서 ìž‘ë™)."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland는 ìƒìœ„ ë²„ì „ì˜ Linux ë°°í¬íŒì´ 필요합니다. X11 ë°ìФí¬í†±ì„ 사용하거나 OS를 변경하세요."), + ("JumpLink", "ì í”„ ë§í¬"), + ("Please Select the screen to be shared(Operate on the peer side).", "공유할 í™”ë©´ì„ ì„ íƒí•˜ì„¸ìš” (피어 측ì—서 ìž‘ë™)"), ("Show RustDesk", "RustDesk 표시"), ("This PC", "ì´ PC"), ("or", "ë˜ëŠ”"), ("Continue with", "계ì†"), ("Elevate", "권한 ìƒìй"), - ("Zoom cursor", "커서 줌"), + ("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...", "ì›ê²© 측ì—서 세션 ìš”ì²­ì„ ìˆ˜ë½í•  때까지 기다리십시오..."), + ("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", "ì ‘ê·¼ê¶Œí•œì˜ í—ˆìš©ì—¬ë¶€ë¥¼ 요청합니다"), + ("Request access to your device", "ìž¥ì¹˜ì— ëŒ€í•œ 액세스 ê¶Œí•œì„ ìš”ì²­"), ("Hide connection management window", "ì—°ê²° 관리 ì°½ 숨기기"), - ("hide_cm_tip", "숨기기는 비밀번호 ì—°ê²°ë§Œ 허용ë˜ê³  ê³ ì • 비밀번호만 사용ë˜ëŠ” 경우ì—ë§Œ 허용ë©ë‹ˆë‹¤"), - ("wayland_experiment_tip", "Wayland ì§€ì›ì€ 실험ì ìž…니다. ë¬´ì¸ ì ‘ê·¼ì´ í•„ìš”í•œ 경우 X11ì„ ì‚¬ìš©í•˜ì‹­ì‹œì˜¤"), - ("Right click to select tabs", "마우스 오른쪽 ë²„íŠ¼ì„ í´ë¦­í•˜ê³  íƒ­ì„ ì„ íƒí•˜ì„¸ìš”"), - ("Skipped", "건너뛰기"), + ("hide_cm_tip", "비밀번호를 통해 ì„¸ì…˜ì„ ìˆ˜ë½í•˜ê³  ì˜êµ¬ 비밀번호를 사용하는 경우ì—ë§Œ 숨기기 허용"), + ("wayland_experiment_tip", "Wayland ì§€ì›ì€ 실험 ë‹¨ê³„ì— ìžˆìœ¼ë©°, ë¬´ì¸ ì ‘ê·¼ì´ í•„ìš”í•œ 경우 X11ì„ ì‚¬ìš©í•´ 주세요."), + ("Right click to select tabs", "마우스 오른쪽 ë²„íŠ¼ì„ í´ë¦­í•˜ì—¬ 탭 ì„ íƒ"), + ("Skipped", "건너뜀"), ("Add to address book", "주소ë¡ì— 추가"), ("Group", "그룹"), ("Search", "검색"), ("Closed manually by web console", "웹 ì½˜ì†”ì— ì˜í•´ 수ë™ìœ¼ë¡œ 닫힘"), ("Local keyboard type", "로컬 키보드 유형"), ("Select local keyboard type", "로컬 키보드 유형 ì„ íƒ"), - ("software_render_tip", "Nvidia 그래픽 카드를 사용하고 ì„¸ì…˜ì´ ì„¤ì •ëœ í›„ ì›ê²© ì°½ì´ ì¦‰ì‹œ 닫히는 경우 nouveau 드ë¼ì´ë²„를 설치하고 소프트웨어 ë Œë”ë§ì„ 사용하ë„ë¡ ì„ íƒí•˜ëŠ” ê²ƒì´ ë„ì›€ì´ ë  ìˆ˜ 있습니다. 소프트웨어를 재시작하면 ì ìš©ë©ë‹ˆë‹¤."), + ("software_render_tip", "Linuxì—서 Nvidia 그래픽 카드를 사용 중ì¸ë° ì›ê²© ì°½ì´ ì—°ê²° 즉시 닫히는 경우 오픈 소스 Nouveau 드ë¼ì´ë²„로 전환하고 소프트웨어 ë Œë”ë§ì„ 사용하기로 ì„ íƒí•˜ëŠ” ê²ƒì´ ë„ì›€ì´ ë  ìˆ˜ 있습니다. 소프트웨어를 재시작해야 합니다."), ("Always use software rendering", "í•­ìƒ ì†Œí”„íŠ¸ì›¨ì–´ ë Œë”ë§ ì‚¬ìš©"), - ("config_input", "키보드를 통해 ì›ê²© ë°ìФí¬í†±ì„ 제어할 수 있으려면 RustDeskì˜ \"ìž…ë ¥ 모니터ë§\" ê¶Œí•œì„ ë¶€ì—¬í•´ 주세요"), - ("config_microphone", "마ì´í¬ë¥¼ 통한 오디오 ì „ì†¡ì„ ì§€ì›í•˜ë ¤ë©´ RustDeskì— \"ë…¹ìŒ\" ê¶Œí•œì„ ë¶€ì—¬í•´ 주세요"), - ("request_elevation_tip", "ìƒëŒ€ë°©ì´ 권한 ìƒìŠ¹ì„ ìš”ì²­í•  ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤"), + ("config_input", "키보드로 ì›ê²© ë°ìФí¬í†±ì„ 제어하려면 RustDeskì— \"ìž…ë ¥ 모니터ë§\" ê¶Œí•œì„ ë¶€ì—¬í•´ì•¼ 합니다."), + ("config_microphone", "ì›ê²©ìœ¼ë¡œ 통화하려면 RustDeskì— \"오디오 ë…¹ìŒ\" ê¶Œí•œì„ ë¶€ì—¬í•´ì•¼ 합니다."), + ("request_elevation_tip", "ì›ê²© ì¸¡ì— ì‚¬ëžŒì´ ìžˆëŠ” 경우 권한 ìƒìŠ¹ì„ ìš”ì²­í•  ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤."), ("Wait", "대기"), ("Elevation Error", "권한 ìƒìй 오류"), ("Ask the remote user for authentication", "ì›ê²© 사용ìžì—게 ì¸ì¦ 요청"), - ("Choose this if the remote account is administrator", "ì›ê²© ê³„ì •ì´ ê´€ë¦¬ìžì¸ 경우 ì„ íƒí•˜ì„¸ìš”"), - ("Transmit the username and password of administrator", "관리ìžì˜ ì‚¬ìš©ìž ì´ë¦„ê³¼ 비밀번호를 전송합니다"), - ("still_click_uac_tip", "í†µì œëœ ì‚¬ìš©ìžëŠ” 여전히 RustDesk를 실행하는 UAC ì°½ì—서 확ì¸ì„ í´ë¦­í•´ì•¼ 합니다"), + ("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", "권한 ìƒìŠ¹ì´ ì™„ë£Œë˜ì—ˆìŠµë‹ˆë‹¤"), + ("wait_accept_uac_tip", "ì›ê²© 사용ìžê°€ UAC 대화 ìƒìžë¥¼ 수ë½í•  때까지 기다리세요."), + ("Elevate successfully", "권한 ìƒìŠ¹ì´ ì„±ê³µí•˜ì˜€ìŠµë‹ˆë‹¤"), ("uppercase", "대문ìž"), ("lowercase", "소문ìž"), ("digit", "숫ìž"), - ("special character", "특수문ìž"), + ("special character", "특수 문ìž"), ("length>=8", "8ìž ì´ìƒ"), ("Weak", "약함"), ("Medium", "보통"), ("Strong", "ê°•ë ¥"), - ("Switch Sides", "제어 ë°©í–¥ 반전"), + ("Switch Sides", "측면 전환"), ("Please confirm if you want to share your desktop?", "ë°ìФí¬íƒ‘ì„ ê³µìœ í•˜ì‹œê² ìŠµë‹ˆê¹Œ?"), ("Display", "디스플레ì´"), ("Default View Style", "기본 보기 스타ì¼"), @@ -443,10 +439,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("FPS", "FPS"), ("Auto", "ìžë™"), ("Other Default Options", "기타 기본 옵션"), - ("Voice call", "ìŒì„±í†µí™”"), - ("Text chat", "채팅"), - ("Stop voice call", "ìŒì„±í†µí™” 종료"), - ("relay_hint_tip", "다ì´ë ‰íЏ ì—°ê²°ì´ ì•ˆë  ìˆ˜ë„ ìžˆìœ¼ë‹ˆ ë¦´ë ˆì´ ì—°ê²°ì„ ì‹œë„해보세요. \në˜í•œ ë¦´ë ˆì´ ì—°ê²°ì„ ë°”ë¡œ 사용하고 싶다면 ID ë’¤ì— /rì„ ì¶”ê°€í•˜ë©´ ë˜ê³ , 최근 ë°©ë¬¸ì— í•´ë‹¹ 카드가 존재한다면 카드 옵션ì—서 ë¦´ë ˆì´ ì—°ê²°ì„ ê°•ì œí•˜ë„ë¡ ì„ íƒí•  ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤."), + ("Voice call", "ìŒì„± 통화"), + ("Text chat", "í…스트 채팅"), + ("Stop voice call", "ìŒì„± 통화 종료"), + ("relay_hint_tip", "ì§ì ‘ ì—°ê²°ì´ ë¶ˆê°€ëŠ¥í•  수 있으며 릴레ì´ë¥¼ 통해 ì—°ê²°ì„ ì‹œë„í•  수 있습니다. ë˜í•œ 첫 번째 시ë„ì—서 릴레ì´ë¥¼ 사용하려면 ì•„ì´ë””ì— \"/r\" 접미사를 추가하거나 최근 세션 ì¹´ë“œì— \"í•­ìƒ ë¦´ë ˆì´ë¥¼ 통해 ì—°ê²°\" ì˜µì…˜ì´ ìžˆëŠ” 경우 ì´ ì˜µì…˜ì„ ì„ íƒí•˜ë©´ ë©ë‹ˆë‹¤."), ("Reconnect", "다시 ì—°ê²°"), ("Codec", "ì½”ë±"), ("Resolution", "í•´ìƒë„"), @@ -454,17 +450,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Set one-time password length", "ì¼íšŒìš© 비밀번호 ê¸¸ì´ ì„¤ì •"), ("RDP Settings", "RDP 설정"), ("Sort by", "ì •ë ¬ 기준"), - ("New Connection", "새로운 ì—°ê²°"), + ("New Connection", "새 ì—°ê²°"), ("Restore", "ë³µì›"), ("Minimize", "최소화"), ("Maximize", "최대화"), ("Your Device", "ë‚´ 장치"), - ("empty_recent_tip", "최근 ì„¸ì…˜ì´ ì—†ìŠµë‹ˆë‹¤. 새 ì„¸ì…˜ì„ ì‹œìž‘í•´ë³´ì„¸ìš”"), - ("empty_favorite_tip", "장치 ì¦ê²¨ì°¾ê¸°ê°€ 없습니다. 새 ì¦ê²¨ì°¾ê¸°ë¥¼ 추가해보세요"), - ("empty_lan_tip", "제어ë˜ëŠ” 장치가 발견ë˜ì§€ 않았습니다."), - ("empty_address_book_tip", "현재 주소ë¡ì— 제어ë˜ëŠ” í´ë¼ì´ì–¸íŠ¸ê°€ 없습니다"), - ("eg: admin", "예: 관리ìž"), - ("Empty Username", "사용ìžëª…ì´ ë¹„ì–´ìžˆìŠµë‹ˆë‹¤"), + ("empty_recent_tip", "어머나, 최근 ì„¸ì…˜ì´ ì—†ë„¤ìš”!\n새로운 ê²ƒì„ ê³„íší•  시간입니다."), + ("empty_favorite_tip", "ì•„ì§ ì¦ê²¨ì°¾ëŠ” 피어가 없나요?\n연결하고 ì‹¶ì€ í”¼ì–´ë¥¼ 찾아 ì¦ê²¨ì°¾ê¸°ì— 추가해 보세요!"), + ("empty_lan_tip", "오 아니요, ì•„ì§ í”¼ì–´ë¥¼ 발견하지 못한 것 같습니다."), + ("empty_address_book_tip", "오, ì´ê²Œ 무슨 ì¼ì¸ì§€ 주소ë¡ì— 현재 ë‚˜ì—´ëœ í”¼ì–´ê°€ 없는 것 같습니다."), + ("Empty Username", "ì‚¬ìš©ìž ì´ë¦„ì´ ë¹„ì–´ìžˆìŠµë‹ˆë‹¤"), ("Empty Password", "비밀번호가 비어있습니다"), ("Me", "나"), ("identical_file_tip", "ì´ íŒŒì¼ì€ ìƒëŒ€ë°©ì˜ 파ì¼ê³¼ ì¼ì¹˜í•©ë‹ˆë‹¤."), @@ -472,189 +467,247 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("View Mode", "보기 모드"), ("login_linux_tip", "X ë°ìФí¬íƒ‘ì„ í™œì„±í™”í•˜ë ¤ë©´ 제어ë˜ëŠ” 터미ë„ì˜ Linux ê³„ì •ì— ë¡œê·¸ì¸í•˜ì„¸ìš”"), ("verify_rustdesk_password_tip", "RustDesk 비밀번호 확ì¸"), - ("remember_account_tip", "ì´ ê³„ì •ì„ ê¸°ì–µí•˜ì„¸ìš”"), - ("os_account_desk_tip", "모니터가 없는 환경ì—서 ì´ ê³„ì •ì€ ì œì–´ë˜ëŠ” ì‹œìŠ¤í…œì— ë¡œê·¸ì¸í•˜ê³  ë°ìФí¬íƒ‘ì„ í™œì„±í™”í•˜ëŠ” ë° ì‚¬ìš©ë©ë‹ˆë‹¤"), + ("remember_account_tip", "ì´ ê³„ì • 기억하기"), + ("os_account_desk_tip", "ì´ ê³„ì •ì€ ì›ê²© OSì— ë¡œê·¸ì¸í•˜ê³  헤드리스ì—서 ë°ìФí¬í†± ì„¸ì…˜ì„ í™œì„±í™”í•˜ëŠ” ë° ì‚¬ìš©ë©ë‹ˆë‹¤."), ("OS Account", "OS 계정"), - ("another_user_login_title_tip", "다른 사용ìžê°€ 로그ì¸ë˜ì–´ 있습니다"), - ("another_user_login_text_tip", "ì—°ê²° 종료"), - ("xorg_not_found_title_tip", "Xorgê°€ 설치ë˜ì§€ 않았습니다"), - ("xorg_not_found_text_tip", "Xorg를 설치해주세요"), - ("no_desktop_title_tip", "ë°ìФí¬íƒ‘ì´ ì„¤ì¹˜ë˜ì§€ 않았습니다"), - ("no_desktop_text_tip", "ë°ìФí¬íƒ‘ì„ ì„¤ì¹˜í•´ì£¼ì„¸ìš”"), - ("No need to elevate", "권한 ìƒìŠ¹ì´ í•„ìš”í•˜ì§€ 않습니다."), - ("System Sound", "시스템 사운드"), + ("another_user_login_title_tip", "다른 사용ìžê°€ ì´ë¯¸ 로그ì¸í–ˆìŠµë‹ˆë‹¤"), + ("another_user_login_text_tip", "ì—°ê²° ëŠê¸°"), + ("xorg_not_found_title_tip", "Xorg를 ì°¾ì„ ìˆ˜ 없습니다"), + ("xorg_not_found_text_tip", "Xorg를 설치해 주세요"), + ("no_desktop_title_tip", "사용 가능한 ë°ìФí¬í†± í™˜ê²½ì´ ì—†ìŠµë‹ˆë‹¤"), + ("no_desktop_text_tip", "GNOME ë°ìФí¬í†±ì„ 설치해 주세요"), + ("No need to elevate", "권한 ìƒìŠ¹ì´ í•„ìš”ì—†ìŠµë‹ˆë‹¤"), + ("System Sound", "시스템 소리"), ("Default", "기본"), - ("New RDP", "새로운 RDP"), + ("New RDP", "새 RDP"), ("Fingerprint", "지문"), ("Copy Fingerprint", "지문 복사"), ("no fingerprints", "ì§€ë¬¸ì´ ì—†ìŠµë‹ˆë‹¤"), - ("Select a peer", "ë™ë£Œë¥¼ ì„ íƒí•˜ì„¸ìš”"), - ("Select peers", "ë™ë£Œ ì„ íƒ"), + ("Select a peer", "피어 ì„ íƒ"), + ("Select peers", "피어 ì„ íƒ"), ("Plugins", "플러그ì¸"), - ("Uninstall", "제거"), + ("Uninstall", "설치 제거"), ("Update", "ì—…ë°ì´íЏ"), - ("Enable", "활성화"), - ("Disable", "비활성화"), + ("Enable", "사용함"), + ("Disable", "사용 안 함"), ("Options", "옵션"), - ("resolution_original_tip", "기본 í•´ìƒë„"), - ("resolution_fit_local_tip", "로컬 í•´ìƒë„로 변경"), - ("resolution_custom_tip", "맞춤 í•´ìƒë„"), - ("Collapse toolbar", "툴바 접기"), - ("Accept and Elevate", "권한 ìƒìй 승ì¸"), - ("accept_and_elevate_btn_tooltip", "UAC 권한 ìƒìй ë° ì—°ê²° 승ì¸"), - ("clipboard_wait_response_timeout_tip", "복사 ì‘답 ì‹œê°„ì´ ì´ˆê³¼ë˜ì—ˆìŠµë‹ˆë‹¤."), - ("Incoming connection", "ì—°ê²°ì´ ìš”ì²­ë˜ì—ˆìŠµë‹ˆë‹¤"), - ("Outgoing connection", "나가는 ì—°ê²°"), - ("Exit", "나가기"), + ("resolution_original_tip", "ì›ë³¸ í•´ìƒë„"), + ("resolution_fit_local_tip", "로컬 í™”ë©´ì— ë§žì¶¤"), + ("resolution_custom_tip", "ì‚¬ìš©ìž ì§€ì • í•´ìƒë„"), + ("Collapse toolbar", "ë„구 ëª¨ìŒ ì ‘ê¸°"), + ("Accept and Elevate", "ìˆ˜ë½ ë° ê¶Œí•œ ìƒìй"), + ("accept_and_elevate_btn_tooltip", "ì—°ê²°ì„ ìˆ˜ë½í•˜ê³  UAC ê¶Œí•œì„ ë†’ìž…ë‹ˆë‹¤."), + ("clipboard_wait_response_timeout_tip", "복사 ì‘ë‹µì„ ê¸°ë‹¤ë¦¬ëŠ” ë™ì•ˆ ì‹œê°„ì´ ì´ˆê³¼ë˜ì—ˆìŠµë‹ˆë‹¤."), + ("Incoming connection", "수신 ì—°ê²°"), + ("Outgoing connection", "발신 ì—°ê²°"), + ("Exit", "종료"), ("Open", "열기"), - ("logout_tip", "ì •ë§ ë¡œê·¸ì•„ì›ƒí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"), + ("logout_tip", "로그아웃하시겠습니까?"), ("Service", "서비스"), ("Start", "시작"), ("Stop", "중지"), - ("exceed_max_devices", "관리ë˜ëŠ” 장치가 ìµœëŒ€ì¹˜ì— ë„달했습니다."), + ("exceed_max_devices", "관리ë˜ëŠ” ìž¥ì¹˜ì˜ ìµœëŒ€ ìˆ˜ì— ë„달했습니다."), ("Sync with recent sessions", "최근 세션과 ë™ê¸°í™”"), ("Sort tags", "태그 ì •ë ¬"), ("Open connection in new tab", "새 탭ì—서 ì—°ê²° 열기"), - ("Move tab to new window", "íƒ­ì„ ìƒˆ 창으로 ì´ë™"), + ("Move tab to new window", "새 창으로 탭 ì´ë™"), ("Can not be empty", "비워둘 수 없습니다"), - ("Already exists", "ì´ë¯¸ 존재 합니다"), + ("Already exists", "ì´ë¯¸ 존재합니다"), ("Change Password", "비밀번호 변경"), - ("Refresh Password", "비밀번호 새로고침"), + ("Refresh Password", "비밀번호 새로 고침"), ("ID", "ID"), - ("Grid View", "그리드 보기"), - ("List View", "리스트 보기"), + ("Grid View", "ê²©ìž ë³´ê¸°"), + ("List View", "ëª©ë¡ ë³´ê¸°"), ("Select", "ì„ íƒ"), ("Toggle Tags", "태그 전환"), - ("pull_ab_failed_tip", "주소ë¡ì„ 가져오지 못했습니다."), - ("push_ab_failed_tip", "ì£¼ì†Œë¡ ì—…ë¡œë“œ 실패"), - ("synced_peer_readded_tip", "최근 ì„¸ì…˜ì— ìžˆëŠ” 장치는 주소ë¡ì— 다시 ë™ê¸°í™”ë©ë‹ˆë‹¤."), + ("pull_ab_failed_tip", "주소ë¡ì„ 새로 고치지 못했습니다"), + ("push_ab_failed_tip", "주소ë¡ì„ ì„œë²„ì— ë™ê¸°í™”하지 못했습니다"), + ("synced_peer_readded_tip", "최근 ì„¸ì…˜ì— ìžˆë˜ ìž¥ì¹˜ë“¤ì´ ì£¼ì†Œë¡ìœ¼ë¡œ 다시 ë™ê¸°í™”ë  ê²ƒìž…ë‹ˆë‹¤."), ("Change Color", "ìƒ‰ìƒ ë³€ê²½"), ("Primary Color", "기본 색ìƒ"), ("HSV Color", "HSV 색ìƒ"), - ("Installation Successful!", "설치 성공!"), - ("Installation failed!", "설치 실패!"), + ("Installation Successful!", "ì„¤ì¹˜ì— ì„±ê³µí–ˆìŠµë‹ˆë‹¤!"), + ("Installation failed!", "ì„¤ì¹˜ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤!"), ("Reverse mouse wheel", "마우스 휠 반전"), ("{} sessions", "{} 세션"), - ("scam_title", "ë‹¹ì‹ ì€ ì‚¬ê¸°ë¥¼ ë‹¹í–ˆì„ ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤"), - ("scam_text1", "모르는 사람과 통화 중ì´ê³  ê·¸ë“¤ì´ RustDesk를 사용하여 서비스를 시작하ë¼ê³  요청하는 경우, 계ì†í•˜ì§€ ë§ê³  즉시 전화를 ëŠìœ¼ì„¸ìš”"), - ("scam_text2", "ê·¸ë“¤ì€ ê·€í•˜ì˜ ëˆì´ë‚˜ 기타 ê°œì¸ ì •ë³´ë¥¼ 훔치려는 ì‚¬ê¸°ê¾¼ì¼ ê°€ëŠ¥ì„±ì´ ë†’ìŠµë‹ˆë‹¤"), + ("scam_title", "사기를 당하고 ìžˆì„ ìˆ˜ 있습니다!"), + ("scam_text1", "알지 못하고 신뢰할 수 없는 ì‚¬ëžŒì´ ì „í™”ë¥¼ 걸어 RustDesk를 사용하고 서비스를 시작하ë¼ê³  요청하는 경우 ê³„ì† ì§„í–‰í•˜ì§€ ë§ê³  즉시 전화를 ëŠìœ¼ì„¸ìš”."), + ("scam_text2", "ì‚¬ê¸°ê¾¼ì´ ê·€í•˜ì˜ ëˆì´ë‚˜ 기타 ê°œì¸ ì •ë³´ë¥¼ 훔치려 í•  ê°€ëŠ¥ì„±ì´ ë†’ìŠµë‹ˆë‹¤."), ("Don't show again", "다시 표시하지 않ìŒ"), ("I Agree", "ë™ì˜"), ("Decline", "ê±°ì ˆ"), - ("Timeout in minutes", "시간 초과(ë¶„)"), - ("auto_disconnect_option_tip", "비활성 세션 ìžë™ 종료"), - ("Connection failed due to inactivity", "장시간 활ë™ì´ 없어 ì—°ê²°ì´ ìžë™ìœ¼ë¡œ 종료ë˜ì—ˆìŠµë‹ˆë‹¤"), + ("Timeout in minutes", "시간 초과 (ë¶„)"), + ("auto_disconnect_option_tip", "사용ìžê°€ 비활성 ìƒíƒœì¼ 때 수신 세션 ìžë™ 종료"), + ("Connection failed due to inactivity", "활ë™ì´ 없어 ìžë™ìœ¼ë¡œ ì—°ê²°ì´ ëŠì–´ì¡ŒìŠµë‹ˆë‹¤"), ("Check for software update on startup", "시작 시 소프트웨어 ì—…ë°ì´íЏ 확ì¸"), - ("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk Server Pro를 {} 버전 ì´ìƒìœ¼ë¡œ 업그레ì´ë“œí•˜ì‹­ì‹œì˜¤!"), - ("pull_group_failed_tip", "그룹 정보를 가져오지 못했습니다"), - ("Filter by intersection", "êµì°¨ë¡œë¡œ í•„í„°ë§"), - ("Remove wallpaper during incoming sessions", "세션 수ë½ì‹œ 배경화면 제거"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk Server Pro를 {} 버전 ì´ìƒìœ¼ë¡œ 업그레ì´ë“œí•˜ì„¸ìš”!"), + ("pull_group_failed_tip", "그룹 새로 ê³ ì¹¨ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤"), + ("Filter by intersection", "êµì°¨í•´ì„œ í•„í„°ë§"), + ("Remove wallpaper during incoming sessions", "수신 세션 ë™ì•ˆ 배경화면 제거"), ("Test", "테스트"), - ("display_is_plugged_out_msg", "디스플레ì´ê°€ ì—°ê²°ë˜ì–´ 있지 않습니다. 첫 번째 디스플레ì´ë¡œ 전환하세요."), + ("display_is_plugged_out_msg", "디스플레ì´ê°€ 분리ë˜ì–´ 있으면 첫 번째 디스플레ì´ë¡œ 전환합니다."), ("No displays", "ë””ìŠ¤í”Œë ˆì´ ì—†ìŒ"), ("Open in new window", "새 ì°½ì—서 열기"), ("Show displays as individual windows", "디스플레ì´ë¥¼ 개별 창으로 표시"), - ("Use all my displays for the remote session", "ì›ê²© ì„¸ì…˜ì— ë‚´ 디스플레ì´ë¥¼ ëª¨ë‘ ì‚¬ìš©"), - ("selinux_tip", "SELinux를 활성화하면 RustDeskê°€ 호스트로 제대로 실행ë˜ì§€ ì•Šì„ ìˆ˜ 있습니다"), + ("Use all my displays for the remote session", "ì›ê²© ì„¸ì…˜ì— ë‚´ 모든 ë””ìŠ¤í”Œë ˆì´ ì‚¬ìš©"), + ("selinux_tip", "SELinuxê°€ 장치ì—서 활성화ë˜ì–´ 있어 RustDeskê°€ ì œì–´ëœ ìƒíƒœë¡œ 제대로 ìž‘ë™í•˜ì§€ ì•Šì„ ìˆ˜ 있습니다."), ("Change view", "보기 변경"), ("Big tiles", "í° íƒ€ì¼"), ("Small tiles", "ìž‘ì€ íƒ€ì¼"), - ("List", "리스트"), + ("List", "목ë¡"), ("Virtual display", "ê°€ìƒ ë””ìŠ¤í”Œë ˆì´"), - ("Plug out all", "ì „ì› ëª¨ë‘ ë„기"), + ("Plug out all", "모든 플러그를 뽑으세요"), ("True color (4:4:4)", "트루컬러 (4:4:4)"), - ("Enable blocking user input", "ì‚¬ìš©ìž ìž…ë ¥ 차단 허용"), - ("id_input_tip", "ìž…ë ¥ëœ ID, IP, ë„ë©”ì¸ê³¼ í¬íЏ(:)를 입력할 수 있습니다.\n다른 ì„œë²„ì— ìžˆëŠ” ìž¥ì¹˜ì— ì—°ê²°í•˜ë ¤ë©´ 서버 주소(@?key=)를 추가하세요"), + ("Enable blocking user input", "ì‚¬ìš©ìž ìž…ë ¥ 차단 사용함"), + ("id_input_tip", "ID, ì§ì ‘ IP ë˜ëŠ” í¬íŠ¸ê°€ 있는 ë„ë©”ì¸ (:)ì„ ìž…ë ¥í•  수 있습니다.\n다른 ì„œë²„ì— ìžˆëŠ” ìž¥ì¹˜ì— ì•¡ì„¸ìŠ¤í•˜ë ¤ë©´ 서버 주소 (@?key=)를 추가하세요. 예를들어 \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\n공용 ì„œë²„ì˜ ìž¥ì¹˜ì— ì•¡ì„¸ìŠ¤í•˜ë ¤ë©´ \"@public\"ì„ ìž…ë ¥í•˜ì„¸ìš”. 공용 서버ì—서는 키가 필요하지 않습니다.\n\n첫 번째 ì—°ê²°ì—서 ë¦´ë ˆì´ ì—°ê²°ì„ ê°•ì œë¡œ 사용하려면 ID ëì— \"/r\"ì„ ì¶”ê°€í•©ë‹ˆë‹¤, 예를들면 \"9123456234/r\"."), ("privacy_mode_impl_mag_tip", "모드 1"), ("privacy_mode_impl_virtual_display_tip", "모드 2"), - ("Enter privacy mode", "ê°œì¸ì •ë³´ 보호 모드 사용"), + ("Enter privacy mode", "ê°œì¸ì •ë³´ 보호 모드 시작"), ("Exit privacy mode", "ê°œì¸ì •ë³´ 보호 모드 종료"), ("idd_not_support_under_win10_2004_tip", "ê°„ì ‘ ë””ìŠ¤í”Œë ˆì´ ë“œë¼ì´ë²„는 ì§€ì›ë˜ì§€ 않습니다. Windows 10 버전 2004 ì´ìƒì´ 필요합니다."), - ("input_source_1_tip", "입력소스 1"), - ("input_source_2_tip", "입력소스 2"), + ("input_source_1_tip", "ìž…ë ¥ 소스 1"), + ("input_source_2_tip", "ìž…ë ¥ 소스 2"), ("Swap control-command key", "Control ë° Command 키 êµì²´"), - ("swap-left-right-mouse", "마우스 왼쪽 버튼과 오른쪽 버튼 바꾸기"), - ("2FA code", "2단계 ì¸ì¦ 코드"), - ("More", "ë”보기"), - ("enable-2fa-title", "2단계 ì¸ì¦ 활성화"), - ("enable-2fa-desc", "지금 ì¸ì¦ìžë¥¼ 설정하세요. 휴대í°ì´ë‚˜ ë°ìФí¬í†± 컴퓨터ì—서 Authy, Microsoft ë˜ëŠ” Google Authenticator와 ê°™ì€ ì¸ì¦ê¸°ë¥¼ 사용할 수 있습니다. ì¸ì¦ê¸°ë¡œ QR 코드를 스캔하고 í‘œì‹œëœ ì½”ë“œë¥¼ 입력하면 2단계 ì¸ì¦ì´ 활성화ë©ë‹ˆë‹¤. "), - ("wrong-2fa-code", "ì´ ì½”ë“œëŠ” 확ì¸í•  수 없습니다. ì¸ì¦ 코드와 현지 시간 ì„¤ì •ì´ ì˜¬ë°”ë¥¸ì§€ 확ì¸í•˜ì„¸ìš”."), - ("enter-2fa-title", "2단계 ì¸ì¦"), + ("swap-left-right-mouse", "마우스 왼쪽 버튼과 오른쪽 버튼 êµì²´"), + ("2FA code", "ì´ì¤‘ ì¸ì¦ 코드"), + ("More", "ë” ë§Žì€"), + ("enable-2fa-title", "ì´ì¤‘ ì¸ì¦ 사용함"), + ("enable-2fa-desc", "지금 ì¸ì¦ì•±ì„ 설정해 주세요. 휴대í°ì´ë‚˜ ë°ìФí¬í†±ì—서 Authy, Microsoft ë˜ëŠ” Google ì¸ì¦ê¸°ì™€ ê°™ì€ ì¸ì¦ê¸° ì•±ì„ ì‚¬ìš©í•  수 있습니다.\n\n앱으로 QR 코드를 스캔하고 ì•±ì— í‘œì‹œëœ ì½”ë“œë¥¼ 입력하면 ì´ì¤‘ ì¸ì¦ì´ 가능합니다."), + ("wrong-2fa-code", "코드를 확ì¸í•  수 없습니다. 코드와 현지 시간 ì„¤ì •ì´ ì˜¬ë°”ë¥¸ì§€ 확ì¸í•©ë‹ˆë‹¤"), + ("enter-2fa-title", "ì´ì¤‘ ì¸ì¦"), ("Email verification code must be 6 characters.", "ì´ë©”ì¼ ì¸ì¦ 코드는 6ìžì—¬ì•¼ 합니다."), - ("2FA code must be 6 digits.", "2단계 ì¸ì¦ 코드는 6ìžë¦¬ì—¬ì•¼ 합니다."), - ("Multiple Windows sessions found", "여러 Windows ì„¸ì…˜ì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤."), - ("Please select the session you want to connect to", "연결하려는 ì„¸ì…˜ì„ ì„ íƒí•˜ì„¸ìš”."), + ("2FA code must be 6 digits.", "ì´ì¤‘ ì¸ì¦ 코드는 6ìžë¦¬ì—¬ì•¼ 합니다."), + ("Multiple Windows sessions found", "여러 Windows ì„¸ì…˜ì´ ë°œê²¬ë˜ì—ˆìŠµë‹ˆë‹¤"), + ("Please select the session you want to connect to", "ì—°ê²°í•  ì„¸ì…˜ì„ ì„ íƒí•´ 주세요"), ("powered_by_me", "RustDesk 제공"), - ("outgoing_only_desk_tip", "ì´ê²ƒì€ 맞춤형 버전입니다.\n다른 ìž¥ì¹˜ì— ì—°ê²°í•  수 있지만 다른 장치는 ê·€í•˜ì˜ ìž¥ì¹˜ì— ì—°ê²°í•  수 없습니다."), - ("preset_password_warning", "ì´ ë§žì¶¤í˜• ì—ë””ì…˜ì€ ë¯¸ë¦¬ ì„¤ì •ëœ ë¹„ë°€ë²ˆí˜¸ê°€ í¬í•¨ë˜ì–´ 있습니다. ì´ ë¹„ë°€ë²ˆí˜¸ë¥¼ 아는 ì‚¬ëžŒì€ ëˆ„êµ¬ë‚˜ ê·€í•˜ì˜ ìž¥ì¹˜ë¥¼ 완전히 제어할 수 있습니다. ì´ ìƒí™©ì„ 예ìƒí•˜ì§€ 못했다면, 즉시 소프트웨어를 삭제하시기 ë°”ëžë‹ˆë‹¤."), + ("outgoing_only_desk_tip", "ì´ê²ƒì€ 맞춤형 ì—디션입니다.\n다른 ìž¥ì¹˜ì— ì—°ê²°í•  수는 있지만 ê·€í•˜ì˜ ê¸°ê¸°ì— ì—°ê²°í•  수 없습니다."), + ("preset_password_warning", "ì´ ë§žì¶¤í˜• ì—디션ì—는 미리 ì„¤ì •ëœ ë¹„ë°€ë²ˆí˜¸ê°€ 함께 제공ë©ë‹ˆë‹¤. ì´ ë¹„ë°€ë²ˆí˜¸ë¥¼ 아는 사람ì´ë¼ë©´ 누구나 기기를 완전히 제어할 수 있습니다. 예ìƒì¹˜ 못한 경우 즉시 소프트웨어를 제거하세요."), ("Security Alert", "보안 경고"), ("My address book", "ë‚´ 주소ë¡"), ("Personal", "ê°œì¸"), ("Owner", "소유ìž"), - ("Set shared password", "공유 암호 설정"), - ("Exist in", "존재함"), - ("Read-only", "ì½ê¸°ì „ìš©"), + ("Set shared password", "공유 비밀번호 설정"), + ("Exist in", "ë‹¤ìŒ ìœ„ì¹˜ 존재"), + ("Read-only", "ì½ê¸° ì „ìš©"), ("Read/Write", "ì½ê¸°/쓰기"), ("Full Control", "ì „ì²´ 제어"), - ("share_warning_tip", "ìœ„ì˜ í•­ëª©ë“¤ì€ ë‹¤ë¥¸ 사람들과 공유ë˜ë©°, 다른 ì‚¬ëžŒë“¤ì´ ë³¼ 수 있습니다."), + ("share_warning_tip", "ìœ„ì˜ í•„ë“œëŠ” 공유ë˜ê³  다른 사람들ì—게 보입니다."), ("Everyone", "모ë‘"), ("ab_web_console_tip", "웹 ì½˜ì†”ì— ëŒ€í•´ ë” ì•Œì•„ë³´ê¸°"), - ("allow-only-conn-window-open-tip", "RustDesk ì°½ì´ ì—´ë ¤ 있는 경우ì—ë§Œ ì—°ê²° 허용"), - ("no_need_privacy_mode_no_physical_displays_tip", "ë¬¼ë¦¬ì  ë””ìŠ¤í”Œë ˆì´ê°€ 없으므로 프ë¼ì´ë²„시 모드를 사용할 필요가 없습니다."), - ("Follow remote cursor", "ì›ê²© 커서 따르기"), - ("Follow remote window focus", "ì›ê²© ì°½ í¬ì»¤ìФ 따르기"), - ("default_proxy_tip", "기본 프로토콜과 í¬íŠ¸ëŠ” Socks5와 1080입니다."), + ("allow-only-conn-window-open-tip", "RustDesk ì°½ì´ ì—´ë ¤ ìžˆì„ ë•Œë§Œ ì—°ê²° 허용"), + ("no_need_privacy_mode_no_physical_displays_tip", "실제 디스플레ì´ê°€ 없으므로 ê°œì¸ ì •ë³´ 보호 모드를 사용할 필요가 없습니다."), + ("Follow remote cursor", "ì›ê²© 커서 ë”°ë¼ê°€ê¸°"), + ("Follow remote window focus", "ì›ê²© ì°½ ì´ˆì  ë”°ë¼ê°€ê¸°"), + ("default_proxy_tip", "기본 프로토콜 ë° í¬íŠ¸ëŠ” Socks5 ë° 1080입니다"), ("no_audio_input_device_tip", "오디오 ìž…ë ¥ 장치를 ì°¾ì„ ìˆ˜ 없습니다."), - ("Incoming", "수신중"), - ("Outgoing", "발신중"), - ("Clear Wayland screen selection", "Wayland 화면 ì„ íƒ ì·¨ì†Œ"), - ("clear_Wayland_screen_selection_tip", "화면 ì„ íƒì„ 취소한 후 다시 공유할 í™”ë©´ì„ ì„ íƒí•  수 있습니다."), + ("Incoming", "수신"), + ("Outgoing", "발신"), + ("Clear Wayland screen selection", "Wayland 화면 ì„ íƒ ì§€ìš°ê¸°"), + ("clear_Wayland_screen_selection_tip", "화면 ì„ íƒì„ 지운 후, 공유할 í™”ë©´ì„ ë‹¤ì‹œ ì„ íƒí•  수 있습니다."), ("confirm_clear_Wayland_screen_selection_tip", "Wayland 화면 ì„ íƒì„ ì •ë§ ì·¨ì†Œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"), - ("android_new_voice_call_tip", "새로운 ìŒì„± 통화 ìš”ì²­ì´ ìžˆìŠµë‹ˆë‹¤. 수ë½í•˜ë©´ 오디오가 ìŒì„± 통신으로 전환ë©ë‹ˆë‹¤."), - ("texture_render_tip", "í…스처 ë Œë”ë§ì„ 사용하면 ì´ë¯¸ì§€ê°€ ë” ë¶€ë“œëŸ¬ì›Œì§‘ë‹ˆë‹¤. ë Œë”ë§ ë¬¸ì œê°€ ë°œìƒí•˜ë©´ ì´ ì˜µì…˜ì„ ë¹„í™œì„±í™”í•´ 보세요."), + ("android_new_voice_call_tip", "새 ìŒì„± 통화 ìš”ì²­ì´ ìˆ˜ì‹ ë˜ì—ˆìŠµë‹ˆë‹¤. 수ë½í•˜ë©´ 오디오가 ìŒì„± 통신으로 전환ë©ë‹ˆë‹¤."), + ("texture_render_tip", "í…스처 ë Œë”ë§ì„ 사용하여 ì‚¬ì§„ì„ ë” ë¶€ë“œëŸ½ê²Œ 만듭니다. ë Œë”ë§ ë¬¸ì œê°€ ë°œìƒí•˜ë©´ ì´ ì˜µì…˜ì„ ë¹„í™œì„±í™”í•  수 있습니다."), ("Use texture rendering", "í…스처 ë Œë”ë§ ì‚¬ìš©"), - ("Floating window", "플로팅 윈ë„ìš°"), - ("floating_window_tip", "RustDesk 백그ë¼ìš´ë“œ 서비스를 유지하는 ê²ƒì´ ì¢‹ìŠµë‹ˆë‹¤."), + ("Floating window", "플로팅 ì°½"), + ("floating_window_tip", "RustDesk 백그ë¼ìš´ë“œ 서비스를 유지하는 ë° ë„ì›€ì´ ë©ë‹ˆë‹¤"), ("Keep screen on", "화면 ì¼œì§ ìœ ì§€"), ("Never", "ì—†ìŒ"), ("During controlled", "제어ë˜ëŠ” ë™ì•ˆ"), - ("During service is on", "서비스가 켜져 있는 ë™ì•ˆ"), - ("Capture screen using DirectX", "다ì´ë ‰íЏX를 사용한 화면 캡처"), + ("During service is on", "서비스 중"), + ("Capture screen using DirectX", "DirectX를 사용하여 화면 캡처"), ("Back", "뒤로"), ("Apps", "앱"), - ("Volume up", "볼륨 ì¦ê°€"), - ("Volume down", "볼륨 ê°ì†Œ"), - ("Power", "파워"), - ("Telegram bot", "텔레그램 ë´‡"), - ("enable-bot-tip", "ì´ ê¸°ëŠ¥ì„ í™œì„±í™”í•˜ë©´ 봇으로부터 2FA 코드를 ë°›ì„ ìˆ˜ 있습니다. ë˜í•œ ì—°ê²° ì•Œë¦¼ìœ¼ë¡œë„ ìž‘ë™í•  수 있습니다."), - ("enable-bot-desc", "1. @BotFather와 ì±„íŒ…ì„ ì‹œìž‘í•˜ì„¸ìš”.\n2. \"/newbot\" 명령어를 보내세요. 토í°ì„ 받게 ë©ë‹ˆë‹¤.\n3. 새로 ìƒì„±ëœ 봇과 ì±„íŒ…ì„ ì‹œìž‘í•˜ê³  \"/hello\" ë“±ì˜ ëª…ë ¹ì–´ë¥¼ ë³´ë‚´ ë´‡ì„ í™œì„±í™”í•˜ì„¸ìš”."), - ("cancel-2fa-confirm-tip", "2FA를 ì •ë§ ì·¨ì†Œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"), - ("cancel-bot-confirm-tip", "텔레그램 ë´‡ì„ ì •ë§ ì‚­ì œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"), - ("About RustDesk", "RustDesk 대하여"), - ("Send clipboard keystrokes", "í´ë¦½ë³´ë“œ 키 ìž…ë ¥ 전송"), - ("network_error_tip", "ë„¤íŠ¸ì›Œí¬ ì—°ê²°ì„ í™•ì¸í•œ 후 다시 시ë„하세요."), - ("Unlock with PIN", "핀으로 잠금 í•´ì œ"), + ("Volume up", "볼륨 높ì´ê¸°"), + ("Volume down", "볼륨 낮추기"), + ("Power", "ì „ì›"), + ("Telegram bot", "Telegram ë´‡"), + ("enable-bot-tip", "ì´ ê¸°ëŠ¥ì„ í™œì„±í™”í•˜ë©´ ë´‡ì—서 ì´ì¤‘ ì¸ì¤‘ 코드를 ë°›ì„ ìˆ˜ 있습니다. ë˜í•œ ì—°ê²° 알림 ê¸°ëŠ¥ë„ í•  수 있습니다."), + ("enable-bot-desc", "1. @BotFather와 ì±„íŒ…ì„ ì‹œìž‘í•©ë‹ˆë‹¤.\n2. \"/newbot\" ëª…ë ¹ì„ ë³´ë‚´ì£¼ì„¸ìš”. ì´ ë‹¨ê³„ë¥¼ 완료하면 토í°ì„ 받게 ë©ë‹ˆë‹¤.\n3. 새로 만든 봇과 ì±„íŒ…ì„ ì‹œìž‘í•©ë‹ˆë‹¤. \"/hello\"와 ê°™ì´ ì•žì— ìŠ¬ëž˜ì‹œ (\"/\")로 시작하는 메시지를 ë³´ë‚´ 활성화합니다."), + ("cancel-2fa-confirm-tip", "ì´ì¤‘ ì¸ì¦ì„ 취소하시겠습니까?"), + ("cancel-bot-confirm-tip", "Telegram ë´‡ì„ ì·¨ì†Œí•˜ì‹œê² ìŠµë‹ˆê¹Œ?"), + ("About RustDesk", "RustDesk ì •ë³´"), + ("Send clipboard keystrokes", "í´ë¦½ë³´ë“œ 키 ìž…ë ¥ 보내기"), + ("network_error_tip", "ë„¤íŠ¸ì›Œí¬ ì—°ê²°ì„ í™•ì¸í•œ ë‹¤ìŒ ìž¬ì‹œë„를 í´ë¦­í•˜ì„¸ìš”."), + ("Unlock with PIN", "PIN으로 잠금 í•´ì œ"), ("Requires at least {} characters", "최소 {}ìž ì´ìƒ 필요합니다."), - ("Wrong PIN", "ìž˜ëª»ëœ í•€"), - ("Set PIN", "í•€ 설정"), - ("Enable trusted devices", "신뢰할 수 있는 장치 활성화"), + ("Wrong PIN", "ìž˜ëª»ëœ PIN"), + ("Set PIN", "PIN 설정"), + ("Enable trusted devices", "신뢰할 수 있는 장치 사용함"), ("Manage trusted devices", "신뢰할 수 있는 장치 관리"), ("Platform", "플랫í¼"), ("Days remaining", "ì¼ ë‚¨ìŒ"), - ("enable-trusted-devices-tip", "신뢰할 수 있는 기기ì—서 2FA ê²€ì¦ ê±´ë„ˆë›°ê¸°"), - ("Parent directory", "ìƒìœ„ 디렉토리"), + ("enable-trusted-devices-tip", "신뢰할 수 있는 장치ì—서 ì´ì¤‘ ì¸ì¦ 건너뛰기"), + ("Parent directory", "ìƒìœ„ 디렉터리"), ("Resume", "재개"), ("Invalid file name", "ìž˜ëª»ëœ íŒŒì¼ ì´ë¦„"), - ("one-way-file-transfer-tip", "단방향 íŒŒì¼ ì „ì†¡ì€ ì œì–´ë˜ëŠ” 쪽ì—서 활성화ë©ë‹ˆë‹¤."), - ("Authentication Required", "ì¸ì¦ 필요함"), + ("one-way-file-transfer-tip", "제어ë˜ëŠ” 측ì—서는 단방향 íŒŒì¼ ì „ì†¡ì´ ê°€ëŠ¥í•©ë‹ˆë‹¤."), + ("Authentication Required", "ì¸ì¦ í•„ìš”"), ("Authenticate", "ì¸ì¦"), - ("web_id_input_tip", "ë™ì¼í•œ ì„œë²„ì— ìžˆëŠ” ID를 입력할 수 있습니다. 웹 í´ë¼ì´ì–¸íЏì—서는 ì§ì ‘ IP ì ‘ì†ì´ ì§€ì›ë˜ì§€ 않습니다.\n 다른 ì„œë²„ì— ìžˆëŠ” ìž¥ì¹˜ì— ì ‘ì†í•˜ë ¤ë©´ 서버 주소(@?key=)를 추가하세요. 예:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\n 공용 ì„œë²„ì— ìžˆëŠ” ìž¥ì¹˜ì— ì ‘ì†í•˜ë ¤ë©´ \"@public\"ì„ ìž…ë ¥í•˜ì„¸ìš”. 공용 서버ì—서는 키가 필요하지 않습니다."), + ("web_id_input_tip", "ë™ì¼í•œ ì„œë²„ì— ID를 입력할 수 있으며, 웹 í´ë¼ì´ì–¸íЏì—서는 다ì´ë ‰íЏ IP 액세스가 ì§€ì›ë˜ì§€ 않습니다.\n다른 ì„œë²„ì— ìžˆëŠ” ìž¥ì¹˜ì— ì•¡ì„¸ìŠ¤í•˜ë ¤ë©´ 서버 주소 (@?key=)를 추가해 주세요. 예를 들어 \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\n공용 서버ì—서 ìž¥ì¹˜ì— ì•¡ì„¸ìŠ¤í•˜ë ¤ë©´ \"@public\"ì„ ìž…ë ¥í•´ 주세요. 공용 서버ì—는 키가 필요하지 않습니다."), ("Download", "다운로드"), ("Upload folder", "í´ë” 업로드"), ("Upload files", "íŒŒì¼ ì—…ë¡œë“œ"), - ("Clipboard is synchronized", "í´ë¦½ë³´ë“œê°€ ë™ê¸°í™”ë¨"), + ("Clipboard is synchronized", "í´ë¦½ë³´ë“œê°€ ë™ê¸°í™”ë˜ì—ˆìŠµë‹ˆë‹¤"), ("Update client clipboard", "í´ë¼ì´ì–¸íЏ í´ë¦½ë³´ë“œ ì—…ë°ì´íЏ"), ("Untagged", "태그 ì—†ìŒ"), - ("new-version-of-{}-tip", ""), + ("new-version-of-{}-tip", "{}ì˜ ìƒˆ ë²„ì „ì„ ì‚¬ìš©í•  수 있습니다"), + ("Accessible devices", "액세스 가능한 장치"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "RustDesk í´ë¼ì´ì–¸íŠ¸ë¥¼ ì›ê²© 버전 {} ì´ìƒìœ¼ë¡œ 업그레ì´ë“œí•´ 주세요!"), + ("d3d_render_tip", "D3D ë Œë”ë§ì´ 활성화ë˜ë©´ ì¼ë¶€ 기기ì—서는 ì›ê²© í™”ë©´ì´ ê²€ì€ìƒ‰ìœ¼ë¡œ í‘œì‹œë  ìˆ˜ 있습니다."), + ("Use D3D rendering", "D3D ë Œë”ë§ ì‚¬ìš©"), + ("Printer", "프린터"), + ("printer-os-requirement-tip", "프린터 출력 ê¸°ëŠ¥ì€ Windows 10 ì´ìƒì´ 필요합니다."), + ("printer-requires-installed-{}-client-tip", "ì›ê²© ì¸ì‡„ ê¸°ëŠ¥ì„ ì‚¬ìš©í•˜ë ¤ë©´ ì´ ìž¥ì¹˜ì— {}를 설치해야 합니다."), + ("printer-{}-not-installed-tip", "{} 프린터가 설치ë˜ì§€ 않았습니다."), + ("printer-{}-ready-tip", "{} 프린터가 설치ë˜ì–´ 사용할 준비가 ë˜ì—ˆìŠµë‹ˆë‹¤."), + ("Install {} Printer", "{} 프린터 설치"), + ("Outgoing Print Jobs", "발신 ì¸ì‡„ 작업"), + ("Incoming Print Jobs", "수신 ì¸ì‡„ 작업"), + ("Incoming Print Job", "수신 ì¸ì‡„ 작업"), + ("use-the-default-printer-tip", "기본 프린터 사용"), + ("use-the-selected-printer-tip", "ì„ íƒí•œ 프린터 사용"), + ("auto-print-tip", "ì„ íƒí•œ 프린터를 사용하여 ìžë™ìœ¼ë¡œ ì¸ì‡„합니다."), + ("print-incoming-job-confirm-tip", "ì›ê²©ì—서 ì¸ì‡„ ìž‘ì—…ì„ ë°›ì•˜ìŠµë‹ˆë‹¤. 옆ì—서 실행하시겠습니까?"), + ("remote-printing-disallowed-tile-tip", "ì›ê²© ì¸ì‡„ 허용 안 함"), + ("remote-printing-disallowed-text-tip", "ì œì–´ì¸¡ì˜ ê¶Œí•œ 설정ì—서 ì›ê²© ì¸ì‡„를 거부합니다."), + ("save-settings-tip", "설정 저장"), + ("dont-show-again-tip", "다시 표시하지 않ìŒ"), + ("Take screenshot", "스í¬ë¦°ìƒ· ì°ê¸°"), + ("Taking screenshot", "스í¬ë¦°ìƒ· ì°ëŠ” 중"), + ("screenshot-merged-screen-not-supported-tip", "현재 다중 디스플레ì´ì˜ 스í¬ë¦°ìƒ· ë³‘í•©ì´ ì§€ì›ë˜ì§€ 않습니다. ë‹¨ì¼ ë””ìŠ¤í”Œë ˆì´ë¡œ 전환한 후 다시 시ë„í•´ 주세요."), + ("screenshot-action-tip", "스í¬ë¦°ìƒ·ì„ ê³„ì† ì§„í–‰í•  ë°©ë²•ì„ ì„ íƒí•´ 주세요."), + ("Save as", "다른 ì´ë¦„으로 저장"), + ("Copy to clipboard", "í´ë¦½ë³´ë“œì— 복사"), + ("Enable remote printer", "ì›ê²© 프린터 사용함"), + ("Downloading {}", "{} 다운로드 중"), + ("{} Update", "{} ì—…ë°ì´íЏ"), + ("{}-to-update-tip", "{}ê°€ 지금 닫히고 새 ë²„ì „ì„ ì„¤ì¹˜í•©ë‹ˆë‹¤."), + ("download-new-version-failed-tip", "ë‹¤ìš´ë¡œë“œì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. 다시 시ë„하거나 \"다운로드\" ë²„íŠ¼ì„ í´ë¦­í•˜ì—¬ 릴리스 페ì´ì§€ì—서 다운로드하고 수ë™ìœ¼ë¡œ 업그레ì´ë“œí•  수 있습니다."), + ("Auto update", "ìžë™ ì—…ë°ì´íЏ"), + ("update-failed-check-msi-tip", "설치 방법 확ì¸ì— 실패했습니다. \"다운로드\" ë²„íŠ¼ì„ í´ë¦­í•˜ì—¬ 릴리스 페ì´ì§€ì—서 다운로드하고 수ë™ìœ¼ë¡œ 업그레ì´ë“œí•˜ì„¸ìš”."), + ("websocket_tip", "WebSocketì„ ì‚¬ìš©í•  때는 ë¦´ë ˆì´ ì—°ê²°ë§Œ ì§€ì›ë©ë‹ˆë‹¤."), + ("Use WebSocket", "웹소켓 사용"), + ("Trackpad speed", "트랙패드 ì†ë„"), + ("Default trackpad speed", "기본 트랙패드 ì†ë„"), + ("Numeric one-time password", "ìˆ«ìž ì¼íšŒìš© 비밀번호"), + ("Enable IPv6 P2P connection", "IPv6 P2P ì—°ê²° 사용"), + ("Enable UDP hole punching", "UDP 홀 펀칭 사용"), + ("View camera", "ì¹´ë©”ë¼ ë³´ê¸°"), + ("Enable camera", "ì¹´ë©”ë¼ ì‚¬ìš©í•¨"), + ("No cameras", "ì¹´ë©”ë¼ ì—†ìŒ"), + ("view_camera_unsupported_tip", "ì›ê²© 장치가 ì¹´ë©”ë¼ ë³´ê¸°ë¥¼ ì§€ì›í•˜ì§€ 않습니다."), + ("Terminal", "터미ë„"), + ("Enable terminal", "í„°ë¯¸ë„ ì‚¬ìš©í•¨"), + ("New tab", "새 탭"), + ("Keep terminal sessions on disconnect", "ì—°ê²°ì´ ëŠì–´ì ¸ë„ í„°ë¯¸ë„ ì„¸ì…˜ 유지"), + ("Terminal (Run as administrator)", "í„°ë¯¸ë„ (ê´€ë¦¬ìž ê¶Œí•œìœ¼ë¡œ 실행)"), + ("terminal-admin-login-tip", "제어ë˜ëŠ” ì¸¡ì˜ ê´€ë¦¬ìž ì‚¬ìš©ìž ì´ë¦„ê³¼ 비밀번호를 입력하세요."), + ("Failed to get user token.", "ì‚¬ìš©ìž í† í°ì„ 가져오는 ë° ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤."), + ("Incorrect username or password.", "ì‚¬ìš©ìž ì´ë¦„ì´ë‚˜ 비밀번호가 올바르지 않습니다."), + ("The user is not an administrator.", "사용ìžê°€ 관리ìžê°€ 아닙니다."), + ("Failed to check if the user is an administrator.", "사용ìžê°€ 관리ìžì¸ì§€ 확ì¸í•˜ëŠ” ë° ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤."), + ("Supported only in the installed version.", "ì„¤ì¹˜ëœ ë²„ì „ì—서만 ì§€ì›ë©ë‹ˆë‹¤."), + ("elevation_username_tip", "ì‚¬ìš©ìž ì´ë¦„ ë˜ëŠ” ë„ë©”ì¸\\ì‚¬ìš©ìž ì´ë¦„ ìž…ë ¥"), + ("Preparing for installation ...", "설치 준비 중 ..."), ].iter().cloned().collect(); } diff --git a/src/lang/kz.rs b/src/lang/kz.rs index 1f88ff77389..a48f7c946c7 100644 --- a/src/lang/kz.rs +++ b/src/lang/kz.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Тек a-z, A-Z, 0-9 және _ (аÑтынғы-Ñызық) таңбалары Ñ€Ò±Ò›Ñат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 араÑÑ‹."), + ("id_change_tip", "Тек a-z, A-Z, 0-9, - (dash) және _ (аÑтынғы-Ñызық) таңбалары Ñ€Ò±Ò›Ñат етілген. Бірінші таңба a-z, A-Z болуы қажет. Ұзындығы 6 мен 16 араÑÑ‹."), ("Website", "Web-Ñайт"), ("About", "Туралы"), ("Slogan_tip", ""), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS ÒšÒ±Ð¿Ð¸Ñ Ñөзі"), ("install_tip", "UAC кеÑірінен, RustDesk кейбірде қашықтағы жақ ретінде Ð´Ò±Ñ€Ñ‹Ñ Ð¶Ò±Ð¼Ñ‹Ñ Ñ–Ñтей алмайды. UAC'пен қиындықты болдырмау үшін, төмендегі батырманы баÑып RustDesk'ті жүйеге орнатыңыз."), ("Click to upgrade", "Жаңғырту үшін баÑыңыз"), - ("Click to download", "Жүктеу үшін баÑыңыз"), - ("Click to update", "Жаңарту үшін баÑыңыз"), ("Configure", "Қалыптау"), ("config_acc", "Сіздің Ð–Ò±Ð¼Ñ‹Ñ Ò¯Ñтеліңізді қашықтан баÑқару үшін, RustDesk'ке \"Қолжетімділік\" Ñ€Ò±Ò›Ñаттарын беруіңіз керек."), ("config_screen", "Сіздің Ð–Ò±Ð¼Ñ‹Ñ Ò¯Ñтеліңізге қашықтан қол жеткізу үшін, RustDesk'ке \"Екіренді Жазу\" Ñ€Ò±Ò›Ñаттарын беруіңіз керек."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Файыл алмаÑуға Ñ€Ò±Ò›Ñат берілмеген"), ("Note", "Ðота"), ("Connection", "ҚоÑылым"), - ("Share Screen", "Екіренді БөліÑу"), + ("Share screen", "Екіренді БөліÑу"), ("Chat", "Чат"), ("Total", "Барлығы"), ("items", "зат"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Екіренді ТүÑіру"), ("Input Control", "Еңгізуді БаÑқару/Қадағалау"), ("Audio Capture", "Ðудио ТүÑіру"), - ("File Connection", "Файыл ҚоÑылымы"), - ("Screen Connection", "Екірен ҚоÑылымы"), ("Do you accept?", "ҚабылдайÑыз ба?"), ("Open System Setting", "Жүйе Орнатпаларын Ðшу"), ("How to get Android input permission?", "Android еңгізу Ñ€Ò±Ò›Ñатын қалай алуға болады?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Қашықтағы жақтағы RustDesk клиентін {} немеÑе одан жоғары нұÑқаға жаңартуды өтінеміз!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Камераны Көру"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lt.rs b/src/lang/lt.rs index 33db01f930a..72df3e737b4 100644 --- a/src/lang/lt.rs +++ b/src/lang/lt.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS slaptažodis"), ("install_tip", "Kai kuriais atvejais UAC gali priversti RustDesk netinkamai veikti nuotoliniame pagrindiniame kompiuteryje. NorÄ—dami apeiti UAC, spustelÄ—kite toliau esantį mygtukÄ…, kad įdiegtumÄ—te RustDesk į savo kompiuterį."), ("Click to upgrade", "SpustelÄ—kite, jei norite atnaujinti"), - ("Click to download", "SpustelÄ—kite norÄ—dami atsisiųsti"), - ("Click to update", "SpustelÄ—kite norÄ—dami atnaujinti"), ("Configure", "KonfigÅ«ruoti"), ("config_acc", "NorÄ—dami nuotoliniu bÅ«du valdyti darbalaukį, turite suteikti RustDesk \"prieigos\" leidimus"), ("config_screen", "NorÄ—dami nuotoliniu bÅ«du pasiekti darbalaukį, turite suteikti RustDesk leidimus \"ekrano kopija\""), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "NÄ—ra leidimo perkelti failus"), ("Note", "Pastaba"), ("Connection", "RyÅ¡ys"), - ("Share Screen", "Bendrinti ekranÄ…"), + ("Share screen", "Bendrinti ekranÄ…"), ("Chat", "Pokalbis"), ("Total", "IÅ¡ viso"), ("items", "elementai"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ekrano nuotrauka"), ("Input Control", "Ä®vesties valdymas"), ("Audio Capture", "Garso fiksavimas"), - ("File Connection", "Failo ryÅ¡ys"), - ("Screen Connection", "Ekrano jungtis"), ("Do you accept?", "Ar sutinki?"), ("Open System Setting", "Atviros sistemos nustatymas"), ("How to get Android input permission?", "Kaip gauti Android įvesties leidimÄ…?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Dar neturite parankinių nuotolinių seansų."), ("empty_lan_tip", "Nuotolinių mazgų nerasta."), ("empty_address_book_tip", "Adresų knygelÄ—je nÄ—ra nuotolinių kompiuterių."), - ("eg: admin", "pvz.: administratorius"), ("Empty Username", "TuÅ¡Äias naudotojo vardas"), ("Empty Password", "TuÅ¡Äias slaptažodis"), ("Me", "AÅ¡"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "PraÅ¡ome atnaujinti nuotolinÄ—s pusÄ—s RustDesk klientÄ… į {} ar naujesnÄ™ versijÄ…!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "PeržiÅ«rÄ—ti kamerÄ…"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/lv.rs b/src/lang/lv.rs index 81c3bedae5f..1b4beb30176 100644 --- a/src/lang/lv.rs +++ b/src/lang/lv.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "garums %min% lÄ«dz %max%"), ("starts with a letter", "sÄkas ar burtu"), ("allowed characters", "atļautÄs rakstzÄ«mes"), - ("id_change_tip", "Atļautas tikai rakstzÄ«mes a-z, A-Z, 0-9 un _ (pasvÄ«trojums). Pirmajam burtam ir jÄbÅ«t a-z, A-Z. Garums no 6 lÄ«dz 16."), + ("id_change_tip", "Atļautas tikai rakstzÄ«mes a-z, A-Z, 0-9, - (domuzÄ«me) un _ (pasvÄ«trojums). Pirmajam burtam ir jÄbÅ«t a-z, A-Z. Garums no 6 lÄ«dz 16."), ("Website", "TÄ«mekļa vietne"), ("About", "Par"), ("Slogan_tip", "RadÄ«ts ar sirdi Å¡ajÄ haotiskajÄ pasaulÄ“!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS parole"), ("install_tip", "UAC dēļ RustDesk dažos gadÄ«jumos nevar pareizi darboties kÄ attÄlÄ puse. Lai izvairÄ«tos no UAC, lÅ«dzu, noklikšķiniet uz tÄlÄk esoÅ¡Äs pogas, lai instalÄ“tu RustDesk sistÄ“mÄ."), ("Click to upgrade", "JauninÄt"), - ("Click to download", "LejupielÄdÄ“t"), - ("Click to update", "AtjauninÄt"), ("Configure", "KonfigurÄ“t"), ("config_acc", "Lai attÄlinÄti vadÄ«tu savu darbvirsmu, jums ir jÄpiešķir RustDesk \"PieejamÄ«ba\" atļaujas."), ("config_screen", "Lai attÄlinÄti piekļūtu darbvirsmai, jums ir jÄpiešķir RustDesk \"EkrÄna tverÅ¡ana\" atļaujas."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nav atļaujas failu pÄrsÅ«tīšanai"), ("Note", "PiezÄ«me"), ("Connection", "Savienojums"), - ("Share Screen", "Koplietot ekrÄnu"), + ("Share screen", "Koplietot ekrÄnu"), ("Chat", "TÄ“rzēšana"), ("Total", "KopÄ"), ("items", "vienumi"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "EkrÄna tverÅ¡ana"), ("Input Control", "Ievades vadÄ«ba"), ("Audio Capture", "Audio tverÅ¡ana"), - ("File Connection", "Failu savienojums"), - ("Screen Connection", "EkrÄna savienojums"), ("Do you accept?", "Vai JÅ«s pieņemat?"), ("Open System Setting", "AtvÄ“rt sistÄ“mas iestatÄ«jumus"), ("How to get Android input permission?", "KÄ iegÅ«t Android ievades atļauju?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "VÄ“l nav iecienÄ«tÄkÄs sesijas?\nAtradÄ«sim kÄdu, ar ko sazinÄties, un pievienosim to jÅ«su izlasei!"), ("empty_lan_tip", "Ak nÄ“! Å Ä·iet, ka mÄ“s vÄ“l neesam atklÄjuÅ¡i nevienu sesiju."), ("empty_address_book_tip", "Ak vai, izskatÄs, ka jÅ«su adreÅ¡u grÄmatÄ Å¡obrÄ«d nav neviena sesija."), - ("eg: admin", "piemÄ“ram: admin"), ("Empty Username", "TukÅ¡s lietotÄjvÄrds"), ("Empty Password", "TukÅ¡a parole"), ("Me", "Es"), @@ -655,6 +650,64 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Clipboard is synchronized", "Starpliktuve ir sinhronizÄ“ta"), ("Update client clipboard", "AtjauninÄt klienta starpliktuvi"), ("Untagged", "NeatzÄ«mÄ“ts"), - ("new-version-of-{}-tip", ""), + ("new-version-of-{}-tip", "Ir pieejama jauna {} versija"), + ("Accessible devices", "Pieejamas ierÄ«ces"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "LÅ«dzu, jauniniet attÄlÄs puses RustDesk klientu uz versiju {} vai jaunÄku!"), + ("d3d_render_tip", "Ja ir iespÄ“jota D3D renderēšana, dažÄs ierÄ«cÄ“s tÄlvadÄ«bas pults ekrÄns var bÅ«t melns."), + ("Use D3D rendering", "Izmantot D3D renderēšanu"), + ("Printer", "Printeris"), + ("printer-os-requirement-tip", "Printera izejoÅ¡ajai funkcijai nepiecieÅ¡ama operÄ“tÄjsistÄ“ma Windows 10 vai jaunÄka versija."), + ("printer-requires-installed-{}-client-tip", "Lai izmantotu attÄlo drukÄÅ¡anu, Å¡ajÄ ierÄ«cÄ“ ir jÄinstalÄ“ {}."), + ("printer-{}-not-installed-tip", "Printeris {} nav instalÄ“ts."), + ("printer-{}-ready-tip", "Printeris {} ir instalÄ“ts un gatavs lietoÅ¡anai."), + ("Install {} Printer", "InstalÄ“t {} printeri"), + ("Outgoing Print Jobs", "IzejoÅ¡ie drukas darbi"), + ("Incoming Print Jobs", "IenÄkoÅ¡ie drukas darbi"), + ("Incoming Print Job", "IenÄkoÅ¡ais drukas darbs"), + ("use-the-default-printer-tip", "Izmantot noklusÄ“juma printeri"), + ("use-the-selected-printer-tip", "Izmantot atlasÄ«to printeri"), + ("auto-print-tip", "DrukÄjiet automÄtiski, izmantojot atlasÄ«to printeri."), + ("print-incoming-job-confirm-tip", "JÅ«s saņēmÄt drukas darbu no attÄlÄs ierÄ«ces. Vai vÄ“laties to izpildÄ«t savÄ pusÄ“?"), + ("remote-printing-disallowed-tile-tip", "AttÄlÄ drukÄÅ¡ana ir aizliegta"), + ("remote-printing-disallowed-text-tip", "KontrolÄ“tÄs puses atļauju iestatÄ«jumi liedz attÄlo drukÄÅ¡anu."), + ("save-settings-tip", "SaglabÄt iestatÄ«jumus"), + ("dont-show-again-tip", "NerÄdÄ«t Å¡o vÄ“lreiz"), + ("Take screenshot", "Uzņemt ekrÄnuzņēmumu"), + ("Taking screenshot", "EkrÄnuzņēmuma uzņemÅ¡ana"), + ("screenshot-merged-screen-not-supported-tip", "VairÄku displeju ekrÄnuzņēmumu apvienoÅ¡ana paÅ¡laik netiek atbalstÄ«ta. LÅ«dzu, pÄrslÄ“dzieties uz vienu displeju un mēģiniet vÄ“lreiz."), + ("screenshot-action-tip", "LÅ«dzu, atlasiet, kÄ turpinÄt darbu ar ekrÄnuzņēmumu."), + ("Save as", "SaglabÄt kÄ"), + ("Copy to clipboard", "KopÄ“t starpliktuvÄ“"), + ("Enable remote printer", "IespÄ“jot attÄlo printeri"), + ("Downloading {}", "Notiek {} lejupielÄde"), + ("{} Update", "{} atjauninÄjums"), + ("{}-to-update-tip", "{} tagad tiks aizvÄ“rts un tiks instalÄ“ta jaunÄ versija."), + ("download-new-version-failed-tip", "LejupielÄde neizdevÄs. Varat mēģinÄt vÄ“lreiz vai noklikšķinÄt uz pogas \"LejupielÄdÄ“t\", lai lejupielÄdÄ“tu no laidiena lapas un manuÄli jauninÄtu."), + ("Auto update", "AutomÄtiskÄ atjauninÄÅ¡ana"), + ("update-failed-check-msi-tip", "Instalēšanas metodes pÄrbaude neizdevÄs. LÅ«dzu, noklikšķiniet uz pogas \"LejupielÄdÄ“t\", lai lejupielÄdÄ“tu no laidiena lapas un manuÄli jauninÄtu."), + ("websocket_tip", "Izmantojot WebSocket, tiek atbalstÄ«ti tikai releja savienojumi."), + ("Use WebSocket", "Lietot WebSocket"), + ("Trackpad speed", "SkÄrienpaliktņa Ätrums"), + ("Default trackpad speed", "NoklusÄ“juma skÄrienpaliktņa Ätrums"), + ("Numeric one-time password", "Vienreiz lietojama ciparu parole"), + ("Enable IPv6 P2P connection", "IespÄ“jot IPv6 P2P savienojumu"), + ("Enable UDP hole punching", "IespÄ“jot UDP caurumu veidoÅ¡anu"), + ("View camera", "SkatÄ«t kameru"), + ("Enable camera", "IespÄ“jot kameru"), + ("No cameras", "Nav kameru"), + ("view_camera_unsupported_tip", "AttÄlÄ ierÄ«ce neatbalsta kameras skatīšanos."), + ("Terminal", "TerminÄlis"), + ("Enable terminal", "IespÄ“jot terminÄli"), + ("New tab", "Jauna cilne"), + ("Keep terminal sessions on disconnect", "Atvienojoties saglabÄt terminÄļa sesijas"), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nb.rs b/src/lang/nb.rs index eb3564b86db..6b0c4f29d0e 100644 --- a/src/lang/nb.rs +++ b/src/lang/nb.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9 og _ (understrek) er tillat. Den første bokstaven skal være a-z, A-Z. Lengde mellom 6 og 16."), + ("id_change_tip", "Kun tegnene a-z, A-Z, 0-9, - (dash) og _ (understrek) er tillat. Den første bokstaven skal være a-z, A-Z. Lengde mellom 6 og 16."), ("Website", "Hjemmeside"), ("About", "Om"), ("Slogan_tip", ""), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Operativsystempassord"), ("install_tip", "PÃ¥ grunn av UAC kan RustDesk ikke fungere korrekt i enkelte tillfeller pÃ¥ fjernskrivebordet. For Ã¥ unngÃ¥ UAC klikker du pÃ¥ knappen nedenfor for Ã¥ installere RustDesk pÃ¥ systemet"), ("Click to upgrade", "Klikk for Ã¥ oppgradere"), - ("Click to download", "Klikk for Ã¥ laste ned"), - ("Click to update", "Klikk for Ã¥ oppdatere"), ("Configure", "Konfigurer"), ("config_acc", "For Ã¥ kontrollere ditt skrivebord med fjernstyring mÃ¥ du gi RustDesk \"Access \" Rettigheter."), ("config_screen", "For Ã¥ kunne fÃ¥ adgang til ditt skrivebord med fjernstyring, mÃ¥ du gi RustDesk \"skjerstøtte \" tillatelser."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ingen tillatelse til Ã¥ overføre filen"), ("Note", "Notat"), ("Connection", "Tilkobling"), - ("Share Screen", "Del skjermen"), + ("Share screen", "Del skjermen"), ("Chat", "Chat"), ("Total", "Total"), ("items", "Objekter"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Skjermopptak"), ("Input Control", "Input kontroll"), ("Audio Capture", "Lydopptak"), - ("File Connection", "Filtilkobling"), - ("Screen Connection", "Skjermtilkobing"), ("Do you accept?", "Akepterer du?"), ("Open System Setting", "Ã…pne systeminnstillinger"), ("How to get Android input permission?", "Hvordan fÃ¥r jeg en Android-input tillatelse?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", "f.eks.: admin"), ("Empty Username", "Tøm brukernavn"), ("Empty Password", "Tøm passord"), ("Me", "Meg"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Vis kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/nl.rs b/src/lang/nl.rs index 17bce069519..652a7a8041e 100644 --- a/src/lang/nl.rs +++ b/src/lang/nl.rs @@ -3,60 +3,60 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = [ ("Status", "Status"), ("Your Desktop", "Uw Bureaublad"), - ("desk_tip", "Uw bureaublad is toegankelijk via de ID en het wachtwoord hieronder."), + ("desk_tip", "Uw bureaublad is toegankelijk met dit ID en wachtwoord."), ("Password", "Wachtwoord"), ("Ready", "Klaar"), ("Established", "Opgezet"), ("connecting_status", "Verbinding maken met het RustDesk netwerk..."), - ("Enable service", "Service Inschakelen"), + ("Enable service", "Service inschakelen"), ("Start service", "Start service"), ("Service is running", "De service loopt."), ("Service is not running", "De service loopt niet"), - ("not_ready_status", "Niet klaar, controleer de netwerkverbinding"), + ("not_ready_status", "Niet verbonden met de server, controleer de netwerkverbinding"), ("Control Remote Desktop", "Beheer Extern Bureaublad"), - ("Transfer file", "Bestand Overzetten"), + ("Transfer file", "Bestand overzetten"), ("Connect", "Verbinden"), - ("Recent sessions", "Recente Behandelingen"), + ("Recent sessions", "Recente sessies"), ("Address book", "Adresboek"), ("Confirmation", "Bevestiging"), - ("TCP tunneling", "TCP tunneling"), + ("TCP tunneling", "TCP-tunneling"), ("Remove", "Verwijder"), ("Refresh random password", "Vernieuw willekeurig wachtwoord"), ("Set your own password", "Stel uw eigen wachtwoord in"), - ("Enable keyboard/mouse", "Toetsenbord/Muis Inschakelen"), - ("Enable clipboard", "Klembord Inschakelen"), - ("Enable file transfer", "Bestandsoverdracht Inschakelen"), - ("Enable TCP tunneling", "TCP tunneling Inschakelen"), + ("Enable keyboard/mouse", "Toetsenbord/muis inschakelen"), + ("Enable clipboard", "Klembord inschakelen"), + ("Enable file transfer", "Bestandsoverdracht inschakelen"), + ("Enable TCP tunneling", "TCP-tunneling inschakelen"), ("IP Whitelisting", "IP Witte Lijst"), - ("ID/Relay Server", "ID/Relay Server"), - ("Import server config", "Importeer Serverconfiguratie"), - ("Export Server Config", "Exporteer Serverconfiguratie"), + ("ID/Relay Server", "ID-/Relayserver"), + ("Import server config", "Importeer serverconfiguratie"), + ("Export Server Config", "Exporteer serverconfiguratie"), ("Import server configuration successfully", "Importeren serverconfiguratie is geslaagd"), ("Export server configuration successfully", "Exporteren serverconfiguratie is geslaagd"), - ("Invalid server configuration", "Ongeldige Serverconfiguratie"), + ("Invalid server configuration", "Ongeldige serverconfiguratie"), ("Clipboard is empty", "Klembord is leeg"), ("Stop service", "Stop service"), ("Change ID", "Wijzig ID"), - ("Your new ID", "Uw nieuw ID"), + ("Your new ID", "Uw nieuwe ID"), ("length %min% to %max%", "lengte %min% tot %max%"), ("starts with a letter", "begint met een letter"), ("allowed characters", "toegestane tekens"), - ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), + ("id_change_tip", "Alleen de letters a-z, A-Z, 0-9, - (dash), _ (underscore) kunnen worden gebruikt. De eerste letter moet a-z, A-Z zijn. De lengte moet tussen 6 en 16 liggen."), ("Website", "Website"), ("About", "Over"), - ("Slogan_tip", "Ontwikkeld met het hart voor deze chaotische wereld!"), + ("Slogan_tip", "Met hart gemaakt in deze chaotische wereld!"), ("Privacy Statement", "Privacyverklaring"), ("Mute", "Geluid uit"), - ("Build Date", "Versie datum"), + ("Build Date", "Datum"), ("Version", "Versie"), ("Home", "Startpagina"), - ("Audio Input", "Audio Ingang"), + ("Audio Input", "Audioingang"), ("Enhancements", "Verbeteringen"), - ("Hardware Codec", "Hardware Codec"), - ("Adaptive bitrate", "Aangepaste Bitsnelheid"), - ("ID Server", "Server ID"), - ("Relay Server", "Relay Server"), - ("API Server", "API Server"), + ("Hardware Codec", "Hardwarecodec"), + ("Adaptive bitrate", "Bitrate automatisch aanpassen"), + ("ID Server", "ID-server"), + ("Relay Server", "Relay-server"), + ("API Server", "API-server"), ("invalid_http", "Moet beginnen met http:// of https://"), ("Invalid IP", "Ongeldig IP"), ("Invalid format", "Ongeldig formaat"), @@ -68,43 +68,43 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Close", "Sluit"), ("Retry", "Probeer opnieuw"), ("OK", "OK"), - ("Password Required", "Wachtwoord vereist"), + ("Password Required", "Wachtwoord Vereist"), ("Please enter your password", "Geef uw wachtwoord in"), ("Remember password", "Wachtwoord onthouden"), - ("Wrong Password", "Verkeerd wachtwoord"), + ("Wrong Password", "Verkeerd Wachtwoord"), ("Do you want to enter again?", "Wilt u het opnieuw invoeren?"), ("Connection Error", "Fout bij verbinding"), ("Error", "Fout"), - ("Reset by the peer", "Reset door de peer"), + ("Reset by the peer", "Door de peer gereset"), ("Connecting...", "Verbinding maken..."), - ("Connection in progress. Please wait.", "Verbinding in uitvoering. Even geduld a.u.b."), + ("Connection in progress. Please wait.", "Verbinding wordt gemaakt. Even geduld a.u.b."), ("Please try 1 minute later", "Probeer 1 minuut later"), - ("Login Error", "Login Fout"), + ("Login Error", "Loginfout"), ("Successful", "Geslaagd"), ("Connected, waiting for image...", "Verbonden, wacht op beeld..."), ("Name", "Naam"), ("Type", "Type"), ("Modified", "Gewijzigd"), ("Size", "Grootte"), - ("Show Hidden Files", "Toon verborgen bestanden"), - ("Receive", "Ontvangen"), - ("Send", "Verzenden"), + ("Show Hidden Files", "Toon Verborgen Bestanden"), + ("Receive", "Ontvang"), + ("Send", "Verzend"), ("Refresh File", "Bestand Verversen"), ("Local", "Lokaal"), - ("Remote", "Op afstand"), + ("Remote", "Op Afstand"), ("Remote Computer", "Externe Computer"), ("Local Computer", "Lokale Computer"), ("Confirm Delete", "Bevestig Verwijderen"), ("Delete", "Verwijder"), ("Properties", "Eigenschappen"), - ("Multi Select", "Meervoudig selecteren"), + ("Multi Select", "Meervoudig Selecteren"), ("Select All", "Selecteer Alle"), - ("Unselect All", "De-selecteer alles"), + ("Unselect All", "De-selecteer Alle"), ("Empty Directory", "Lege Map"), ("Not an empty directory", "Geen lege map"), ("Are you sure you want to delete this file?", "Weet u zeker dat u dit bestand wilt verwijderen?"), ("Are you sure you want to delete this empty directory?", "Weet u zeker dat u deze lege map wilt verwijderen?"), - ("Are you sure you want to delete the file of this directory?", "Weet u zeker dat u het bestand uit deze map wilt verwijderen?"), + ("Are you sure you want to delete the file of this directory?", "Weet u zeker dat u de bestanden uit deze map wilt verwijderen?"), ("Do this for all conflicts", "Doe dit voor alle conflicten"), ("This is irreversible!", "Dit is onomkeerbaar!"), ("Deleting", "Verwijderen"), @@ -112,16 +112,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Waiting", "Wachten"), ("Finished", "Voltooid"), ("Speed", "Snelheid"), - ("Custom Image Quality", "Aangepaste beeldkwaliteit"), + ("Custom Image Quality", "Aangepaste Beeldkwaliteit"), ("Privacy mode", "Privacymodus"), ("Block user input", "Gebruikersinvoer blokkeren"), - ("Unblock user input", "Gebruikersinvoer opheffen"), + ("Unblock user input", "Gebruikersinvoer deblokkeren"), ("Adjust Window", "Venster Aanpassen"), ("Original", "Origineel"), ("Shrink", "Verkleinen"), ("Stretch", "Uitrekken"), ("Scrollbar", "Schuifbalk"), - ("ScrollAuto", "Auto Schuiven"), + ("ScrollAuto", "Automatisch schuiven"), ("Good image quality", "Goede beeldkwaliteit"), ("Balanced", "Gebalanceerd"), ("Optimize reaction time", "Optimaliseer reactietijd"), @@ -130,8 +130,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Show quality monitor", "Kwaliteitsmonitor tonen"), ("Disable clipboard", "Klembord uitschakelen"), ("Lock after session end", "Vergrendelen na einde sessie"), - ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Invoegen"), - ("Insert Lock", "Vergrendeling Invoegen"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del Invoeren"), + ("Insert Lock", "Vergrendelen"), ("Refresh", "Vernieuwen"), ("ID does not exist", "ID bestaat niet"), ("Failed to connect to rendezvous server", "Verbinding met rendez-vous-server mislukt"), @@ -139,32 +139,30 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remote desktop is offline", "Extern bureaublad is offline"), ("Key mismatch", "Code onjuist"), ("Timeout", "Time-out"), - ("Failed to connect to relay server", "Verbinding met relayserver mislukt"), - ("Failed to connect via rendezvous server", "Verbinding via rendez-vous-server mislukt"), - ("Failed to connect via relay server", "Verbinding via relaisserver mislukt"), - ("Failed to make direct connection to remote desktop", "Onmogelijk direct verbinding te maken met extern bureaublad"), + ("Failed to connect to relay server", "Verbinden met relayserver mislukt"), + ("Failed to connect via rendezvous server", "Verbinden via rendez-vous-server mislukt"), + ("Failed to connect via relay server", "Verbinden via relaisserver mislukt"), + ("Failed to make direct connection to remote desktop", "Direct verbinden met extern bureaublad is mislukt"), ("Set Password", "Wachtwoord Instellen"), ("OS Password", "OS Wachtwoord"), - ("install_tip", "U gebruikt een niet geïnstalleerde versie. Als gevolg van UAC-beperkingen is het in sommige gevallen niet mogelijk om als controleterminal de muis en het toetsenbord te bedienen of het scherm over te nemen. Klik op de knop hieronder om RustDesk op het systeem te installeren om het bovenstaande probleem te voorkomen."), + ("install_tip", "Door UAC-beperkingen lukt het niet altijd om uw bureaublad op afstand te bedienen. Installeer RustDesk op het systeem om dit probleem te voorkomen."), ("Click to upgrade", "Klik voor upgrade"), - ("Click to download", "Klik om te downloaden"), - ("Click to update", "Klik om bij te werken"), ("Configure", "Configureren"), - ("config_acc", "Om uw bureaublad op afstand te kunnen bedienen, moet u RustDesk \"toegankelijkheid\" toestemming geven."), - ("config_screen", "Om toegang te krijgen tot het externe bureaublad, moet u RustDesk de toestemming \"schermregistratie\" geven."), + ("config_acc", "Om uw apparaat op afstand te kunnen bedienen, moet u RustDesk toestemming voor Toegankelijkheid geven."), + ("config_screen", "Om uw apparaat op afstand te kunnen bedienen, moet u RustDesk toestemming voor Schermopname geven."), ("Installing ...", "Installeren ..."), ("Install", "Installeer"), ("Installation", "Installatie"), - ("Installation Path", "Installatie Pad"), - ("Create start menu shortcuts", "Startmenu snelkoppelingen maken"), + ("Installation Path", "Locatie"), + ("Create start menu shortcuts", "Startmenu-snelkoppelingen maken"), ("Create desktop icon", "Bureaubladpictogram maken"), ("agreement_tip", "Het starten van de installatie betekent het accepteren van de licentieovereenkomst."), ("Accept and Install", "Accepteren en installeren"), ("End-user license agreement", "Licentieovereenkomst eindgebruiker"), ("Generating ...", "Genereert ..."), ("Your installation is lower version.", "Uw installatie is een lagere versie."), - ("not_close_tcp_tip", "Gelieve dit venster niet te sluiten wanneer u de tunnel gebruikt"), - ("Listening ...", "Luisteren ..."), + ("not_close_tcp_tip", "Sluit dit venster niet zolang u de tunnel gebruikt"), + ("Listening ...", "Luistert ..."), ("Remote Host", "Externe Host"), ("Remote Port", "Externe Poort"), ("Action", "Actie"), @@ -172,7 +170,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Local Port", "Lokale Poort"), ("Local Address", "Lokaal Adres"), ("Change Local Port", "Wijzig Lokale Poort"), - ("setup_server_tip", "Als u een snellere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server aan te maken"), + ("setup_server_tip", "Als u een hogere verbindingssnelheid nodig heeft, kunt u ervoor kiezen om uw eigen server aan te maken"), ("Too short, at least 6 characters.", "Te kort, minstens 6 tekens."), ("The confirmation is not identical.", "De bevestiging is niet identiek."), ("Permissions", "Machtigingen"), @@ -194,10 +192,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Rename", "Naam wijzigen"), ("Space", "Spatie"), ("Create desktop shortcut", "Snelkoppeling op bureaublad maken"), - ("Change Path", "Pad wijzigen"), + ("Change Path", "Pad Wijzigen"), ("Create Folder", "Map Maken"), ("Please enter the folder name", "Geef de mapnaam op"), - ("Fix it", "Repareer het"), + ("Fix it", "Repareer"), ("Warning", "Waarschuwing"), ("Login screen using Wayland is not supported", "Aanmeldingsscherm via Wayland wordt niet ondersteund"), ("Reboot required", "Opnieuw opstarten vereist"), @@ -205,20 +203,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("x11 expected", "x11 verwacht"), ("Port", "Poort"), ("Settings", "Instellingen"), - ("Username", "Gebruikersnaam"), + ("Username", "Gebruiker"), ("Invalid port", "Ongeldige poort"), ("Closed manually by the peer", "Handmatig gesloten door de peer"), - ("Enable remote configuration modification", "Wijziging configuratie op afstand inschakelen"), + ("Enable remote configuration modification", "Configuratiewijziging op afstand inschakelen"), ("Run without install", "Uitvoeren zonder installatie"), - ("Connect via relay", "Verbinden via relais"), + ("Connect via relay", "Verbinden via relay"), ("Always connect via relay", "Altijd verbinden via relay"), - ("whitelist_tip", "Alleen een IP-adres op de witte lijst krijgt toegang tot mijn toestel"), + ("whitelist_tip", "Alleen IP-adressen op de witte lijst krijgen toegang tot mijn toestel"), ("Login", "Log In"), ("Verify", "Controleer"), ("Remember me", "Herinner mij"), ("Trust this device", "Vertrouw dit apparaat"), - ("Verification code", "Verificatie code"), - ("verification_tip", "Er is een nieuw apparaat gedetecteerd en er is een verificatiecode naar het geregistreerde e-mailadres gestuurd, voer de verificatiecode in om de verbinding voort te zetten."), + ("Verification code", "Verificatiecode"), + ("verification_tip", "Er is een verificatiecode naar het geregistreerde e-mailadres gestuurd, voer de verificatiecode in om de verbinding voort te zetten."), ("Logout", "Log Uit"), ("Tags", "Labels"), ("Search ID", "Zoek ID"), @@ -238,24 +236,24 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Remove from Favorites", "Verwijderen uit Favorieten"), ("Empty", "Leeg"), ("Invalid folder name", "Ongeldige mapnaam"), - ("Socks5 Proxy", "Socks5 Proxy"), - ("Socks5/Http(s) Proxy", "Socks5/Http(s) Proxy"), + ("Socks5 Proxy", "SOCKS5 Proxy"), + ("Socks5/Http(s) Proxy", "SOCKS5/HTTP(S) Proxy"), ("Discovered", "Ontdekt"), - ("install_daemon_tip", "Om bij het opstarten van de computer te kunnen beginnen, moet u de systeemservice installeren."), - ("Remote ID", "Externe ID"), + ("install_daemon_tip", "Om te starten bij het opstarten van de computer, moet u de systeemservice installeren."), + ("Remote ID", "Extern ID"), ("Paste", "Plakken"), - ("Paste here?", "Hier plakken"), + ("Paste here?", "Hier plakken?"), ("Are you sure to close the connection?", "Weet u zeker dat u de verbinding wilt sluiten?"), ("Download new version", "Download nieuwe versie"), - ("Touch mode", "Aanraak modus"), + ("Touch mode", "Aanraakmodus"), ("Mouse mode", "Muismodus"), ("One-Finger Tap", "Een-Vinger Tik"), ("Left Mouse", "Linkermuis"), ("One-Long Tap", "Een-Vinger-Lange-Tik"), ("Two-Finger Tap", "Twee-Vingers-Tik"), - ("Right Mouse", "Rechter muis"), + ("Right Mouse", "Rechtermuis"), ("One-Finger Move", "Een-Vinger-Verplaatsing"), - ("Double Tap & Move", "Dubbel Tik en Verplaatsen"), + ("Double Tap & Move", "Dubbel-Tik en Verplaatsen"), ("Mouse Drag", "Muis Slepen"), ("Three-Finger vertically", "Drie-Vinger verticaal"), ("Mouse Wheel", "Muiswiel"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Geen toestemming voor bestandsoverdracht"), ("Note", "Opmerking"), ("Connection", "Verbinding"), - ("Share Screen", "Scherm Delen"), + ("Share screen", "Scherm Delen"), ("Chat", "Chat"), ("Total", "Totaal"), ("items", "items"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Schermopname"), ("Input Control", "Invoercontrole"), ("Audio Capture", "Audio Opnemen"), - ("File Connection", "Bestandsverbinding"), - ("Screen Connection", "Schermverbinding"), ("Do you accept?", "Geeft u toestemming?"), ("Open System Setting", "Systeeminstelling Openen"), ("How to get Android input permission?", "Hoe krijg ik Android invoer toestemming?"), @@ -304,18 +300,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Turned off", "Uitgeschakeld"), ("Language", "Taal"), ("Keep RustDesk background service", "RustDesk achtergronddienst behouden"), - ("Ignore Battery Optimizations", "Negeer Batterij Optimalisaties"), + ("Ignore Battery Optimizations", "Negeer Batterij-optimalisaties"), ("android_open_battery_optimizations_tip", "Ga naar de volgende pagina met instellingen"), ("Start on boot", "Starten bij Opstarten"), - ("Start the screen sharing service on boot, requires special permissions", "Start de schermdelings service bij het opstarten, vereist speciale rechten"), + ("Start the screen sharing service on boot, requires special permissions", "Start de schermdelingsservice bij het opstarten, vereist speciale rechten"), ("Connection not allowed", "Verbinding niet toegestaan"), - ("Legacy mode", "Verouderde modus"), - ("Map mode", "Map mode"), + ("Legacy mode", "Legacymodus"), + ("Map mode", "Mapmodus"), ("Translate mode", "Vertaalmodus"), ("Use permanent password", "Gebruik permanent wachtwoord"), ("Use both passwords", "Gebruik beide wachtwoorden"), ("Set permanent password", "Stel permanent wachtwoord in"), - ("Enable remote restart", "Schakel Herstart op afstand in"), + ("Enable remote restart", "Herstart op afstand inschakelen"), ("Restart remote device", "Apparaat op afstand herstarten"), ("Are you sure you want to restart", "Weet u zeker dat u wilt herstarten"), ("Restarting remote device", "Apparaat op afstand herstarten"), @@ -336,19 +332,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Relay Connection", "Relaisverbinding"), ("Secure Connection", "Beveiligde Verbinding"), ("Insecure Connection", "Onveilige Verbinding"), - ("Scale original", "Oorspronkelijke schaal"), - ("Scale adaptive", "Schaalaanpassing"), + ("Scale original", "Oorspronkelijk formaat"), + ("Scale adaptive", "Automatisch schalen"), ("General", "Algemeen"), ("Security", "Beveiliging"), ("Theme", "Thema"), ("Dark Theme", "Donker Thema"), - ("Light Theme", "Lichte Thema"), + ("Light Theme", "Licht Thema"), ("Dark", "Donker"), ("Light", "Licht"), - ("Follow System", "Volg Systeem"), - ("Enable hardware codec", "Hardware codec inschakelen"), + ("Follow System", "Volg systeem"), + ("Enable hardware codec", "Hardwarecodec inschakelen"), ("Unlock Security Settings", "Beveiligingsinstellingen vrijgeven"), - ("Enable audio", "Audio Inschakelen"), + ("Enable audio", "Audio inschakelen"), ("Unlock Network Settings", "Netwerkinstellingen Vrijgeven"), ("Server", "Server"), ("Direct IP Access", "Directe IP toegang"), @@ -363,25 +359,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unpin Toolbar", "Werkbalk Losmaken"), ("Recording", "Opnemen"), ("Directory", "Map"), - ("Automatically record incoming sessions", "Automatisch inkomende sessies opnemen"), - ("Automatically record outgoing sessions", ""), - ("Change", "Wissel"), + ("Automatically record incoming sessions", "Inkomende sessies automatisch opnemen"), + ("Automatically record outgoing sessions", "Uitgaande sessies automatisch opnemen"), + ("Change", "Aanpassen"), ("Start session recording", "Start de sessieopname"), ("Stop session recording", "Stop de sessieopname"), - ("Enable recording session", "Opnamesessie Activeren"), + ("Enable recording session", "Sessieopname activeren"), ("Enable LAN discovery", "LAN-detectie inschakelen"), - ("Deny LAN discovery", "LAN-detectie Weigeren"), + ("Deny LAN discovery", "LAN-detectie weigeren"), ("Write a message", "Schrijf een bericht"), - ("Prompt", "Verzoek"), + ("Prompt", "Melding"), ("Please wait for confirmation of UAC...", "Wacht op bevestiging van UAC..."), ("elevated_foreground_window_tip", "Het momenteel geopende venster van de op afstand bediende computer vereist hogere rechten. Daarom is het momenteel niet mogelijk de muis en het toetsenbord te gebruiken. Vraag de gebruiker wiens computer u op afstand bedient om het venster te minimaliseren of de rechten te verhogen. Om dit probleem in de toekomst te voorkomen, wordt aanbevolen de software te installeren op de op afstand bediende computer."), ("Disconnected", "Afgesloten"), ("Other", "Andere"), ("Confirm before closing multiple tabs", "Bevestig voordat u meerdere tabbladen sluit"), - ("Keyboard Settings", "Toetsenbord instellingen"), + ("Keyboard Settings", "Toetsenbordinstellingen"), ("Full Access", "Volledige Toegang"), ("Screen Share", "Scherm Delen"), - ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of een hogere versie."), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vereist Ubuntu 21.04 of hoger."), ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vereist een hogere versie van Linux distro. Probeer X11 desktop of verander van OS."), ("JumpLink", "JumpLink"), ("Please Select the screen to be shared(Operate on the peer side).", "Selecteer het scherm dat moet worden gedeeld (Bediening aan de kant van de peer)."), @@ -390,18 +386,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("or", "of"), ("Continue with", "Ga verder met"), ("Elevate", "Verhoog"), - ("Zoom cursor", "Cursor Zoomen"), + ("Zoom cursor", "Zoom cursor"), ("Accept sessions via password", "Sessies accepteren via wachtwoord"), ("Accept sessions via click", "Sessies accepteren via klik"), - ("Accept sessions via both", "Accepteer sessies via beide"), + ("Accept sessions via both", "Accepteer sessies via klik of wachtwoord"), ("Please wait for the remote side to accept your session request...", "Wacht tot de andere kant uw sessieverzoek accepteert..."), ("One-time Password", "Eenmalig Wachtwoord"), - ("Use one-time password", "Gebruik een eenmalig Wachtwoord"), - ("One-time password length", "Eenmalig Wachtwoordlengte"), + ("Use one-time password", "Gebruik een eenmalig wachtwoord"), + ("One-time password length", "Lengte eenmalig wachtwoord"), ("Request access to your device", "Toegang tot uw toestel aanvragen"), ("Hide connection management window", "Verberg het venster voor verbindingsbeheer"), ("hide_cm_tip", "Dit kan alleen als de toegang via een permanent wachtwoord verloopt."), - ("wayland_experiment_tip", "Wayland ondersteuning is slechts experimenteel. Gebruik alsjeblieft X11 als u onbeheerde toegang nodig hebt."), + ("wayland_experiment_tip", "Wayland ondersteuning is slechts experimenteel. Gebruik alstublieft X11 als u onbeheerde toegang nodig heeft."), ("Right click to select tabs", "Rechts klikken om tabbladen te selecteren"), ("Skipped", "Overgeslagen"), ("Add to address book", "Toevoegen aan Adresboek"), @@ -412,8 +408,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Selecteer lokaal toetsenbord"), ("software_render_tip", "Als u een NVIDIA grafische kaart hebt en het externe venster sluit onmiddellijk na verbinding, kan het helpen om het nieuwe stuurprogramma te installeren en te kiezen voor software rendering. Een software herstart is vereist."), ("Always use software rendering", "Gebruik altijd software rendering"), - ("config_input", "Om het externe bureaublad vanaf het toetsenbord te kunnen bedienen, moet u RustDesk de rechten \"Invoerbewaking\" geven."), - ("config_microphone", "Om op afstand te kunnen chatten, moet u RustDesk 'Audio opnemen' rechten geven."), + ("config_input", "Om een extern apparaat met uw toetsenbord te kunnen bedienen, moet u RustDesk toestemming voor Invoer Vastleggen geven."), + ("config_microphone", "Om te kunnen chatten moet u RustDesk toestemming voor Microfoon geven."), ("request_elevation_tip", "U kunt ook meer rechten vragen als iemand aan de andere kant aanwezig is."), ("Wait", "Wacht"), ("Elevation Error", "Verhogingsfout"), @@ -433,20 +429,20 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Medium", "Middelmatig"), ("Strong", "Sterk"), ("Switch Sides", "Wissel van kant"), - ("Please confirm if you want to share your desktop?", "Bevestig als u uw bureaublad wilt delen?"), + ("Please confirm if you want to share your desktop?", "Bevestig dat u uw bureaublad wilt delen?"), ("Display", "Weergave"), - ("Default View Style", "Standaard Weergave Stijl"), - ("Default Scroll Style", "Standaard Scroll Stijl"), + ("Default View Style", "Standaard Weergavestijl"), + ("Default Scroll Style", "Standaard Scrollstijl"), ("Default Image Quality", "Standaard Beeldkwaliteit"), ("Default Codec", "Standaard Codec"), ("Bitrate", "Bitrate"), ("FPS", "FPS"), ("Auto", "Auto"), - ("Other Default Options", "Andere Standaardopties"), + ("Other Default Options", "Overige Standaardinstellingen"), ("Voice call", "Spraakoproep"), - ("Text chat", "Tekst chat"), + ("Text chat", "Tekstchat"), ("Stop voice call", "Stop spraakoproep"), - ("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server. \nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."), + ("relay_hint_tip", "Indien een directe verbinding niet mogelijk is, kunt u proberen verbinding te maken via een Relay Server.\nAls u bij de eerste poging een relaisverbinding tot stand wilt brengen, kunt u het achtervoegsel \"/r\" toevoegen aan het ID of de optie \"Altijd verbinden via relaisserver\" selecteren op de externe terminal."), ("Reconnect", "Opnieuw verbinden"), ("Codec", "Codec"), ("Resolution", "Resolutie"), @@ -459,17 +455,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Minimize", "Minimaliseren"), ("Maximize", "Maximaliseren"), ("Your Device", "Uw Apparaat"), - ("empty_recent_tip", "Oeps, geen actuele situatie!\nTijd om een nieuwe te plannen."), - ("empty_favorite_tip", "Nog geen favoriete Station op afstand? Laat ons iemand vinden om mee te verbinden en voeg hem toe aan uw favorieten!"), + ("empty_recent_tip", "Oeps, geen recente sessies!\nTijd om een nieuwe te plannen."), + ("empty_favorite_tip", "Nog geen favoriete stations op afstand? Laat ons iemand vinden om mee te verbinden en voeg hem toe aan uw favorieten!"), ("empty_lan_tip", "Oh nee, het lijkt erop dat we nog geen extern station hebben ontdekt."), ("empty_address_book_tip", "Oh jee, het lijkt erop dat er momenteel geen externe stations in uw adresboek staan."), - ("eg: admin", "bijvoorbeeld: admin"), ("Empty Username", "Gebruikersnaam Leeg"), ("Empty Password", "Wachtwoord Leeg"), ("Me", "Ik"), ("identical_file_tip", "Dit bestand is identiek aan het bestand van het externe station."), ("show_monitors_tip", "Monitoren weergeven in de werkbalk"), - ("View Mode", "Weergave Mode"), + ("View Mode", "Toeschouwermodus"), ("login_linux_tip", "Toegang tot het externe Linux-account"), ("verify_rustdesk_password_tip", "Bevestiging wachtwoord RustDesk"), ("remember_account_tip", "Herinner dit account"), @@ -508,7 +503,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Exit", "Afsluiten"), ("Open", "Open"), ("logout_tip", "Weet u zeker dat u zich wilt afmelden?"), - ("Service", "Service"), + ("Service", "Achtergrondservice"), ("Start", "Start"), ("Stop", "Stop"), ("exceed_max_devices", "Het maximum aantal gecontroleerde apparaten is bereikt."), @@ -544,7 +539,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Timeout in minutes", "Time-out in minuten"), ("auto_disconnect_option_tip", "Inkomende sessies automatisch sluiten bij inactiviteit van de gebruiker"), ("Connection failed due to inactivity", "Automatisch verbinding verbroken wegens inactiviteit"), - ("Check for software update on startup", "Checken voor updates bij opstarten"), + ("Check for software update on startup", "Controleer op updates bij opstarten"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Upgrade RustDesk Server Pro naar versie {} of nieuwer!"), ("pull_group_failed_tip", "Vernieuwen van groep mislukt"), ("Filter by intersection", "Filter op kruising"), @@ -564,28 +559,28 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Plug out all", "Sluit alle"), ("True color (4:4:4)", "Ware kleur (4:4:4)"), ("Enable blocking user input", "Blokkeren van gebruikersinvoer inschakelen"), - ("id_input_tip", "Je kunt een ID, een direct IP of een domein met een poort (:) invoeren. Als je toegang wilt als apparaat op een andere server, voeg dan het serveradres toe (@?key=), bijvoorbeeld \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.Als je toegang wilt als apparaat op een openbare server, voer dan \"@public\" in, voor de openbare server is de sleutel niet nodig."), - ("privacy_mode_impl_mag_tip", "Modus 1"), - ("privacy_mode_impl_virtual_display_tip", "Modus 2"), + ("id_input_tip", "U kunt een ID, een direct IP of een domein met poort (:) invoeren. Als u toegang wilt tot een apparaat op een andere server, voeg dan een serveradres en public key toe (@?key=), bijvoorbeeld \n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.Als je toegang wilt als apparaat op een openbare server, voer dan \"@public\" in, voor de openbare server is de sleutel niet nodig."), + ("privacy_mode_impl_mag_tip", "Modus 1: Overlayscherm"), + ("privacy_mode_impl_virtual_display_tip", "Modus 2: Monitor slaapstand"), ("Enter privacy mode", "Privacymodus openen"), ("Exit privacy mode", "Privacymodus afsluiten"), ("idd_not_support_under_win10_2004_tip", "Het indirecte displaystuurprogramma wordt niet ondersteund. Windows 10 versie 2004 of later is vereist."), - ("input_source_1_tip", "Invoerbron 1"), - ("input_source_2_tip", "Invoerbron 2"), + ("input_source_1_tip", "Invoerbron 1: Standaard"), + ("input_source_2_tip", "Invoerbron 2: Verouderd"), ("Swap control-command key", "Wissel controle-commando toets"), - ("swap-left-right-mouse", "wissel-links-rechts-muis"), + ("swap-left-right-mouse", "Wissel linker- en rechtermuisknop"), ("2FA code", "2FA-code"), ("More", "Meer"), - ("enable-2fa-title", "activeer-2fa-titel"), - ("enable-2fa-desc", "activeer-2fa-desc"), - ("wrong-2fa-code", "foutieve-2fa-code"), - ("enter-2fa-title", "geef-2fa-titel in"), + ("enable-2fa-title", "Tweefactorauthenticatie inschakelen"), + ("enable-2fa-desc", "Stel nu uw authenticator in. U kunt een authenticator-app zoals Authy, Microsoft of Google Authenticator op uw telefoon of desktop gebruiken.\n\nScan de QR-code met uw app en voer de code in die uw app toont om tweefactorauthenticatie in te schakelen."), + ("wrong-2fa-code", "Kan de code niet verifiëren. Controleer of de code en lokale tijdinstellingen correct zijn."), + ("enter-2fa-title", "Tweefactorauthenticatie (2FA)"), ("Email verification code must be 6 characters.", "E-mailverificatiecode moet 6 tekens lang zijn."), ("2FA code must be 6 digits.", "2FA-code moet 6 cijfers lang zijn."), ("Multiple Windows sessions found", "Meerdere Windows-sessies gevonden"), - ("Please select the session you want to connect to", "Selecteer de sessie waarmee je verbinding wilt maken"), + ("Please select the session you want to connect to", "Selecteer de sessie waarmee u verbinding wilt maken"), ("powered_by_me", "Werkt met Rustdesk"), - ("outgoing_only_desk_tip", "Je kan verbinding maken met andere apparaten, maar andere apparaten kunnen geen verbinding maken met dit apparaat."), + ("outgoing_only_desk_tip", "U kan verbinding maken met andere apparaten, maar andere apparaten kunnen geen verbinding maken met u."), ("preset_password_warning", "Dit is een aangepaste editie en wordt geleverd met een vooraf ingesteld wachtwoord. Iedereen die dit wachtwoord kent, kan de volledige controle over het apparaat krijgen."), ("Security Alert", "Beveiligingswaarschuwing"), ("My address book", "Mijn adresboek"), @@ -603,16 +598,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "Geen fysieke schermen, geen privémodus nodig."), ("Follow remote cursor", "Volg de cursor op afstand"), ("Follow remote window focus", "Volg de focus van het venster op afstand"), - ("default_proxy_tip", "Typisch protocol en poort - Socks5 en 1080"), + ("default_proxy_tip", "Standaard protocol en poort: Socks5 en 1080"), ("no_audio_input_device_tip", "Er is geen invoerapparaat gevonden."), ("Incoming", "Inkomend"), ("Outgoing", "Uitgaand"), ("Clear Wayland screen selection", "Wayland-scherm wissen"), - ("clear_Wayland_screen_selection_tip", "Nadat je de schermselectie hebt gewist, kun je het scherm dat je wilt delen opnieuw selecteren."), - ("confirm_clear_Wayland_screen_selection_tip", "Weet je zeker dat je de Wayland-schermselectie wilt wissen?"), + ("clear_Wayland_screen_selection_tip", "Nadat u de schermselectie heeft gewist, kunt u het scherm dat u wilt delen opnieuw selecteren."), + ("confirm_clear_Wayland_screen_selection_tip", "Weet u zeker dat u de Wayland-schermselectie wilt wissen?"), ("android_new_voice_call_tip", "Er is een nieuwe spraakoproep ontvangen. Als u het aanvaardt, schakelt de audio over naar spraakcommunicatie."), ("texture_render_tip", "Pas textuurrendering toe om afbeeldingen vloeiender te maken."), - ("Use texture rendering", "Textuurrendering gebruiken"), + ("Use texture rendering", "Textuurweergave gebruiken"), ("Floating window", "Zwevend venster"), ("floating_window_tip", "Helpt RustDesk op de achtergrond actief te houden"), ("Keep screen on", "Scherm ingeschakeld laten"), @@ -627,9 +622,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Power", "Stroom"), ("Telegram bot", "Telegram bot"), ("enable-bot-tip", "Als u deze functie inschakelt, kunt u een 2FA-code ontvangen van uw bot. Het kan ook fungeren als een verbindingsmelding."), - ("enable-bot-desc", "1, Open een chat met @BotFather.\n2, Verzend het commando \"/newbot\". Als deze stap voltooid is, ontvang je een token.\n3, Start een chat met de nieuw aangemaakte bot. Om hem te activeren stuurt u een bericht dat begint met een schuine streep (\"/\"), bijvoorbeeld \"/hello\".\n"), - ("cancel-2fa-confirm-tip", "Weet je zeker dat je 2FA wilt annuleren?"), - ("cancel-bot-confirm-tip", "Weet je zeker dat je de Telegram-bot wilt annuleren?"), + ("enable-bot-desc", "1, Open een chat met @BotFather.\n2, Verzend het commando \"/newbot\". Als deze stap voltooid is, ontvangt u een token.\n3, Start een chat met de nieuw aangemaakte bot. Om hem te activeren stuurt u een bericht dat begint met een schuine streep (\"/\"), bijvoorbeeld \"/hello\".\n"), + ("cancel-2fa-confirm-tip", "Weet u zeker dat u 2FA wilt annuleren?"), + ("cancel-bot-confirm-tip", "Weet u zeker dat u de Telegram-bot wilt annuleren?"), ("About RustDesk", "Over RustDesk"), ("Send clipboard keystrokes", "Klembord toetsaanslagen verzenden"), ("network_error_tip", "Controleer de netwerkverbinding en selecteer 'Opnieuw proberen'."), @@ -648,7 +643,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("one-way-file-transfer-tip", "Eenzijdige bestandsoverdracht is ingeschakeld aan de gecontroleerde kant."), ("Authentication Required", "Verificatie vereist"), ("Authenticate", "Verificatie"), - ("web_id_input_tip", "Je kunt een ID invoeren op dezelfde server, directe IP-toegang wordt niet ondersteund in de webclient.\nAls je toegang wilt tot een apparaat op een andere server, voeg je het serveradres toe (@?key=), bijvoorbeeld,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls je toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, sleutel is niet nodig voor de publieke server."), + ("web_id_input_tip", "Je kunt een ID invoeren op dezelfde server, directe IP-toegang wordt niet ondersteund in de webclient.\nAls u toegang wilt tot een apparaat op een andere server, voegt u het serveradres toe (@?key=), bijvoorbeeld,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nAls u toegang wilt krijgen tot een apparaat op een publieke server, voer dan \"@public\" in, sleutel is niet nodig voor de publieke server."), ("Download", "Downloaden"), ("Upload folder", "Map uploaden"), ("Upload files", "Bestanden uploaden"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Klembord van client bijwerken"), ("Untagged", "Ongemarkeerd"), ("new-version-of-{}-tip", "Er is een nieuwe versie van {} beschikbaar"), + ("Accessible devices", "Toegankelijke apparaten"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Upgrade de RustDesk client naar versie {} of nieuwer op de externe computer!"), + ("d3d_render_tip", "Wanneer D3D-rendering is ingeschakeld kan het externe scherm op sommige apparaten, zwart zijn."), + ("Use D3D rendering", "Gebruik D3D-rendering"), + ("Printer", "Printer"), + ("printer-os-requirement-tip", "Windows 10 of hoger is vereist om de uitgaande functie met de printer te laten werken."), + ("printer-requires-installed-{}-client-tip", "Om afdrukken op afstand te gebruiken, moet {} geïnstalleerd zijn op dit apparaat."), + ("printer-{}-not-installed-tip", "De printer {} is niet geïnstalleerd."), + ("printer-{}-ready-tip", "De printer {} is geïnstalleerd en klaar voor gebruik."), + ("Install {} Printer", "Installeer {} Printer"), + ("Outgoing Print Jobs", "Uitgaande Afdruktaken"), + ("Incoming Print Jobs", "Inkomende Afdruktaken"), + ("Incoming Print Job", "Inkomende Afdruktaak"), + ("use-the-default-printer-tip", "Gebruik de standaard printer"), + ("use-the-selected-printer-tip", "Gebruik de geselecteerde printer"), + ("auto-print-tip", "Automatisch afdrukken op de geselecteerde printer."), + ("print-incoming-job-confirm-tip", "Er werd een afdruktaak ontvangen van een extern apparaat. Moet ik deze lokaal afdrukken?"), + ("remote-printing-disallowed-tile-tip", "Afdruk op afstand is verboden"), + ("remote-printing-disallowed-text-tip", "Machtigingsinstellingen aan beheerde zijde verhinderen afdrukken op afstand."), + ("save-settings-tip", "Instellingen opslaan"), + ("dont-show-again-tip", "Dit bericht wordt niet meer weergegeven"), + ("Take screenshot", "Maak een schermafbeelding"), + ("Taking screenshot", "Schermafbeelding maken"), + ("screenshot-merged-screen-not-supported-tip", "Schermafbeeldingen van meerdere schermen samenvoegen wordt momenteel niet ondersteund. Schakel over naar een enkel scherm en herhaal de actie."), + ("screenshot-action-tip", "Kies wat je met de gemaakte schermafbeelding wilt doen."), + ("Save as", "Opslaan als"), + ("Copy to clipboard", "Kopiëren naar het klembord"), + ("Enable remote printer", "Printer op afstand inschakelen"), + ("Downloading {}", "Downloaden {}"), + ("{} Update", "{} Updaten"), + ("{}-to-update-tip", "{} zal sluiten en de nieuwe versie installeren."), + ("download-new-version-failed-tip", "Fout bij het downloaden. Je kunt het opnieuw proberen of op de knop Downloaden klikken om de applicatie van de officiële website te downloaden en handmatig bij te werken."), + ("Auto update", "Automatisch updaten"), + ("update-failed-check-msi-tip", "Kan de installatiemethode niet bepalen. Klik op “Downloaden†om de applicatie van de officiële website te downloaden en handmatig bij te werken."), + ("websocket_tip", "Het WebSocketprotocol ondersteunt alleen verbindingen met de repeater."), + ("Use WebSocket", "Gebruik het WebSocketprotocol"), + ("Trackpad speed", "Snelheid Trackpad"), + ("Default trackpad speed", "Standaardsnelheid Trackpad"), + ("Numeric one-time password", "Eenmalig numeriek wachtwoord"), + ("Enable IPv6 P2P connection", "IPv6 P2P-verbinding inschakelen"), + ("Enable UDP hole punching", "UDP-hole punching inschakelen"), + ("View camera", "Camera bekijken"), + ("Enable camera", "Camera inschakelen"), + ("No cameras", "Geen camera's"), + ("view_camera_unsupported_tip", "Het externe apparaat ondersteunt geen cameraweergave."), + ("Terminal", "Terminal"), + ("Enable terminal", "Terminal inschakelen"), + ("New tab", "Nieuw tabblad"), + ("Keep terminal sessions on disconnect", "Terminalsessies bij verbreking van de verbinding behouden"), + ("Terminal (Run as administrator)", "Terminal (Als administrator uitvoeren)"), + ("terminal-admin-login-tip", "Voer de gebruikersnaam en het wachtwoord in van de beheerder van het gecontroleerde apparaat."), + ("Failed to get user token.", "Kan geen gebruikerstoken krijgen."), + ("Incorrect username or password.", "Foutieve gebruikersnaam of wachtwoord."), + ("The user is not an administrator.", "De gebruiker is geen beheerder."), + ("Failed to check if the user is an administrator.", "Fout bij het controleren of de gebruiker een beheerder is."), + ("Supported only in the installed version.", "Alleen ondersteund in de geïnstalleerde versie."), + ("elevation_username_tip", "Voer je gebruikersnaam of domeinnaam in"), + ("Preparing for installation ...", "Voorbereiden voor installatie ..."), ].iter().cloned().collect(); } diff --git a/src/lang/pl.rs b/src/lang/pl.rs index e28430f498e..e08d65f2872 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "o dÅ‚ugoÅ›ci od %min% do %max%"), ("starts with a letter", "rozpoczyna siÄ™ literÄ…"), ("allowed characters", "dozwolone znaki"), - ("id_change_tip", "Nowy ID może być zÅ‚ożony z maÅ‚ych i dużych liter a-zA-z, cyfry 0-9 oraz _ (podkreÅ›lenie). Pierwszym znakiem powinna być litera a-zA-Z, a caÅ‚e ID powinno skÅ‚adać siÄ™ z 6 do 16 znaków."), + ("id_change_tip", "Nowy ID może być zÅ‚ożony z maÅ‚ych i dużych liter a-zA-z, cyfry 0-9, - (dash) oraz _ (podkreÅ›lenie). Pierwszym znakiem powinna być litera a-zA-Z, a caÅ‚e ID powinno skÅ‚adać siÄ™ z 6 do 16 znaków."), ("Website", "Strona internetowa"), ("About", "O aplikacji"), ("Slogan_tip", "Tworzone z miÅ‚oÅ›ciÄ… w tym peÅ‚nym chaosu Å›wiecie!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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Ä™cia problemów z UAC, kliknij poniższy przycisk by zainstalować RustDesk w swoim systemie."), ("Click to upgrade", "Zaktualizuj"), - ("Click to download", "Pobierz"), - ("Click to update", "Uaktualnij"), ("Configure", "Konfiguruj"), ("config_acc", "Konfiguracja konta"), ("config_screen", "Konfiguracja ekranu"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Brak uprawnieÅ„ na przesyÅ‚anie plików"), ("Note", "Notatka"), ("Connection", "Połączenie"), - ("Share Screen", "UdostÄ™pnij ekran"), + ("Share screen", "UdostÄ™pnij ekran"), ("Chat", "Czat"), ("Total", "ÅÄ…cznie"), ("items", "elementów"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Przechwytywanie ekranu"), ("Input Control", "Kontrola wejÅ›cia"), ("Audio Capture", "Przechwytywanie dźwiÄ™ku"), - ("File Connection", "Przekazywanie plików"), - ("Screen Connection", "Przekazywanie ekranu"), ("Do you accept?", "Akceptujesz?"), ("Open System Setting", "Otwórz ustawienia systemowe"), ("How to get Android input permission?", "Jak uzyskać uprawnienia do wprowadzania danych w systemie Android?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Brak ulubionych?\nZnajdźmy kogoÅ›, z kim możesz siÄ™ połączyć i dodaj Go do ulubionych!"), ("empty_lan_tip", "Ojej, wyglÄ…da na to, że nie odkryliÅ›my żadnych urzÄ…dzeÅ„ z RustDesk w Twojej sieci."), ("empty_address_book_tip", "Ojej, wyglÄ…da na to, że nie ma żadnych wpisów w Twojej książce adresowej."), - ("eg: admin", "np. admin"), ("Empty Username", "Pusty użytkownik"), ("Empty Password", "Puste hasÅ‚o"), ("Me", "Ja"), @@ -654,7 +649,65 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Upload files", "WyÅ›lij pliki"), ("Clipboard is synchronized", "Schowek jest zsynchronizowany"), ("Update client clipboard", "Uaktualnij schowek klienta"), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Untagged", "Bez etykiety"), + ("new-version-of-{}-tip", "DostÄ™pna jest nowa wersja {}"), + ("Accessible devices", "DostÄ™pne urzÄ…dzenia"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "ProszÄ™ zaktualizować zdalny klient RustDesk do wersji {} lub nowszej!"), + ("d3d_render_tip", "Kiedy włączenie renderowania D3D jest włączone, ekran zdalnej kontroli może być czarny w niektórych przypadkach"), + ("Use D3D rendering", "Użyj renderowania D3D"), + ("Printer", "Drukarka"), + ("printer-os-requirement-tip", "Funkcja drukowania zdalnego wymaga Windows 10 lub nowszego"), + ("printer-requires-installed-{}-client-tip", "Aby włączyć funkcjÄ™ zdalnego drukowania, {} musi być zainstalowany na tym urzÄ…dzeniu."), + ("printer-{}-not-installed-tip", "Drukarka {} nie jest zainstalowana."), + ("printer-{}-ready-tip", "Drukarka {} jest zainstalowana i gotowa do użycia."), + ("Install {} Printer", "Zainstaluj drukarkÄ™ {}"), + ("Outgoing Print Jobs", "WychodzÄ…ce zadania drukowania"), + ("Incoming Print Jobs", "PrzychodzÄ…ce zadania drukowania"), + ("Incoming Print Job", "PrzychodzÄ…ce zadanie drukowania"), + ("use-the-default-printer-tip", "Użyj domyÅ›lnej drukarki"), + ("use-the-selected-printer-tip", "Użyj wybranej drukarki"), + ("auto-print-tip", "Drukuj automatycznie używajÄ…c wybranej drukarki"), + ("print-incoming-job-confirm-tip", "OtrzymaÅ‚eÅ› zadanie zdalnego drukowania. Chcesz wykonać je po swojej stronie?"), + ("remote-printing-disallowed-tile-tip", "Zdalne drukowanie niedozwolone"), + ("remote-printing-disallowed-text-tip", "Ustawienia uprawnieÅ„ po zdalnej stronie uniemożliwiajÄ… zdalne drukowanie."), + ("save-settings-tip", "Zapisz ustawienia"), + ("dont-show-again-tip", "Nie pokazuj wiÄ™cej"), + ("Take screenshot", "Zrób zrzut ekranu"), + ("Taking screenshot", "Tworzenie zrzutu ekranu"), + ("screenshot-merged-screen-not-supported-tip", "ÅÄ…czenie zrzutów ekranu z wielu wyÅ›wietlaczy nie jest obecnie obsÅ‚ugiwane. Przełącz siÄ™ na pojedynczy wyÅ›wietlacz i spróbuj ponownie."), + ("screenshot-action-tip", "Wybierz sposób kontynuacji zrzutu ekranu."), + ("Save as", "Zapisz jako"), + ("Copy to clipboard", "Kopiuj do schowka"), + ("Enable remote printer", "Włącz zdalne drukowanie"), + ("Downloading {}", "Pobieranie {}"), + ("{} Update", "Aktualizacja {}"), + ("{}-to-update-tip", "{} zostanie teraz zamkniÄ™ty i zostanie zainstalowana nowa wersja."), + ("download-new-version-failed-tip", "Pobieranie nie powiodÅ‚o siÄ™. Możesz spróbować ponownie lub kliknąć przycisk \"Pobierz\", aby pobrać ze strony programu i uaktualnić rÄ™cznie."), + ("Auto update", "Automatyczna aktualizacja"), + ("update-failed-check-msi-tip", "Sprawdzenie metody instalacji nie powiodÅ‚o siÄ™. Kliknij przycisk \"Pobierz\", aby pobrać ze strony wydania i uaktualnić rÄ™cznie."), + ("websocket_tip", "Gdy używasz WebSocket, obsÅ‚ugiwane sÄ… tylko połączenia przekaźnikowe."), + ("Use WebSocket", "Użyj WebSocket"), + ("Trackpad speed", "Szybkość gÅ‚adzika"), + ("Default trackpad speed", "DomyÅ›lna szybkość gÅ‚adzika"), + ("Numeric one-time password", "Jednorazowe hasÅ‚o cyfrowe"), + ("Enable IPv6 P2P connection", "Włącz połączenie P2P IPv6"), + ("Enable UDP hole punching", "Włącz tworzenie tunelu UDP"), + ("View camera", "PodglÄ…d kamery"), + ("Enable camera", "Włącz kamerÄ™"), + ("No cameras", "Brak kamer"), + ("view_camera_unsupported_tip", "Zdalne urzÄ…dzenie nie obsÅ‚uguje podglÄ…du kamery."), + ("Terminal", "Rerminal"), + ("Enable terminal", "Włącz terminal"), + ("New tab", "Nowa zakÅ‚adka"), + ("Keep terminal sessions on disconnect", "Utrzymaj sesjÄ™ terminala przy rozłączeniu"), + ("Terminal (Run as administrator)", "Terminal (uruchom jako administrator)"), + ("terminal-admin-login-tip", "ProszÄ™ wprowadzić użytkownika i hasÅ‚o administratora kontrolowanego urzÄ…dzenia."), + ("Failed to get user token.", "Błąd pobierania tokenu użytkownika."), + ("Incorrect username or password.", "NieprawidÅ‚owy użytkownik lub hasÅ‚o."), + ("The user is not an administrator.", "Użytkownik nie posiada praw administratora."), + ("Failed to check if the user is an administrator.", "Błąd sprawdzania, czy użytkownik jest administratorem."), + ("Supported only in the installed version.", "Wspierane tylko dla zainstalowanej aplikacji."), + ("elevation_username_tip", "Podaj nazwÄ™ użytkownika lub domena\\użytkownik"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs index 13f829f77af..bbfd265936b 100644 --- a/src/lang/pt_PT.rs +++ b/src/lang/pt_PT.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9, - (dash) e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", ""), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Senha do SO"), ("install_tip", "Devido ao UAC, o RustDesk não funciona correctamente em alguns casos. Para evitar o UAC, por favor clique no botão abaixo para instalar o RustDesk no sistema."), ("Click to upgrade", "Clique para atualizar"), - ("Click to download", "Clique para carregar"), - ("Click to update", "Clique para fazer a actualização"), ("Configure", "Configurar"), ("config_acc", "Para controlar o seu Ambiente de Trabalho remotamente, é preciso conceder ao RustDesk permissões de \"Acessibilidade\"."), ("config_screen", "Para aceder ao seu Ambiente de Trabalho remotamente, é preciso conceder ao RustDesk permissões de \"Gravar a Tela\"/"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Sem permissões de transferência de ficheiro"), ("Note", "Nota"), ("Connection", "Ligação"), - ("Share Screen", "Partilhar ecrã"), + ("Share screen", "Partilhar ecrã"), ("Chat", "Conversar"), ("Total", "Total"), ("items", "itens"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de Ecran"), ("Input Control", "Controle de Entrada"), ("Audio Capture", "Captura de Ãudio"), - ("File Connection", "Ligação de Arquivo"), - ("Screen Connection", "Ligação de Ecran"), ("Do you accept?", "Aceita?"), ("Open System Setting", "Abrir Configurações do Sistema"), ("How to get Android input permission?", "Como activar a permissão de entrada do Android?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Ver câmara"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index eff01dd5eee..1a41dc307b9 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "tamanho %min% para %max%"), ("starts with a letter", "começa com uma letra"), ("allowed characters", "caracteres permitidos"), - ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9 e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), + ("id_change_tip", "Somente os caracteres a-z, A-Z, 0-9, - (dash) e _ (sublinhado) são permitidos. A primeira letra deve ser a-z, A-Z. Comprimento entre 6 e 16."), ("Website", "Website"), ("About", "Sobre"), ("Slogan_tip", "Feito de coração neste mundo caótico!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Senha do SO"), ("install_tip", "Devido ao UAC, o RustDesk não funciona corretamente como o lado remoto em alguns casos. Para evitar o UAC, por favor clique no botão abaixo para instalar o RustDesk no sistema."), ("Click to upgrade", "Clique para fazer o upgrade"), - ("Click to download", "Clique para baixar"), - ("Click to update", "Clique para fazer o update"), ("Configure", "Configurar"), ("config_acc", "Para controlar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Acessibilidade\"."), ("config_screen", "Para acessar seu computador remotamente, você precisa conceder ao RustDesk permissões de \"Gravar a Tela\"/"), @@ -230,7 +228,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Username missed", "Nome de usuário requerido"), ("Password missed", "Senha requerida"), ("Wrong credentials", "Nome de usuário ou senha incorretos"), - ("The verification code is incorrect or has expired", ""), + ("The verification code is incorrect or has expired", "O código de verificação está incorreto ou expirou"), ("Edit Tag", "Editar Tag"), ("Forget Password", "Esquecer Senha"), ("Favorites", "Favoritos"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Sem permissão para transferência de arquivo"), ("Note", "Nota"), ("Connection", "Conexão"), - ("Share Screen", "Compartilhar Tela"), + ("Share screen", "Compartilhar Tela"), ("Chat", "Chat"), ("Total", "Total"), ("items", "itens"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Captura de Tela"), ("Input Control", "Controle de Entrada"), ("Audio Capture", "Captura de Ãudio"), - ("File Connection", "Conexão de Arquivo"), - ("Screen Connection", "Conexão de Tela"), ("Do you accept?", "Você aceita?"), ("Open System Setting", "Abrir Configurações do Sistema"), ("How to get Android input permission?", "Como habilitar a permissão de entrada do Android?"), @@ -330,8 +326,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Ratio", "Proporção"), ("Image Quality", "Qualidade de Imagem"), ("Scroll Style", "Estilo de Rolagem"), - ("Show Toolbar", ""), - ("Hide Toolbar", ""), + ("Show Toolbar", "Mostrar Barra de Ferramentas"), + ("Hide Toolbar", "Ocultar Barra de Ferramentas"), ("Direct Connection", "Conexão Direta"), ("Relay Connection", "Conexão via Relay"), ("Secure Connection", "Conexão Segura"), @@ -345,7 +341,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Light Theme", "Tema Claro"), ("Dark", "Escuro"), ("Light", "Claro"), - ("Follow System", "Seguir sistema"), + ("Follow System", "Padrão do sistema"), ("Enable hardware codec", "Habilitar codec de hardware"), ("Unlock Security Settings", "Desbloquear configurações de segurança"), ("Enable audio", "Habilitar áudio"), @@ -359,12 +355,12 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Audio Input Device", "Dispositivo de entrada de áudio"), ("Use IP Whitelisting", "Utilizar lista de IPs confiáveis"), ("Network", "Rede"), - ("Pin Toolbar", ""), - ("Unpin Toolbar", ""), + ("Pin Toolbar", "Fixar Barra de Ferramentas"), + ("Unpin Toolbar", "Desafixar Barra de Ferramentas"), ("Recording", "Gravando"), ("Directory", "Diretório"), ("Automatically record incoming sessions", "Gravar automaticamente sessões de entrada"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Gravar automaticamente sessões de saída"), ("Change", "Alterar"), ("Start session recording", "Iniciar gravação da sessão"), ("Stop session recording", "Parar gravação da sessão"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ainda não há parceiros favoritos?\nVamos encontrar alguém para se conectar e adicioná-lo aos seus favoritos!"), ("empty_lan_tip", "Ah não, parece que ainda não descobrimos nenhum parceiro."), ("empty_address_book_tip", "Oh céus, parece que atualmente não há parceiros listados em seu catálogo de endereços."), - ("eg: admin", "ex. admin"), ("Empty Username", "Nome de Usuário vazio"), ("Empty Password", "Senha Vazia"), ("Me", "Eu"), @@ -534,7 +529,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Installation Successful!", "Instalação bem-sucedida!"), ("Installation failed!", "A instalação falhou!"), ("Reverse mouse wheel", "Inverter rolagem do mouse"), - ("{} sessions", ""), + ("{} sessions", "{} sessões"), ("scam_title", "Você pode estar sendo ENGANADO!"), ("scam_text1", "Se você estiver ao telefone com alguém que NÃO conhece e em quem NÃO confia e essa pessoa pedir para você usar o RustDesk e iniciar o serviço, NÃO faça isso !! e desligue imediatamente."), ("scam_text2", "Provavelmente são golpistas tentando roubar seu dinheiro ou informações privadas."), @@ -547,7 +542,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Check for software update on startup", "Verificar atualizações do software ao iniciar"), ("upgrade_rustdesk_server_pro_to_{}_tip", "Atualize o RustDesk Server Pro para a versão {} ou superior."), ("pull_group_failed_tip", "Não foi possível atualizar o grupo."), - ("Filter by intersection", ""), + ("Filter by intersection", "Filtrar por interseção"), ("Remove wallpaper during incoming sessions", "Remover papel de parede durante sessão remota"), ("Test", "Teste"), ("display_is_plugged_out_msg", "A tela está desconectada. Mudando para a principal."), @@ -565,8 +560,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("True color (4:4:4)", "Cor verdadeira (4:4:4)"), ("Enable blocking user input", "Habilitar bloqueio da entrada do usuário"), ("id_input_tip", "Você pode inserir um ID, um IP direto ou um domínio com uma porta (:).\nPara acessar um dispositivo em outro servidor, adicione o IP do servidor (@?key=), por exemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nPara acessar um dispositivo em um servidor público, insira \"@public\", a chave não é necessária para um servidor público."), - ("privacy_mode_impl_mag_tip", ""), - ("privacy_mode_impl_virtual_display_tip", ""), + ("privacy_mode_impl_mag_tip", "Modo 1"), + ("privacy_mode_impl_virtual_display_tip", "Modo 2"), ("Enter privacy mode", "Entrar no modo privado"), ("Exit privacy mode", "Sair do modo privado"), ("idd_not_support_under_win10_2004_tip", "O driver de tela indireto não é suportado. É necessário o Windows 10, versão 2004 ou superior."), @@ -589,10 +584,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("preset_password_warning", "Atenção: esta edição personalizada vem com uma senha predefinida. Qualquer pessoa que a conhecer poderá controlar totalmente seu dispositivo. Se isso não for o que você deseja, desinstale o software imediatamente."), ("Security Alert", "Alerta de Segurança"), ("My address book", "Minha lista de contatos"), - ("Personal", ""), + ("Personal", "Pessoal"), ("Owner", "Proprietário"), ("Set shared password", "Definir senha compartilhada"), - ("Exist in", ""), + ("Exist in", "Existe em"), ("Read-only", "Apenas leitura"), ("Read/Write", "Leitura/escrita"), ("Full Control", "Controle total"), @@ -603,14 +598,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("no_need_privacy_mode_no_physical_displays_tip", "Sem telas físicas, o modo privado não é necessário"), ("Follow remote cursor", "Seguir cursor remoto"), ("Follow remote window focus", "Seguir janela remota ativa"), - ("default_proxy_tip", ""), + ("default_proxy_tip", "O protocolo e a porta padrão são Socks5 e 1080"), ("no_audio_input_device_tip", "Nenhum dispositivo de entrada de áudio encontrado"), - ("Incoming", ""), - ("Outgoing", ""), + ("Incoming", "Entrada"), + ("Outgoing", "Saída"), ("Clear Wayland screen selection", "Limpar seleção de tela do Wayland"), ("clear_Wayland_screen_selection_tip", "Depois de limpar a seleção de tela, você pode selecioná-la novamente para compartilhar."), ("confirm_clear_Wayland_screen_selection_tip", "Tem certeza que deseja limpar a seleção da tela do Wayland?"), - ("android_new_voice_call_tip", ""), + ("android_new_voice_call_tip", "Uma nova solicitação de chamada de voz foi recebida. Se você aceitar, o áudio será alternado para comunicação por voz."), ("texture_render_tip", "Use renderização de textura para tornar as imagens mais suaves"), ("Use texture rendering", "Usar renderização de textura"), ("Floating window", "Janela flutuante"), @@ -624,7 +619,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Apps", "Apps"), ("Volume up", "Aumentar volume"), ("Volume down", "Diminuir volume"), - ("Power", ""), + ("Power", "Energia"), ("Telegram bot", "Bot Telegram"), ("enable-bot-tip", "Se você ativar este recurso, poderá receber o código 2FA do seu bot. Ele também pode funcionar como uma notificação de conexão."), ("enable-bot-desc", "1. Abra um chat com @BotFather.\n2. Envie o comando \"/newbot\". Você receberá um token após completar esta etapa.\n3. Inicie um chat com o seu bot recém-criado. Envie uma mensagem começando com uma barra invertida (\"/\"), como \"/hello\", para ativá-lo.\n"), @@ -645,16 +640,74 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Parent directory", "Diretório pai"), ("Resume", "Continuar"), ("Invalid file name", "Nome de arquivo inválido"), - ("one-way-file-transfer-tip", ""), - ("Authentication Required", ""), - ("Authenticate", ""), - ("web_id_input_tip", ""), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("one-way-file-transfer-tip", "A transferência de arquivos unidirecional está ativada no dispositivo controlado."), + ("Authentication Required", "Autenticação necessária"), + ("Authenticate", "Autenticar"), + ("web_id_input_tip", "Você pode inserir um ID no mesmo servidor; o acesso direto por IP não é suportado no cliente web.\nSe desejar acessar um dispositivo em outro servidor, por favor, adicione o endereço do servidor (@?key=), por exemplo,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSe desejar acessar um dispositivo em um servidor público, por favor, insira \"@public\", a chave não é necessária para servidores públicos."), + ("Download", "Baixar"), + ("Upload folder", "Carregar pasta"), + ("Upload files", "Carregar arquivos"), + ("Clipboard is synchronized", "A área de transferência está sincronizada"), + ("Update client clipboard", "Atualizar a área de transferência do cliente"), + ("Untagged", "Sem etiqueta"), + ("new-version-of-{}-tip", "Uma nova versão de {} está disponível"), + ("Accessible devices", "Dispositivos acessíveis"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualize o cliente RustDesk para a versão {} ou superior no lado remoto."), + ("d3d_render_tip", "Em algumas máquinas, a tela do controle remoto pode ficar preta ao usar a renderização D3D."), + ("Use D3D rendering", "Usar renderização D3D"), + ("Printer", "Impressora"), + ("printer-os-requirement-tip", "A função de impressão de saída requer Windows 10 ou superior."), + ("printer-requires-installed-{}-client-tip", "{} deve ser instalado neste dispositivo antes que você possa usar a impressão remota."), + ("printer-{}-not-installed-tip", "A impressora {} não está instalada."), + ("printer-{}-ready-tip", "A impressora {} está instalada e operacional."), + ("Install {} Printer", "Instalar impressora {}"), + ("Outgoing Print Jobs", "Trabalhos de impressão enviados"), + ("Incoming Print Jobs", "Trabalhos de impressão recebidos"), + ("Incoming Print Job", "Impressão recebida"), + ("use-the-default-printer-tip", "Usar impressora padrão"), + ("use-the-selected-printer-tip", "Usar impressora selecionada"), + ("auto-print-tip", "Imprimir automaticamente usando a impressora selecionada."), + ("print-incoming-job-confirm-tip", "O dispositivo remoto enviou uma impressão. Deseja imprimir?"), + ("remote-printing-disallowed-tile-tip", "Impressão remota não permitida"), + ("remote-printing-disallowed-text-tip", "As configurações do dispositivo controlado não permitem impressão remota."), + ("save-settings-tip", "Salvar configurações"), + ("dont-show-again-tip", "Não mostrar novamente"), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Visualizar câmera"), + ("Enable camera", "Ativar câmera"), + ("No cameras", "Sem câmeras"), + ("view_camera_unsupported_tip", "O dispositivo remoto não suporta visualização da câmera."), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ro.rs b/src/lang/ro.rs index 8bd79c18950..93eb232da76 100644 --- a/src/lang/ro.rs +++ b/src/lang/ro.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "lungime între %min% È™i %max%"), ("starts with a letter", "începe cu o literă"), ("allowed characters", "caractere permise"), - ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 È™i 16 caractere."), + ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, - (dash), _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 È™i 16 caractere."), ("Website", "Site web"), ("About", "Despre"), ("Slogan_tip", "Făcut din inimă în lumea aceasta haotică!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Parolă sistem"), ("install_tip", "Din cauza restricÈ›iilor CCU, este posibil ca RustDesk să nu funcÈ›ioneze corespunzător. Pentru a evita acest lucru, dă clic pe butonul de mai jos pentru a instala RustDesk."), ("Click to upgrade", "Dă clic pentru a face upgrade"), - ("Click to download", "Dă clic pentru a descărca"), - ("Click to update", "Dă clic pentru a actualiza"), ("Configure", "Configurează"), ("config_acc", "Pentru a controla desktopul la distanță, trebuie să permiÈ›i RustDesk acces la setările de Accesibilitate."), ("config_screen", "Pentru a controla desktopul la distanță, trebuie să permiÈ›i RustDesk acces la setările de ÃŽnregistrare ecran."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nicio permisiune pentru transferul de fiÈ™iere"), ("Note", "ReÈ›ine"), ("Connection", "Conexiune"), - ("Share Screen", "Partajează ecran"), + ("Share screen", "Partajează ecran"), ("Chat", "Mesaje"), ("Total", "Total"), ("items", "elemente"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Capturare ecran"), ("Input Control", "Control intrări"), ("Audio Capture", "Capturare audio"), - ("File Connection", "Conexiune fiÈ™ier"), - ("Screen Connection", "Conexiune ecran"), ("Do you accept?", "AccepÈ›i?"), ("Open System Setting", "Deschide setări sistem"), ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "ÃŽncă nu ai niciun dispozitiv pereche favorit?\nHai să-È›i găsim pe cineva cu care să te conectezi, iar apoi poÈ›i adăuga dispozitivul la Favorite!"), ("empty_lan_tip", "Of! S-ar părea că încă nu am descoperit niciun dispozitiv."), ("empty_address_book_tip", "Măi să fie! Se pare că deocamdată nu figurează niciun dispozitiv în agenda ta."), - ("eg: admin", "ex: admin"), ("Empty Username", "Nume utilizator nespecificat"), ("Empty Password", "Parolă nespecificată"), ("Me", "Eu"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Vezi camera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index b035947d561..30fd697cb84 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -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", "ПоÑледние ÑеанÑÑ‹"), @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "длина %min%...%max%"), ("starts with a letter", "начинаетÑÑ Ñ Ð±ÑƒÐºÐ²Ñ‹"), ("allowed characters", "допуÑтимые Ñимволы"), - ("id_change_tip", "ДопуÑкаютÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ Ñимволы a-z, A-Z, 0-9 и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), + ("id_change_tip", "ДопуÑкаютÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ Ñимволы a-z, A-Z, 0-9, - (dash) и _ (подчёркивание). Первой должна быть буква a-z, A-Z. Длина от 6 до 16."), ("Website", "Сайт"), ("About", "О приложении"), ("Slogan_tip", "Сделано Ñ Ð´ÑƒÑˆÐ¾Ð¹ в Ñтом безумном мире!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Пароль входа в ОС"), ("install_tip", "Ð’ некоторых ÑлучаÑÑ… из-за UAC RustDesk может работать неправильно на удалённом узле. Чтобы избежать возможных проблем Ñ UAC, нажмите кнопку ниже Ð´Ð»Ñ ÑƒÑтановки RustDesk в ÑиÑтеме."), ("Click to upgrade", "Ðажмите, чтобы обновить"), - ("Click to download", "Ðажмите, чтобы Ñкачать"), - ("Click to update", "Ðажмите, чтобы обновить"), ("Configure", "ÐаÑтроить"), ("config_acc", "Чтобы удалённо управлÑть Ñвоим рабочим Ñтолом, вы должны предоÑтавить RustDesk права \"доÑтупа\""), ("config_screen", "Ð”Ð»Ñ ÑƒÐ´Ð°Ð»Ñ‘Ð½Ð½Ð¾Ð³Ð¾ доÑтупа к рабочему Ñтолу вы должны предоÑтавить RustDesk права \"Ñнимок Ñкрана\""), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ðет Ñ€Ð°Ð·Ñ€ÐµÑˆÐµÐ½Ð¸Ñ Ð½Ð° передачу файлов"), ("Note", "Заметка"), ("Connection", "Подключение"), - ("Share Screen", "ДемонÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ñкрана"), + ("Share screen", "ДемонÑÑ‚Ñ€Ð°Ñ†Ð¸Ñ Ñкрана"), ("Chat", "Чат"), ("Total", "Ð’Ñего"), ("items", "Ñлементы"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Захват Ñкрана"), ("Input Control", "Управление вводом"), ("Audio Capture", "Захват аудио"), - ("File Connection", "Подключение передачи файлов"), - ("Screen Connection", "Подключение проÑмотра/ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ñкраном"), ("Do you accept?", "Ð’Ñ‹ ÑоглаÑны?"), ("Open System Setting", "Открыть наÑтройки ÑиÑтемы"), ("How to get Android input permission?", "Как получить разрешение на ввод Android?"), @@ -299,7 +295,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unsupported", "Ðе поддерживаетÑÑ"), ("Peer denied", "Отклонено удалённым узлом"), ("Please install plugins", "УÑтановите плагины"), - ("Peer exit", "Удалённый узел отключён"), + ("Peer exit", "Отключено пользователем"), ("Failed to turn off", "Ðевозможно отключить"), ("Turned off", "Отключён"), ("Language", "Язык"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Ещё нет избранных удалённых узлов?\nДавайте найдём, кого можно добавить в избранное!"), ("empty_lan_tip", "Ðе найдено удалённых узлов."), ("empty_address_book_tip", "Ð’ адреÑной книге нет удалённых узлов."), - ("eg: admin", "например: admin"), ("Empty Username", "ПуÑтое Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ"), ("Empty Password", "ПуÑтой пароль"), ("Me", "Я"), @@ -567,8 +562,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("id_input_tip", "Можно ввеÑти идентификатор, прÑмой IP-Ð°Ð´Ñ€ÐµÑ Ð¸Ð»Ð¸ домен Ñ Ð¿Ð¾Ñ€Ñ‚Ð¾Ð¼ (<домен>:<порт>).\nЕÑли необходимо получить доÑтуп к уÑтройÑтву на другом Ñервере, добавьте Ð°Ð´Ñ€ÐµÑ Ñервера (@<адреÑ_Ñервера>?key=<ключ_значение>), например:\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nЕÑли необходимо получить доÑтуп к уÑтройÑтву на общедоÑтупном Ñервере, введите \"@public\", ключ Ð´Ð»Ñ Ð¿ÑƒÐ±Ð»Ð¸Ñ‡Ð½Ð¾Ð³Ð¾ Ñервера не требуетÑÑ."), ("privacy_mode_impl_mag_tip", "Режим 1"), ("privacy_mode_impl_virtual_display_tip", "Режим 2"), - ("Enter privacy mode", "Включить режим конфиденциальноÑти"), - ("Exit privacy mode", "Отключить режим конфиденциальноÑти"), + ("Enter privacy mode", "Режим конфиденциальноÑти включён"), + ("Exit privacy mode", "Режим конфиденциальноÑти отключён"), ("idd_not_support_under_win10_2004_tip", "Драйвер непрÑмого Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð½Ðµ поддерживаетÑÑ. ТребуетÑÑ Windows 10 верÑии 2004 или новее."), ("input_source_1_tip", "ИÑточник ввода 1"), ("input_source_2_tip", "ИÑточник ввода 2"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Обновить буфер обмена клиента"), ("Untagged", "Без метки"), ("new-version-of-{}-tip", "ДоÑтупна Ð½Ð¾Ð²Ð°Ñ Ð²ÐµÑ€ÑÐ¸Ñ {}"), + ("Accessible devices", "ДоÑтупные уÑтройÑтва"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Обновите клиент RustDesk до верÑии {} или новее на удалённой Ñтороне!"), + ("d3d_render_tip", "При включении визуализации D3D на некоторых уÑтройÑтвах удалённый Ñкран может быть чёрным."), + ("Use D3D rendering", "ИÑпользовать визуализацию D3D"), + ("Printer", "Принтер"), + ("printer-os-requirement-tip", "Ð”Ð»Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ функции иÑходÑщей ÑвÑзи Ñ Ð¿Ñ€Ð¸Ð½Ñ‚ÐµÑ€Ð¾Ð¼ требуетÑÑ Windows 10 или более поздней верÑии."), + ("printer-requires-installed-{}-client-tip", "Чтобы иÑпользовать удалённую печать, {} должен быть уÑтановлен на Ñтом уÑтройÑтве."), + ("printer-{}-not-installed-tip", "Принтер {} не уÑтановлен."), + ("printer-{}-ready-tip", "Принтер {} уÑтановлен и готов к иÑпользованию."), + ("Install {} Printer", "УÑтановить принтер {}"), + ("Outgoing Print Jobs", "ИÑходÑщее задание печати"), + ("Incoming Print Jobs", "ВходÑщее задание печати"), + ("Incoming Print Job", "ВходÑщее задание печати"), + ("use-the-default-printer-tip", "ИÑпользовать принтер по умолчанию"), + ("use-the-selected-printer-tip", "ИÑпользовать выбранный принтер"), + ("auto-print-tip", "ÐвтоматичеÑки выполнÑть печать на выбранном принтере."), + ("print-incoming-job-confirm-tip", "Получено задание на печать Ñ ÑƒÐ´Ð°Ð»Ñ‘Ð½Ð½Ð¾Ð³Ð¾ уÑтройÑтва. Выполнить его локально?"), + ("remote-printing-disallowed-tile-tip", "Ð£Ð´Ð°Ð»Ñ‘Ð½Ð½Ð°Ñ Ð¿ÐµÑ‡Ð°Ñ‚ÑŒ запрещена"), + ("remote-printing-disallowed-text-tip", "ÐаÑтройки разрешений на управлÑемой Ñтороне запрещают удалённую печать."), + ("save-settings-tip", "Сохранить наÑтройки"), + ("dont-show-again-tip", "Больше не показывать"), + ("Take screenshot", "Сделать Ñнимок Ñкрана"), + ("Taking screenshot", "Получение Ñнимка Ñкрана"), + ("screenshot-merged-screen-not-supported-tip", "Объединение Ñнимков Ñкранов Ñ Ð½ÐµÑкольких диÑплеев в наÑтоÑщее Ð²Ñ€ÐµÐ¼Ñ Ð½Ðµ поддерживаетÑÑ. ПереключитеÑÑŒ на один диÑплей и повторите дейÑтвие."), + ("screenshot-action-tip", "Выберите, что делать Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð½Ñ‹Ð¼ Ñнимком Ñкрана."), + ("Save as", "Сохранить в файл"), + ("Copy to clipboard", "Копировать в буфер обмена"), + ("Enable remote printer", "ИÑпользовать удалённый принтер"), + ("Downloading {}", "Скачивание"), + ("{} Update", "Обновить {}"), + ("{}-to-update-tip", "{} закроетÑÑ Ð¸ уÑтановит новую верÑию."), + ("download-new-version-failed-tip", "Ошибка загрузки. Можно повторить попытку или нажать кнопку \"Скачать\", чтобы Ñкачать приложение Ñ Ð¾Ñ„Ð¸Ñ†Ð¸Ð°Ð»ÑŒÐ½Ð¾Ð³Ð¾ Ñайта и обновить вручную."), + ("Auto update", "ÐвтоматичеÑкое обновление"), + ("update-failed-check-msi-tip", "Ðевозможно определить метод уÑтановки. Ðажмите кнопку \"Скачать\", чтобы Ñкачать приложение Ñ Ð¾Ñ„Ð¸Ñ†Ð¸Ð°Ð»ÑŒÐ½Ð¾Ð³Ð¾ Ñайта и обновить его вручную."), + ("websocket_tip", "WebSocket поддерживает только Ð¿Ð¾Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð¸Ñ Ðº ретранÑлÑтору."), + ("Use WebSocket", "ИÑпользовать WebSocket"), + ("Trackpad speed", "СкороÑть трекпада"), + ("Default trackpad speed", "СкороÑть трекпада по умолчанию"), + ("Numeric one-time password", "Цифровой одноразовый пароль"), + ("Enable IPv6 P2P connection", "ИÑпользовать подключение IPv6 P2P"), + ("Enable UDP hole punching", "ИÑпользовать UDP hole punching"), + ("View camera", "ПроÑмотр камеры"), + ("Enable camera", "Включить камеру"), + ("No cameras", "Камера отÑутÑтвует"), + ("view_camera_unsupported_tip", "Удалённое уÑтройÑтво не поддерживает проÑмотр камеры."), + ("Terminal", "Терминал"), + ("Enable terminal", "Включить терминал"), + ("New tab", "ÐÐ¾Ð²Ð°Ñ Ð²ÐºÐ»Ð°Ð´ÐºÐ°"), + ("Keep terminal sessions on disconnect", "СохранÑть ÑеанÑÑ‹ терминала при отключении"), + ("Terminal (Run as administrator)", "Терминал (админиÑтратор)"), + ("terminal-admin-login-tip", "Введите Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸ пароль админиÑтратора управлÑемой Ñтороны."), + ("Failed to get user token.", "Ðевозможно получить токен пользователÑ."), + ("Incorrect username or password.", "Ðеправильное Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð»Ð¸ пароль."), + ("The user is not an administrator.", "Пользователь не ÑвлÑетÑÑ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратором."), + ("Failed to check if the user is an administrator.", "Ðевозможно проверить, ÑвлÑетÑÑ Ð»Ð¸ пользователь админиÑтратором."), + ("Supported only in the installed version.", "ПоддерживаетÑÑ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ в уÑтановочной верÑии."), + ("elevation_username_tip", "Введите Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸Ð»Ð¸ домен\\пользователÑ"), + ("Preparing for installation ...", "Подготовка к уÑтановке..."), ].iter().cloned().collect(); } diff --git a/src/lang/sc.rs b/src/lang/sc.rs new file mode 100644 index 00000000000..73a7161bdc9 --- /dev/null +++ b/src/lang/sc.rs @@ -0,0 +1,713 @@ +lazy_static::lazy_static! { +pub static ref T: std::collections::HashMap<&'static str, &'static str> = + [ + ("Status", "Istadu"), + ("Your Desktop", "Custu elaboradore"), + ("desk_tip", "Podes atzèdere a custu elaboradore impreende s'ID e sa crae de intrada inditados inoghe in suta."), + ("Password", "Crae"), + ("Ready", "Prontu"), + ("Established", "Istabilida"), + ("connecting_status", "Connessione a sa rete RustDesk..."), + ("Enable service", "Abìlita servìtziu"), + ("Start service", "Allughe su servìtziu"), + ("Service is running", "Su servìtziu est in funtzione"), + ("Service is not running", "Su servìtziu no est in funtzione"), + ("not_ready_status", "Non prontu. Verìfica sa connessione"), + ("Control Remote Desktop", "Controlla s'elaboradore remotu"), + ("Transfer file", "Tràmuda documentos"), + ("Connect", "Cunnete·ti"), + ("Recent sessions", "Sessiones reghentes"), + ("Address book", "Rubrica"), + ("Confirmation", "Cunfirma"), + ("TCP tunneling", "Tunnel TCP"), + ("Remove", "Boga"), + ("Refresh random password", "Crae casuale noa"), + ("Set your own password", "Imposta sa crae"), + ("Enable keyboard/mouse", "Abìlita tecladu/ratu"), + ("Enable clipboard", "Abìlita punta de billete"), + ("Enable file transfer", "Abìlita su tramudòngiu de documentos"), + ("Enable TCP tunneling", "Abìlita tunnel TCP"), + ("IP Whitelisting", "IP autorizados"), + ("ID/Relay Server", "Serbidore ID/Tràmuda"), + ("Import server config", "Importa configuratzione serbidore dae sa punta de billete"), + ("Export Server Config", "Esporta configurazione serbidore a sa punta de billete"), + ("Import server configuration successfully", "Configuratzione serbidore importada cumprida"), + ("Export server configuration successfully", "Configuratzione serbidore esportada cumprida"), + ("Invalid server configuration", "Configuratzione serbidore non vàlida"), + ("Clipboard is empty", "Sa punta de billete est bòida"), + ("Stop service", "Firma su servìtziu"), + ("Change ID", "Càmbia ID"), + ("Your new ID", "S'ID nou"), + ("length %min% to %max%", "longària dae %min% a %max%"), + ("starts with a letter", "incumintza cun una lìtera"), + ("allowed characters", "caràteres cunsentidos"), + ("id_change_tip", "Podes impreare petzi sos caràteres a-z, A-Z, 0-9, - (tratigheddu) e _ (sutaliniadu).\nSu primu caràtere depet èssere a-z o A-Z.\nSa longària depet èssere de intre 6 e 16 caràteres."), + ("Website", "Situ web programma"), + ("About", "Info programma"), + ("Slogan_tip", "Fatu cun su coro in custu mundu caòticu!"), + ("Privacy Statement", "Informativa subra de sa riservadesa"), + ("Mute", "Sonu istudadu"), + ("Build Date", "Data build"), + ("Version", "Versione"), + ("Home", "Pàgina printzipale"), + ("Audio Input", "Intrada àudio"), + ("Enhancements", "Megioros"), + ("Hardware Codec", "Codificadore fìsicu (hardware)"), + ("Adaptive bitrate", "Velotzidade de bits adativa"), + ("ID Server", "ID serbidore"), + ("Relay Server", "Serbidore de tràmuda"), + ("API Server", "Serbidore API"), + ("invalid_http", "depet incumintzare cun http:// o https://"), + ("Invalid IP", "Indiritzu IP non vàlidu"), + ("Invalid format", "Formadu non vàlidu"), + ("server_not_support", "Galu non suportadu dae su serbidore"), + ("Not available", "No a disponimentu"), + ("Too frequent", "Tropu fitianu"), + ("Cancel", "Annulla"), + ("Skip", "Ignora"), + ("Close", "Serra"), + ("Retry", "Torra a proare"), + ("OK", "AB"), + ("Password Required", "Bisòngiat sa crae"), + ("Please enter your password", "Inserta sa crae tua"), + ("Remember password", "Ammenta sa crae"), + ("Wrong Password", "Crae isballiada"), + ("Do you want to enter again?", "Boles torrare a intrare?"), + ("Connection Error", "Errore de connessione"), + ("Error", "Errore"), + ("Reset by the peer", "Resetada dae su dispositivu de s'àtera parte"), + ("Connecting...", "Connetende..."), + ("Connection in progress. Please wait.", "Connetende. Iseta."), + ("Please try 1 minute later", "Torra a proare a pustis de 1 minutu"), + ("Login Error", "Faddina de atzessu"), + ("Successful", "Cumpridu"), + ("Connected, waiting for image...", "Connessu, isetende s'immàgine..."), + ("Name", "Nùmene"), + ("Type", "Casta"), + ("Modified", "Modificadu"), + ("Size", "Mannària"), + ("Show Hidden Files", "Mustra sos documentos cuados"), + ("Receive", "Retzi"), + ("Send", "Imbia"), + ("Refresh File", "Annoa sos documentos"), + ("Local", "Locale"), + ("Remote", "Remotu"), + ("Remote Computer", "Elaboradore remotu"), + ("Local Computer", "Elaboradore locale"), + ("Confirm Delete", "Cunfirma s'iscantzelladura"), + ("Delete", "Iscantzella"), + ("Properties", "Propiedades"), + ("Multi Select", "Seletzione mùltipla"), + ("Select All", "Seletziona totu"), + ("Unselect All", "Deseletziona totu"), + ("Empty Directory", "Cartella bòida"), + ("Not an empty directory", "No est una cartella bòida"), + ("Are you sure you want to delete this file?", "Ses seguru de bòlere iscantzellare custu documentu?"), + ("Are you sure you want to delete this empty directory?", "Ses seguru de bòlere iscantzellare custa cartella bòida?"), + ("Are you sure you want to delete the file of this directory?", "Ses seguru de bòlere iscantzellare su documentu de custa cartella?"), + ("Do this for all conflicts", "Ammenta custu issèberu pro totu sos cunflitos"), + ("This is irreversible!", "Custu non si podet annullare!"), + ("Deleting", "Iscantzellende"), + ("files", "documentos"), + ("Waiting", "Isetende"), + ("Finished", "Acabadu"), + ("Speed", "Lestresa"), + ("Custom Image Quality", "Calidade immàgine personalizada"), + ("Privacy mode", "Modalidade de riservadesa"), + ("Block user input", "Bloca sas atziones de utente"), + ("Unblock user input", "Isbloca sas atziones de utente"), + ("Adjust Window", "Adata sa ventana"), + ("Original", "Originale"), + ("Shrink", "Astringhe"), + ("Stretch", "Illàrghia"), + ("Scrollbar", "Istanga de iscurrimentu"), + ("ScrollAuto", "Iscurre in automàticu"), + ("Good image quality", "Calidade bona de s'immàgine"), + ("Balanced", "Bilantziada"), + ("Optimize reaction time", "Otimiza su tempus de reatzione"), + ("Custom", "Profilu personalizadu"), + ("Show remote cursor", "Mustra su cursore remotu"), + ("Show quality monitor", "Mustra sa calidade vìdeu"), + ("Disable clipboard", "Disabìlita sa punta de billete"), + ("Lock after session end", "Bloca a sa fine de sa sessione"), + ("Insert Ctrl + Alt + Del", "Inserta Ctrl + Alt + Del"), + ("Insert Lock", "Blocu insertada"), + ("Refresh", "Annoa"), + ("ID does not exist", "S'ID no esistit"), + ("Failed to connect to rendezvous server", "Errore connessione a su sebidore de atòbiu"), + ("Please try later", "Torra a proare prus a a tardu"), + ("Remote desktop is offline", "S'iscrivania remota no est in lìnia"), + ("Key mismatch", "Sa crae non currispondet"), + ("Timeout", "Tempus iscadidu"), + ("Failed to connect to relay server", "Connessione a su serbidore de tràmuda fallida"), + ("Failed to connect via rendezvous server", "Connessione pro mèdiu de su serbidore de atòbiu fallida"), + ("Failed to connect via relay server", "Connessione pro mèdiu de su serbidore de tràmuda fallida"), + ("Failed to make direct connection to remote desktop", "Connessione direta a s'iscrivania remota fallida"), + ("Set Password", "Imposta sa crae"), + ("OS Password", "Crae sistema operativu"), + ("install_tip", "Pro neghe de su Controllu Contu Utente (UAC), RustDesk diat pòdere non funtzionare comente si tocat comente iscrivania remota.\nPro evitare custu problema, incarca in su butone inoghe in suta pro installare RustDesk a livellu de sistema."), + ("Click to upgrade", "Atualiza"), + ("Configure", "Cunfigura"), + ("config_acc", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Atzessibilidade'."), + ("config_screen", "Pro controllare s'iscrivania dae foras, depes frunire a RustDesk su permissu 'Registratzione ischermu'."), + ("Installing ...", "Installatzione ..."), + ("Install", "Installa"), + ("Installation", "Installatzione"), + ("Installation Path", "Àndala de installatzione"), + ("Create start menu shortcuts", "Crea sos ligàmenes in su menù de incumintzu"), + ("Create desktop icon", "Crea un'icona in s'iscrivania"), + ("agreement_tip", "Incaminende s'installazione, atzetas sos tèrmines de su cuntratu de lissèntzia."), + ("Accept and Install", "Atzeta e installa"), + ("End-user license agreement", "Cuntratu de lissèntzia utente finale"), + ("Generating ...", "Ingendrende ..."), + ("Your installation is lower version.", "Cuta installazione no est atualizada."), + ("not_close_tcp_tip", "Non Serres custa ventana in su mentres chi ses impreende su tunnel"), + ("Listening ...", "Ascurtende ..."), + ("Remote Host", "Istrangiaore (host) remotu"), + ("Remote Port", "Ghenna remota"), + ("Action", "Atzione"), + ("Add", "Annanghe"), + ("Local Port", "Ghenna locale"), + ("Local Address", "Indiritzu locale"), + ("Change Local Port", "Càmbia ghenna locale"), + ("setup_server_tip", "Pro una connessione prus lestra, cunfigura unu serbidore ispetzìficu"), + ("Too short, at least 6 characters.", "Tropu curtza, a su nessi 6 caràteres"), + ("The confirmation is not identical.", "Sa crae de cunfirma non currispondet"), + ("Permissions", "Permissos"), + ("Accept", "Atzeta"), + ("Dismiss", "Naga"), + ("Disconnect", "Iscollega·ti"), + ("Enable file copy and paste", "Permite sa còpia e s'incollòngiu de documentos"), + ("Connected", "Connessu"), + ("Direct and encrypted connection", "Connessione direta e tzifrada"), + ("Relayed and encrypted connection", "Connessione inoltrada (relayed) e tzifrada"), + ("Direct and unencrypted connection", "Connessione direta e non tzifrada"), + ("Relayed and unencrypted connection", "Connessione inoltrada (relayed) e non tzifrada"), + ("Enter Remote ID", "Inserta ID remotu"), + ("Enter your password", "Inserta sa crae tua"), + ("Logging in...", "Intrende..."), + ("Enable RDP session sharing", "Abìlita sa cumpartzidura sessione RDP"), + ("Auto Login", "Atzessu automàticu"), + ("Enable direct IP access", "Abìlita s'intrada direta pro mèdiu de s'IP"), + ("Rename", "Càmbia de nùmene"), + ("Space", "Ispàtziu"), + ("Create desktop shortcut", "Crea unu ligàmene in s'iscrivania"), + ("Change Path", "Modìfica s'àndala"), + ("Create Folder", "Crea una cartella"), + ("Please enter the folder name", "Inserta su nùmene de sa cartella"), + ("Fix it", "Risolve"), + ("Warning", "Avisu"), + ("Login screen using Wayland is not supported", "S'ischemada de intrada no est suportada impreende Wayland"), + ("Reboot required", "B'at bisòngiu de una torrada a aviare"), + ("Unsupported display server", "Serbidore de visualizatzione non suportadu"), + ("x11 expected", "bisòngiat xll"), + ("Port", "Ghenna"), + ("Settings", "Impostatziones"), + ("Username", "Nùmene utente"), + ("Invalid port", "Nùmeru ghenna non vàlidu"), + ("Closed manually by the peer", "Serradu a manu dae su dispositivu remotu"), + ("Enable remote configuration modification", "Abìlita sa modìfica remota de sa cunfiguratzione"), + ("Run without install", "Allughe chene installare"), + ("Connect via relay", "Collega·ti impreende una tràmuda relay"), + ("Always connect via relay", "Collega·ti semper impreende una tràmuda relay"), + ("whitelist_tip", "Si podent connètere a custa iscrivania petzi sos indiritzos IP autorizados"), + ("Login", "Intra"), + ("Verify", "Avèrgua"), + ("Remember me", "Ammenta·ti de mene"), + ("Trust this device", "Registra custu dispositivu comente de fidùtzia"), + ("Verification code", "Còdighe de verìfica"), + ("verification_tip", "Amus imbiadu unu còdighe de averguada a s'indiritzu de posta eletrònica registradu, pro intrare inserta·lu."), + ("Logout", "Essi"), + ("Tags", "Etichetas"), + ("Search ID", "Chirca ID"), + ("whitelist_sep", "Separados dae vìrgulas, puntu e vìrgula, ispatziu o riga a suta"), + ("Add ID", "Annanghe ID"), + ("Add Tag", "Annanghe eticheta"), + ("Unselect all tags", "Deseletziona totu sas etichetas"), + ("Network error", "Errore de rete"), + ("Username missed", "Mancat su nùmene utente"), + ("Password missed", "Mancat sa crae de intrada"), + ("Wrong credentials", "Credentziales isballiadas"), + ("The verification code is incorrect or has expired", "Su còdighe de verìfica no est curretu o est iscadidu"), + ("Edit Tag", "Modìfica eticheta"), + ("Forget Password", "Ismèntiga sa crae"), + ("Favorites", "Preferidos"), + ("Add to Favorites", "Annanghe a sos preferidos"), + ("Remove from Favorites", "Boga dae sos preferidos"), + ("Empty", "Bòidu"), + ("Invalid folder name", "Nùmene de sa cartella non vàlidu"), + ("Socks5 Proxy", "Serbidore intermediàriu Socks5"), + ("Socks5/Http(s) Proxy", "Serbidore intermediàriu Socks5/Http(s)"), + ("Discovered", "Rileva"), + ("install_daemon_tip", "Pro aviare su programma a s'allughìngiu, tocat a l'installare comente servìtziu de sistema."), + ("Remote ID", "ID remotu"), + ("Paste", "Incolla"), + ("Paste here?", "Incollare inoghe?"), + ("Are you sure to close the connection?", "Ses seguru de bòlere serrare sa connessione?"), + ("Download new version", "Iscàrriga sa versione noa"), + ("Touch mode", "Modalidade tocu"), + ("Mouse mode", "Modalidade ratu"), + ("One-Finger Tap", "Tocu cun unu pòddighe"), + ("Left Mouse", "Butone de manca de su ratu"), + ("One-Long Tap", "Tocu longu cun unu pòddighe"), + ("Two-Finger Tap", "Tocu cun duos pòddighes"), + ("Right Mouse", "Butone de destra de su ratu"), + ("One-Finger Move", "Movimentu cun unu pòddighe"), + ("Double Tap & Move", "Tocu dòpiu e movimentu"), + ("Mouse Drag", "Trisinada de su ratu"), + ("Three-Finger vertically", "Tres pòddighes in verticale"), + ("Mouse Wheel", "Rodedda de su ratu"), + ("Two-Finger Move", "Movimentu cun duos pòddighes"), + ("Canvas Move", "Isposta sa tela"), + ("Pinch to Zoom", "Pìtziga pro ismanniare"), + ("Canvas Zoom", "Ismanniamentu tela"), + ("Reset canvas", "Reseta sa tela"), + ("No permission of file transfer", "Perunu permissu pro sa tràmuda de documentos"), + ("Note", "Nota"), + ("Connection", "Connessione"), + ("Share screen", "Cumpartzi ischermu"), + ("Chat", "Tzarrada"), + ("Total", "Totale"), + ("items", "Elementos"), + ("Selected", "Seletzionadu"), + ("Screen Capture", "Catura de ischermu"), + ("Input Control", "Controllu atziones"), + ("Audio Capture", "Catura de s'àudio"), + ("Do you accept?", "Atzetas?"), + ("Open System Setting", "Aberi sas impostatziones de sistema"), + ("How to get Android input permission?", "Comente otènnere s'autorizatzione de intrada (input) in Android?"), + ("android_input_permission_tip1", "Pro chi unu dispositivu remotu potzat controllare unu dispositivu Android pro mèdiu de unu ratu o cun su tocu, depes cunsentire a RustDesk de impreare su servìtziu 'Atzessibilidade'."), + ("android_input_permission_tip2", "Bae a sa pàgina de sas impostatziones de sistema chi s'at a abèrrere a pustis, busca e intra a [Servìtzios installados], allughe su servìtziu [Intrada RustDesk]."), + ("android_new_connection_tip", "Est istada retzida una dimanda noa de controllu pro su dispositivu atuale."), + ("android_service_will_start_tip", "S'ativatzione de Catura ischermu at a aviare in automàticu su servìtziu, permitende a àteros dispositivos de pedire una connessione dae custu dispositivu."), + ("android_stop_service_tip", "Sa serrada de su servìtziu at a tancare in automàticu totu sas connessiones istabilidas."), + ("android_version_audio_tip", "Sa versione atuale de Android non suportat s'achirimentu àudio, faghe s'atualizatzione a Android 10 o versiones prus noas."), + ("android_start_service_tip", "Pro aviare su servìtziu de cumpartzidura de s'ischermu seletziona [Avia su servìtziu] o abìlita s'autorizatzione [Catura de ischermu]."), + ("android_permission_may_not_change_tip", "Sas autorizatziones pro sas connessiones istabilidas non si podent modificare in manera istantànea finas a sa riconnessione."), + ("Account", "Contu"), + ("Overwrite", "Subraiscrie"), + ("This file exists, skip or overwrite this file?", "Custu documentu esistit, boles ignorare o subraiscìere custu archìviu?"), + ("Quit", "Essi"), + ("Help", "Agiudu"), + ("Failed", "Fallidu"), + ("Succeeded", "Cumpridu"), + ("Someone turns on privacy mode, exit", "Calicunu at allutu sa modalidade de riservadesa, essida"), + ("Unsupported", "Non suportadu"), + ("Peer denied", "Atzessu negadu a su dispositivu remotu"), + ("Please install plugins", "Installa sos cumplementos"), + ("Peer exit", "Essida dae su dispostivu remotu"), + ("Failed to turn off", "Non faghet a istudare"), + ("Turned off", "Istuda"), + ("Language", "Limba"), + ("Keep RustDesk background service", "Mantene su servìtziu de RustDesk in s'isfundu"), + ("Ignore Battery Optimizations", "Ignora sas otimizatziones de sa bateria"), + ("android_open_battery_optimizations_tip", "Si boles disabilitare custa funtzione, bae a sas impostatziones de s'aplicatzione RustDesk, aberi sa setzione 'Bateria' e boga sa seletzione a 'Chene restritziones'."), + ("Start on boot", "Avia a s'allughidura"), + ("Start the screen sharing service on boot, requires special permissions", "S'aviu de su servìtziu de cumpartzidura de s'ischermu a s'allughidura tenet bisòngiu de permissos ispetziales"), + ("Connection not allowed", "Connessione non permìtida"), + ("Legacy mode", "Modalidade antiga"), + ("Map mode", "Modalidade mapa"), + ("Translate mode", "Modalidade tradutzione"), + ("Use permanent password", "Imprea una crae de intrada permanente"), + ("Use both passwords", "Imprea craes de intrada monoimpreu e permanente"), + ("Set permanent password", "Imposta sa crae permanente"), + ("Enable remote restart", "Abìlita riaviu dae remotu"), + ("Restart remote device", "Torra a aviare su dispositivu remotu"), + ("Are you sure you want to restart", "Ses seguru de bòlere torrare a allùghere?"), + ("Restarting remote device", "Su dispositivu remotu s'est torrende a allùghere"), + ("remote_restarting_tip", "Torra a allùghere su dispositivu remotu"), + ("Copied", "Copiadu"), + ("Exit Fullscreen", "Essi dae sa modalidade a ischermu intreu"), + ("Fullscreen", "A ischermu intreu"), + ("Mobile Actions", "Atziones mòbiles"), + ("Select Monitor", "Seleziona ischermu"), + ("Control Actions", "Atziones de controllu"), + ("Display Settings", "Impostatziones de visualizatzione"), + ("Ratio", "Raportu"), + ("Image Quality", "Calidade de s'immàgine"), + ("Scroll Style", "Istile de iscurrimentu"), + ("Show Toolbar", "Mustra s'istanga de trastes"), + ("Hide Toolbar", "Cua s'istanga de trastes"), + ("Direct Connection", "Connessione direta"), + ("Relay Connection", "Connessione tramudada (relay)"), + ("Secure Connection", "Connessione segura"), + ("Insecure Connection", "Connessione non segura"), + ("Scale original", "Iscala originale"), + ("Scale adaptive", "Iscala adativa"), + ("General", "Generale"), + ("Security", "Seguresa"), + ("Theme", "Tema"), + ("Dark Theme", "Tema iscuru"), + ("Light Theme", "Tema craru"), + ("Dark", "Iscuru"), + ("Light", "Craru"), + ("Follow System", "Sistema"), + ("Enable hardware codec", "Abìlita codificadore fìsicu"), + ("Unlock Security Settings", "Isbloca sas impostatziones de seguresa"), + ("Enable audio", "Abìlita àudio"), + ("Unlock Network Settings", "Isbloca impostatziones de rete"), + ("Server", "Serbidore"), + ("Direct IP Access", "Atzessu IP diretu"), + ("Proxy", "Serbidore intermediàriu"), + ("Apply", "Àplica"), + ("Disconnect all devices?", "Boles iscollegare totu sos dispositivos?"), + ("Clear", "Isbòida"), + ("Audio Input Device", "Dispositivu intrada àudio"), + ("Use IP Whitelisting", "Imprea elencu IP autorizados"), + ("Network", "Rete"), + ("Pin Toolbar", "Bloca s'istanga de trastes"), + ("Unpin Toolbar", "Isbloca s'istanga de trastes"), + ("Recording", "Registratzione"), + ("Directory", "Cartella"), + ("Automatically record incoming sessions", "Registra in automàticu sas sessiones in intrada"), + ("Automatically record outgoing sessions", "Registra in automàticu sas sessiones in essida"), + ("Change", "Modìfica"), + ("Start session recording", "Incumintza sa registrazione de sa sessione"), + ("Stop session recording", "Firma sa registrazione de sa sessione"), + ("Enable recording session", "Abìlita sa registrazione de sa sessione"), + ("Enable LAN discovery", "Abìlita su rilevamentu LAN"), + ("Deny LAN discovery", "Disabìlita su rilevamentu LAN"), + ("Write a message", "Iscrie unu messàgiu"), + ("Prompt", "Pedi"), + ("Please wait for confirmation of UAC...", "Iseta sa cunfirma de s'UAC..."), + ("elevated_foreground_window_tip", "Sa ventana atuale de s'elaboradore remotu tenet bisòngiu, pro funtzionare, de privilègios prus mannos, duncas non faghet a impreare in manera temporànea su ratu e su tecladu.\nSi podet pedire a s'utente remotu de minimare a icona sa ventana atuale o de seletzionare su pulsante de artària in sa ventana de gestione de sa connessione.\nPro evitare custu problema, ti cussigiamus de installare su programma in su dispositivu remotu."), + ("Disconnected", "Iscollegadu"), + ("Other", "Àteru"), + ("Confirm before closing multiple tabs", "Cunfirma in antis de serrare prus ischedas"), + ("Keyboard Settings", "Impostatziones de tecladu"), + ("Full Access", "Atzessu cumpridu"), + ("Screen Share", "Cumpartzidura de ischermu"), + ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland tenet bisòngiu de Ubuntu 21.04 o versione prus noa."), + ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland tenet bisòngiu de una versione prus noa de sa distributzione Linux.\nProa X11 pro elaboradores o càmbia su sistema operativu."), + ("JumpLink", "Bae a"), + ("Please Select the screen to be shared(Operate on the peer side).", "Seletziona s'ischermu de cumpartzire (òpera dae s'ala de su dispositivu remotu)."), + ("Show RustDesk", "Mustra RustDesk"), + ("This PC", "Custu PC"), + ("or", "O"), + ("Continue with", "Sighi cun"), + ("Elevate", "Cresche"), + ("Zoom cursor", "Cursore de ismanniamentu"), + ("Accept sessions via password", "Atzeta sessiones cun sa crae"), + ("Accept sessions via click", "Atzeta sessiones cun sas incarcadas"), + ("Accept sessions via both", "Atzeta sessiones cun totu sas duas craes"), + ("Please wait for the remote side to accept your session request...", "Iseta chi su dispositivu remotu atzetet sa dimanda de sessione..."), + ("One-time Password", "Crae monoimpreu"), + ("Use one-time password", "Imprea crae monoimpreu"), + ("One-time password length", "Longària crae monoimpreu"), + ("Request access to your device", "Pedi s'atzessu a su dispositivu"), + ("Hide connection management window", "Cua sa ventana de gestione de sas connessiones"), + ("hide_cm_tip", "Permite de cuare petzi si s'atzetant sessiones cun crae permanente"), + ("wayland_experiment_tip", "Su suportu Wayland est in fase isperimentale, si boles un'atzessu istàbile imprea X11."), + ("Right click to select tabs", "Incarca cun su pulsante destru pro seletzionare sas ischedas"), + ("Skipped", "Brincadu"), + ("Add to address book", "Annanghe a sa rubrica"), + ("Group", "Grupu"), + ("Search", "Chirca"), + ("Closed manually by web console", "Serra in manera manuale dae sa console web"), + ("Local keyboard type", "Casta de tecladu locale"), + ("Select local keyboard type", "Seletziona sa casta de tecladu locale"), + ("software_render_tip", "Si in s'elaboradore cun Linux b'at un'ischeda grafica Nvidia e sa ventana remota si serrat deretu a pustis de sa connessione, installa su driver nou a còdighe abertu e imprea sa renderitzatzione tràmite programma (software).\nDiat pòdere bisongiare a torrare a allùghere su programma."), + ("Always use software rendering", "Imprea semper sa renderizatzione tràmite programma"), + ("config_input", "Pro controllare s'elaboradore remotu cun su tecladu bisòngiat a frunire a RustDesk sos permissos de 'Monitoràgiu insertada'."), + ("config_microphone", "Per pòdere mutire, bisòngiat a frunire su premissu 'Registra àudio' a RustDesk."), + ("request_elevation_tip", "Si b'at calicunu in s'ala remota si podet pedire sa crèschida."), + ("Wait", "Iseta"), + ("Elevation Error", "Faddina durante sa crèschida de sos deretos"), + ("Ask the remote user for authentication", "Pedi s'autenticatzione a s'utente remotu"), + ("Choose this if the remote account is administrator", "Issèbera custa optzione si su contu remotu est amministradore"), + ("Transmit the username and password of administrator", "Trasmite su nùmene utente e sa crae de intrada de s'amministradore"), + ("still_click_uac_tip", "Torra a pedire chi s'utente remotu seletziones 'AB' in sa ventana UAC de s'esecutzione de RustDesk."), + ("Request Elevation", "Pedi sa crèschida de sos deretos"), + ("wait_accept_uac_tip", "Iseta chi s'utente remotu atzetet sa ventana de diàlogu UAC."), + ("Elevate successfully", "Crèschida de sos deretos cumprida"), + ("uppercase", "Majùscula"), + ("lowercase", "Minùscula"), + ("digit", "Nùmeru"), + ("special character", "Caràtere ispetziale"), + ("length>=8", "Lunghezza >= 8"), + ("Weak", "Dèbile"), + ("Medium", "Mesana"), + ("Strong", "Forte"), + ("Switch Sides", "Càmbia ala"), + ("Please confirm if you want to share your desktop?", "Boles cumpartzire s'elaboradore?"), + ("Display", "Visualizatzione"), + ("Default View Style", "Istile de visualiztazione predefinidu"), + ("Default Scroll Style", "Istile de iscurrimentu predefinidu"), + ("Default Image Quality", "Calidade de s'immàgine predefinida"), + ("Default Codec", "Codificadore predefinidu"), + ("Bitrate", "Tassu de bits"), + ("FPS", "FPS"), + ("Auto", "Automàticu"), + ("Other Default Options", "Àteras optziones predefinidas"), + ("Voice call", "Mutida vocale"), + ("Text chat", "Tzarrada de testu"), + ("Stop voice call", "Interrumpe sa mutida vocale"), + ("relay_hint_tip", "Si non faghet a si connètere in manera direta, podes proare a ti collegare impreende unu serbidore de tràmuda.\nIn prus, si boles imprearevsu serbidore de tràmuda in su primu tentativu, podes annànghere a s'ID su suffissu '/r\' o seletzionare in s'ischeda si esistit s'optzione 'Collega·ti semper impreende una tràmuda relay'."), + ("Reconnect", "Collega·ti torra"), + ("Codec", "Codificadore"), + ("Resolution", "Risolutzione"), + ("No transfers in progress", "Peruna tràmuda in cursu"), + ("Set one-time password length", "Imposta sa longària de sa crae monoimpreu"), + ("RDP Settings", "Impostatziones RDP"), + ("Sort by", "Ã’rdina pro"), + ("New Connection", "Connessione noa"), + ("Restore", "Riprìstina"), + ("Minimize", "Mìnima"), + ("Maximize", "Massimiza"), + ("Your Device", "Custu dispositivu"), + ("empty_recent_tip", "Non b'at galu peruna sessione reghente!\nPianifica·nde una."), + ("empty_favorite_tip", "Galu peruna connessione?\nBusca calicunu cun chie ti collegare e annanghe·lu a sos preferidos!"), + ("empty_lan_tip", "Paret a beru chi non siat istada atzapada peruna connessione."), + ("empty_address_book_tip", "Paret chi pro como in sa rubrica non b'apat connessiones."), + ("Empty Username", "Nùmene utente bòidu"), + ("Empty Password", "Crae bòida"), + ("Me", "Deo"), + ("identical_file_tip", "Custu archìviu est pretzisu a su chi b'at in su dispositivu remotu."), + ("show_monitors_tip", "Mustra sos ischermos in s'istanga de sos trastes"), + ("View Mode", "Modalidade de visualizatzione"), + ("login_linux_tip", "Intra a su contu de Linux remotu"), + ("verify_rustdesk_password_tip", "Cunfirma sa crae de RustDesk"), + ("remember_account_tip", "Ammenta custu contu"), + ("os_account_desk_tip", "Custu contu s'impreat pro intrare a su sistema operativu remotu e ativare sa sessione de s'elaboradore in modalidade non presidiada."), + ("OS Account", "Contu sistema operativu"), + ("another_user_login_title_tip", "Un'àteru utente at giai fatu s'atzessu."), + ("another_user_login_text_tip", "Separadu"), + ("xorg_not_found_title_tip", "Xorg no atzapadu."), + ("xorg_not_found_text_tip", "Installa Xorg."), + ("no_desktop_title_tip", "Non b'at perunu ambiente de elaboradore a disponimentu."), + ("no_desktop_text_tip", "Installa s'ambiente de elaboradore GNOME."), + ("No need to elevate", "Crèschida de sos privilègios non pedida"), + ("System Sound", "Dispositivu àudio de sistema"), + ("Default", "Predefinida"), + ("New RDP", "RDP nou"), + ("Fingerprint", "Firma digitale"), + ("Copy Fingerprint", "Còpia firma digitale"), + ("no fingerprints", "Peruna firma digitale"), + ("Select a peer", "Seletziona su dispositivu remotu"), + ("Select peers", "Seletziona sos dispositivos remotos"), + ("Plugins", "Cumplementos"), + ("Uninstall", "Disinstalla"), + ("Update", "Atualiza"), + ("Enable", "Abìlita"), + ("Disable", "Disabìlita"), + ("Options", "Optziones"), + ("resolution_original_tip", "Risolutzione originale"), + ("resolution_fit_local_tip", "Adata sa risolutzione locale"), + ("resolution_custom_tip", "Risolutzione personalizada"), + ("Collapse toolbar", "Mìnima s'istanga de sos trastes"), + ("Accept and Elevate", "Atzeta e cresche"), + ("accept_and_elevate_btn_tooltip", "Atzeta sa connessione e cresche sos permissos UAC."), + ("clipboard_wait_response_timeout_tip", "Tempus de isetu de rispota dae sa còpia iscadidu."), + ("Incoming connection", "Connessiones in intrada"), + ("Outgoing connection", "Connessiones in essida"), + ("Exit", "Essi dae RustDesk"), + ("Open", "Aberi RustDesk"), + ("logout_tip", "Ses seguru de bòlere essire?"), + ("Service", "Servìtziu"), + ("Start", "Allughe"), + ("Stop", "Firma"), + ("exceed_max_devices", "Ses arribbadu a su nùmeru màssimu de dispositivos chi podes manigiare."), + ("Sync with recent sessions", "Sincroniza cun sas sessiones reghentes"), + ("Sort tags", "Ã’rdina sas etichetas"), + ("Open connection in new tab", "Aberi sa connessione in un'ischeda noa"), + ("Move tab to new window", "Move s'ischeda a sa ventana imbeniente"), + ("Can not be empty", "Non podet èssere bòidu"), + ("Already exists", "Esistit giai"), + ("Change Password", "Modìfica sa crae"), + ("Refresh Password", "Annoa sa crae"), + ("ID", "ID"), + ("Grid View", "Vista grìllia"), + ("List View", "Vista elencu"), + ("Select", "Seletziona"), + ("Toggle Tags", "Allughe/istuda eticheta"), + ("pull_ab_failed_tip", "Non faghet a annoare sa rubrica"), + ("push_ab_failed_tip", "Non faghet a sincronizare sa rubrica cun su serbidore"), + ("synced_peer_readded_tip", "Sos dispositivos chi bi sunt in sas sessiones reghentes s'ant a torrare a sincronizare in sa rubrica."), + ("Change Color", "Modìfica colore"), + ("Primary Color", "Colore primàriu"), + ("HSV Color", "Colore HSV"), + ("Installation Successful!", "Installatzione cumprida"), + ("Installation failed!", "Installtazione fallida"), + ("Reverse mouse wheel", "Funtzione rodedda ratu furriada"), + ("{} sessions", "{} sessiones"), + ("scam_title", "Ti diant pòdere àere TRAMPADU!"), + ("scam_text1", "Si ses in su telèfonu cun calicunu chi NON connosches NON FIDADU chi t'at pedidu de impreare RustDesk e de allùghere su servìtziu, non sigas e tanca deretu."), + ("scam_text2", "Est dàbile chi siat unu trampadore chi chircat de furare su dinare tuo o àteras informatziones privadas tuas."), + ("Don't show again", "Non mustres prus"), + ("I Agree", "Atzeto"), + ("Decline", "No atzeto"), + ("Timeout in minutes", "Tempus de iscadèntzia in minutos"), + ("auto_disconnect_option_tip", "Serra in automàticu sas sessiones in intrada pro inatividade de s'utente"), + ("Connection failed due to inactivity", "Connessione non resèssida pro neghe de inatividade"), + ("Check for software update on startup", "A s'allughìngiu avèrgua sa presèntzia de atualizatziones pro su programma"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "Atualiza RustDesk Server Pro a sa versione {} o prus noa!"), + ("pull_group_failed_tip", "Non faghet a annoare su grupu"), + ("Filter by intersection", "Filtra pro rugrada"), + ("Remove wallpaper during incoming sessions", "Boga s'isfundu durante sas sessiones in intrada"), + ("Test", "Proa"), + ("display_is_plugged_out_msg", "S'ischermu est iscollegadu, colo a su primu ischermu."), + ("No displays", "Perunu ischermu"), + ("Open in new window", "Aberi in una ventana noa"), + ("Show displays as individual windows", "Mustra sos ischermos comente ventanas individuales"), + ("Use all my displays for the remote session", "In sa sessione remota imprea totu sos ischermos"), + ("selinux_tip", "In custu dispositivu est abilitadu SELinux, chi diat pòdere su funtzionamentu curretu de RustDesk comente ala controllada."), + ("Change view", "Modìfica vista"), + ("Big tiles", "Iconas mannas"), + ("Small tiles", "Iconas minores"), + ("List", "Elencu"), + ("Virtual display", "Ischermu virtuale"), + ("Plug out all", "Iscollega totu"), + ("True color (4:4:4)", "Colore reale (4:4:4)"), + ("Enable blocking user input", "Abìlita blocu insertada utente"), + ("id_input_tip", "Podes insertare un'ID, un'IP diretu o unu domìniu cun una ghenna (:).\nSi boles atzèdere a unu dispositivu in un'àteru serbidore, annanghe s'indiritzu de su serbidore (@?key=), a esèmpiu\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi boles atzèdere a unu dispositivu in unu serbidore pùblicu, inserta \"@public\", pro su serbidore pùblicu sa crae non serbit\n\nSi boles fortzare s'impreu de una connessione de inoltru a sa prima connessione, annanghe \"/r\" a sa fine de s'ID, a esèmpiu \"9123456234/r\"."), + ("privacy_mode_impl_mag_tip", "Manera 1"), + ("privacy_mode_impl_virtual_display_tip", "Manera 2"), + ("Enter privacy mode", "Intra in modalidade de riservadesa"), + ("Exit privacy mode", "Essi dae sa modalidade de riservadesa"), + ("idd_not_support_under_win10_2004_tip", "Su driver vìdeu indiretu no est suportadu. Bisòngiat Windows 10, versione 2004 o prus noa."), + ("input_source_1_tip", "Fonte intrada (1)"), + ("input_source_2_tip", "Fonte intrada (2)"), + ("Swap control-command key", "Cuncàmbia tecla controllu-cumandu"), + ("swap-left-right-mouse", "Cuncàmbia pulsante mancu-destru ratu"), + ("2FA code", "Còdighe 2FA"), + ("More", "Àteru"), + ("enable-2fa-title", "Abìlita s'autenticatzione a duos fases"), + ("enable-2fa-desc", "Cunfigura s'autenticadore.\nPodes impreare un'aplicatzione de autenticatzione che a Authy, Microsoft o Google Authenticator in su telèfonu o elaboredore.\n\nPro abilitare s'autenticatzione a duas fases iscansi su còdighe QR cun s'aplicatzione e inserta su còdighe mustradu dae s'aplicatzione."), + ("wrong-2fa-code", "Non faghet a averguare su còdighe.\nVerìfica chi sas impostatziones de su còdighe e de s'ora locale siant curretas"), + ("enter-2fa-title", "Autenticatzione a duas fases"), + ("Email verification code must be 6 characters.", "Su còdighe de verìfica posta eletrònica depet cuntènnere 6 caràteres."), + ("2FA code must be 6 digits.", "Su còdighe 2FA depet èssere fatu de 6 tzifras."), + ("Multiple Windows sessions found", "Sessiones de Windows mùltiplas atzapadas"), + ("Please select the session you want to connect to", "Seletziona sa sessione cun chi ti boles cunnètere"), + ("powered_by_me", "Alimentadu dae RustDesk"), + ("outgoing_only_desk_tip", "Custa est un'editzione personalizada.\nTi podes connètere a àteros dispositivos, ma sos àteros dispositivos non si podent connètere a custu dispositivu."), + ("preset_password_warning", "Custa est un'editzione personalizada e benit frunida cun una crae de intrada pre-impostada.\nTotu sos chi connoschent custa crae diant pòdere otènnere su controllu totale de su dispositivu.\nSi non ti l'isetaias, disinstalla deretu su programma."), + ("Security Alert", "Avisu de seguresa"), + ("My address book", "Rubrica"), + ("Personal", "Personale"), + ("Owner", "Proprietàriu"), + ("Set shared password", "Imposta una crae cumpartzida"), + ("Exist in", "Esistit in"), + ("Read-only", "Leghidura ebbia"), + ("Read/Write", "Leghidura/iscritura"), + ("Full Control", "Controllu totale"), + ("share_warning_tip", "Sos campos inoghe in subra sunt cumpartzidos e sos àteros los pòdent bìdere."), + ("Everyone", "Totus"), + ("ab_web_console_tip", "Àteras informatziones subra de sa console web"), + ("allow-only-conn-window-open-tip", "Permite sa connessione petzi si sa ventana RustDesk est aberta"), + ("no_need_privacy_mode_no_physical_displays_tip", "Perunu ischermu fìsicu, peruna netzessidade de impreare sa modalidade de riservadesa."), + ("Follow remote cursor", "Sighi su cursore remotu"), + ("Follow remote window focus", "Sighi su focus de sa ventana remota"), + ("default_proxy_tip", "Protocollu e ghenna predefinidos sunt Socks5 e 1080"), + ("no_audio_input_device_tip", "Perunu dispositivu de intrada àudio atzapadu."), + ("Incoming", "In intrada"), + ("Outgoing", "In essida"), + ("Clear Wayland screen selection", "Annulla seletzione ischermada Wayland"), + ("clear_Wayland_screen_selection_tip", "A pustis de àere annulladu sa seletzione de ischermu, podes torrare a seletzionare s'ischermu de cumpartzire."), + ("confirm_clear_Wayland_screen_selection_tip", "Ses seguru de bòlere annullare sa seletzione de ischermu Wayland?"), + ("android_new_voice_call_tip", "As retzidu una rechuesta noa de mutida vocale. Si l'atzetas, sàudio at a colare a sa comunicatzione vocale."), + ("texture_render_tip", "Imprea sa tessidura de renderizatzione pro fàghere sas immàgines prus flùidas. Si atzapas problemas, proa a disabilitare custa optzione."), + ("Use texture rendering", "Imprea sa tessidura de renderizatzione"), + ("Floating window", "Ventana gallegiante"), + ("floating_window_tip", "Agiudat a mantènnere su servìtziu in s'isfundu de RustDesk"), + ("Keep screen on", "Mantene s'ischermu allutu"), + ("Never", "Mai"), + ("During controlled", "Durante su controllu"), + ("During service is on", "Cando su servìtziu est ativu"), + ("Capture screen using DirectX", "Catura s'ischermu impreende DirectX"), + ("Back", "In segus"), + ("Apps", "Aplicatziones"), + ("Volume up", "Volume +"), + ("Volume down", "Volume -"), + ("Power", "Alimentatzione"), + ("Telegram bot", "Bot de Telegram"), + ("enable-bot-tip", "Si abilitas custa funtzione, podes retzire su còdighe 2FA dae su bot tuo.\nPodes funtzionare fintzas comente notìfica de connessione."), + ("enable-bot-desc", "1. aberi una tzarrada cun @BotFather.\n2. Inbia su cumandu \"/newbot\", a pustis de àere fatu custu passàgiu as a retzire unu getone.\n3. Incumintza una tzarrada cun su bot tuo creadu como. Imbia unu messàgiu chi incumintzat cun un'istanga (\"/\") a tipu \"/salude\".\n"), + ("cancel-2fa-confirm-tip", "Ses seguru de bòlere annullare sa 2FA?"), + ("cancel-bot-confirm-tip", "Ses seguru de bòlere annullare Telegram?"), + ("About RustDesk", "Informatziones subra de RustDesk"), + ("Send clipboard keystrokes", "Imbia fileras teclas puntas de billete"), + ("network_error_tip", "Controlla sa connessione de rete, e a pustis seletziona 'Torra a proare'."), + ("Unlock with PIN", "Abìlita s'isblocu cun PIN"), + ("Requires at least {} characters", "Bisòngiant a su nessi {} caràteres"), + ("Wrong PIN", "PIN isballiadu"), + ("Set PIN", "Imposta su PIN"), + ("Enable trusted devices", "Abìlita dispositivos fidados"), + ("Manage trusted devices", "Manìgia sos dispositivos fidados"), + ("Platform", "Prataforma"), + ("Days remaining", "Dies chi abarrant"), + ("enable-trusted-devices-tip", "Brinca sa verìfica 2FA in sos dispositivos fidados"), + ("Parent directory", "Cartella printzipale"), + ("Resume", "Sighi"), + ("Invalid file name", "Nùmene archìviu non vàlidu"), + ("one-way-file-transfer-tip", "In s'ala controllada est abilitada sa tràmuda de archìvios a una diretzione ebbia."), + ("Authentication Required", "Dimanda de autenticatzione"), + ("Authenticate", "Autèntica"), + ("web_id_input_tip", "Podes insertare un'ID in su matessi serbidore, in su cliente web no est suportadu s'atzessu cun IP diretu.\nSi boles atzèdere a unu dispositivu in un'àteru serbidore, annanghe s'indiritzu de su serbidore (@?key=), a esèmpiu,\n9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nSi boles intrare a unu dispositivu in unu serbidore pùblicu, inserta \"@public\", non b'at bisòngiu de sa crae pro su serbidore pùblicu."), + ("Download", "Iscàrriga"), + ("Upload folder", "Cartella de carrigamentu"), + ("Upload files", "Carrigamentu de archìvios upload"), + ("Clipboard is synchronized", "Sa punta de billete est sincronizada"), + ("Update client clipboard", "Annoa sa punta de billete de su cliente"), + ("Untagged", "Chene tag"), + ("new-version-of-{}-tip", "B'at una versione noa de {} a disponimentu"), + ("Accessible devices", "Dispositivos atzessìbiles"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Atualiza su cliente RustDesk remotu a sa versione {} o prus noa!"), + ("d3d_render_tip", "Cando sa renderizatzione D3D est abilitada, s'ischermu de controllu remotu diat pòdere èssere nieddu in unas cantas màchinas"), + ("Use D3D rendering", "Imprea sa renderizatzione D3D"), + ("Printer", "Imprentadora"), + ("printer-os-requirement-tip", "Pro pòdere impreare s'imprentadora in seddida bisòngiat a installare {} in custu dispositivu."), + ("printer-requires-installed-{}-client-tip", "Pro sa funtzionalidade de imprenta in essida b'at bisòngiu de Window 10 o prus nou."), + ("printer-{}-not-installed-tip", "S'imprentadora {} no est installada"), + ("printer-{}-ready-tip", "S'imprentadora {} est installada e pronta pro s'impreu."), + ("Install {} Printer", "Installa s'imprentadora {}"), + ("Outgoing Print Jobs", "Traballos de imprenta in essida"), + ("Incoming Print Jobs", "Traballos de imprenta in intrada"), + ("Incoming Print Job", "Traballu de imprenta in intrada"), + ("use-the-default-printer-tip", "Imprea s'imprentadora predefinida"), + ("use-the-selected-printer-tip", "Imprea s'imprentadora seletzionada"), + ("auto-print-tip", "Imprenta in automàticu impreende s'imprentadora seletzionada."), + ("print-incoming-job-confirm-tip", "As retzidu unu traballu de imprenta dae remotu. Lu boles esecutare dae s'ala tua?"), + ("remote-printing-disallowed-tile-tip", "Imprenta remota disabilitada"), + ("remote-printing-disallowed-text-tip", "Sas impostatziones de sos permissos de s'ala controllada negant s'imprenta remota."), + ("save-settings-tip", "Sarva sas impostatziones"), + ("dont-show-again-tip", "Non mustres prus custu messàgiu"), + ("Take screenshot", "Faghe un'ischermada"), + ("Taking screenshot", "Faghende un'ischermada"), + ("screenshot-merged-screen-not-supported-tip", "S'unione de sa catura de ischermadas de prus ischermos como no est suportada.\nCola a un'ischermu ebbia e torra a proare."), + ("screenshot-action-tip", "Seletziona comente sighire cun s'ischermada."), + ("Save as", "Sarva comente"), + ("Copy to clipboard", "Còpia in punta de billete"), + ("Enable remote printer", "Abìlita imprentadora remota"), + ("Downloading {}", "Iscarrighende {}"), + ("{} Update", "Atualiza {}"), + ("{}-to-update-tip", "{} s'at a serrare e a installare sa versione nova"), + ("download-new-version-failed-tip", "Iscarrigamentu fallidu.\nPodes torrare a proare o seletzionare 'Iscàrriga' pro iscarrigare e atualizare a manera manuale."), + ("Auto update", "Atualizatzione automàtica"), + ("update-failed-check-msi-tip", "Controllu de sa manera de installatzione fallidu.\nSeletziona 'Iscàrriga' pro iscarrigare su programma e l'atualizare a manera manuale."), + ("websocket_tip", "Cando impreas WebSocket, sunt suportadas petzi sas connessiones de tràmuda relay"), + ("Use WebSocket", "Imprea WebSocket"), + ("Trackpad speed", "Velotzidade de su pannellu tàtile"), + ("Default trackpad speed", "Velotzidade predefinida de su pannellu tàtile"), + ("Numeric one-time password", "Crae numèrica monoimpreu"), + ("Enable IPv6 P2P connection", "Abìlita connessione P2P IPv6"), + ("Enable UDP hole punching", "Abìlita s'istampadura UDP"), + ("View camera", "Mustra sa càmera"), + ("Enable camera", "Abìlita sa càmera"), + ("No cameras", "Peruna càmera"), + ("view_camera_unsupported_tip", "Su dispositivu remotu non suportat sa visualizatzione de sa càmera"), + ("Terminal", "Terminale"), + ("Enable terminal", "Abìlita su terminale"), + ("New tab", "Ischeda noa"), + ("Keep terminal sessions on disconnect", "Cando ti disconnetes mantene aberta sa sessione de terminale"), + ("Terminal (Run as administrator)", "Terminale (imprea comente amministradore)"), + ("terminal-admin-login-tip", "Inserta su nùmene utente e sa crae de intrada de s'amministradore de s'ala controllada."), + ("Failed to get user token.", "Otenimentu de su getone de utente fallidu."), + ("Incorrect username or password.", "Nùmene utente o crae de intrada isballiados."), + ("The user is not an administrator.", "S'utente no est un'amministradore."), + ("Failed to check if the user is an administrator.", "Non faghet a verificare si s'utente est un'amministradore."), + ("Supported only in the installed version.", "Suportadu petzi in sa versione installada."), + ("elevation_username_tip", "Inserta Nùmene utente o domìniu de fonte\\nùmene Utente"), + ("Preparing for installation ...", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 96c7977cca2..c32168c70dd 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "dĺžka medzi %min% a %max%"), ("starts with a letter", "zaÄína písmenom"), ("allowed characters", "povolené znaky"), - ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9 a _ (podÄiarkovník). Prvý znak musí byÅ¥ a-z, A-Z. Dĺžka musí byÅ¥ medzi 6 a 16 znakmi."), + ("id_change_tip", "Povolené sú len znaky a-z, A-Z, 0-9, - (dash) a _ (podÄiarkovník). Prvý znak musí byÅ¥ a-z, A-Z. Dĺžka musí byÅ¥ medzi 6 a 16 znakmi."), ("Website", "Webová stránka"), ("About", "O RustDesk"), ("Slogan_tip", "Stvorené srdcom v tomto chaotickom svete!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Heslo do operaÄného systému"), ("install_tip", "V niektorých prípadoch RustDesk nefunguje správne z dôvodu riadenia užívateľských oprávnení (UAC). Vyhnete sa tomu kliknutím na nižšie zobrazene tlaÄítko a nainÅ¡talovaním RuskDesk do systému."), ("Click to upgrade", "Kliknutím nainÅ¡talujete aktualizáciu"), - ("Click to download", "Kliknutím potvrÄte stiahnutie"), - ("Click to update", "Kliknutím aktualizovaÅ¥"), ("Configure", "NastaviÅ¥"), ("config_acc", "Aby bolo možné na diaľku ovládaÅ¥ vaÅ¡u plochu, je potrebné aplikácii RustDesk udeliÅ¥ práva \"DostupnosÅ¥\"."), ("config_screen", "Aby bolo možné na diaľku sledovaÅ¥ vaÅ¡u obrazovku, je potrebné aplikácii RustDesk udeliÅ¥ práva \"Zachytávanie obsahu obrazovky\"."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Prenos súborov nie je povolený"), ("Note", "Poznámka"), ("Connection", "Pripojenie"), - ("Share Screen", "ZdielaÅ¥ obrazovku"), + ("Share screen", "ZdielaÅ¥ obrazovku"), ("Chat", "Chat"), ("Total", "Celkom"), ("items", "položiek"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Snímanie obrazovky"), ("Input Control", "Ovládanie vstupných zariadení"), ("Audio Capture", "Snímanie zvuku"), - ("File Connection", "Pripojenie súborov"), - ("Screen Connection", "Pripojenie obrazu"), ("Do you accept?", "Súhlasíte?"), ("Open System Setting", "Otvorenie nastavení systému"), ("How to get Android input permission?", "Ako v systéme Android povoliÅ¥ oprávnenie písaÅ¥ zo vstupného zariadenia?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "EÅ¡te nemáte obľúbeného partnera?\nNájdite niekoho, s kým sa môžete spojiÅ¥, a pridajte si ho do obľúbených!"), ("empty_lan_tip", "Ale nie, zdá sa, že sme zatiaľ neobjavili žiadnu protistranu."), ("empty_address_book_tip", "Ach bože, zdá sa, že vo vaÅ¡om adresári momentálne nie sú uvedení žiadni kolegovia."), - ("eg: admin", "napr. admin"), ("Empty Username", "Prázdne používateľské meno"), ("Empty Password", "Prázdne heslo"), ("Me", "Ja"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Aktualizujte klienta RustDesk na verziu {} alebo novÅ¡iu na vzdialenej strane!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "ZobraziÅ¥ kameru"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sl.rs b/src/lang/sl.rs index fad447b692f..021b6dabef4 100755 --- a/src/lang/sl.rs +++ b/src/lang/sl.rs @@ -3,7 +3,7 @@ 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"), + ("desk_tip", "S spodnjim IDjem in geslom omogoÄite oddaljeni nadzor vaÅ¡ega raÄunalnika"), ("Password", "Geslo"), ("Ready", "Pripravljen"), ("Established", "Povezava vzpostavljena"), @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "dolžina od %min% do %max%"), ("starts with a letter", "zaÄne se s Ärko"), ("allowed characters", "dovoljeni znaki"), - ("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."), + ("id_change_tip", "Dovoljeni znaki so a-z, A-Z (brez Å¡umnikov), 0-9, - (dash) in _. Prvi znak mora biti Ärka, dolžina od 6 do 16 znakov."), ("Website", "Spletna stran"), ("About", "O programu"), ("Slogan_tip", ""), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -190,7 +188,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), + ("Enable direct IP access", "OmogoÄi neposredni dostop preko IP naslova"), ("Rename", "Preimenuj"), ("Space", "Prazno"), ("Create desktop shortcut", "Ustvari bližnjico na namizju"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ni pravic za prenos datotek"), ("Note", "Opomba"), ("Connection", "Povezava"), - ("Share Screen", "Deli zaslon"), + ("Share screen", "Deli zaslon"), ("Chat", "Pogovor"), ("Total", "Skupaj"), ("items", "elementi"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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?"), @@ -364,7 +360,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "Snemanje"), ("Directory", "Imenik"), ("Automatically record incoming sessions", "Samodejno snemaj vhodne seje"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "Samodejno snemaj odhodne seje"), ("Change", "Spremeni"), ("Start session recording", "ZaÄni snemanje seje"), ("Stop session recording", "Ustavi snemanje seje"), @@ -412,8 +408,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select local keyboard type", "Izberite lokalno vrsto tipkovnice"), ("software_render_tip", "ÄŒe na Linuxu uporabljate Nvidino grafiÄno kartico in se oddaljeno okno zapre takoj po vzpostavitvi povezave, lahko pomaga preklop na odprtokodni gonilnik Nouveau in uporaba programskega upodabljanja. Potreben je ponovni zagon programa."), ("Always use software rendering", "Vedno uporabi programsko upodabljanje"), - ("config_input", "Za nadzor oddaljenega namizja s tipkovnico, rabi RustDesk pravico »Nadzor vnosa«."), - ("config_microphone", "Za zajem zvoka, rabi RustDesk pravico »Snemanje zvoka«."), + ("config_input", "RustDesk potrebuje pravico »Nadzor vnosa« za nadzor oddaljenega namizja s tipkovnico."), + ("config_microphone", "RustDesk potrebuje pravico »Snemanje zvoka« za zajemanje zvoka."), ("request_elevation_tip", "Lahko tudi zaprosite za dvig pravic, Äe je kdo na oddaljeni strani."), ("Wait", "ÄŒakaj"), ("Elevation Error", "Napaka pri povzdigovanju"), @@ -446,7 +442,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Voice call", "Glasovni klic"), ("Text chat", "Besedilni klepet"), ("Stop voice call", "Prekini glasovni klic"), - ("relay_hint_tip", "Morda neposredna povezava ni možna; lahko se poikusite povezati preko posrednika. ÄŒe želite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate »/r«, ali pa izberete možnost »Vedno poveži preko posrednika« v kartici nedavnih sej, Äe le-ta obstja."), + ("relay_hint_tip", "Morda neposredna povezava ni možna; lahko se poizkusite povezati preko posrednika. ÄŒe želite uporabiti posrednika ob prvem poizkusu vzpotavljanja povezave, lahko na konec IDja dodate »/r«, ali pa izberete možnost »Vedno poveži preko posrednika« v kartici nedavnih sej, Äe le-ta obstja."), ("Reconnect", "Ponovna povezava"), ("Codec", "Kodek"), ("Resolution", "LoÄljivost"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Nimate Å¡e priljubljenih partnerjev?\nVzpostavite povezavo, in jo dodajte med priljubljene."), ("empty_lan_tip", "Nismo naÅ¡li Å¡e nobenih partnerjev."), ("empty_address_book_tip", "VaÅ¡ adresar je prazen."), - ("eg: admin", "npr. admin"), ("Empty Username", "Prazno uporabniÅ¡ko ime"), ("Empty Password", "Prazno geslo"), ("Me", "Jaz"), @@ -649,12 +644,70 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Authentication Required", "Potrebno je preverjanje pristnosti"), ("Authenticate", "Preverjanje pristnosti"), ("web_id_input_tip", "Vnesete lahko ID iz istega strežnika, neposredni dostop preko IP naslova v spletnem odjemalcu ni podprt.\nÄŒe želite dostopati do naprave na drugem strežniku, pripnite naslov strežnika (@?key=), npr. 9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=.\nÄŒe želite dostopati do naprave na javnem strežniku, vnesite »@public«; kljuÄ za javni strežnik ni potreben."), - ("Download", ""), - ("Upload folder", ""), - ("Upload files", ""), - ("Clipboard is synchronized", ""), - ("Update client clipboard", ""), - ("Untagged", ""), - ("new-version-of-{}-tip", ""), + ("Download", "Prenos"), + ("Upload folder", "Naloži mapo"), + ("Upload files", "Naloži datoteke"), + ("Clipboard is synchronized", "OdložiÅ¡Äe je usklajeno"), + ("Update client clipboard", "Osveži odjemalÄevo odložiÅ¡Äe"), + ("Untagged", "NeoznaÄeno"), + ("new-version-of-{}-tip", "Na voljo je nova razliÄica {}"), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Prosimo, nadgradite RustDesk odjemalec na razliÄico {} ali novejÅ¡o na oddaljeni strani."), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Pogled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sq.rs b/src/lang/sq.rs index ad76f2f9cb2..a8a1a061f91 100644 --- a/src/lang/sq.rs +++ b/src/lang/sq.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9 dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), + ("id_change_tip", "Lejohen Vetëm karkteret a-z,A-Z,0-9, - (dash) dhe _(nënvizimet).Shkronja e parë duhet të jetë a-z, A-Z. Gjatesia midis 6 dhe 16."), ("Website", "Faqe ëebi"), ("About", "Rreth"), ("Slogan_tip", ""), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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\"."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nuk ka leje për transferimin e dosjesve"), ("Note", "Shënime"), ("Connection", "Lidhja"), - ("Share Screen", "Ndaj ekranin"), + ("Share screen", "Ndaj ekranin"), ("Chat", "Biseda"), ("Total", "Total"), ("items", "artikuj"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", ""), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sr.rs b/src/lang/sr.rs index 286658657a0..f26db2360b1 100644 --- a/src/lang/sr.rs +++ b/src/lang/sr.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9 i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), + ("id_change_tip", "Dozvoljeni su samo a-z, A-Z, 0-9, - (dash) i _ (donja crta) znakovi. Prvi znak mora biti slovo a-z, A-Z. Dužina je od 6 do 16."), ("Website", "Web sajt"), ("About", "O programu"), ("Slogan_tip", ""), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Nemate pravo prenosa datoteka"), ("Note", "Primedba"), ("Connection", "Konekcija"), - ("Share Screen", "Podeli ekran"), + ("Share screen", "Podeli ekran"), ("Chat", "Dopisivanje"), ("Total", "Ukupno"), ("items", "stavki"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("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?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Pregled kamere"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sv.rs b/src/lang/sv.rs index fcb2fe1ae41..9e495ba0131 100644 --- a/src/lang/sv.rs +++ b/src/lang/sv.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Bara a-z, A-Z, 0-9 och _ (understräck) tecken är tillÃ¥tna. Den första bokstaven mÃ¥ste vara a-z, A-Z. Längd mellan 6 och 16."), + ("id_change_tip", "Bara a-z, A-Z, 0-9, - (dash) och _ (understräck) tecken är tillÃ¥tna. Den första bokstaven mÃ¥ste vara a-z, A-Z. Längd mellan 6 och 16."), ("Website", "Hemsida"), ("About", "Om"), ("Slogan_tip", ""), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "OS lösenord"), ("install_tip", "PÃ¥ grund av UAC, kan inte RustDesk fungera ordentligt pÃ¥ klientsidan. För att undvika problem med UAC, tryck pÃ¥ knappen nedan för att installera RustDesk pÃ¥ systemet."), ("Click to upgrade", "Klicka för att nedgradera"), - ("Click to download", "Klicka för att ladda ner"), - ("Click to update", "Klicka för att uppdatera"), ("Configure", "Konfigurera"), ("config_acc", "För att kontrollera din dator pÃ¥ distans mÃ¥ste du ge RustDesk \"Tillgänglighets\" rättigheter."), ("config_screen", "För att kontrollera din dator pÃ¥ distans mÃ¥ste du ge RustDesk \"Skärminspelnings\" rättigheter."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Rättigheter saknas"), ("Note", "Notering"), ("Connection", "Anslutning"), - ("Share Screen", "Dela skärm"), + ("Share screen", "Dela skärm"), ("Chat", "Chatt"), ("Total", "Totalt"), ("items", "föremÃ¥l"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Skärminspelning"), ("Input Control", "Inputkontroll"), ("Audio Capture", "Ljudinspelning"), - ("File Connection", "Fil anslutning"), - ("Screen Connection", "Skärm anslutning"), ("Do you accept?", "Accepterar du?"), ("Open System Setting", "Öppna systeminställnig"), ("How to get Android input permission?", "Hur fÃ¥r man Android rättigheter?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Visa kamera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ta.rs b/src/lang/ta.rs new file mode 100644 index 00000000000..e642180d9f6 --- /dev/null +++ b/src/lang/ta.rs @@ -0,0 +1,713 @@ +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", "இணைபà¯à®ªà¯ நிலை"), + ("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", "à®à®Ÿà®¿/ரிலே சரà¯à®µà®°à¯"), + ("Import server config", "சரà¯à®µà®°à¯ உளà¯à®³à®®à¯ˆà®µà¯ இறகà¯à®•à¯à®®à®¤à®¿"), + ("Export Server Config", "சரà¯à®µà®°à¯ உளà¯à®³à®®à¯ˆà®µà¯ à®à®±à¯à®±à¯à®®à®¤à®¿"), + ("Import server configuration successfully", "சரà¯à®µà®°à¯ உளà¯à®³à®®à¯ˆà®µà¯ இறகà¯à®•à¯à®®à®¤à®¿ வெறà¯à®±à®¿"), + ("Export server configuration successfully", "சரà¯à®µà®°à¯ உளà¯à®³à®®à¯ˆà®µà¯ à®à®±à¯à®±à¯à®®à®¤à®¿ வெறà¯à®±à®¿"), + ("Invalid server configuration", "தவறான சரà¯à®µà®°à¯ உளà¯à®³à®®à¯ˆà®µà¯"), + ("Clipboard is empty", "கிளிபà¯à®ªà¯‹à®°à¯à®Ÿà¯ காலி"), + ("Stop service", "சேவையை நிறà¯à®¤à¯à®¤à¯"), + ("Change ID", "à®à®Ÿà®¿ மாறà¯à®±à¯"), + ("Your new ID", "உஙà¯à®•ள௠பà¯à®¤à®¿à®¯ à®à®Ÿà®¿"), + ("length %min% to %max%", "நீளம௠%min% à®®à¯à®¤à®²à¯ %max%"), + ("starts with a letter", "ஒர௠எழà¯à®¤à¯à®¤à®¾à®²à¯ தொடஙà¯à®•à¯"), + ("allowed characters", "அனà¯à®®à®¤à®¿à®•à¯à®•பà¯à®ªà®Ÿà¯à®Ÿ எழà¯à®¤à¯à®¤à¯à®•à¯à®•ளà¯"), + ("id_change_tip", "à®à®Ÿà®¿_மாறà¯à®±_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Website", "இணையதளமà¯"), + ("About", "பறà¯à®±à®¿"), + ("Slogan_tip", "சà¯à®²à¯‹à®•à®®à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Privacy Statement", "தனியà¯à®°à®¿à®®à¯ˆ அறிகà¯à®•ை"), + ("Mute", "ஒலியடகà¯à®•வà¯à®®à¯"), + ("Build Date", "கடà¯à®Ÿà®ªà¯à®ªà®Ÿà¯à®Ÿ தேதி"), + ("Version", "பதிபà¯à®ªà¯"), + ("Home", "வீடà¯"), + ("Audio Input", "ஒலி உளà¯à®³à¯€à®Ÿà¯"), + ("Enhancements", "மேமà¯à®ªà®¾à®Ÿà¯à®•ளà¯"), + ("Hardware Codec", "வனà¯à®ªà¯Šà®°à¯à®³à¯ கோடெகà¯"), + ("Adaptive bitrate", "தகவமைபà¯à®ªà¯ பிடà¯à®°à¯‡à®Ÿà¯"), + ("ID Server", "à®à®Ÿà®¿ சரà¯à®µà®°à¯"), + ("Relay Server", "ரிலே சரà¯à®µà®°à¯"), + ("API Server", "API சரà¯à®µà®°à¯"), + ("invalid_http", "தவறான_http"), + ("Invalid IP", "தவறான IP"), + ("Invalid format", "தவறான வடிவமà¯"), + ("server_not_support", "சரà¯à®µà®°à¯_ஆதரவà¯_இலà¯à®²à¯ˆ"), + ("Not available", "இலà¯à®²à¯ˆ"), + ("Too frequent", "அடிகà¯à®•டி"), + ("Cancel", "ரதà¯à®¤à¯à®šà¯†à®¯à¯"), + ("Skip", "தவிரà¯"), + ("Close", "மூடà¯"), + ("Retry", "மீணà¯à®Ÿà¯à®®à¯ à®®à¯à®¯à®²à®µà¯à®®à¯"), + ("OK", "சரி"), + ("Password Required", "கடவà¯à®šà¯à®šà¯Šà®²à¯_தேவை"), + ("Please enter your password", "உஙà¯à®•ள௠கடவà¯à®šà¯à®šà¯Šà®²à¯à®²à¯ˆ உளà¯à®³à®¿à®Ÿà¯à®•"), + ("Remember password", "கடவà¯à®šà¯à®šà¯Šà®²à¯à®²à¯ˆ நினைவ௠கொளà¯"), + ("Wrong Password", "தவறான கடவà¯à®šà¯à®šà¯Šà®²à¯"), + ("Do you want to enter again?", "மீணà¯à®Ÿà¯à®®à¯ à®®à¯à®¯à®²à®µà¯à®®à®¾?"), + ("Connection Error", "இணைபà¯à®ªà¯ பிழை"), + ("Error", "பிழை"), + ("Reset by the peer", "பியர௠மூலம௠மீடà¯à®Ÿà®®à¯ˆ"), + ("Connecting...", "இணைபà¯à®ªà¯ ..."), + ("Connection in progress. Please wait.", "இணைபà¯à®ªà¯ à®®à¯à®¯à®±à¯à®šà®¿à®¯à®¿à®²à¯. காதà¯à®¤à®¿à®°à¯à®•à¯à®•வà¯à®®à¯..."), + ("Please try 1 minute later", "1 நிமிடம௠கழிதà¯à®¤à¯ à®®à¯à®¯à®²à®µà¯à®®à¯"), + ("Login Error", "பதிவ௠பிழை"), + ("Successful", "வெறà¯à®±à®¿à®•à®°à®®à¯"), + ("Connected, waiting for image...", "இணைபà¯à®ªà¯ தயாரà¯, படதà¯à®¤à¯à®•à¯à®•ாக காதà¯à®¤à®¿à®°à¯à®•à¯à®•ிறதà¯..."), + ("Name", "பெயரà¯"), + ("Type", "வகை"), + ("Modified", "மாறà¯à®±à®ªà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("Size", "அளவà¯"), + ("Show Hidden Files", "மறைநà¯à®¤ கோபà¯à®ªà¯à®•ளை காடà¯à®Ÿà¯"), + ("Receive", "பெறà¯"), + ("Send", "அனà¯à®ªà¯à®ªà¯"), + ("Refresh File", "கோபà¯à®ªà¯ பà¯à®¤à¯à®ªà¯à®ªà®¿"), + ("Local", "உளà¯à®³à¯‚à®°à¯"), + ("Remote", "ரிமோடà¯"), + ("Remote Computer", "ரிமோட௠கணினி"), + ("Local Computer", "உளà¯à®³à¯‚ர௠கணினி"), + ("Confirm Delete", "நீகà¯à®•à¯à®µà®¤à¯ˆ உறà¯à®¤à®¿à®šà¯†à®¯à¯"), + ("Delete", "நீகà¯à®•à¯"), + ("Properties", "பணà¯à®ªà¯à®•ளà¯"), + ("Multi Select", "பலவறà¯à®±à¯ˆ தேரà¯à®µà¯"), + ("Select All", "அனைதà¯à®¤à¯à®®à¯ தேரà¯à®µà¯"), + ("Unselect All", "அனைதà¯à®¤à¯à®®à¯ தேரà¯à®µà¯ நீகà¯à®•à¯"), + ("Empty Directory", "காலியான கோபà¯à®ªà¯à®•à¯à®•à¯à®´à¯"), + ("Not an empty directory", "காலியான கோபà¯à®ªà¯à®•à¯à®•à¯à®´à¯ அலà¯à®²"), + ("Are you sure you want to delete this file?", "கோபà¯à®ªà¯ˆ நீகà¯à®• உறà¯à®¤à®¿à®¯à®¾?"), + ("Are you sure you want to delete this empty directory?", "காலி கோபà¯à®ªà¯à®±à¯ˆà®¯à¯ˆ நீகà¯à®• உறà¯à®¤à®¿à®¯à®¾?"), + ("Are you sure you want to delete the file of this directory?", "கோபà¯à®ªà¯à®±à¯ˆà®¯à®¿à®©à¯ கோபà¯à®ªà¯à®•ளை நீகà¯à®• உறà¯à®¤à®¿à®¯à®¾?"), + ("Do this for all conflicts", "அனைதà¯à®¤à¯ à®®à¯à®°à®£à¯à®ªà®¾à®Ÿà¯à®•ளà¯à®•à¯à®•à¯à®®à¯ இதை செயà¯"), + ("This is irreversible!", "இத௠மீளாதà¯!"), + ("Deleting", "நீகà¯à®•à¯à®¤à®²à¯"), + ("files", "கோபà¯à®ªà¯à®•ளà¯"), + ("Waiting", "காதà¯à®¤à®¿à®°à¯à®•à¯à®•à¯à®®à¯"), + ("Finished", "à®®à¯à®Ÿà®¿à®¨à¯à®¤à®¤à¯"), + ("Speed", "வேகமà¯"), + ("Custom Image Quality", "தனிபà¯à®ªà®Ÿà¯à®Ÿ பà¯à®•ைபà¯à®ªà®Ÿ தரமà¯"), + ("Privacy mode", "தனியà¯à®°à®¿à®®à¯ˆ à®®à¯à®±à¯ˆ"), + ("Block user input", "பயனர௠உளà¯à®³à¯€à®Ÿà¯à®Ÿà¯ˆà®¤à¯ தடà¯"), + ("Unblock user input", "பயனர௠உளà¯à®³à¯€à®Ÿà¯ தடை நீகà¯à®•à¯"), + ("Adjust Window", "சாளரம௠சரிசெயà¯"), + ("Original", "அசலà¯"), + ("Shrink", "கà¯à®±à¯à®•à¯à®•à¯"), + ("Stretch", "நீடà¯à®Ÿà¯"), + ("Scrollbar", "ஸà¯à®•à¯à®°à¯‹à®²à¯ படà¯à®Ÿà®¿"), + ("ScrollAuto", "ஸà¯à®•à¯à®°à¯‹à®²à¯à®†à®Ÿà¯à®Ÿà¯‹"), + ("Good image quality", "நலà¯à®² பà¯à®•ைபà¯à®ªà®Ÿ தரமà¯"), + ("Balanced", "சமநிலை"), + ("Optimize reaction time", "எதிரà¯à®µà®¿à®©à¯ˆ நேரதà¯à®¤à¯ˆ மேமà¯à®ªà®¾à®Ÿà¯"), + ("Custom", "தனிபà¯à®ªà®Ÿà¯à®Ÿ"), + ("Show remote cursor", "ரிமோட௠கரà¯à®šà®°à¯ காடà¯à®Ÿà¯"), + ("Show quality monitor", "தரம௠காடà¯à®Ÿà¯"), + ("Disable clipboard", "கிளிபà¯à®ªà¯‹à®°à¯à®Ÿà¯ˆ மறை"), + ("Lock after session end", "அமரà¯à®µà¯ à®®à¯à®Ÿà®¿à®µà¯à®•à¯à®•à¯à®ªà¯ பின௠மறை"), + ("Insert Ctrl + Alt + Del", "Ctrl + Alt + Del செயà¯"), + ("Insert Lock", "மறை செயà¯"), + ("Refresh", "பà¯à®¤à¯à®ªà¯à®ªà®¿"), + ("ID does not exist", "à®à®Ÿà®¿ இலà¯à®²à¯ˆ"), + ("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", "OS கடவà¯à®šà¯à®šà¯Šà®²à¯"), + ("install_tip", "நிறà¯à®µà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Click to upgrade", "மேமà¯à®ªà®Ÿà¯à®¤à¯à®¤ கிளிக௠செயà¯"), + ("Configure", "உளà¯à®³à®®à¯ˆ"), + ("config_acc", "உளà¯à®³à®®à¯ˆà®µà¯_அகà¯à®•ெஸà¯à®¸à¯"), + ("config_screen", "config_screen"), + ("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", "tcp_மூடாதே_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("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", "தà¯à®£à¯à®Ÿà®¿"), + ("Enable file copy and paste", "கோபà¯à®ªà¯ நகல௠மறà¯à®±à¯à®®à¯ பேஸà¯à®Ÿà¯ இயகà¯à®•à¯"), + ("Connected", "இணைகà¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("Direct and encrypted connection", "நேரடி மறà¯à®±à¯à®®à¯ மறையான இணைபà¯à®ªà¯"), + ("Relayed and encrypted connection", "ரிலே மறà¯à®±à¯à®®à¯ மறையான இணைபà¯à®ªà¯"), + ("Direct and unencrypted connection", "நேரடி மறà¯à®±à¯à®®à¯ மறையான இணைபà¯à®ªà¯"), + ("Relayed and unencrypted connection", "ரிலே மறà¯à®±à¯à®®à¯ மறையான இணைபà¯à®ªà¯"), + ("Enter Remote ID", "தொலை à®à®Ÿà®¿à®¯à¯ˆ உளà¯à®³à®¿à®Ÿà¯"), + ("Enter your password", "உஙà¯à®•ள௠கடவà¯à®šà¯à®šà¯Šà®²à¯à®²à¯ˆ உளà¯à®³à®¿à®Ÿà¯"), + ("Logging in...", "பதிவ௠மà¯à®¯à®±à¯à®šà®¿à®•à¯à®•ிறதà¯..."), + ("Enable RDP session sharing", "RDP அமரà¯à®µà¯ பகிரà¯à®µà¯ இயகà¯à®•à¯"), + ("Auto Login", "தானியஙà¯à®•௠உளà¯à®¨à¯à®´à¯ˆà®µà¯"), + ("Enable direct IP access", "நேரடி IP அனà¯à®®à®¤à®¿à®ªà¯à®ªà¯ இயகà¯à®•à¯"), + ("Rename", "பெயர௠மாறà¯à®±à¯"), + ("Space", "இடமà¯"), + ("Create desktop shortcut", "டெஸà¯à®•à¯à®Ÿà®¾à®ªà¯ à®à®•ானை உரà¯à®µà®¾à®•à¯à®•à¯"), + ("Change Path", "பாதை மாறà¯à®±à¯"), + ("Create Folder", "கோபà¯à®ªà¯à®•à¯à®•à¯à®´à¯ உரà¯à®µà®¾à®•à¯à®•à¯"), + ("Please enter the folder name", "கோபà¯à®ªà¯à®•à¯à®•à¯à®´à¯à®µà®¿à®©à¯ பெயரை உளà¯à®³à®¿à®Ÿà¯"), + ("Fix it", "சரி செயà¯"), + ("Warning", "எசà¯à®šà®°à®¿à®•à¯à®•ை"), + ("Login screen using Wayland is not supported", "Wayland உளà¯à®¨à¯à®´à¯ˆà®µà¯à®¤à¯ திரை ஆதரவிலà¯à®²à¯ˆ"), + ("Reboot required", "மறà¯à®¤à¯Šà®Ÿà®•à¯à®•ம௠தேவை"), + ("Unsupported display server", "திரை சரà¯à®µà®°à¯ ஆதரவ௠இலà¯à®²à¯ˆ"), + ("x11 expected", "x11 எதிரà¯à®ªà®¾à®°à¯à®•à¯à®•பà¯à®ªà®Ÿà¯à®•ிறதà¯"), + ("Port", "போரà¯à®Ÿà¯"), + ("Settings", "அமைபà¯à®ªà¯à®•ளà¯"), + ("Username", "பயனரà¯à®ªà¯†à®¯à®°à¯"), + ("Invalid port", "தவறான போரà¯à®Ÿà¯"), + ("Closed manually by the peer", "பியர௠மூலம௠மூடபà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("Enable remote configuration modification", "தொலை அமைபà¯à®ªà¯ மாறà¯à®±à¯ இயகà¯à®•à¯"), + ("Run without install", "நிறà¯à®µà®²à¯ இலà¯à®²à®¾à®®à®²à¯ இயகà¯à®•à¯"), + ("Connect via relay", "ரிலே மூலம௠இணைகà¯à®•வà¯à®®à¯"), + ("Always connect via relay", "எபà¯à®ªà¯‹à®¤à¯à®®à¯ ரிலே மூலம௠இணைகà¯à®•வà¯à®®à¯"), + ("whitelist_tip", "வெளà¯à®³à¯ˆà®ªà¯à®ªà®Ÿà¯à®Ÿà®¿à®¯à®²à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Login", "உளà¯à®¨à¯à®´à¯ˆ"), + ("Verify", "உறà¯à®¤à®¿à®ªà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("Remember me", "நினைவ௠கொளà¯"), + ("Trust this device", "இநà¯à®¤ சாதனதà¯à®¤à¯ˆ நமà¯à®ªà¯"), + ("Verification code", "சரிபாரà¯à®ªà¯à®ªà¯ கà¯à®±à®¿à®¯à¯€à®Ÿà¯"), + ("verification_tip", "சரிபாரà¯à®ªà¯à®ªà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Logout", "வெளியேறà¯"), + ("Tags", "கà¯à®±à®¿à®šà¯à®šà¯Šà®±à¯à®•ளà¯"), + ("Search ID", "à®à®Ÿà®¿ தேடà¯"), + ("whitelist_sep", "அனà¯à®®à®¤à®¿à®ªà¯à®ªà®Ÿà¯à®Ÿà®¿à®¯à®²à¯_sep"), + ("Add ID", "à®à®Ÿà®¿ சேரà¯"), + ("Add Tag", "கà¯à®±à®¿à®šà¯à®šà¯Šà®±à¯à®•ள௠சேரà¯"), + ("Unselect all tags", "அனைதà¯à®¤à¯ கà¯à®±à®¿à®šà¯à®šà¯Šà®±à¯à®•ளைத௠தேரà¯à®µà¯ நீகà¯à®•à¯"), + ("Network error", "நெடà¯à®µà¯Šà®°à¯à®•௠பிழை"), + ("Username missed", "பயனரà¯à®ªà¯†à®¯à®°à¯ தவறவிடà¯à®Ÿà®¤à¯"), + ("Password missed", "கடவà¯à®šà¯à®šà¯Šà®²à¯ தவறவிடà¯à®Ÿà®¤à¯"), + ("Wrong credentials", "தவறான சானà¯à®±à¯à®•ளà¯"), + ("The verification code is incorrect or has expired", "சரிபாரà¯à®ªà¯à®ªà¯à®•௠கà¯à®±à®¿à®¯à¯€à®Ÿà¯ தவறானத௠அலà¯à®²à®¤à¯ காலாவதி"), + ("Edit Tag", "கà¯à®±à®¿à®šà¯à®šà¯Šà®±à¯à®•ள௠மாறà¯à®±à¯"), + ("Forget Password", "கடவà¯à®šà¯à®šà¯Šà®²à¯à®²à¯ˆ மறநà¯à®¤à¯à®µà®¿à®Ÿà¯"), + ("Favorites", "விரà¯à®ªà¯à®ªà®™à¯à®•ளà¯"), + ("Add to Favorites", "விரà¯à®ªà¯à®ªà®™à¯à®•ளà¯à®•à¯à®•௠சேரà¯"), + ("Remove from Favorites", "விரà¯à®ªà¯à®ªà®™à¯à®•ளà¯à®•à¯à®•௠நீகà¯à®•à¯"), + ("Empty", "காலி"), + ("Invalid folder name", "தவறான கோபà¯à®ªà¯à®•à¯à®•à¯à®´à¯ பெயரà¯"), + ("Socks5 Proxy", "Socks5 பà¯à®°à®¾à®•à¯à®¸à®¿"), + ("Socks5/Http(s) Proxy", "Socks5/Http(s) பà¯à®°à®¾à®•à¯à®¸à®¿"), + ("Discovered", "கணà¯à®Ÿà¯à®ªà®¿à®Ÿà®¿à®•à¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("install_daemon_tip", "டீமானà¯_நிறà¯à®µà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Remote ID", "தொலை à®à®Ÿà®¿"), + ("Paste", "பேஸà¯à®Ÿà¯"), + ("Paste here?", "இஙà¯à®•ே பேஸà¯à®Ÿà¯ செயà¯?"), + ("Are you sure to close the connection?", "இணைபà¯à®ªà¯ˆ மூட உறà¯à®¤à®¿à®¯à®¾?"), + ("Download new version", "பà¯à®¤à®¿à®¯ பதிபà¯à®ªà¯ˆ பதிவிறகà¯à®•à¯"), + ("Touch mode", "தொடà¯à®¤à®²à¯ à®®à¯à®±à¯ˆ"), + ("Mouse mode", "சà¯à®Ÿà¯à®Ÿà®¿ à®®à¯à®±à¯ˆ"), + ("One-Finger Tap", "ஒர௠விரல௠தடà¯à®Ÿà¯"), + ("Left Mouse", "இடத௠சà¯à®Ÿà¯à®Ÿà®¿"), + ("One-Long Tap", "ஒர௠நீணà¯à®Ÿ தடà¯à®Ÿà¯"), + ("Two-Finger Tap", "இர௠விரல௠தடà¯à®Ÿà¯"), + ("Right Mouse", "வலத௠சà¯à®Ÿà¯à®Ÿà®¿"), + ("One-Finger Move", "ஒர௠விரல௠நகரà¯à®¤à¯à®¤à®²à¯"), + ("Double Tap & Move", "இரடà¯à®Ÿà¯ˆ தடà¯à®Ÿà¯ மறà¯à®±à¯à®®à¯ நகரà¯à®¤à¯à®¤à®²à¯"), + ("Mouse Drag", "சà¯à®Ÿà¯à®Ÿà®¿ இழà¯à®¤à¯à®¤à®²à¯"), + ("Three-Finger vertically", "மூனà¯à®±à¯ விரல௠செஙà¯à®•à¯à®¤à¯à®¤à®¾à®•"), + ("Mouse Wheel", "சà¯à®Ÿà¯à®Ÿà®¿ சகà¯à®•à®°à®®à¯"), + ("Two-Finger Move", "இர௠விரல௠நகரà¯à®¤à¯à®¤à®²à¯"), + ("Canvas Move", "கேனà¯à®µà®¾à®¸à¯ நகரà¯à®¤à¯à®¤à®²à¯"), + ("Pinch to Zoom", "சிமà¯à®Ÿà¯à®Ÿà®¿ பெரிதாகà¯à®•லà¯"), + ("Canvas Zoom", "கேனà¯à®µà®¾à®¸à¯ பெரிதாகà¯à®•லà¯"), + ("Reset canvas", "கேனà¯à®µà®¾à®¸à¯ மீடà¯à®Ÿà®®à¯ˆ"), + ("No permission of file transfer", "கோபà¯à®ªà¯ பரிமாறà¯à®± அனà¯à®®à®¤à®¿ இலà¯à®²à¯ˆ"), + ("Note", "கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Connection", "இணைபà¯à®ªà¯"), + ("Share screen", ""), + ("Chat", "அரடà¯à®Ÿà¯ˆ"), + ("Total", "மொதà¯à®¤à®®à¯"), + ("items", "பொரà¯à®Ÿà¯à®•ளà¯"), + ("Selected", "தேரà¯à®¨à¯à®¤à¯†à®Ÿà¯à®•à¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("Screen Capture", "திரை பிடிபà¯à®ªà¯"), + ("Input Control", "உளà¯à®³à¯€à®Ÿà¯ கடà¯à®Ÿà¯à®ªà¯à®ªà®¾à®Ÿà¯"), + ("Audio Capture", "ஒலி பிடிபà¯à®ªà¯"), + ("Do you accept?", "நீஙà¯à®•ள௠à®à®±à¯à®±à¯à®•à¯à®•ொளà¯à®•ிறீரà¯à®•ளா?"), + ("Open System Setting", "சிஸà¯à®Ÿà®®à¯ அமைபà¯à®ªà¯à®•ளைத௠திற"), + ("How to get Android input permission?", "Android உளà¯à®³à¯€à®Ÿà¯ அனà¯à®®à®¤à®¿ எபà¯à®ªà®Ÿà®¿ பெறà¯à®µà®¤à¯?"), + ("android_input_permission_tip1", "RustDesk இநà¯à®¤ Android சாதனதà¯à®¤à¯ˆ கடà¯à®Ÿà¯à®ªà¯à®ªà®Ÿà¯à®¤à¯à®¤ \"அணà¯à®•ல௠சேவைகளà¯\" அனà¯à®®à®¤à®¿ தேவை."), + ("android_input_permission_tip2", "சிஸà¯à®Ÿà®®à¯ அமைபà¯à®ªà¯à®•ளில௠[நிறà¯à®µà®ªà¯à®ªà®Ÿà¯à®Ÿ சேவைகளà¯] கணà¯à®Ÿà¯à®ªà®¿à®Ÿà®¿à®¤à¯à®¤à¯, RustDesk சேவை இயகà¯à®•வà¯à®®à¯."), + ("android_new_connection_tip", "பà¯à®¤à®¿à®¯ கடà¯à®Ÿà¯à®ªà¯à®ªà®¾à®Ÿà¯à®Ÿà¯ கோரிகà¯à®•ை வநà¯à®¤à¯à®³à¯à®³à®¤à¯"), + ("android_service_will_start_tip", "திரை பிடிபà¯à®ªà¯ இயகà¯à®•ினால௠சேவை தானாக தொடஙà¯à®•à¯à®®à¯"), + ("android_stop_service_tip", "சேவை நிறà¯à®¤à¯à®¤à®¿à®©à®¾à®²à¯ எலà¯à®²à®¾ இணைபà¯à®ªà¯à®•ளà¯à®®à¯ மூடிவிடà¯à®®à¯"), + ("android_version_audio_tip", "Android 10+ தேவை ஒலி பிடிபà¯à®ªà¯à®•à¯à®•à¯"), + ("android_start_service_tip", "[சேவை தொடஙà¯à®•à¯] தடà¯à®Ÿà®µà¯à®®à¯ அலà¯à®²à®¤à¯ [திரை பிடிபà¯à®ªà¯] இயகà¯à®•வà¯à®®à¯"), + ("android_permission_may_not_change_tip", "அனà¯à®®à®¤à®¿à®•ள௠உடனே மாறாமல௠இரà¯à®•à¯à®•லாமà¯, மீணà¯à®Ÿà¯à®®à¯ இணைகà¯à®•வà¯à®®à¯"), + ("Account", "கணகà¯à®•à¯"), + ("Overwrite", "மேலெழà¯à®¤à¯"), + ("This file exists, skip or overwrite this file?", "கோபà¯à®ªà¯ உளà¯à®³à®¤à¯, தவிரà¯à®•à¯à®•வா அலà¯à®²à®¤à¯ மேலெழà¯à®¤à®µà®¾?"), + ("Quit", "வெளியேறà¯"), + ("Help", "உதவி"), + ("Failed", "தோலà¯à®µà®¿"), + ("Succeeded", "வெறà¯à®±à®¿"), + ("Someone turns on privacy mode, exit", "தனியà¯à®°à®¿à®®à¯ˆ à®®à¯à®±à¯ˆ இயகà¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯, வெளியேறà¯"), + ("Unsupported", "ஆதரவ௠இலà¯à®²à¯ˆ"), + ("Peer denied", "இணையாளர௠மறà¯à®¤à¯à®¤à®¾à®°à¯"), + ("Please install plugins", "இணைபà¯à®ªà¯à®•ளை நிறà¯à®µà¯à®™à¯à®•ளà¯"), + ("Peer exit", "இணையாளர௠வெளியேறினாரà¯"), + ("Failed to turn off", "அணைகà¯à®• à®®à¯à®Ÿà®¿à®¯à®µà®¿à®²à¯à®²à¯ˆ"), + ("Turned off", "அணைகà¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("Language", "மொழி"), + ("Keep RustDesk background service", "RustDesk பினà¯à®ªà¯à®² சேவையை வைதà¯à®¤à®¿à®°à¯"), + ("Ignore Battery Optimizations", "பேடà¯à®Ÿà®°à®¿ மேமà¯à®ªà®Ÿà¯à®¤à¯à®¤à®²à¯à®•ளை பà¯à®±à®•à¯à®•ணி"), + ("android_open_battery_optimizations_tip", "RustDesk கà¯à®•௠பேடà¯à®Ÿà®°à®¿ மேமà¯à®ªà®Ÿà¯à®¤à¯à®¤à®²à¯ அணைகà¯à®• அமைபà¯à®ªà¯à®•ளà¯à®•à¯à®•௠செலà¯à®²à®µà¯à®®à¯"), + ("Start on boot", "தà¯à®µà®•à¯à®•தà¯à®¤à®¿à®²à¯ தொடஙà¯à®•à¯"), + ("Start the screen sharing service on boot, requires special permissions", "தà¯à®µà®•à¯à®•தà¯à®¤à®¿à®²à¯ திரை பகிரà¯à®µà¯ தொடஙà¯à®•à¯, சிறபà¯à®ªà¯ அனà¯à®®à®¤à®¿ தேவை"), + ("Connection not allowed", "இணைபà¯à®ªà¯ அனà¯à®®à®¤à®¿à®•à¯à®•பà¯à®ªà®Ÿà®µà®¿à®²à¯à®²à¯ˆ"), + ("Legacy mode", "பழைய à®®à¯à®±à¯ˆ"), + ("Map mode", "வரைபட à®®à¯à®±à¯ˆ"), + ("Translate mode", "மொழிபெயரà¯à®ªà¯à®ªà¯ à®®à¯à®±à¯ˆ"), + ("Use permanent password", "நிரநà¯à®¤à®° கடவà¯à®šà¯à®šà¯Šà®²à¯ பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("Use both passwords", "இரணà¯à®Ÿà¯ கடவà¯à®šà¯à®šà¯Šà®²à¯à®•ளà¯à®®à¯ பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("Set permanent password", "நிரநà¯à®¤à®° கடவà¯à®šà¯à®šà¯Šà®²à¯ அமை"), + ("Enable 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 Toolbar", "கரà¯à®µà®¿à®ªà¯à®ªà®Ÿà¯à®Ÿà®¿à®¯à¯ˆà®•௠காடà¯à®Ÿà¯"), + ("Hide Toolbar", "கரà¯à®µà®¿à®ªà¯à®ªà®Ÿà¯à®Ÿà®¿à®¯à¯ˆ மறை"), + ("Direct Connection", "நேரடி இணைபà¯à®ªà¯"), + ("Relay Connection", "ரிலே இணைபà¯à®ªà¯"), + ("Secure Connection", "பாதà¯à®•ாபà¯à®ªà®¾à®© இணைபà¯à®ªà¯"), + ("Insecure Connection", "பாதà¯à®•ாபà¯à®ªà®±à¯à®± இணைபà¯à®ªà¯"), + ("Scale original", "அசல௠அளவà¯"), + ("Scale adaptive", "தகவமைபà¯à®ªà¯ அளவà¯"), + ("General", "பொதà¯"), + ("Security", "பாதà¯à®•ாபà¯à®ªà¯"), + ("Theme", "தீமà¯"), + ("Dark Theme", "இரà¯à®£à¯à®Ÿ தீமà¯"), + ("Light Theme", "வெளிசà¯à®š தீமà¯"), + ("Dark", "இரà¯à®£à¯à®Ÿ"), + ("Light", "வெளிசà¯à®šà®®à¯"), + ("Follow System", "சிஸà¯à®Ÿà®¤à¯à®¤à¯ˆà®ªà¯ பினà¯à®ªà®±à¯à®±à¯"), + ("Enable hardware codec", "வனà¯à®ªà¯Šà®°à¯à®³à¯ கோடெகà¯à®•ை இயகà¯à®•à¯"), + ("Unlock Security Settings", "பாதà¯à®•ாபà¯à®ªà¯ அமைபà¯à®ªà¯à®•ளை திற"), + ("Enable audio", "ஒலியை இயகà¯à®•à¯"), + ("Unlock Network Settings", "நெடà¯à®µà¯Šà®°à¯à®•௠அமைபà¯à®ªà¯à®•ளை திற"), + ("Server", "சரà¯à®µà®°à¯"), + ("Direct IP Access", "நேரடி IP அணà¯à®•லà¯"), + ("Proxy", "பà¯à®°à®¾à®•à¯à®¸à®¿"), + ("Apply", "பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("Disconnect all devices?", "அனைதà¯à®¤à¯ சாதனஙà¯à®•ளையà¯à®®à¯ தà¯à®£à¯à®Ÿà®¿à®•à¯à®•வா?"), + ("Clear", "தெளிவà¯à®ªà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("Audio Input Device", "ஒலி உளà¯à®³à¯€à®Ÿà¯ சாதனமà¯"), + ("Use IP Whitelisting", "IP அனà¯à®®à®¤à®¿à®ªà¯à®ªà®Ÿà¯à®Ÿà®¿à®¯à®²à¯ˆà®ªà¯ பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("Network", "நெடà¯à®µà¯Šà®°à¯à®•à¯"), + ("Pin Toolbar", "கரà¯à®µà®¿à®ªà¯à®ªà®Ÿà¯à®Ÿà®¿à®¯à¯ˆ பின௠செயà¯"), + ("Unpin Toolbar", "கரà¯à®µà®¿à®ªà¯à®ªà®Ÿà¯à®Ÿà®¿à®¯à¯ˆ அனà¯à®ªà®¿à®©à¯ செயà¯"), + ("Recording", "பதிவà¯"), + ("Directory", "கோபà¯à®ªà®•à®®à¯"), + ("Automatically record incoming sessions", "உளà¯à®µà®°à¯à®®à¯ அமரà¯à®µà¯à®•ளை தானாக பதிவ௠செயà¯"), + ("Automatically record outgoing sessions", "வெளியேறà¯à®®à¯ அமரà¯à®µà¯à®•ளை தானாக பதிவ௠செயà¯"), + ("Change", "மாறà¯à®±à¯"), + ("Start session recording", "அமரà¯à®µà¯ பதிவைத௠தொடஙà¯à®•à¯"), + ("Stop session recording", "அமரà¯à®µà¯ பதிவை நிறà¯à®¤à¯à®¤à¯"), + ("Enable 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 பதிபà¯à®ªà¯ தேவை. X11 à®®à¯à®¯à®±à¯à®šà®¿à®•à¯à®•வà¯à®®à¯ அலà¯à®²à®¤à¯ OS மாறà¯à®±à®µà¯à®®à¯."), + ("JumpLink", "ஜமà¯à®ªà¯ லிஙà¯à®•à¯"), + ("Please Select the screen to be shared(Operate on the peer side).", "பகிரபà¯à®ªà®Ÿ வேணà¯à®Ÿà®¿à®¯ திரை தேரà¯à®¨à¯à®¤à¯†à®Ÿà¯à®•à¯à®•வà¯à®®à¯"), + ("Show RustDesk", "RustDesk ஠காடà¯à®Ÿà¯"), + ("This PC", "இநà¯à®¤ PC"), + ("or", "அலà¯à®²à®¤à¯"), + ("Continue with", "உடன௠தொடரà¯"), + ("Elevate", "உயரà¯à®¤à¯à®¤à¯"), + ("Zoom cursor", "கரà¯à®šà®°à¯ˆ பெரிதாகà¯à®•à¯"), + ("Accept sessions via password", "கடவà¯à®šà¯à®šà¯Šà®²à¯ வழியாக அமரà¯à®µà¯à®•ளை à®à®±à¯à®±à¯"), + ("Accept sessions via click", "கிளிக௠வழியாக அமரà¯à®µà¯à®•ளை à®à®±à¯à®±à¯"), + ("Accept sessions via both", "இரணà¯à®Ÿà¯ வழியிலà¯à®®à¯ அமரà¯à®µà¯à®•ளை à®à®±à¯à®±à¯"), + ("Please wait for the remote side to accept your session request...", "அமரà¯à®µà¯ கோரிகà¯à®•ை à®à®±à¯à®ªà®¤à®±à¯à®•ாக காதà¯à®¤à®¿à®°à¯à®•à¯à®•வà¯à®®à¯..."), + ("One-time Password", "à®’à®°à¯à®®à¯à®±à¯ˆ கடவà¯à®šà¯à®šà¯Šà®²à¯"), + ("Use one-time password", "à®’à®°à¯à®®à¯à®±à¯ˆ கடவà¯à®šà¯à®šà¯Šà®²à¯ பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("One-time password length", "à®’à®°à¯à®®à¯à®±à¯ˆ கடவà¯à®šà¯à®šà¯Šà®²à¯ நீளமà¯"), + ("Request access to your device", "உஙà¯à®•ள௠சாதனதà¯à®¤à®¿à®±à¯à®•௠அணà¯à®•ல௠கோரவà¯à®®à¯"), + ("Hide connection management window", "இணைபà¯à®ªà¯ மேலாணà¯à®®à¯ˆ சாளரதà¯à®¤à¯ˆ மறை"), + ("hide_cm_tip", "இணைபà¯à®ªà¯_மேலாளரà¯_மறை_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("wayland_experiment_tip", "வேலேணà¯à®Ÿà¯_சோதனை_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Right click to select tabs", "தாவலà¯à®•ளைத௠தேரà¯à®¨à¯à®¤à¯†à®Ÿà¯à®•à¯à®• வலத௠கிளிக௠செயà¯à®¯à®µà¯à®®à¯"), + ("Skipped", "தவிரà¯à®•à¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("Add to address book", "à®®à¯à®•வரி பà¯à®¤à¯à®¤à®•தà¯à®¤à®¿à®²à¯ சேரà¯"), + ("Group", "கà¯à®´à¯"), + ("Search", "தேடà¯"), + ("Closed manually by web console", "வெப௠கனà¯à®šà¯‹à®²à®¾à®²à¯ மூடபà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("Local keyboard type", "உளà¯à®³à¯‚ர௠விசைபலகை வகை"), + ("Select local keyboard type", "உளà¯à®³à¯‚ர௠விசைபலகை வகை தேரà¯à®µà¯"), + ("software_render_tip", "மெனà¯à®ªà¯Šà®°à¯à®³à¯_ரெணà¯à®Ÿà®°à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Always use software rendering", "எபà¯à®ªà¯‹à®¤à¯à®®à¯ மெனà¯à®ªà¯Šà®°à¯à®³à¯ ரெணà¯à®Ÿà®°à®¿à®™à¯"), + ("config_input", "உளà¯à®³à¯€à®Ÿà¯ கடà¯à®Ÿà¯à®ªà¯à®ªà®¾à®Ÿà¯à®Ÿà¯ அனà¯à®®à®¤à®¿ தேவை"), + ("config_microphone", "மைகà¯à®°à¯‹à®ƒà®ªà¯‹à®©à¯ அனà¯à®®à®¤à®¿ தேவை"), + ("request_elevation_tip", "உயரà¯à®µà¯_கோரிகà¯à®•ை_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Wait", "காதà¯à®¤à®¿à®°à¯"), + ("Elevation Error", "உயரà¯à®µà¯ பிழை"), + ("Ask the remote user for authentication", "தொலை பயனர௠அஙà¯à®•ீகாரம௠கோரà¯"), + ("Choose this if the remote account is administrator", "தொலை கணகà¯à®•௠நிரà¯à®µà®¾à®•ி எனில௠தேரà¯à®µà¯"), + ("Transmit the username and password of administrator", "நிரà¯à®µà®¾à®•ி பயனரà¯à®ªà¯†à®¯à®°à¯ கடவà¯à®šà¯à®šà¯Šà®²à¯ அனà¯à®ªà¯à®ªà¯"), + ("still_click_uac_tip", "uac_à®_இனà¯à®©à¯à®®à¯_சொடà¯à®•à¯à®•வà¯à®®à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Request Elevation", "உயரà¯à®µà¯ கோரிகà¯à®•ை"), + ("wait_accept_uac_tip", "uac_à®à®±à¯à®ªà¯à®•à¯à®•ாக_காதà¯à®¤à®¿à®°à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Elevate successfully", "வெறà¯à®±à®¿à®•ரமாக உயரà¯à®¤à¯à®¤à®ªà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("uppercase", "பெரிய எழà¯à®¤à¯à®¤à¯"), + ("lowercase", "சிறிய எழà¯à®¤à¯à®¤à¯"), + ("digit", "எணà¯"), + ("special character", "சிறபà¯à®ªà¯ எழà¯à®¤à¯à®¤à¯"), + ("length>=8", "நீளமà¯>=8"), + ("Weak", "பலவீனமà¯"), + ("Medium", "நடà¯à®¤à¯à®¤à®°à®®à¯"), + ("Strong", "வலà¯à®µà®¾à®©"), + ("Switch Sides", "பகà¯à®•ம௠மாறà¯à®±à¯"), + ("Please confirm if you want to share your desktop?", "டெஸà¯à®•à¯à®Ÿà®¾à®ªà¯ பகிர உறà¯à®¤à®¿à®ªà¯à®ªà®Ÿà¯à®¤à¯à®¤à®µà¯à®®à¯?"), + ("Display", "காடà¯à®šà®¿"), + ("Default View Style", "இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ காடà¯à®šà®¿ பாணி"), + ("Default Scroll Style", "இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ ஸà¯à®•à¯à®°à¯‹à®²à¯ பாணி"), + ("Default Image Quality", "இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ படதà¯à®¤à®°à®®à¯"), + ("Default Codec", "இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ கோடெகà¯"), + ("Bitrate", "பிடà¯à®°à¯‡à®Ÿà¯"), + ("FPS", "FPS"), + ("Auto", "தானியஙà¯à®•à¯"), + ("Other Default Options", "பிற இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ விரà¯à®ªà¯à®ªà®™à¯à®•ளà¯"), + ("Voice call", "கà¯à®°à®²à¯ அழைபà¯à®ªà¯"), + ("Text chat", "உரை அரடà¯à®Ÿà¯ˆ"), + ("Stop voice call", "கà¯à®°à®²à¯ அழைபà¯à®ªà¯ நிறà¯à®¤à¯à®¤à¯"), + ("relay_hint_tip", "ரிலே_கà¯à®±à®¿à®ªà¯à®ªà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Reconnect", "மீணà¯à®Ÿà¯à®®à¯ இணை"), + ("Codec", "கோடெகà¯"), + ("Resolution", "தெளிவà¯à®¤à¯à®¤à®¿à®±à®©à¯"), + ("No transfers in progress", "பரிமாறà¯à®±à®®à¯ எதà¯à®µà¯à®®à¯ நடைபெறவிலà¯à®²à¯ˆ"), + ("Set one-time password length", "à®’à®°à¯à®®à¯à®±à¯ˆ கடவà¯à®šà¯à®šà¯Šà®²à¯ நீளம௠அமை"), + ("RDP Settings", "RDP அமைபà¯à®ªà¯à®•ளà¯"), + ("Sort by", "வரிசைபà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("New Connection", "பà¯à®¤à®¿à®¯ இணைபà¯à®ªà¯"), + ("Restore", "மீடà¯à®Ÿà®®à¯ˆ"), + ("Minimize", "கà¯à®±à¯ˆà®•à¯à®•வà¯à®®à¯"), + ("Maximize", "பெரிதாகà¯à®•à¯"), + ("Your Device", "உஙà¯à®•ள௠சாதனமà¯"), + ("empty_recent_tip", "காலி_சமீபதà¯à®¤à®¿à®¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("empty_favorite_tip", "காலி_விரà¯à®ªà¯à®ªà®®à®¾à®©_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("empty_lan_tip", "காலி_லேனà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("empty_address_book_tip", "காலி_à®®à¯à®•வரி_பà¯à®¤à¯à®¤à®•_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Empty Username", "காலி பயனரà¯à®ªà¯†à®¯à®°à¯"), + ("Empty Password", "காலி கடவà¯à®šà¯à®šà¯Šà®²à¯"), + ("Me", "நானà¯"), + ("identical_file_tip", "ஒரே_மாதிரியான_கோபà¯à®ªà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("show_monitors_tip", "மானிடà¯à®Ÿà®°à¯à®•ளை_காடà¯à®Ÿà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("View Mode", "காடà¯à®šà®¿ à®®à¯à®±à¯ˆ"), + ("login_linux_tip", "லினகà¯à®¸à¯_உளà¯à®¨à¯à®´à¯ˆà®µà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("verify_rustdesk_password_tip", "rustdesk_கடவà¯à®šà¯à®šà¯Šà®²à¯_சரிபாரà¯à®ªà¯à®ªà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("remember_account_tip", "கணகà¯à®•ை_நினைவிலà¯_கொளà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("os_account_desk_tip", "os_கணகà¯à®•à¯_டெஸà¯à®•à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("OS Account", "OS கணகà¯à®•à¯"), + ("another_user_login_title_tip", "மறà¯à®±à¯Šà®°à¯_பயனரà¯_உளà¯à®¨à¯à®´à¯ˆà®µà¯_தலைபà¯à®ªà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("another_user_login_text_tip", "மறà¯à®±à¯Šà®°à¯_பயனரà¯_உளà¯à®¨à¯à®´à¯ˆà®µà¯_உரை_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("xorg_not_found_title_tip", "xorg_காணபà¯à®ªà®Ÿà®µà®¿à®²à¯à®²à¯ˆ_தலைபà¯à®ªà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("xorg_not_found_text_tip", "xorg_காணபà¯à®ªà®Ÿà®µà®¿à®²à¯à®²à¯ˆ_உரை_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("no_desktop_title_tip", "டெஸà¯à®•à¯à®Ÿà®¾à®ªà¯_இலà¯à®²à¯ˆ_தலைபà¯à®ªà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("no_desktop_text_tip", "டெஸà¯à®•à¯à®Ÿà®¾à®ªà¯_இலà¯à®²à¯ˆ_உரை_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("No need to elevate", "உயரà¯à®¤à¯à®¤ தேவையிலà¯à®²à¯ˆ"), + ("System Sound", "சிஸà¯à®Ÿà®®à¯ ஒலி"), + ("Default", "இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ"), + ("New RDP", "பà¯à®¤à®¿à®¯ RDP"), + ("Fingerprint", "கைரேகை"), + ("Copy Fingerprint", "கைரேகை நகலà¯"), + ("no fingerprints", "கைரேகைகள௠இலà¯à®²à¯ˆ"), + ("Select a peer", "பியர௠தேரà¯à®µà¯"), + ("Select peers", "பியரà¯à®•ள௠தேரà¯à®µà¯"), + ("Plugins", "இணைபà¯à®ªà¯à®•ளà¯"), + ("Uninstall", "நிறà¯à®µà®²à¯ நீகà¯à®•à¯"), + ("Update", "பà¯à®¤à¯à®ªà¯à®ªà®¿"), + ("Enable", "இயகà¯à®•à¯"), + ("Disable", "அணை"), + ("Options", "விரà¯à®ªà¯à®ªà®™à¯à®•ளà¯"), + ("resolution_original_tip", "அசல௠தெளிவà¯à®¤à¯à®¤à®¿à®±à®©à¯"), + ("resolution_fit_local_tip", "உளà¯à®³à¯‚ர௠பொரà¯à®¤à¯à®¤à®®à¯"), + ("resolution_custom_tip", "தனிபà¯à®ªà®¯à®©à¯ தெளிவà¯à®¤à¯à®¤à®¿à®±à®©à¯"), + ("Collapse toolbar", "கரà¯à®µà®¿à®ªà¯à®ªà®Ÿà¯à®Ÿà®¿ மூடà¯"), + ("Accept and Elevate", "à®à®±à¯à®±à¯ உயரà¯à®¤à¯à®¤à¯"), + ("accept_and_elevate_btn_tooltip", "à®à®±à¯à®±à¯_உயரà¯à®¤à¯à®¤à¯_பொதà¯à®¤à®¾à®©à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("clipboard_wait_response_timeout_tip", "கிளிபà¯à®ªà¯‹à®°à¯à®Ÿà¯_பதிலà¯_நேரமà¯à®Ÿà®¿à®µà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Incoming connection", "உளà¯à®µà®°à¯à®®à¯ இணைபà¯à®ªà¯"), + ("Outgoing connection", "வெளியேறà¯à®®à¯ இணைபà¯à®ªà¯"), + ("Exit", "வெளியேறà¯"), + ("Open", "திற"), + ("logout_tip", "வெளியேறà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Service", "சேவை"), + ("Start", "தொடஙà¯à®•à¯"), + ("Stop", "நிறà¯à®¤à¯à®¤à¯"), + ("exceed_max_devices", "அதிகபடà¯à®š சாதனஙà¯à®•ளை மீறியதà¯"), + ("Sync with recent sessions", "சமீபதà¯à®¤à®¿à®¯ அமரà¯à®µà¯à®•ளà¯à®Ÿà®©à¯ ஒதà¯à®¤à®¿à®šà¯ˆ"), + ("Sort tags", "கà¯à®±à®¿à®šà¯à®šà¯Šà®±à¯à®•ள௠வரிசை"), + ("Open connection in new tab", "பà¯à®¤à®¿à®¯ தாவலில௠இணைபà¯à®ªà¯ திற"), + ("Move tab to new window", "தாவல௠பà¯à®¤à®¿à®¯ சாளரதà¯à®¤à¯à®•à¯à®•௠நகரà¯à®¤à¯à®¤à¯"), + ("Can not be empty", "காலியாக à®®à¯à®Ÿà®¿à®¯à®¾à®¤à¯"), + ("Already exists", "à®à®±à¯à®•னவே உளà¯à®³à®¤à¯"), + ("Change Password", "கடவà¯à®šà¯à®šà¯Šà®²à¯ மாறà¯à®±à¯"), + ("Refresh Password", "கடவà¯à®šà¯à®šà¯Šà®²à¯ பà¯à®¤à¯à®ªà¯à®ªà®¿"), + ("ID", "à®à®Ÿà®¿"), + ("Grid View", "கிரிட௠காடà¯à®šà®¿"), + ("List View", "படà¯à®Ÿà®¿à®¯à®²à¯ காடà¯à®šà®¿"), + ("Select", "தேரà¯à®µà¯"), + ("Toggle Tags", "கà¯à®±à®¿à®šà¯à®šà¯Šà®±à¯à®•ள௠மாறà¯à®±à¯"), + ("pull_ab_failed_tip", "à®®à¯à®•வரி பà¯à®¤à¯à®¤à®•ம௠பà¯à®¤à¯à®ªà¯à®ªà®¿à®ªà¯à®ªà¯ தோலà¯à®µà®¿"), + ("push_ab_failed_tip", "à®®à¯à®•வரி பà¯à®¤à¯à®¤à®•ம௠சிஙà¯à®•௠தோலà¯à®µà®¿"), + ("synced_peer_readded_tip", "சிஙà¯à®•௠பியர௠மீணà¯à®Ÿà¯à®®à¯ சேரà¯à®•à¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("Change Color", "நிறம௠மாறà¯à®±à¯"), + ("Primary Color", "à®®à¯à®¤à®©à¯à®®à¯ˆ நிறமà¯"), + ("HSV Color", "HSV நிறமà¯"), + ("Installation Successful!", "நிறà¯à®µà®²à¯ வெறà¯à®±à®¿!"), + ("Installation failed!", "நிறà¯à®µà®²à¯ தோலà¯à®µà®¿!"), + ("Reverse mouse wheel", "சà¯à®Ÿà¯à®Ÿà®¿ சகà¯à®•ரம௠தலைகீழà¯"), + ("{} sessions", "{} அமரà¯à®µà¯à®•ளà¯"), + ("scam_title", "மோசடி எசà¯à®šà®°à®¿à®•à¯à®•ை"), + ("scam_text1", "தொலைபேசி மோசடியின௠பலியாகலாமà¯!"), + ("scam_text2", "RustDesk ஊழியர௠இவà¯à®µà®¾à®±à¯ தொடரà¯à®ªà¯ கொளà¯à®³ மாடà¯à®Ÿà®¾à®°à¯à®•ளà¯"), + ("Don't show again", "மீணà¯à®Ÿà¯à®®à¯ காடà¯à®Ÿ வேணà¯à®Ÿà®¾à®®à¯"), + ("I Agree", "à®à®±à¯à®•ிறேனà¯"), + ("Decline", "மறà¯"), + ("Timeout in minutes", "நிமிடஙà¯à®•ளில௠நேரமà¯à®Ÿà®¿à®µà¯"), + ("auto_disconnect_option_tip", "தானியஙà¯à®•௠தà¯à®£à¯à®Ÿà®¿à®ªà¯à®ªà¯ விரà¯à®ªà¯à®ªà®®à¯"), + ("Connection failed due to inactivity", "செயலினà¯à®®à¯ˆà®¯à®¾à®²à¯ இணைபà¯à®ªà¯ தோலà¯à®µà®¿"), + ("Check for software update on startup", "தொடகà¯à®•தà¯à®¤à®¿à®²à¯ மெனà¯à®ªà¯Šà®°à¯à®³à¯ பà¯à®¤à¯à®ªà¯à®ªà®¿à®ªà¯à®ªà¯ சரிபாரà¯"), + ("upgrade_rustdesk_server_pro_to_{}_tip", "RustDesk Server Pro {} கà¯à®•௠மேமà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("pull_group_failed_tip", "கà¯à®´à¯ இழà¯à®•à¯à®• தோலà¯à®µà®¿"), + ("Filter by intersection", "கà¯à®±à¯à®•à¯à®•à¯à®µà¯†à®Ÿà¯à®Ÿà®¾à®²à¯ வடிகடà¯à®Ÿà¯"), + ("Remove wallpaper during incoming sessions", "உளà¯à®µà®°à¯à®®à¯ அமரà¯à®µà¯à®•ளில௠வாலà¯à®ªà¯‡à®ªà¯à®ªà®°à¯ நீகà¯à®•à¯"), + ("Test", "சோதனை"), + ("display_is_plugged_out_msg", "காடà¯à®šà®¿ அடாபà¯à®Ÿà®°à¯ தà¯à®£à¯à®Ÿà®¿à®•à¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("No displays", "காடà¯à®šà®¿à®•ள௠இலà¯à®²à¯ˆ"), + ("Open in new window", "பà¯à®¤à®¿à®¯ சாளரதà¯à®¤à®¿à®²à¯ திற"), + ("Show displays as individual windows", "காடà¯à®šà®¿à®•ளை தனி சாளரஙà¯à®•ளாக காடà¯à®Ÿà¯"), + ("Use all my displays for the remote session", "அனைதà¯à®¤à¯ காடà¯à®šà®¿à®•ளையà¯à®®à¯ தொலை அமரà¯à®µà¯à®•à¯à®•௠பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("selinux_tip", "SELinux இயகà¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯, RustDesk அனà¯à®®à®¤à®¿ வேணà¯à®Ÿà¯à®®à¯"), + ("Change view", "காடà¯à®šà®¿ மாறà¯à®±à¯"), + ("Big tiles", "பெரிய ஓடà¯à®•ளà¯"), + ("Small tiles", "சிறிய ஓடà¯à®•ளà¯"), + ("List", "படà¯à®Ÿà®¿à®¯à®²à¯"), + ("Virtual display", "மெயà¯à®¨à®¿à®•ர௠காடà¯à®šà®¿"), + ("Plug out all", "அனைதà¯à®¤à¯ˆà®¯à¯à®®à¯ தà¯à®£à¯à®Ÿà®¿"), + ("True color (4:4:4)", "உணà¯à®®à¯ˆ நிறம௠(4:4:4)"), + ("Enable blocking user input", "பயனர௠உளà¯à®³à¯€à®Ÿà¯ தடà¯à®ªà¯à®ªà¯ இயகà¯à®•à¯"), + ("id_input_tip", "à®à®Ÿà®¿ உளà¯à®³à¯€à®Ÿà¯ எழà¯à®¤à¯à®¤à¯à®•ள௠எணà¯à®•ள௠மடà¯à®Ÿà¯à®®à¯"), + ("privacy_mode_impl_mag_tip", "Windows Magnifier API"), + ("privacy_mode_impl_virtual_display_tip", "Virtual Display Driver"), + ("Enter privacy mode", "தனியà¯à®°à®¿à®®à¯ˆ à®®à¯à®±à¯ˆà®¯à®¿à®²à¯ நà¯à®´à¯ˆ"), + ("Exit privacy mode", "தனியà¯à®°à®¿à®®à¯ˆ à®®à¯à®±à¯ˆà®¯à®¿à®²à®¿à®°à¯à®¨à¯à®¤à¯ வெளியேறà¯"), + ("idd_not_support_under_win10_2004_tip", "Virtual Display Driver Windows 10 2004 கà¯à®•௠கீழ௠ஆதரவிலà¯à®²à¯ˆ"), + ("input_source_1_tip", "உளà¯à®³à¯€à®Ÿà¯ மூலம௠= விசைபà¯à®ªà®²à®•ை"), + ("input_source_2_tip", "உளà¯à®³à¯€à®Ÿà¯ மூலம௠= சà¯à®Ÿà¯à®Ÿà®¿"), + ("Swap control-command key", "control-command விசை மாறà¯à®±à¯"), + ("swap-left-right-mouse", "இடதà¯-வலத௠சà¯à®Ÿà¯à®Ÿà®¿ மாறà¯à®±à¯"), + ("2FA code", "2FA கà¯à®±à®¿à®¯à¯€à®Ÿà¯"), + ("More", "மேலà¯à®®à¯"), + ("enable-2fa-title", "இர௠காரணி à®…à®™à¯à®•ீகாரம௠இயகà¯à®•à¯"), + ("enable-2fa-desc", "RustDesk இர௠காரணி à®…à®™à¯à®•ீகாரம௠ஆதரிகà¯à®•ிறதà¯"), + ("wrong-2fa-code", "தவறான 2FA கà¯à®±à®¿à®¯à¯€à®Ÿà¯"), + ("enter-2fa-title", "2FA கà¯à®±à®¿à®¯à¯€à®Ÿà¯ உளà¯à®³à®¿à®Ÿà¯"), + ("Email verification code must be 6 characters.", "மினà¯à®©à®žà¯à®šà®²à¯ சரிபாரà¯à®ªà¯à®ªà¯ 6 எழà¯à®¤à¯à®¤à¯à®•ளà¯"), + ("2FA code must be 6 digits.", "2FA கà¯à®±à®¿à®¯à¯€à®Ÿà¯ 6 எணà¯à®•ளà¯"), + ("Multiple Windows sessions found", "பல Windows அமரà¯à®µà¯à®•ள௠கணà¯à®Ÿà®±à®¿à®¯à®ªà¯à®ªà®Ÿà¯à®Ÿà®©"), + ("Please select the session you want to connect to", "இணைகà¯à®• விரà¯à®®à¯à®ªà¯à®®à¯ அமரà¯à®µà¯ தேரà¯à®µà¯"), + ("powered_by_me", "எனà¯à®©à®¾à®²à¯ இயகà¯à®•பà¯à®ªà®Ÿà¯à®•ிறதà¯"), + ("outgoing_only_desk_tip", "வெளியேறà¯à®®à¯ அமரà¯à®µà¯à®•ள௠மடà¯à®Ÿà¯à®®à¯ ஆதரவà¯"), + ("preset_password_warning", "à®®à¯à®©à¯à®©à®®à¯ˆà®µà¯ கடவà¯à®šà¯à®šà¯Šà®²à¯ எசà¯à®šà®°à®¿à®•à¯à®•ை"), + ("Security Alert", "பாதà¯à®•ாபà¯à®ªà¯ எசà¯à®šà®°à®¿à®•à¯à®•ை"), + ("My address book", "எனத௠மà¯à®•வரி பà¯à®¤à¯à®¤à®•à®®à¯"), + ("Personal", "தனிபà¯à®ªà®Ÿà¯à®Ÿ"), + ("Owner", "உரிமையாளரà¯"), + ("Set shared password", "பகிரபà¯à®ªà®Ÿà¯à®Ÿ கடவà¯à®šà¯à®šà¯Šà®²à¯ அமை"), + ("Exist in", "இல௠உளà¯à®³à®¤à¯"), + ("Read-only", "படிகà¯à®• மடà¯à®Ÿà¯à®®à¯"), + ("Read/Write", "படி/எழà¯à®¤à¯"), + ("Full Control", "à®®à¯à®´à¯ கடà¯à®Ÿà¯à®ªà¯à®ªà®¾à®Ÿà¯"), + ("share_warning_tip", "பியர௠பகிரà¯à®µà¯ அனà¯à®®à®¤à®¿ தேவை"), + ("Everyone", "அனைவரà¯à®®à¯"), + ("ab_web_console_tip", "வெப௠கனà¯à®šà¯‹à®²à®¿à®²à¯ à®®à¯à®•வரி பà¯à®¤à¯à®¤à®•ம௠நிரà¯à®µà®•ி"), + ("allow-only-conn-window-open-tip", "இணைபà¯à®ªà¯_சாளரமà¯_திறகà¯à®•_மடà¯à®Ÿà¯à®®à¯_அனà¯à®®à®¤à®¿_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("no_need_privacy_mode_no_physical_displays_tip", "தனியà¯à®°à®¿à®®à¯ˆ_à®®à¯à®±à¯ˆ_தேவையிலà¯à®²à¯ˆ_பரà¯à®ªà¯à®ªà¯Šà®°à¯à®³à¯_காடà¯à®šà®¿à®•ளà¯_இலà¯à®²à¯ˆ_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Follow remote cursor", "தொலை கரà¯à®šà®°à¯ பினà¯à®ªà®±à¯à®±à¯"), + ("Follow remote window focus", "தொலை சாளர கவனம௠பினà¯à®ªà®±à¯à®±à¯"), + ("default_proxy_tip", "இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ_பà¯à®°à®¾à®•à¯à®¸à®¿_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("no_audio_input_device_tip", "ஒலி_உளà¯à®³à¯€à®Ÿà¯à®Ÿà¯_சாதனமà¯_இலà¯à®²à¯ˆ_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Incoming", "உளà¯à®µà®°à¯à®®à¯"), + ("Outgoing", "வெளியேறà¯à®®à¯"), + ("Clear Wayland screen selection", "Wayland திரை தேரà¯à®µà¯ அழி"), + ("clear_Wayland_screen_selection_tip", "wayland_திரை_தேரà¯à®µà¯_அழி_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("confirm_clear_Wayland_screen_selection_tip", "wayland_திரை_தேரà¯à®µà¯_அழிகà¯à®•_உறà¯à®¤à®¿à®ªà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("android_new_voice_call_tip", "android_பà¯à®¤à®¿à®¯_கà¯à®°à®²à¯_அழைபà¯à®ªà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("texture_render_tip", "டெகà¯à®¸à¯à®šà¯à®šà®°à¯_ரெணà¯à®Ÿà®°à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Use texture rendering", "texture ரெணà¯à®Ÿà®°à®¿à®™à¯ பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("Floating window", "மிதகà¯à®•à¯à®®à¯ சாளரமà¯"), + ("floating_window_tip", "மிதகà¯à®•à¯à®®à¯_சாளரமà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Keep screen on", "திரை இயகà¯à®•தà¯à®¤à®¿à®²à¯ வை"), + ("Never", "à®’à®°à¯à®ªà¯‹à®¤à¯à®®à¯ இலà¯à®²à¯ˆ"), + ("During controlled", "கடà¯à®Ÿà¯à®ªà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯à®®à¯à®ªà¯‹à®¤à¯"), + ("During service is on", "சேவை இயகà¯à®•தà¯à®¤à®¿à®²à¯ இரà¯à®•à¯à®•à¯à®®à¯à®ªà¯‹à®¤à¯"), + ("Capture screen using DirectX", "DirectX பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à®¿ திரை பிடிபà¯à®ªà¯"), + ("Back", "பினà¯"), + ("Apps", "ஆபà¯à®¸à¯"), + ("Volume up", "ஒலி அதிகரி"), + ("Volume down", "ஒலி கà¯à®±à¯ˆ"), + ("Power", "மின௠படà¯à®Ÿà®©à¯"), + ("Telegram bot", "Telegram போடà¯"), + ("enable-bot-tip", "போடà¯_இயகà¯à®•_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("enable-bot-desc", "RustDesk Telegram போட௠ஆதரிகà¯à®•ிறதà¯"), + ("cancel-2fa-confirm-tip", "2fa_ரதà¯à®¤à¯_உறà¯à®¤à®¿_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("cancel-bot-confirm-tip", "போடà¯_ரதà¯à®¤à¯_உறà¯à®¤à®¿_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("About RustDesk", "RustDesk பறà¯à®±à®¿"), + ("Send clipboard keystrokes", "கிளிபà¯à®ªà¯‹à®°à¯à®Ÿà¯ விசைதà¯à®¤à®³ உளà¯à®³à¯€à®Ÿà¯ அனà¯à®ªà¯à®ªà¯"), + ("network_error_tip", "நெடà¯à®µà¯Šà®°à¯à®•à¯_பிழை_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Unlock with PIN", "PIN உடன௠திற"), + ("Requires at least {} characters", "கà¯à®±à¯ˆà®¨à¯à®¤à®¤à¯ {} எழà¯à®¤à¯à®¤à¯à®•ள௠தேவை"), + ("Wrong PIN", "தவறான PIN"), + ("Set PIN", "PIN அமை"), + ("Enable trusted devices", "நமà¯à®ªà®•மான சாதனஙà¯à®•ள௠இயகà¯à®•à¯"), + ("Manage trusted devices", "நமà¯à®ªà®•மான சாதனஙà¯à®•ள௠நிரà¯à®µà®•ி"), + ("Platform", "இயஙà¯à®•à¯à®¤à®³à®®à¯"), + ("Days remaining", "மீதமà¯à®³à¯à®³ நாடà¯à®•ளà¯"), + ("enable-trusted-devices-tip", "நமà¯à®ªà®•மான_சாதனஙà¯à®•ளà¯_இயகà¯à®•_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Parent directory", "மேல௠கோபà¯à®ªà®•à®®à¯"), + ("Resume", "தொடரà¯"), + ("Invalid file name", "தவறான கோபà¯à®ªà¯ பெயரà¯"), + ("one-way-file-transfer-tip", "à®’à®°à¯à®µà®´à®¿_கோபà¯à®ªà¯_பரிமாறà¯à®±_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Authentication Required", "à®…à®™à¯à®•ீகாரம௠தேவை"), + ("Authenticate", "à®…à®™à¯à®•ீகரி"), + ("web_id_input_tip", "வலை_à®à®Ÿà®¿_உளà¯à®³à¯€à®Ÿà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Download", "பதிவிறகà¯à®•à¯"), + ("Upload folder", "கோபà¯à®ªà®•ம௠à®à®±à¯à®±à¯"), + ("Upload files", "கோபà¯à®ªà¯à®•ள௠à®à®±à¯à®±à¯"), + ("Clipboard is synchronized", "கிளிபà¯à®ªà¯‹à®°à¯à®Ÿà¯ ஒதà¯à®¤à®¿à®šà¯ˆà®•à¯à®•பà¯à®ªà®Ÿà¯à®Ÿà®¤à¯"), + ("Update client clipboard", "கிளையன௠கிளிபà¯à®ªà¯‹à®°à¯à®Ÿà¯ பà¯à®¤à¯à®ªà¯à®ªà®¿"), + ("Untagged", "கà¯à®±à®¿à®šà¯à®šà¯Šà®²à¯ இலà¯à®²à®¾à®¤"), + ("new-version-of-{}-tip", "{}_பà¯à®¤à®¿à®¯_பதிபà¯à®ªà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Accessible devices", "அணà¯à®•கà¯à®•ூடிய சாதனஙà¯à®•ளà¯"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "ரிமோடà¯_rustdesk_கிளையனà¯à®Ÿà¯ˆ_{}_மேமà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("d3d_render_tip", "d3d_ரெணà¯à®Ÿà®°à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Use D3D rendering", "D3D ரெணà¯à®Ÿà®°à®¿à®™à¯ பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("Printer", "அசà¯à®šà¯à®ªà¯à®ªà¯Šà®±à®¿"), + ("printer-os-requirement-tip", "பிரிணà¯à®Ÿà®°à¯_os_தேவை_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("printer-requires-installed-{}-client-tip", "பிரிணà¯à®Ÿà®°à¯_தேவை_நிறà¯à®µà®ªà¯à®ªà®Ÿà¯à®Ÿ_{}_கிளையணà¯à®Ÿà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("printer-{}-not-installed-tip", "பிரிணà¯à®Ÿà®°à¯_{}_நிறà¯à®µà®ªà¯à®ªà®Ÿà®µà®¿à®²à¯à®²à¯ˆ_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("printer-{}-ready-tip", "பிரிணà¯à®Ÿà®°à¯_{}_தயாரà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Install {} Printer", "{} அசà¯à®šà¯à®ªà¯à®ªà¯Šà®±à®¿ நிறà¯à®µà¯"), + ("Outgoing Print Jobs", "வெளியேறà¯à®®à¯ அசà¯à®šà¯ வேலைகளà¯"), + ("Incoming Print Jobs", "உளà¯à®µà®°à¯à®®à¯ அசà¯à®šà¯ வேலைகளà¯"), + ("Incoming Print Job", "உளà¯à®µà®°à¯à®®à¯ அசà¯à®šà¯ வேலை"), + ("use-the-default-printer-tip", "இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ_அசà¯à®šà¯à®ªà¯à®ªà¯Šà®±à®¿à®¯à¯ˆ_பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("use-the-selected-printer-tip", "தேரà¯à®¨à¯à®¤à¯†à®Ÿà¯à®•à¯à®•பà¯à®ªà®Ÿà¯à®Ÿ_அசà¯à®šà¯à®ªà¯à®ªà¯Šà®±à®¿à®¯à¯ˆ_பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("auto-print-tip", "தானியஙà¯à®•à¯_அசà¯à®šà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("print-incoming-job-confirm-tip", "உளà¯à®µà®°à¯à®®à¯_அசà¯à®šà¯_வேலையை_உறà¯à®¤à®¿à®ªà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("remote-printing-disallowed-tile-tip", "ரிமோடà¯_அசà¯à®šà®¿à®Ÿà¯à®¤à®²à¯_அனà¯à®®à®¤à®¿à®•à¯à®•பà¯à®ªà®Ÿà®¾à®¤_டைலà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("remote-printing-disallowed-text-tip", "ரிமோடà¯_அசà¯à®šà®¿à®Ÿà¯à®¤à®²à¯_அனà¯à®®à®¤à®¿à®•à¯à®•பà¯à®ªà®Ÿà®¾à®¤_உரை_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("save-settings-tip", "அமைபà¯à®ªà¯à®•ளை_சேமி_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("dont-show-again-tip", "மீணà¯à®Ÿà¯à®®à¯_காடà¯à®Ÿà®¾à®¤à¯‡_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Take screenshot", "திரைபà¯à®ªà®¿à®Ÿà®¿à®ªà¯à®ªà¯ எடà¯"), + ("Taking screenshot", "திரைபà¯à®ªà®¿à®Ÿà®¿à®ªà¯à®ªà¯ எடà¯à®¤à¯à®¤à¯à®•à¯à®•ொணà¯à®Ÿà®¿à®°à¯à®•à¯à®•ிறதà¯"), + ("screenshot-merged-screen-not-supported-tip", "ஸà¯à®•ிரீனà¯à®·à®¾à®Ÿà¯_இணைகà¯à®•பà¯à®ªà®Ÿà¯à®Ÿ_திரை_ஆதரவறà¯à®±_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("screenshot-action-tip", "ஸà¯à®•ிரீனà¯à®·à®¾à®Ÿà¯_செயலà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Save as", "இபà¯à®ªà®Ÿà®¿ சேமி"), + ("Copy to clipboard", "கிளிபà¯à®ªà¯‹à®°à¯à®Ÿà®¿à®²à¯ நகலà¯"), + ("Enable remote printer", "தொலை அசà¯à®šà¯à®ªà¯à®ªà¯Šà®±à®¿ இயகà¯à®•à¯"), + ("Downloading {}", "{} பதிவிறகà¯à®•à¯à®•ிறதà¯"), + ("{} Update", "{} பà¯à®¤à¯à®ªà¯à®ªà®¿à®ªà¯à®ªà¯"), + ("{}-to-update-tip", "{}_பà¯à®¤à¯à®ªà¯à®ªà®¿à®•à¯à®•_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("download-new-version-failed-tip", "பà¯à®¤à®¿à®¯_பதிபà¯à®ªà¯_பதிவிறகà¯à®•à®®à¯_தோலà¯à®µà®¿_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Auto update", "தானியஙà¯à®•௠பà¯à®¤à¯à®ªà¯à®ªà®¿à®ªà¯à®ªà¯"), + ("update-failed-check-msi-tip", "பà¯à®¤à¯à®ªà¯à®ªà®¿à®ªà¯à®ªà¯_தோலà¯à®µà®¿_எமà¯à®Žà®¸à¯à®_சரிபாரà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("websocket_tip", "வெபà¯à®šà®¾à®•à¯à®•ெடà¯_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Use WebSocket", "WebSocket பயனà¯à®ªà®Ÿà¯à®¤à¯à®¤à¯"), + ("Trackpad speed", "டிராகà¯à®ªà¯‡à®Ÿà¯ வேகமà¯"), + ("Default trackpad speed", "இயலà¯à®ªà¯à®¨à®¿à®²à¯ˆ டிராகà¯à®ªà¯‡à®Ÿà¯ வேகமà¯"), + ("Numeric one-time password", "எண௠ஒரà¯à®®à¯à®±à¯ˆ கடவà¯à®šà¯à®šà¯Šà®²à¯"), + ("Enable IPv6 P2P connection", "IPv6 P2P இணைபà¯à®ªà¯ இயகà¯à®•à¯"), + ("Enable UDP hole punching", "UDP hole punching இயகà¯à®•à¯"), + ("View camera", "கேமரா பாரà¯"), + ("Enable camera", "கேமரா இயகà¯à®•à¯"), + ("No cameras", "கேமராகà¯à®•ள௠இலà¯à®²à¯ˆ"), + ("view_camera_unsupported_tip", "கேமரா_காடà¯à®šà®¿_ஆதரவறà¯à®±_கà¯à®±à®¿à®ªà¯à®ªà¯"), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), + ].iter().cloned().collect(); +} diff --git a/src/lang/template.rs b/src/lang/template.rs index 9f1293ce198..d1f7788354f 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", ""), ("install_tip", ""), ("Click to upgrade", ""), - ("Click to download", ""), - ("Click to update", ""), ("Configure", ""), ("config_acc", ""), ("config_screen", ""), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", ""), ("Note", ""), ("Connection", ""), - ("Share Screen", ""), + ("Share screen", ""), ("Chat", ""), ("Total", ""), ("items", ""), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", ""), ("Input Control", ""), ("Audio Capture", ""), - ("File Connection", ""), - ("Screen Connection", ""), ("Do you accept?", ""), ("Open System Setting", ""), ("How to get Android input permission?", ""), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", ""), ("empty_lan_tip", ""), ("empty_address_book_tip", ""), - ("eg: admin", ""), ("Empty Username", ""), ("Empty Password", ""), ("Me", ""), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", ""), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/th.rs b/src/lang/th.rs index 83fac2ab8b4..671491695bb 100644 --- a/src/lang/th.rs +++ b/src/lang/th.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "ความยาวตั้งà¹à¸•่ %min% ถึง %max%"), ("starts with a letter", "เริ่มต้นด้วยตัวอัà¸à¸©à¸£"), ("allowed characters", "ตัวอัà¸à¸‚ระที่อนุà¸à¸²à¸•"), - ("id_change_tip", "อนุà¸à¸²à¸•เฉพาะตัวอัà¸à¸©à¸£ a-z A-Z 0-9 à¹à¸¥à¸° _ (ขีดล่าง) เท่านั้น โดยตัวอัà¸à¸©à¸£à¸‚ึ้นต้นจะต้องเป็น a-z หรือไม่à¸à¹‡ A-Z à¹à¸¥à¸°à¸¡à¸µà¸„วามยาวระหว่าง 6 ถึง 16 ตัวอัà¸à¸©à¸£"), + ("id_change_tip", "อนุà¸à¸²à¸•เฉพาะตัวอัà¸à¸©à¸£ a-z A-Z 0-9, - (dash) à¹à¸¥à¸° _ (ขีดล่าง) เท่านั้น โดยตัวอัà¸à¸©à¸£à¸‚ึ้นต้นจะต้องเป็น a-z หรือไม่à¸à¹‡ A-Z à¹à¸¥à¸°à¸¡à¸µà¸„วามยาวระหว่าง 6 ถึง 16 ตัวอัà¸à¸©à¸£"), ("Website", "เว็บไซต์"), ("About", "เà¸à¸µà¹ˆà¸¢à¸§à¸à¸±à¸š"), ("Slogan_tip", "ทำด้วยใจ ในโลà¸à¸—ี่วุ่นวาย!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "รหัสผ่านระบบปà¸à¸´à¸šà¸±à¸•ิà¸à¸²à¸£"), ("install_tip", "เนื่องด้วยข้อจำà¸à¸±à¸”ของà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™ UAC ทำให้ RustDesk ไม่สามารถทำงานได้ปà¸à¸•ิในà¸à¸±à¹ˆà¸‡à¸›à¸¥à¸²à¸¢à¸—างในบางครั้ง เพื่อหลีà¸à¹€à¸¥à¸µà¹ˆà¸¢à¸‡à¸‚้อจำà¸à¸±à¸”ของ UAC à¸à¸£à¸¸à¸“าà¸à¸”ปุ่มด้านล่างเพื่อติดตั้ง RustDesk ไปยังระบบของคุณ"), ("Click to upgrade", "คลิà¸à¹€à¸žà¸·à¹ˆà¸­à¸­à¸±à¸›à¹€à¸à¸£à¸”"), - ("Click to download", "คลิà¸à¹€à¸žà¸·à¹ˆà¸­à¸”าวน์โหลด"), - ("Click to update", "คลิà¸à¹€à¸žà¸·à¹ˆà¸­à¸­à¸±à¸›à¹€à¸”ต"), ("Configure", "ปรับà¹à¸•่งค่า"), ("config_acc", "เพื่อที่จะควบคุมเดสà¸à¹Œà¸—็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุà¸à¸²à¸•สิทธิ์ \"à¸à¸²à¸£à¹€à¸‚้าถึง\" ให้à¹à¸à¹ˆ RustDesk"), ("config_screen", "เพื่อที่จะควบคุมเดสà¸à¹Œà¸—็อปปลายทางของคุณ คุณจำเป็นจะต้องอนุà¸à¸²à¸•สิทธิ์ \"à¸à¸²à¸£à¸šà¸±à¸™à¸—ึà¸à¸ à¸²à¸žà¸«à¸™à¹‰à¸²à¸ˆà¸­\" ให้à¹à¸à¹ˆ RustDesk"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "ไม่มีสิทธิ์ในà¸à¸²à¸£à¸–่ายโอนไฟล์"), ("Note", "บันทึà¸à¸‚้อความ"), ("Connection", "à¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸­à¸¡à¸•่อ"), - ("Share Screen", "à¹à¸Šà¸£à¹Œà¸«à¸™à¹‰à¸²à¸ˆà¸­"), + ("Share screen", "à¹à¸Šà¸£à¹Œà¸«à¸™à¹‰à¸²à¸ˆà¸­"), ("Chat", "à¹à¸Šà¸—"), ("Total", "รวม"), ("items", "รายà¸à¸²à¸£"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "บันทึà¸à¸«à¸™à¹‰à¸²à¸ˆà¸­"), ("Input Control", "ควบคุมอินพุท"), ("Audio Capture", "บันทึà¸à¹€à¸ªà¸µà¸¢à¸‡"), - ("File Connection", "à¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸­à¸¡à¸•่อไฟล์"), - ("Screen Connection", "à¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸­à¸¡à¸•่อหน้าจอ"), ("Do you accept?", "ยอมรับหรือไม่?"), ("Open System Setting", "เปิดà¸à¸²à¸£à¸•ั้งค่าระบบ"), ("How to get Android input permission?", "เปิดสิทธิ์à¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™à¸­à¸´à¸™à¸žà¸¸à¸—ของà¹à¸­à¸™à¸”รอยด์ได้อย่างไร?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "ยังไม่มีà¸à¸²à¸£à¹€à¸Šà¸·à¹ˆà¸­à¸¡à¸•่อรายà¸à¸²à¸£à¹‚ปรดเหรอ? มาเริ่มต้นหาใครซัà¸à¸„นเพื่อเชื่อมต่อด้วย à¹à¸¥à¸°à¹€à¸žà¸´à¹ˆà¸¡à¹€à¸‚้าไปยังรายà¸à¸²à¸£à¹‚ปรดของคุณà¸à¸±à¸™"), ("empty_lan_tip", "ไม่นะ ดูเหมือนว่าเราจะยังไม่พบใครตรงนี้"), ("empty_address_book_tip", "ดูเหมือนว่าคุณยังไม่มีใครถูà¸à¸šà¸±à¸™à¸—ึà¸à¹ƒà¸™à¸ªà¸¡à¸¸à¸”รายชื่อของคุณ"), - ("eg: admin", "เช่น ผู้ดูà¹à¸¥à¸£à¸°à¸šà¸š"), ("Empty Username", "ชื่อผู้ใช้งานว่างเปล่า"), ("Empty Password", "รหัสผ่านว่างเปล่า"), ("Me", "ฉัน"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "à¸à¸£à¸¸à¸“าอัปเดต RustDesk ไคลเอนต์ไปยังเวอร์ชัน {} หรือใหม่à¸à¸§à¹ˆà¸²à¸—ี่à¸à¸±à¹ˆà¸‡à¸›à¸¥à¸²à¸¢à¸—าง!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "ดูà¸à¸¥à¹‰à¸­à¸‡"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 630add8bb56..28b649daab8 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", ""), ("starts with a letter", ""), ("allowed characters", ""), - ("id_change_tip", "Yalnızca a-z, A-Z, 0-9 ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), + ("id_change_tip", "Yalnızca a-z, A-Z, 0-9, - (dash) ve _ (alt çizgi) karakterlerini kullanabilirsiniz. İlk karakter a-z veya A-Z olmalıdır. Uzunluk 6 ile 16 karakter arasında olmalıdır."), ("Website", "Website"), ("About", "Hakkında"), ("Slogan_tip", ""), @@ -135,7 +135,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Refresh", "Yenile"), ("ID does not exist", "ID bulunamadı"), ("Failed to connect to rendezvous server", "ID oluÅŸturma sunucusuna baÄŸlanılamadı"), - ("Please try later", "DaÄŸa sonra tekrar deneyiniz"), + ("Please try later", "Daha sonra tekrar deneyiniz"), ("Remote desktop is offline", "Uzak masaüstü kapalı"), ("Key mismatch", "Anahtar uyumlu deÄŸil"), ("Timeout", "Zaman aşımı"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "İşletim Sistemi Åžifresi"), ("install_tip", "Kullanıcı Hesabı Denetimi nedeniyle, RustDesk bir uzak masaüstü olarak düzgün çalışmayabilir. Bu sorunu önlemek için, RustDesk'i sistem seviyesinde kurmak için aÅŸağıdaki butona tıklayın."), ("Click to upgrade", "Yükseltmek için tıklayınız"), - ("Click to download", "İndirmek için tıklayınız"), - ("Click to update", "Güncellemek için tıklayınız"), ("Configure", "Ayarla"), ("config_acc", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"EriÅŸilebilirlik\""), ("config_screen", "Masaüstünüzü dışarıdan kontrol etmek için RustDesk'e \"Ekran Kaydı\" iznini vermeniz gerekir."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Dosya aktarımı izni yok"), ("Note", "Not"), ("Connection", "BaÄŸlantı"), - ("Share Screen", "Ekranı PaylaÅŸ"), + ("Share screen", "Ekranı PaylaÅŸ"), ("Chat", "MesajlaÅŸ"), ("Total", "Toplam"), ("items", "öğeler"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ekran görüntüsü"), ("Input Control", "GiriÅŸ Kontrolü"), ("Audio Capture", "Ses Yakalama"), - ("File Connection", "Dosya BaÄŸlantısı"), - ("Screen Connection", "Ekran BaÄŸlantısı"), ("Do you accept?", "Kabul ediyor musun?"), ("Open System Setting", "Sistem Ayarını Aç"), ("How to get Android input permission?", "Android giriÅŸ izni nasıl alınır?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Henüz favori cihazınız yok mu?\nBaÄŸlanacak ve favorilere eklemek için birini bulalım!"), ("empty_lan_tip", "Hayır, henüz hiçbir cihaz bulamadık gibi görünüyor."), ("empty_address_book_tip", "Üzgünüm, ÅŸu anda adres defterinizde kayıtlı cihaz yok gibi görünüyor."), - ("eg: admin", "örn: admin"), ("Empty Username", "BoÅŸ Kullanıcı Adı"), ("Empty Password", "BoÅŸ Parola"), ("Me", "Ben"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Kamerayı görüntüle"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index 89854769ea4..1c9365c6b83 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "長度在 %min% 與 %max% 之間"), ("starts with a letter", "以字æ¯é–‹é ­"), ("allowed characters", "å…許的字元"), - ("id_change_tip", "僅能使用以下字元:a-zã€A-Zã€0-9ã€_ (底線)。第一個字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), + ("id_change_tip", "僅能使用以下字元:a-zã€A-Zã€0-9〠- (dash)ã€_ (底線)。第一個字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"), ("Website", "網站"), ("About", "關於"), ("Slogan_tip", "在這個混沌的世界中用心製作ï¼"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "作業系統密碼"), ("install_tip", "UAC 會導致 RustDesk 在æŸäº›æƒ…æ³ä¸‹ç„¡æ³•正常作為é ç«¯ç«¯é»žé‹ä½œã€‚è‹¥è¦é¿é–‹ UAC,請點é¸ä¸‹æ–¹æŒ‰éˆ•å°‡ RustDesk 安è£åˆ°ç³»çµ±ä¸­ã€‚"), ("Click to upgrade", "點é¸ä»¥å‡ç´š"), - ("Click to download", "點é¸ä»¥ä¸‹è¼‰"), - ("Click to update", "點é¸ä»¥æ›´æ–°"), ("Configure", "設定"), ("config_acc", "為了é ç«¯æŽ§åˆ¶æ‚¨çš„æ¡Œé¢ï¼Œæ‚¨éœ€è¦æŽˆäºˆ RustDeskã€Œç„¡éšœç¤™åŠŸèƒ½ã€æ¬Šé™ã€‚"), ("config_screen", "為了é ç«¯å­˜å–您的桌é¢ï¼Œæ‚¨éœ€è¦æŽˆäºˆ RustDeskã€Œèž¢å¹•éŒ„è£½ã€æ¬Šé™ã€‚"), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "沒有檔案傳輸權é™"), ("Note", "備註"), ("Connection", "連線"), - ("Share Screen", "螢幕分享"), + ("Share screen", "螢幕分享"), ("Chat", "èŠå¤©"), ("Total", "總計"), ("items", "個項目"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "ç•«é¢éŒ„製"), ("Input Control", "輸入控制"), ("Audio Capture", "音訊錄製"), - ("File Connection", "檔案連線"), - ("Screen Connection", "ç•«é¢é€£ç·š"), ("Do you accept?", "æ˜¯å¦æŽ¥å—?"), ("Open System Setting", "開啟系統設定"), ("How to get Android input permission?", "如何å–å¾— Android 的輸入權é™ï¼Ÿ"), @@ -364,7 +360,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Recording", "錄製"), ("Directory", "路徑"), ("Automatically record incoming sessions", "自動錄製連入的工作階段"), - ("Automatically record outgoing sessions", ""), + ("Automatically record outgoing sessions", "自動錄製連出的工作階段"), ("Change", "變更"), ("Start session recording", "開始錄影"), ("Stop session recording", "åœæ­¢éŒ„å½±"), @@ -417,7 +413,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("request_elevation_tip", "如果é ç«¯ä½¿ç”¨è€…å¯ä»¥æ“作電腦,您å¯ä»¥è«‹æ±‚æå‡æ¬Šé™ã€‚"), ("Wait", "等待"), ("Elevation Error", "æ¬Šé™æå‡å¤±æ•—"), - ("Ask the remote user for authentication", "請求é ç«¯ä½¿ç”¨è€…進行驗證驗證"), + ("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 視窗點é¸ã€Œæ˜¯ã€ã€‚"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "空空如也"), ("empty_lan_tip", "å–”ä¸ï¼Œçœ‹ä¾†æˆ‘å€‘ç›®å‰æ‰¾ä¸åˆ°ä»»ä½•夥伴。"), ("empty_address_book_tip", "è€å¤©ï¼Œçœ‹ä¾†æ‚¨çš„通訊錄中沒有任何夥伴。"), - ("eg: admin", "例如:admin"), ("Empty Username", "空使用者帳號"), ("Empty Password", "空密碼"), ("Me", "我"), @@ -498,7 +493,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Options", "é¸é …"), ("resolution_original_tip", "原始解æžåº¦"), ("resolution_fit_local_tip", "èª¿æ•´æˆæœ¬æ©Ÿè§£æžåº¦"), - ("resolution_custom_tip", "自動解æžåº¦"), + ("resolution_custom_tip", "自訂解æžåº¦"), ("Collapse toolbar", "收回工具列"), ("Accept and Elevate", "接å—並æå‡æ¬Šé™"), ("accept_and_elevate_btn_tooltip", "接å—連線並æå‡ UAC 權é™ã€‚"), @@ -526,7 +521,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Select", "鏿“‡"), ("Toggle Tags", "åˆ‡æ›æ¨™ç±¤"), ("pull_ab_failed_tip", "通訊錄更新失敗"), - ("push_ab_failed_tip", "æˆåŠŸåŒæ­¥é€šè¨ŠéŒ„至伺æœå™¨"), + ("push_ab_failed_tip", "åŒæ­¥é€šè¨ŠéŒ„至伺æœå™¨å¤±æ•—"), ("synced_peer_readded_tip", "最近工作階段中存在的è£ç½®å°‡æœƒè¢«é‡æ–°åŒæ­¥åˆ°é€šè¨ŠéŒ„。"), ("Change Color", "更改é¡è‰²"), ("Primary Color", "基本色"), @@ -646,9 +641,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Resume", "繼續"), ("Invalid file name", "無效檔å"), ("one-way-file-transfer-tip", "è¢«æŽ§ç«¯å•Ÿç”¨äº†å–®å‘æª”案傳輸"), - ("Authentication Required", "需è¦é©—證驗證"), + ("Authentication Required", "需è¦é©—è­‰"), ("Authenticate", "èªè­‰"), - ("web_id_input_tip", "您å¯ä»¥è¼¸å…¥åŒä¸€å€‹ä¼ºæœå™¨å…§çš„ ID,Web ç”¨æˆ¶ç«¯ä¸æ”¯æ´ç›´æŽ¥ IP å­˜å–。\n如果您è¦å­˜å–使–¼å…¶ä»–伺æœå™¨ä¸Šçš„è£ç½®ï¼Œè«‹åœ¨ ID 之後新增伺æœå™¨ä½å€ï¼ˆ@<伺æœå™¨ä½å€>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\nè¦å­˜å–公共伺æœå™¨ä¸Šçš„è£ç½®ï¼Œè«‹è¼¸å…¥ã€Œ@publicã€ï¼Œä¸éœ€è¼¸å…¥é‡‘鑰。"), + ("web_id_input_tip", "您å¯ä»¥è¼¸å…¥åŒä¸€å€‹ä¼ºæœå™¨å…§çš„ ID,Web å®¢æˆ¶ç«¯ä¸æ”¯æ´ç›´æŽ¥ IP å­˜å–。\n如果您è¦å­˜å–使–¼å…¶ä»–伺æœå™¨ä¸Šçš„è£ç½®ï¼Œè«‹åœ¨ ID 之後新增伺æœå™¨ä½å€ï¼ˆ@<伺æœå™¨ä½å€>?key=<金鑰>)\n例如:9123456234@192.168.16.1:21117?key=5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=\nè¦å­˜å–公共伺æœå™¨ä¸Šçš„è£ç½®ï¼Œè«‹è¼¸å…¥ã€Œ@publicã€ï¼Œä¸éœ€è¼¸å…¥é‡‘鑰。"), ("Download", "下載"), ("Upload folder", "上傳資料夾"), ("Upload files", "上傳檔案"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "更新客戶端的剪貼簿"), ("Untagged", "無標籤"), ("new-version-of-{}-tip", "有新版本的 {} å¯ç”¨"), + ("Accessible devices", "å¯å­˜å–çš„è£ç½®"), + ("upgrade_remote_rustdesk_client_to_{}_tip", "è«‹å°‡é ç«¯ RustDesk 客戶端å‡ç´šåˆ° {} 或更新版本ï¼"), + ("d3d_render_tip", "當啟用 D3D 渲染時,æŸäº›æ©Ÿå™¨å¯èƒ½æœƒç„¡æ³•顯示é ç«¯ç•«é¢ã€‚"), + ("Use D3D rendering", "使用 D3D 渲染"), + ("Printer", "å°è¡¨æ©Ÿ"), + ("printer-os-requirement-tip", "å°è¡¨æ©Ÿçš„å‚³å‡ºåŠŸèƒ½éœ€è¦ Windows 10 或更高版本。"), + ("printer-requires-installed-{}-client-tip", "為了使用é ç«¯åˆ—å°åŠŸèƒ½ï¼Œè«‹å®‰è£ {} 到此設備。"), + ("printer-{}-not-installed-tip", "{} å°è¡¨æ©Ÿæœªå®‰è£ã€‚"), + ("printer-{}-ready-tip", "{} å°è¡¨æ©Ÿå·²å®‰è£ï¼Œæ‚¨å¯ä»¥ä½¿ç”¨åˆ—å°åŠŸèƒ½äº†ã€‚"), + ("Install {} Printer", "å®‰è£ {} å°è¡¨æ©Ÿ"), + ("Outgoing Print Jobs", "傳出的列å°ä»»å‹™"), + ("Incoming Print Jobs", "傳入的列å°ä»»å‹™"), + ("Incoming Print Job", "傳入的列å°ä»»å‹™"), + ("use-the-default-printer-tip", "使用é è¨­çš„å°è¡¨æ©Ÿ"), + ("use-the-selected-printer-tip", "使用é¸å–çš„å°è¡¨æ©Ÿ"), + ("auto-print-tip", "使用é¸å–çš„å°è¡¨æ©Ÿè‡ªå‹•執行"), + ("print-incoming-job-confirm-tip", "您收到一個é ç«¯åˆ—å°ä»»å‹™ï¼Œæ‚¨æƒ³åœ¨æœ¬åœ°åŸ·è¡Œå®ƒå—Žï¼Ÿ"), + ("remote-printing-disallowed-tile-tip", "ä¸å…許é ç«¯åˆ—å°"), + ("remote-printing-disallowed-text-tip", "被控端的權é™è¨­ç½®æ‹’絕了é ç«¯åˆ—å°ã€‚"), + ("save-settings-tip", "儲存設定"), + ("dont-show-again-tip", "ä¸å†é¡¯ç¤ºæ­¤è¨Šæ¯"), + ("Take screenshot", "æ“·å–ç•«é¢"), + ("Taking screenshot", "正在擷å–ç•«é¢"), + ("screenshot-merged-screen-not-supported-tip", "ç›®å‰ä¸æ”¯æ´åˆä½µå¤šå€‹èž¢å¹•的截圖。請切æ›è‡³å–®ä¸€èž¢å¹•後å†è©¦ã€‚"), + ("screenshot-action-tip", "è«‹é¸æ“‡è¦å¦‚何處ç†é€™å¼µæˆªåœ–。"), + ("Save as", "å¦å­˜ç‚º"), + ("Copy to clipboard", "複製到剪貼簿"), + ("Enable remote printer", "啟用é ç«¯åˆ—å°"), + ("Downloading {}", "正在下載 {} ä¸¦å®‰è£æ–°ç‰ˆæœ¬ã€‚"), + ("{} Update", "{} æ›´æ–°"), + ("{}-to-update-tip", "å³å°‡é—œé–‰ {} ä¸¦å®‰è£æ–°ç‰ˆæœ¬ã€‚"), + ("download-new-version-failed-tip", "下載失敗,您å¯ä»¥é‡è©¦æˆ–點擊\"下載\"按鈕以從發布網å€ä¸‹è¼‰ï¼Œä¸¦æ‰‹å‹•å‡ç´šã€‚"), + ("Auto update", "自動更新"), + ("update-failed-check-msi-tip", "å®‰è£æ–¹å¼åµæ¸¬å¤±æ•—,請點擊\"下載\"按鈕以從發布網å€ä¸‹è¼‰ï¼Œä¸¦æ‰‹å‹•å‡ç´šã€‚"), + ("websocket_tip", "使用 WebSocket æ™‚ï¼Œåªæ”¯æ´ä½¿ç”¨ä¸­ç¹¼é€£æŽ¥ã€‚"), + ("Use WebSocket", "使用 WebSocket"), + ("Trackpad speed", "觸控æ¿é€Ÿåº¦"), + ("Default trackpad speed", "é è¨­è§¸æŽ§æ¿é€Ÿåº¦"), + ("Numeric one-time password", "數字一次性密碼"), + ("Enable IPv6 P2P connection", "啟用 IPv6 P2P 連線"), + ("Enable UDP hole punching", "啟用 UDP 打洞"), + ("View camera", "檢視相機"), + ("Enable camera", "å…許查看é¡é ­"), + ("No cameras", "沒有é¡é ­"), + ("view_camera_unsupported_tip", "您的é ç«¯è¨­å‚™ä¸æ”¯æ´æŸ¥çœ‹é¡é ­"), + ("Terminal", "終端機"), + ("Enable terminal", "啟用終端機"), + ("New tab", "新分é "), + ("Keep terminal sessions on disconnect", "åœ¨æ–·ç·šæ™‚ä¿æŒçµ‚端機的工作階段"), + ("Terminal (Run as administrator)", "終端機(使用系統管ç†å“¡åŸ·è¡Œï¼‰"), + ("terminal-admin-login-tip", "請輸入被控端系統管ç†å“¡çš„使用者å稱與密碼"), + ("Failed to get user token.", "å–得使用者權æ–失敗"), + ("Incorrect username or password.", "使用者åç¨±æˆ–å¯†ç¢¼ä¸æ­£ç¢º"), + ("The user is not an administrator.", "ä½¿ç”¨è€…ä¸¦ä¸æ˜¯ç³»çµ±ç®¡ç†å“¡"), + ("Failed to check if the user is an administrator.", "æª¢æŸ¥ä½¿ç”¨è€…æ˜¯å¦æ˜¯ç³»çµ±ç®¡ç†å“¡æ™‚失敗了"), + ("Supported only in the installed version.", "åƒ…æ”¯æ´æ–¼å·²å®‰è£çš„版本"), + ("elevation_username_tip", "輸入使用者å稱或網域\\使用者å稱"), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/uk.rs b/src/lang/uk.rs index 9f0dfdefbbc..8ea7805aa3e 100644 --- a/src/lang/uk.rs +++ b/src/lang/uk.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "від %min% до %max% Ñимволів"), ("starts with a letter", "починаєтьÑÑ Ð· літери"), ("allowed characters", "дозволені Ñимволи"), - ("id_change_tip", "ДопуÑкаютьÑÑ Ð»Ð¸ÑˆÐµ Ñимволи a-z, A-Z, 0-9 Ñ– _ (підкреÑленнÑ). Першою повинна бути літера a-z, A-Z. Довжина — від 6 до 16 Ñимволів"), + ("id_change_tip", "ДопуÑкаютьÑÑ Ð»Ð¸ÑˆÐµ Ñимволи a-z, A-Z, 0-9, - (dash) Ñ– _ (підкреÑленнÑ). Першою повинна бути літера a-z, A-Z. Довжина — від 6 до 16 Ñимволів"), ("Website", "Веб-Ñайт"), ("About", "Про заÑтоÑунок"), ("Slogan_tip", "Створено з душею в цьому хаотичному Ñвіті!"), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Пароль ОС"), ("install_tip", "Через UAC, в деÑких випадках RustDesk може працювати некоректно на віддаленому вузлі. Щоб уникнути UAC, натиÑніть кнопку нижче Ð´Ð»Ñ Ð²ÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð½Ñ RustDesk в ÑиÑтемі"), ("Click to upgrade", "ÐатиÑніть, щоб перевірити наÑвніÑть оновлень"), - ("Click to download", "ÐатиÑніть, щоб отримати"), - ("Click to update", "ÐатиÑніть, щоб оновити"), ("Configure", "Ðалаштувати"), ("config_acc", "Ð”Ð»Ñ Ð²Ñ–Ð´Ð´Ð°Ð»ÐµÐ½Ð¾Ð³Ð¾ ÐºÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð°ÑˆÐ¾ÑŽ Ñтільницею, вам необхідно надати RustDesk дозволи \"Спеціальні можливоÑті\""), ("config_screen", "Ð”Ð»Ñ Ð²Ñ–Ð´Ð´Ð°Ð»ÐµÐ½Ð¾Ð³Ð¾ доÑтупу до вашої Ñтільниці, вам необхідно надати RustDesk дозволи на \"Ð—Ð°Ð¿Ð¸Ñ ÐµÐºÑ€Ð°Ð½Ð°\""), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Ðемає дозволу на передачу файлів"), ("Note", "Примітка"), ("Connection", "ПідключеннÑ"), - ("Share Screen", "ПоділитиÑÑ ÐµÐºÑ€Ð°Ð½Ð¾Ð¼"), + ("Share screen", "ПоділитиÑÑ ÐµÐºÑ€Ð°Ð½Ð¾Ð¼"), ("Chat", "Чат"), ("Total", "Ð’Ñього"), ("items", "елементи"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ð—Ð°Ñ…Ð¾Ð¿Ð»ÐµÐ½Ð½Ñ ÐµÐºÑ€Ð°Ð½Ð°"), ("Input Control", "ÐšÐµÑ€ÑƒÐ²Ð°Ð½Ð½Ñ Ð²Ð²ÐµÐ´ÐµÐ½Ð½Ñм"), ("Audio Capture", "Ð—Ð°Ñ…Ð¾Ð¿Ð»ÐµÐ½Ð½Ñ Ð°ÑƒÐ´Ñ–Ð¾"), - ("File Connection", "Файлове підключеннÑ"), - ("Screen Connection", "ÐŸÑ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ ÐµÐºÑ€Ð°Ð½Ð°"), ("Do you accept?", "Ви згодні?"), ("Open System Setting", "Відкрити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ ÑиÑтеми"), ("How to get Android input permission?", "Як отримати дозвіл на Ð²Ð²ÐµÐ´ÐµÐ½Ð½Ñ Ð² Android?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "ДоÑÑ– немає улюблених вузлів?\nДавайте організуємо нове Ð¿Ñ–Ð´ÐºÐ»ÑŽÑ‡ÐµÐ½Ð½Ñ Ñ‚Ð° додамо його до улюблених!"), ("empty_lan_tip", "О ні, Ñхоже ми ще не виÑвили жодного віддаленого приÑтрою."), ("empty_address_book_tip", "Ой лишенько, Ñхоже у вашій адреÑній книзі немає жодного віддаленого приÑтрою."), - ("eg: admin", "напр., admin"), ("Empty Username", "Ðезаповнене імʼÑ"), ("Empty Password", "Ðезаповнений пароль"), ("Me", "Я"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", "Оновити буфер обміну клієнта"), ("Untagged", "Без міток"), ("new-version-of-{}-tip", "ДоÑтупна нова верÑÑ–Ñ {}"), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", "Будь лаÑка, оновіть RustDesk клієнт на віддаленому приÑтрої до верÑÑ–Ñ— {} чи новіше!"), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "ПереглÑд камери"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vi.rs similarity index 93% rename from src/lang/vn.rs rename to src/lang/vi.rs index 1ee2cd6d026..e6faa4f31ba 100644 --- a/src/lang/vn.rs +++ b/src/lang/vi.rs @@ -41,7 +41,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("length %min% to %max%", "độ dài %min% đến %max%"), ("starts with a letter", "bắt đầu bằng má»™t chữ"), ("allowed characters", "các ký tá»± cho phép"), - ("id_change_tip", "Các kí tá»± Ä‘uợc phép là: từ a-z, A-Z, 0-9 và _ (dấu gạch dưới). Kí tá»± đầu tiên phải bắt đầu từ a-z, A-Z. Äá»™ dài kí tá»± từ 6 đến 16"), + ("id_change_tip", "Các kí tá»± Ä‘uợc phép là: từ a-z, A-Z, 0-9, - (dash) và _ (dấu gạch dưới). Kí tá»± đầu tiên phải bắt đầu từ a-z, A-Z. Äá»™ dài kí tá»± từ 6 đến 16"), ("Website", "Trang web"), ("About", "Giá»›i thiệu"), ("Slogan_tip", ""), @@ -147,8 +147,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("OS Password", "Mật khẩu hệ Ä‘iá»u hành"), ("install_tip", "Do UAC, RustDesk sẽ không thể hoạt động đúng cách là bên từ xa trong vài trưá»ng hợp. Äể tránh UAC, hãy nhấn cái nút dưới đây để cài RustDesk vào hệ thống."), ("Click to upgrade", "Nhấn để nâng cấp"), - ("Click to download", "Nhấn để tải xuống"), - ("Click to update", "Nhấn để cập nhật"), ("Configure", "Cài đặt"), ("config_acc", "Äể có thể Ä‘iá»u khiển máy tính từ xa, bạn cần phải cung cấp quyá»n \"Trợ năng\" cho RustDesk"), ("config_screen", "Äể có thể truy cập máy tính từ xa, bạn cần phải cung cấp quyá»n \"Ghi Màn Hình\" cho RustDesk."), @@ -267,7 +265,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("No permission of file transfer", "Không có quyá»n truyá»n tệp tin"), ("Note", "Ghi nhá»›"), ("Connection", "Kết nối"), - ("Share Screen", "Chia sẻ màn hình"), + ("Share screen", "Chia sẻ màn hình"), ("Chat", "Chat"), ("Total", "Tổng"), ("items", "items"), @@ -275,8 +273,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Screen Capture", "Ghi màn hình"), ("Input Control", "Äiá»u khiển đầu vào"), ("Audio Capture", "Ghi âm thanh"), - ("File Connection", "Kết nối tệp tin"), - ("Screen Connection", "Kết nối màn hình"), ("Do you accept?", "Bạn có chấp nhận không?"), ("Open System Setting", "Mở cài đặt hệ thống"), ("How to get Android input permission?", "Cách để có quyá»n nhập trên Android?"), @@ -463,7 +459,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("empty_favorite_tip", "Chưa có ngưá»i dùng yêu thích nào cả?\nHãy tìm ai đó để kết nối cùng và thêm há» vào danh sách yêu thích!"), ("empty_lan_tip", "Ôi không, có vẻ như chúng ta chưa phát hiện ra bất cứ ngưá»i dùng nào cả."), ("empty_address_book_tip", "Ôi bạn Æ¡i, có vẻ như bạn chưa thêm ai vào quyển địa chỉ cả."), - ("eg: admin", "ví dụ: admin"), ("Empty Username", "Tên tài khoản trống"), ("Empty Password", "Mật khẩu trống"), ("Me", "Tôi"), @@ -656,5 +651,63 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Update client clipboard", ""), ("Untagged", ""), ("new-version-of-{}-tip", ""), + ("Accessible devices", ""), + ("upgrade_remote_rustdesk_client_to_{}_tip", ""), + ("d3d_render_tip", ""), + ("Use D3D rendering", ""), + ("Printer", ""), + ("printer-os-requirement-tip", ""), + ("printer-requires-installed-{}-client-tip", ""), + ("printer-{}-not-installed-tip", ""), + ("printer-{}-ready-tip", ""), + ("Install {} Printer", ""), + ("Outgoing Print Jobs", ""), + ("Incoming Print Jobs", ""), + ("Incoming Print Job", ""), + ("use-the-default-printer-tip", ""), + ("use-the-selected-printer-tip", ""), + ("auto-print-tip", ""), + ("print-incoming-job-confirm-tip", ""), + ("remote-printing-disallowed-tile-tip", ""), + ("remote-printing-disallowed-text-tip", ""), + ("save-settings-tip", ""), + ("dont-show-again-tip", ""), + ("Take screenshot", ""), + ("Taking screenshot", ""), + ("screenshot-merged-screen-not-supported-tip", ""), + ("screenshot-action-tip", ""), + ("Save as", ""), + ("Copy to clipboard", ""), + ("Enable remote printer", ""), + ("Downloading {}", ""), + ("{} Update", ""), + ("{}-to-update-tip", ""), + ("download-new-version-failed-tip", ""), + ("Auto update", ""), + ("update-failed-check-msi-tip", ""), + ("websocket_tip", ""), + ("Use WebSocket", ""), + ("Trackpad speed", ""), + ("Default trackpad speed", ""), + ("Numeric one-time password", ""), + ("Enable IPv6 P2P connection", ""), + ("Enable UDP hole punching", ""), + ("View camera", "Xem camera"), + ("Enable camera", ""), + ("No cameras", ""), + ("view_camera_unsupported_tip", ""), + ("Terminal", ""), + ("Enable terminal", ""), + ("New tab", ""), + ("Keep terminal sessions on disconnect", ""), + ("Terminal (Run as administrator)", ""), + ("terminal-admin-login-tip", ""), + ("Failed to get user token.", ""), + ("Incorrect username or password.", ""), + ("The user is not an administrator.", ""), + ("Failed to check if the user is an administrator.", ""), + ("Supported only in the installed version.", ""), + ("elevation_username_tip", ""), + ("Preparing for installation ...", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 693f36dbcd6..433bb5f36de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,14 +39,14 @@ use common::*; mod auth_2fa; #[cfg(feature = "cli")] pub mod cli; +#[cfg(not(target_os = "ios"))] +mod clipboard; #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] pub mod core_main; mod custom_server; mod lang; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod port_forward; -#[cfg(not(target_os = "ios"))] -mod clipboard; #[cfg(all(feature = "flutter", feature = "plugin_framework"))] #[cfg(not(any(target_os = "android", target_os = "ios")))] @@ -55,6 +55,9 @@ pub mod plugin; #[cfg(not(any(target_os = "android", target_os = "ios")))] mod tray; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +mod updater; + mod ui_cm_interface; mod ui_interface; mod ui_session_interface; @@ -68,3 +71,5 @@ pub mod privacy_mode; #[cfg(windows)] pub mod virtual_display_manager; + +mod kcp_stream; diff --git a/src/main.rs b/src/main.rs index f295363aa90..274d7735cce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ use librustdesk::*; #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] fn main() { if !common::global_init() { + eprintln!("Global initialization failed."); return; } common::test_rendezvous_server(); diff --git a/src/platform/gtk_sudo.rs b/src/platform/gtk_sudo.rs index 9aeea1e2b01..37b541cbe5a 100644 --- a/src/platform/gtk_sudo.rs +++ b/src/platform/gtk_sudo.rs @@ -5,7 +5,9 @@ use crate::lang::translate; use gtk::{glib, prelude::*}; use hbb_common::{ anyhow::{bail, Error}, - log, ResultType, + log, + platform::linux::CMD_SH, + ResultType, }; use nix::{ libc::{fcntl, kill}, @@ -463,12 +465,12 @@ fn ui_parent( fn child(su_user: Option, args: Vec) -> ResultType<()> { // https://doc.rust-lang.org/std/env/consts/constant.OS.html let os = std::env::consts::OS; - let bsd = os == "freebsd" || os == "dragonfly" || os == "netbsd" || os == "openbad"; + let bsd = os == "freebsd" || os == "dragonfly" || os == "netbsd" || os == "openbsd"; let mut params = vec!["sudo".to_string()]; if su_user.is_some() { params.push("-S".to_string()); } - params.push("/bin/sh".to_string()); + params.push(CMD_SH.to_string()); params.push("-c".to_string()); let command = args diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 08cf0fb9a90..ec6210e2971 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -24,6 +24,7 @@ use std::{ }, time::{Duration, Instant}, }; +use terminfo::{capability as cap, Database}; use users::{get_user_by_name, os::unix::UserExt}; use wallpaper; @@ -32,8 +33,20 @@ type Xdo = *const c_void; pub const PA_SAMPLE_RATE: u32 = 48000; static mut UNMODIFIED: bool = true; +const INVALID_TERM_VALUES: [&str; 3] = ["", "unknown", "dumb"]; +const SHELL_PROCESSES: [&str; 4] = ["bash", "zsh", "fish", "sh"]; + lazy_static::lazy_static! { pub static ref IS_X11: bool = hbb_common::platform::linux::is_x11_or_headless(); + static ref DATABASE_XTERM_256COLOR: Option = { + match Database::from_name("xterm-256color") { + Ok(database) => Some(database), + Err(err) => { + log::error!("Failed to initialize xterm-256color database: {}", err); + None + } + } + }; } thread_local! { @@ -255,6 +268,70 @@ fn start_uinput_service() { }); } +/// Suggests the best terminal type based on the environment. +/// +/// The function prioritizes terminal types in the following order: +/// 1. `screen-256color`: Preferred when running inside `tmux` or `screen` sessions, +/// as these multiplexers often support advanced terminal features. +/// 2. `xterm-256color`: Selected if the terminal supports 256 colors, which is +/// suitable for modern terminal applications. +/// 3. `xterm`: Used as a fallback for basic terminal compatibility. +/// +/// Terminals like `linux` and `vt100` are excluded because they lack support for +/// modern features required by many applications. +fn suggest_best_term() -> String { + if is_running_in_tmux() || is_running_in_screen() { + return "screen-256color".to_string(); + } + if term_supports_256_colors("xterm-256color") { + return "xterm-256color".to_string(); + } + "xterm".to_string() +} + +fn is_running_in_tmux() -> bool { + std::env::var("TMUX").is_ok() +} + +fn is_running_in_screen() -> bool { + std::env::var("STY").is_ok() +} + +fn supports_256_colors(db: &Database) -> bool { + db.get::().map_or(false, |n| n.0 >= 256) +} + +fn term_supports_256_colors(term: &str) -> bool { + match term { + "xterm-256color" => DATABASE_XTERM_256COLOR + .as_ref() + .map_or(false, |db| supports_256_colors(db)), + _ => Database::from_name(term).map_or(false, |db| supports_256_colors(&db)), + } +} + +fn get_cur_term(uid: &str) -> Option { + if uid.is_empty() { + return None; + } + + if let Ok(term) = std::env::var("TERM") { + if !INVALID_TERM_VALUES.contains(&term.as_str()) { + return Some(term); + } + } + + for proc in SHELL_PROCESSES { + // Construct a regex pattern to match either the process name followed by '$' or 'bin/' followed by the process name. + let term = get_env("TERM", uid, &format!("{}$|bin/{}", proc, proc)); + if !INVALID_TERM_VALUES.contains(&term.as_str()) { + return Some(term); + } + } + + None +} + #[inline] fn try_start_server_(desktop: Option<&Desktop>) -> ResultType> { match desktop { @@ -272,6 +349,10 @@ fn try_start_server_(desktop: Option<&Desktop>) -> ResultType> { if !desktop.home.is_empty() { envs.push(("HOME", desktop.home.clone())); } + envs.push(( + "TERM", + get_cur_term(&desktop.uid).unwrap_or_else(|| suggest_best_term()), + )); run_as_user( vec!["--server"], Some((desktop.uid.clone(), desktop.username.clone())), @@ -320,7 +401,7 @@ fn set_x11_env(desktop: &Desktop) { #[inline] fn stop_rustdesk_servers() { let _ = run_cmds(&format!( - r##"ps -ef | grep -E '{} +--server' | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + r##"ps -ef | grep -E '{} +--server' | awk '{{print $2}}' | xargs -r kill -9"##, crate::get_app_name().to_lowercase(), )); } @@ -328,11 +409,11 @@ fn stop_rustdesk_servers() { #[inline] fn stop_subprocess() { let _ = run_cmds(&format!( - r##"ps -ef | grep '/etc/{}/xorg.conf' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + r##"ps -ef | grep '/etc/{}/xorg.conf' | grep -v grep | awk '{{print $2}}' | xargs -r kill -9"##, crate::get_app_name().to_lowercase(), )); let _ = run_cmds(&format!( - r##"ps -ef | grep -E '{} +--cm-no-ui' | grep -v grep | awk '{{printf("kill -9 %d\n", $2)}}' | bash"##, + r##"ps -ef | grep -E '{} +--cm-no-ui' | grep -v grep | awk '{{print $2}}' | xargs -r kill -9"##, crate::get_app_name().to_lowercase(), )); } @@ -369,6 +450,12 @@ fn should_start_server( && ((*cm0 && last_restart.elapsed().as_secs() > 60) || last_restart.elapsed().as_secs() > 3600) { + let terminal_session_count = crate::ipc::get_terminal_session_count().unwrap_or(0); + if terminal_session_count > 0 { + // There are terminal sessions, so we don't restart the server. + // We also need to keep `cm0` unchanged, so that we can reach this branch the next time. + return false; + } // restart server if new connections all closed, or every one hour, // as a workaround to resolve "SpotUdp" (dns resolve) // and x server get displays failure issue @@ -517,7 +604,8 @@ pub fn get_active_userid() -> String { } fn get_cm() -> bool { - if let Ok(output) = Command::new("ps").args(vec!["aux"]).output() { + // We use `CMD_PS` instead of `ps` to suppress some audit messages on some systems. + if let Ok(output) = Command::new(CMD_PS.as_str()).args(vec!["aux"]).output() { for line in String::from_utf8_lossy(&output.stdout).lines() { if line.contains(&format!( "{} --cm", @@ -619,8 +707,36 @@ pub fn is_prelogin() -> bool { if is_flatpak() { return false; } - let n = get_active_userid().len(); - n < 4 && n > 1 + let name = get_active_username(); + if let Ok(res) = run_cmds(&format!("getent passwd {}", name)) { + return res.contains("/bin/false") || res.contains("/usr/sbin/nologin"); + } + false +} + +// Check "Lock". +// "Switch user" can't be checked, because `get_values_of_seat0(&[0])` does not return the session. +// The logged in session is "online" not "active". +// And the "Switch user" screen is usually Wayland login session, which we do not support. +pub fn is_locked() -> bool { + if is_prelogin() { + return false; + } + + let values = get_values_of_seat0(&[0]); + // Though the values can't be empty, we still add check here for safety. + // Because we cannot guarantee whether the internal implementation will change in the future. + // https://github.com/rustdesk/hbb_common/blob/ebb4d4a48cf7ed6ca62e93f8ed124065c6408536/src/platform/linux.rs#L119 + if values.is_empty() { + log::debug!("Failed to check is locked, values vector is empty."); + return false; + } + let session = &values[0]; + if session.is_empty() { + log::debug!("Failed to check is locked, session is empty."); + return false; + } + is_session_locked(session) } pub fn is_root() -> bool { @@ -991,7 +1107,7 @@ mod desktop { pub sid: String, pub username: String, pub uid: String, - pub protocal: String, + pub protocol: String, pub display: String, pub xauth: String, pub home: String, @@ -1002,12 +1118,12 @@ mod desktop { impl Desktop { #[inline] pub fn is_wayland(&self) -> bool { - self.protocal == DISPLAY_SERVER_WAYLAND + self.protocol == DISPLAY_SERVER_WAYLAND } #[inline] pub fn is_login_wayland(&self) -> bool { - super::is_gdm_user(&self.username) && self.protocal == DISPLAY_SERVER_WAYLAND + super::is_gdm_user(&self.username) && self.protocol == DISPLAY_SERVER_WAYLAND } #[inline] @@ -1017,7 +1133,7 @@ mod desktop { fn get_display_xauth_xwayland(&mut self) { let tray = format!("{} +--tray", crate::get_app_name().to_lowercase()); - for _ in 0..5 { + for _ in 1..=10 { let display_proc = vec![ XWAYLAND, IBUS_DAEMON, @@ -1030,7 +1146,7 @@ mod desktop { self.xauth = get_env("XAUTHORITY", &self.uid, proc); self.wl_display = get_env("WAYLAND_DISPLAY", &self.uid, proc); if !self.display.is_empty() && !self.xauth.is_empty() { - break; + return; } } sleep_millis(300); @@ -1038,7 +1154,7 @@ mod desktop { } fn get_display_x11(&mut self) { - for _ in 0..10 { + for _ in 1..=10 { let display_proc = vec![ XWAYLAND, IBUS_DAEMON, @@ -1053,6 +1169,9 @@ mod desktop { break; } } + if !self.display.is_empty() { + break; + } sleep_millis(300); } @@ -1064,7 +1183,7 @@ mod desktop { } self.display = self .display - .replace(&whoami::hostname(), "") + .replace(&hbb_common::whoami::hostname(), "") .replace("localhost", ""); } @@ -1122,7 +1241,7 @@ mod desktop { fn get_xauth_x11(&mut self) { // try by direct access to window manager process by name let tray = format!("{} +--tray", crate::get_app_name().to_lowercase()); - for _ in 0..10 { + for _ in 1..=10 { let display_proc = vec![ XWAYLAND, IBUS_DAEMON, @@ -1138,6 +1257,9 @@ mod desktop { break; } } + if !self.xauth.is_empty() { + break; + } sleep_millis(300); } @@ -1246,7 +1368,7 @@ mod desktop { self.sid = seat0_values[0].clone(); self.uid = seat0_values[1].clone(); self.username = seat0_values[2].clone(); - self.protocal = get_display_server_of_session(&self.sid).into(); + self.protocol = get_display_server_of_session(&self.sid).into(); if self.is_login_wayland() { self.display = "".to_owned(); self.xauth = "".to_owned(); @@ -1327,7 +1449,8 @@ pub fn run_me_with(secs: u32) { .unwrap_or("".into()) .to_string_lossy() .to_string(); - std::process::Command::new("sh") + // We use `CMD_SH` instead of `sh` to suppress some audit messages on some systems. + std::process::Command::new(CMD_SH.as_str()) .arg("-c") .arg(&format!("sleep {secs}; {exe}")) .spawn() diff --git a/src/platform/macos.mm b/src/platform/macos.mm index 0f963b97f7b..92ee5170b98 100644 --- a/src/platform/macos.mm +++ b/src/platform/macos.mm @@ -153,8 +153,28 @@ size_t bitDepth(CGDisplayModeRef mode) { return depth; } +static bool isHiDPIMode(CGDisplayModeRef mode) { + // Check if the mode is HiDPI by comparing pixel width to width + // If pixel width is greater than width, it's a HiDPI mode + return CGDisplayModeGetPixelWidth(mode) > CGDisplayModeGetWidth(mode); +} + +CFArrayRef getAllModes(CGDirectDisplayID display) { + // Create options dictionary to include HiDPI modes + CFMutableDictionaryRef options = CFDictionaryCreateMutable( + kCFAllocatorDefault, + 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks); + // Include HiDPI modes + CFDictionarySetValue(options, kCGDisplayShowDuplicateLowResolutionModes, kCFBooleanTrue); + CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, options); + CFRelease(options); + return allModes; +} + extern "C" bool MacGetModeNum(CGDirectDisplayID display, uint32_t *numModes) { - CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + CFArrayRef allModes = getAllModes(display); if (allModes == NULL) { return false; } @@ -163,12 +183,12 @@ size_t bitDepth(CGDisplayModeRef mode) { return true; } -extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, uint32_t max, uint32_t *numModes) { +extern "C" bool MacGetModes(CGDirectDisplayID display, uint32_t *widths, uint32_t *heights, bool *hidpis, uint32_t max, uint32_t *numModes) { CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); if (currentMode == NULL) { return false; } - CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + CFArrayRef allModes = getAllModes(display); if (allModes == NULL) { CGDisplayModeRelease(currentMode); return false; @@ -181,6 +201,7 @@ size_t bitDepth(CGDisplayModeRef mode) { bitDepth(currentMode) == bitDepth(mode)) { widths[realNum] = (uint32_t)CGDisplayModeGetWidth(mode); heights[realNum] = (uint32_t)CGDisplayModeGetHeight(mode); + hidpis[realNum] = isHiDPIMode(mode); realNum++; } } @@ -201,7 +222,6 @@ size_t bitDepth(CGDisplayModeRef mode) { return true; } - static bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { CGError rc; CGDisplayConfigRef config; @@ -220,30 +240,55 @@ static bool setDisplayToMode(CGDirectDisplayID display, CGDisplayModeRef mode) { return true; } -extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height) +// Set the display to a specific mode based on width and height. +// Returns true if the display mode was successfully changed, false otherwise. +// If no such mode is available, it will not change the display mode. +// +// If `tryHiDPI` is true, it will try to set the display to a HiDPI mode if available. +// If no HiDPI mode is available, it will fall back to a non-HiDPI mode with the same resolution. +// If `tryHiDPI` is false, it sets the display to the first mode with the same resolution, no matter if it's HiDPI or not. +extern "C" bool MacSetMode(CGDirectDisplayID display, uint32_t width, uint32_t height, bool tryHiDPI) { bool ret = false; CGDisplayModeRef currentMode = CGDisplayCopyDisplayMode(display); if (currentMode == NULL) { return ret; } - CFArrayRef allModes = CGDisplayCopyAllDisplayModes(display, NULL); + CFArrayRef allModes = getAllModes(display); + if (allModes == NULL) { CGDisplayModeRelease(currentMode); return ret; } int numModes = CFArrayGetCount(allModes); + CGDisplayModeRef preferredHiDPIMode = NULL; + CGDisplayModeRef fallbackMode = NULL; for (int i = 0; i < numModes; i++) { CGDisplayModeRef mode = (CGDisplayModeRef)CFArrayGetValueAtIndex(allModes, i); if (width == CGDisplayModeGetWidth(mode) && height == CGDisplayModeGetHeight(mode) && CGDisplayModeGetRefreshRate(currentMode) == CGDisplayModeGetRefreshRate(mode) && bitDepth(currentMode) == bitDepth(mode)) { - ret = setDisplayToMode(display, mode); - break; + + if (isHiDPIMode(mode)) { + preferredHiDPIMode = mode; + break; + } else { + fallbackMode = mode; + if (!tryHiDPI) { + break; + } + } } } + + if (preferredHiDPIMode) { + ret = setDisplayToMode(display, preferredHiDPIMode); + } else if (fallbackMode) { + ret = setDisplayToMode(display, fallbackMode); + } + CGDisplayModeRelease(currentMode); CFRelease(allModes); return ret; -} \ No newline at end of file +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index b3c5546a6fc..f0ff5cb5850 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -27,12 +27,19 @@ use include_dir::{include_dir, Dir}; use objc::rc::autoreleasepool; use objc::{class, msg_send, sel, sel_impl}; use scrap::{libc::c_void, quartz::ffi::*}; -use std::path::PathBuf; +use std::{ + collections::HashMap, + os::unix::process::CommandExt, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; static PRIVILEGES_SCRIPTS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/platform/privileges_scripts"); static mut LATEST_SEED: i32 = 0; +const UPDATE_TEMP_DIR: &str = "/tmp/.rustdeskupdate"; + extern "C" { fn CGSCurrentCursorSeed() -> i32; fn CGEventCreate(r: *const c_void) -> *const c_void; @@ -48,12 +55,13 @@ extern "C" { display: u32, widths: *mut u32, heights: *mut u32, + hidpis: *mut BOOL, max: u32, numModes: *mut u32, ) -> BOOL; fn majorVersion() -> u32; fn MacGetMode(display: u32, width: *mut u32, height: *mut u32) -> BOOL; - fn MacSetMode(display: u32, width: u32, height: u32) -> BOOL; + fn MacSetMode(display: u32, width: u32, height: u32, tryHiDPI: bool) -> BOOL; } pub fn major_version() -> u32 { @@ -155,6 +163,9 @@ pub fn install_service() -> bool { is_installed_daemon(false) } +// Remember to check if `update_daemon_agent()` need to be changed if changing `is_installed_daemon()`. +// No need to merge the existing dup code, because the code in these two functions are too critical. +// New code should be written in a common function. pub fn is_installed_daemon(prompt: bool) -> bool { let daemon = format!("{}_service.plist", crate::get_full_name()); let agent = format!("{}_server.plist", crate::get_full_name()); @@ -218,6 +229,70 @@ pub fn is_installed_daemon(prompt: bool) -> bool { false } +fn update_daemon_agent(agent_plist_file: String, update_source_dir: String, sync: bool) { + let update_script_file = "update.scpt"; + let Some(update_script) = PRIVILEGES_SCRIPTS_DIR.get_file(update_script_file) else { + return; + }; + let Some(update_script_body) = update_script.contents_utf8().map(correct_app_name) else { + return; + }; + + let Some(daemon_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("daemon.plist") else { + return; + }; + let Some(daemon_plist_body) = daemon_plist.contents_utf8().map(correct_app_name) else { + return; + }; + let Some(agent_plist) = PRIVILEGES_SCRIPTS_DIR.get_file("agent.plist") else { + return; + }; + let Some(agent_plist_body) = agent_plist.contents_utf8().map(correct_app_name) else { + return; + }; + + let func = move || { + let mut binding = std::process::Command::new("osascript"); + let mut cmd = binding + .arg("-e") + .arg(update_script_body) + .arg(daemon_plist_body) + .arg(agent_plist_body) + .arg(&get_active_username()) + .arg(std::process::id().to_string()) + .arg(update_source_dir); + match cmd.status() { + Err(e) => { + log::error!("run osascript failed: {}", e); + } + _ => { + let installed = std::path::Path::new(&agent_plist_file).exists(); + log::info!("Agent file {} installed: {}", &agent_plist_file, installed); + if installed { + // Unload first, or load may not work if already loaded. + // We hope that the load operation can immediately trigger a start. + std::process::Command::new("launchctl") + .args(&["unload", "-w", &agent_plist_file]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .ok(); + let status = std::process::Command::new("launchctl") + .args(&["load", "-w", &agent_plist_file]) + .status(); + log::info!("launch server, status: {:?}", &status); + } + } + } + }; + if sync { + func(); + } else { + std::thread::spawn(func); + } +} + fn correct_app_name(s: &str) -> String { let s = s.replace("rustdesk", &crate::get_app_name().to_lowercase()); let s = s.replace("RustDesk", &crate::get_app_name()); @@ -491,6 +566,38 @@ pub fn is_prelogin() -> bool { get_active_userid() == "0" } +// https://stackoverflow.com/questions/11505255/osx-check-if-the-screen-is-locked +// No "CGSSessionScreenIsLocked" can be found when macOS is not locked. +// +// `ioreg -n Root -d1` returns `"CGSSessionScreenIsLocked"=Yes` +// `ioreg -n Root -d1 -a` returns +// ``` +// ... +// CGSSessionScreenIsLocked +// +// ... +// ``` +pub fn is_locked() -> bool { + match std::process::Command::new("ioreg") + .arg("-n") + .arg("Root") + .arg("-d1") + .output() + { + Ok(output) => { + let output_str = String::from_utf8_lossy(&output.stdout); + // Although `"CGSSessionScreenIsLocked"=Yes` was printed on my macOS, + // I also check `"CGSSessionScreenIsLocked"=true` for better compability. + output_str.contains("\"CGSSessionScreenIsLocked\"=Yes") + || output_str.contains("\"CGSSessionScreenIsLocked\"=true") + } + Err(e) => { + log::error!("Failed to query ioreg for the lock state: {}", e); + false + } + } +} + pub fn is_root() -> bool { crate::username() == "root" } @@ -515,53 +622,6 @@ pub fn lock_screen() { pub fn start_os_service() { log::info!("Username: {}", crate::username()); - let mut sys = System::new(); - let path = - std::fs::canonicalize(std::env::current_exe().unwrap_or_default()).unwrap_or_default(); - let mut server = get_server_start_time(&mut sys, &path); - if server.is_none() { - log::error!("Agent not started yet, please restart --server first to make delegate work",); - std::process::exit(-1); - } - let my_start_time = sys - .process((std::process::id() as usize).into()) - .map(|p| p.start_time()) - .unwrap_or_default() as i64; - log::info!("Startime: {my_start_time} vs {:?}", server); - - std::thread::spawn(move || loop { - std::thread::sleep(std::time::Duration::from_secs(1)); - if server.is_none() { - server = get_server_start_time(&mut sys, &path); - } - let Some((start_time, pid)) = server else { - log::error!( - "Agent not started yet, please restart --server first to make delegate work", - ); - std::process::exit(-1); - }; - if my_start_time <= start_time + 3 { - log::error!( - "Agent start later, {my_start_time} vs {start_time}, please start --server first to make delegate work, earlier more 3 seconds", - ); - std::process::exit(-1); - } - // only refresh this pid and check if valid, no need to refresh all processes since refreshing all is expensive, about 10ms on my machine - if !sys.refresh_process_specifics(pid, ProcessRefreshKind::new()) { - server = None; - continue; - } - if let Some(p) = sys.process(pid.into()) { - if let Some(p) = get_server_start_time_of(p, &path) { - server = Some((p, pid)); - } else { - server = None; - } - } else { - server = None; - } - }); - if let Err(err) = crate::ipc::start("_service") { log::error!("Failed to start ipc_service: {}", err); } @@ -649,6 +709,156 @@ pub fn quit_gui() { }; } +pub fn update_me() -> ResultType<()> { + let is_installed_daemon = is_installed_daemon(false); + let option_stop_service = "stop-service"; + let is_service_stopped = hbb_common::config::option2bool( + option_stop_service, + &crate::ui_interface::get_option(option_stop_service), + ); + + let cmd = std::env::current_exe()?; + // RustDesk.app/Contents/MacOS/RustDesk + let app_dir = cmd + .parent() + .and_then(|p| p.parent()) + .and_then(|p| p.parent()) + .map(|d| d.to_string_lossy().to_string()); + let Some(app_dir) = app_dir else { + bail!("Unknown app directory of current exe file: {:?}", cmd); + }; + + if is_installed_daemon && !is_service_stopped { + let agent = format!("{}_server.plist", crate::get_full_name()); + let agent_plist_file = format!("/Library/LaunchAgents/{}", agent); + std::process::Command::new("launchctl") + .args(&["unload", "-w", &agent_plist_file]) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .ok(); + update_daemon_agent(agent_plist_file, app_dir, true); + } else { + // `kill -9` may not work without "administrator privileges" + let update_body = format!( + r#" +do shell script " +pgrep -x 'RustDesk' | grep -v {} | xargs kill -9 && rm -rf /Applications/RustDesk.app && ditto '{}' /Applications/RustDesk.app && chown -R {}:staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app +" with prompt "RustDesk wants to update itself" with administrator privileges + "#, + std::process::id(), + app_dir, + get_active_username() + ); + match Command::new("osascript") + .arg("-e") + .arg(update_body) + .status() + { + Ok(status) if !status.success() => { + log::error!("osascript execution failed with status: {}", status); + } + Err(e) => { + log::error!("run osascript failed: {}", e); + } + _ => {} + } + } + std::process::Command::new("open") + .arg("-n") + .arg(&format!("/Applications/{}.app", crate::get_app_name())) + .spawn() + .ok(); + // leave open a little time + std::thread::sleep(std::time::Duration::from_millis(300)); + Ok(()) +} + +pub fn update_to(file: &str) -> ResultType<()> { + update_extracted(UPDATE_TEMP_DIR)?; + Ok(()) +} + +pub fn extract_update_dmg(file: &str) { + let mut evt: HashMap<&str, String> = + HashMap::from([("name", "extract-update-dmg".to_string())]); + match extract_dmg(file, UPDATE_TEMP_DIR) { + Ok(_) => { + log::info!("Extracted dmg file to {}", UPDATE_TEMP_DIR); + } + Err(e) => { + evt.insert("err", e.to_string()); + log::error!("Failed to extract dmg file {}: {}", file, e); + } + } + let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned()); + #[cfg(feature = "flutter")] + crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt); +} + +fn extract_dmg(dmg_path: &str, target_dir: &str) -> ResultType<()> { + let mount_point = "/Volumes/RustDeskUpdate"; + let target_path = Path::new(target_dir); + + if target_path.exists() { + std::fs::remove_dir_all(target_path)?; + } + std::fs::create_dir_all(target_path)?; + + Command::new("hdiutil") + .args(&["attach", "-nobrowse", "-mountpoint", mount_point, dmg_path]) + .status()?; + + struct DmgGuard(&'static str); + impl Drop for DmgGuard { + fn drop(&mut self) { + let _ = Command::new("hdiutil") + .args(&["detach", self.0, "-force"]) + .status(); + } + } + let _guard = DmgGuard(mount_point); + + let app_name = "RustDesk.app"; + let src_path = format!("{}/{}", mount_point, app_name); + let dest_path = format!("{}/{}", target_dir, app_name); + + let copy_status = Command::new("ditto") + .args(&[&src_path, &dest_path]) + .status()?; + + if !copy_status.success() { + bail!("Failed to copy application {:?}", copy_status); + } + + if !Path::new(&dest_path).exists() { + bail!( + "Copy operation failed - destination not found at {}", + dest_path + ); + } + + Ok(()) +} + +fn update_extracted(target_dir: &str) -> ResultType<()> { + let exe_path = format!("{}/RustDesk.app/Contents/MacOS/RustDesk", target_dir); + let _child = unsafe { + Command::new(&exe_path) + .arg("--update") + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .pre_exec(|| { + hbb_common::libc::setsid(); + Ok(()) + }) + .spawn()? + }; + Ok(()) +} + 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 _ @@ -661,7 +871,7 @@ pub fn hide_dock() { } #[inline] -fn get_server_start_time_of(p: &Process, path: &PathBuf) -> Option { +fn get_server_start_time_of(p: &Process, path: &Path) -> Option { let cmd = p.cmd(); if cmd.len() <= 1 { return None; @@ -679,7 +889,7 @@ fn get_server_start_time_of(p: &Process, path: &PathBuf) -> Option { } #[inline] -fn get_server_start_time(sys: &mut System, path: &PathBuf) -> Option<(i64, Pid)> { +fn get_server_start_time(sys: &mut System, path: &Path) -> Option<(i64, Pid)> { sys.refresh_processes_specifics(ProcessRefreshKind::new()); for (_, p) in sys.processes() { if let Some(t) = get_server_start_time_of(p, path) { @@ -697,33 +907,62 @@ pub fn handle_application_should_open_untitled_file() { } } +/// Get all resolutions of the display. The resolutions are: +/// 1. Sorted by width and height in descending order, with duplicates removed. +/// 2. Filtered out if the width is less than 800 (800x600) if there are too many (e.g., >15). +/// 3. Contain HiDPI resolutions and the real resolutions. +/// +/// We don't need to distinguish between HiDPI and real resolutions. +/// When the controlling side changes the resolution, it will call `change_resolution_directly()`. +/// `change_resolution_directly()` will try to use the HiDPI resolution first. +/// This is how teamviewer does it for now. +/// +/// If we need to distinguish HiDPI and real resolutions, we can add a flag to the `Resolution` struct. pub fn resolutions(name: &str) -> Vec { let mut v = vec![]; if let Ok(display) = name.parse::() { let mut num = 0; unsafe { if YES == MacGetModeNum(display, &mut num) { - let (mut widths, mut heights) = (vec![0; num as _], vec![0; num as _]); + let (mut widths, mut heights, mut _hidpis) = + (vec![0; num as _], vec![0; num as _], vec![NO; num as _]); let mut real_num = 0; if YES == MacGetModes( display, widths.as_mut_ptr(), heights.as_mut_ptr(), + _hidpis.as_mut_ptr(), num, &mut real_num, ) { if real_num <= num { - for i in 0..real_num { - let resolution = Resolution { + v = (0..real_num) + .map(|i| Resolution { width: widths[i as usize] as _, height: heights[i as usize] as _, ..Default::default() - }; - if !v.contains(&resolution) { - v.push(resolution); + }) + .collect::>(); + // Sort by (w, h), desc + v.sort_by(|a, b| { + if a.width == b.width { + b.height.cmp(&a.height) + } else { + b.width.cmp(&a.width) } + }); + // Remove duplicates + v.dedup_by(|a, b| a.width == b.width && a.height == b.height); + // Filter out the ones that are less than width 800 (800x600) if there are too many. + // We can also do this filtering on the client side, but it is better not to change the client side to reduce the impact. + if v.len() > 15 { + // Most width > 800, so it's ok to remove the small ones. + v.retain(|r| r.width >= 800); + } + if v.len() > 15 { + // Ignore if the length is still too long. } } } @@ -751,7 +990,7 @@ pub fn current_resolution(name: &str) -> ResultType { pub fn change_resolution_directly(name: &str, width: usize, height: usize) -> ResultType<()> { let display = name.parse::().map_err(|e| anyhow!(e))?; unsafe { - if NO == MacSetMode(display, width as _, height as _) { + if NO == MacSetMode(display, width as _, height as _, true) { bail!("MacSetMode failed"); } } diff --git a/src/platform/mod.rs b/src/platform/mod.rs index d0ddd09bf70..ea14cb997b6 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -27,7 +27,11 @@ pub mod linux_desktop_manager; pub mod gtk_sudo; #[cfg(not(any(target_os = "android", target_os = "ios")))] -use hbb_common::{message_proto::CursorData, ResultType}; +use hbb_common::{ + message_proto::CursorData, + sysinfo::{Pid, System}, + ResultType, +}; use std::sync::{Arc, Mutex}; #[cfg(not(any(target_os = "macos", target_os = "android", target_os = "ios")))] pub const SERVICE_INTERVAL: u64 = 300; @@ -137,6 +141,71 @@ pub fn is_prelogin() -> bool { false } +// Note: This method is inefficient on Windows. It will get all the processes. +// It should only be called when performance is not critical. +// If we wanted to get the command line ourselves, there would be a lot of new code. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn get_pids_of_process_with_args, S2: AsRef>( + name: S1, + args: &[S2], +) -> Vec { + // This function does not work when the process is 32-bit and the OS is 64-bit Windows, + // `process.cmd()` always returns [] in this case. + // So we use `windows::get_pids_with_args_by_wmic()` instead. + #[cfg(all(target_os = "windows", not(target_pointer_width = "64")))] + { + return windows::get_pids_with_args_by_wmic(name, args); + } + #[cfg(not(all(target_os = "windows", not(target_pointer_width = "64"))))] + { + let name = name.as_ref().to_lowercase(); + let system = System::new_all(); + system + .processes() + .iter() + .filter(|(_, process)| { + process.name().to_lowercase() == name + && process.cmd().len() == args.len() + 1 + && args.iter().enumerate().all(|(i, arg)| { + process.cmd()[i + 1].to_lowercase() == arg.as_ref().to_lowercase() + }) + }) + .map(|(&pid, _)| pid) + .collect() + } +} + +// Note: This method is inefficient on Windows. It will get all the processes. +// It should only be called when performance is not critical. +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_pids_of_process_with_first_arg, S2: AsRef>( + name: S1, + arg: S2, +) -> Vec { + // This function does not work when the process is 32-bit and the OS is 64-bit Windows, + // `process.cmd()` always returns [] in this case. + // So we use `windows::get_pids_with_first_arg_by_wmic()` instead. + #[cfg(all(target_os = "windows", not(target_pointer_width = "64")))] + { + return windows::get_pids_with_first_arg_by_wmic(name, arg); + } + #[cfg(not(all(target_os = "windows", not(target_pointer_width = "64"))))] + { + let name = name.as_ref().to_lowercase(); + let system = System::new_all(); + system + .processes() + .iter() + .filter(|(_, process)| { + process.name().to_lowercase() == name + && process.cmd().len() >= 2 + && process.cmd()[1].to_lowercase() == arg.as_ref().to_lowercase() + }) + .map(|(&pid, _)| pid) + .collect() + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/platform/privileges_scripts/daemon.plist b/src/platform/privileges_scripts/daemon.plist index 61efc25eca4..59f103a3138 100644 --- a/src/platform/privileges_scripts/daemon.plist +++ b/src/platform/privileges_scripts/daemon.plist @@ -12,7 +12,7 @@ /bin/sh -c - sleep 3; if pgrep -f '/Applications/RustDesk.app/Contents/MacOS/RustDesk --server' > /dev/null; then /Applications/RustDesk.app/Contents/MacOS/RustDesk --service; fi + /Applications/RustDesk.app/Contents/MacOS/service RunAtLoad diff --git a/src/platform/privileges_scripts/install.scpt b/src/platform/privileges_scripts/install.scpt index c38320db5b5..797d02c9e24 100644 --- a/src/platform/privileges_scripts/install.scpt +++ b/src/platform/privileges_scripts/install.scpt @@ -12,5 +12,5 @@ on run {daemon_file, agent_file, user} set sh to sh1 & sh2 & sh3 & sh4 & sh5 - do shell script sh with prompt "RustDesk want to install daemon and agent" with administrator privileges + do shell script sh with prompt "RustDesk wants to install daemon and agent" with administrator privileges end run diff --git a/src/platform/privileges_scripts/uninstall.scpt b/src/platform/privileges_scripts/uninstall.scpt index 3d91e61f93d..4a19fb3fd1e 100644 --- a/src/platform/privileges_scripts/uninstall.scpt +++ b/src/platform/privileges_scripts/uninstall.scpt @@ -3,4 +3,4 @@ set sh2 to "/bin/rm /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" set sh3 to "/bin/rm /Library/LaunchAgents/com.carriez.RustDesk_server.plist;" set sh to sh1 & sh2 & sh3 -do shell script sh with prompt "RustDesk want to unload daemon" with administrator privileges \ No newline at end of file +do shell script sh with prompt "RustDesk wants to unload daemon" with administrator privileges \ No newline at end of file diff --git a/src/platform/privileges_scripts/update.scpt b/src/platform/privileges_scripts/update.scpt new file mode 100644 index 00000000000..dffb70bd7d5 --- /dev/null +++ b/src/platform/privileges_scripts/update.scpt @@ -0,0 +1,18 @@ +on run {daemon_file, agent_file, user, cur_pid, source_dir} + + set unload_service to "launchctl unload -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist || true;" + + set kill_others to "pgrep -x 'RustDesk' | grep -v " & cur_pid & " | xargs kill -9;" + + set copy_files to "rm -rf /Applications/RustDesk.app && ditto " & source_dir & " /Applications/RustDesk.app && chown -R " & quoted form of user & ":staff /Applications/RustDesk.app && xattr -r -d com.apple.quarantine /Applications/RustDesk.app;" + + set sh1 to "echo " & quoted form of daemon_file & " > /Library/LaunchDaemons/com.carriez.RustDesk_service.plist && chown root:wheel /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" + + set sh2 to "echo " & quoted form of agent_file & " > /Library/LaunchAgents/com.carriez.RustDesk_server.plist && chown root:wheel /Library/LaunchAgents/com.carriez.RustDesk_server.plist;" + + set sh3 to "launchctl load -w /Library/LaunchDaemons/com.carriez.RustDesk_service.plist;" + + set sh to unload_service & kill_others & copy_files & sh1 & sh2 & sh3 + + do shell script sh with prompt "RustDesk wants to update itself" with administrator privileges +end run diff --git a/src/platform/windows.cc b/src/platform/windows.cc index 04095a2d63f..d83a1b0c480 100644 --- a/src/platform/windows.cc +++ b/src/platform/windows.cc @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -11,6 +13,7 @@ #include #include #include +#include extern "C" uint32_t get_session_user_info(PWSTR bufin, uint32_t nin, uint32_t id); @@ -227,7 +230,7 @@ extern "C" return IsWindows10OrGreater(); } - HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user, DWORD *pDwTokenPid) + HANDLE LaunchProcessWin(LPCWSTR cmd, DWORD dwSessionId, BOOL as_user, BOOL show, DWORD *pDwTokenPid) { HANDLE hProcess = NULL; HANDLE hToken = NULL; @@ -237,8 +240,13 @@ extern "C" ZeroMemory(&si, sizeof si); si.cb = sizeof si; si.dwFlags = STARTF_USESHOWWINDOW; + if (show) + { + si.lpDesktop = (LPWSTR)L"winsta0\\default"; + si.wShowWindow = SW_SHOW; + } wchar_t buf[MAX_PATH]; - wcscpy_s(buf, sizeof(buf), cmd); + wcscpy_s(buf, MAX_PATH, cmd); PROCESS_INFORMATION pi; LPVOID lpEnvironment = NULL; DWORD dwCreationFlags = DETACHED_PROCESS; @@ -543,6 +551,9 @@ extern "C" DWORD count; auto rdp = "rdp"; auto nrdp = strlen(rdp); + // https://github.com/rustdesk/rustdesk/discussions/937#discussioncomment-12373814 citrix session + auto ica = "ica"; + auto nica = strlen(ica); if (WTSEnumerateSessionsA(WTS_CURRENT_SERVER_HANDLE, NULL, 1, &pInfos, &count)) { for (DWORD i = 0; i < count; i++) @@ -558,7 +569,7 @@ extern "C" WTSFreeMemory(pInfos); return id; } - if (!strnicmp(info.pWinStationName, rdp, nrdp)) + if (!strnicmp(info.pWinStationName, rdp, nrdp) || !strnicmp(info.pWinStationName, ica, nica)) { rdp_or_console = info.SessionId; } @@ -614,6 +625,8 @@ extern "C" auto info = pInfos[i]; auto rdp = "rdp"; auto nrdp = strlen(rdp); + auto ica = "ica"; + auto nica = strlen(ica); if (info.State == WTSActive) { if (info.pWinStationName == NULL) continue; @@ -626,6 +639,9 @@ extern "C" else if (include_rdp && !strnicmp(info.pWinStationName, rdp, nrdp)) { sessionIds.push_back(std::wstring(L"RDP:") + std::to_wstring(info.SessionId)); } + else if (include_rdp && !strnicmp(info.pWinStationName, ica, nica)) { + sessionIds.push_back(std::wstring(L"ICA:") + std::to_wstring(info.SessionId)); + } } } WTSFreeMemory(pInfos); @@ -851,4 +867,168 @@ extern "C" return isRunning; } -} // end of extern "C" \ No newline at end of file +} // end of extern "C" + +// Remote printing +extern "C" +{ +// Dynamic loading of XPS Print functions +typedef HRESULT(WINAPI *StartXpsPrintJobFunc)( + LPCWSTR printerName, + LPCWSTR jobName, + LPCWSTR outputFileName, + HANDLE progressEvent, + HANDLE completionEvent, + UINT8* printablePagesOn, + UINT32 printablePagesOnCount, + IXpsPrintJob** xpsPrintJob, + IXpsPrintJobStream** documentStream, + IXpsPrintJobStream** printTicketStream); + +static HMODULE xpsPrintModule = nullptr; +static StartXpsPrintJobFunc StartXpsPrintJobPtr = nullptr; + +static bool InitXpsPrint() +{ + if (xpsPrintModule == nullptr) + { + xpsPrintModule = LoadLibraryA("XpsPrint.dll"); + if (xpsPrintModule == nullptr) + { + flog("Failed to load XpsPrint.dll. Error: %d\n", GetLastError()); + return false; + } + + StartXpsPrintJobPtr = (StartXpsPrintJobFunc)GetProcAddress(xpsPrintModule, "StartXpsPrintJob"); + if (StartXpsPrintJobPtr == nullptr) + { + flog("Failed to get StartXpsPrintJob function. Error: %d\n", GetLastError()); + FreeLibrary(xpsPrintModule); + xpsPrintModule = nullptr; + return false; + } + } + return true; +} +#pragma warning(push) +#pragma warning(disable : 4995) + +#define PRINT_XPS_CHECK_HR(hr, msg) \ + if (FAILED(hr)) \ + { \ + _com_error err(hr); \ + flog("%s Error: %s\n", msg, err.ErrorMessage()); \ + return -1; \ + } + + int PrintXPSRawData(LPWSTR printerName, BYTE *rawData, ULONG dataSize) + { + // Check if XPS Print DLL is available + if (!InitXpsPrint()) + { + flog("XPS Print functionality not available on this system\n"); + return -1; + } + + BOOL isCoInitializeOk = FALSE; + HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + if (hr == RPC_E_CHANGED_MODE) + { + hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + } + if (hr == S_OK) + { + isCoInitializeOk = TRUE; + } + std::shared_ptr coInitGuard(nullptr, [isCoInitializeOk](int *) { + if (isCoInitializeOk) CoUninitialize(); + }); + + IXpsOMObjectFactory *xpsFactory = nullptr; + hr = CoCreateInstance( + __uuidof(XpsOMObjectFactory), + nullptr, + CLSCTX_INPROC_SERVER, + __uuidof(IXpsOMObjectFactory), + reinterpret_cast(&xpsFactory)); + PRINT_XPS_CHECK_HR(hr, "Failed to create XPS object factory."); + std::shared_ptr xpsFactoryGuard( + xpsFactory, + [](IXpsOMObjectFactory *xpsFactory) { + xpsFactory->Release(); + }); + + HANDLE completionEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + if (completionEvent == nullptr) + { + flog("Failed to create completion event. Last error: %d\n", GetLastError()); + return -1; + } + std::shared_ptr completionEventGuard( + &completionEvent, + [](HANDLE *completionEvent) { + CloseHandle(*completionEvent); + }); + + IXpsPrintJob *job = nullptr; + IXpsPrintJobStream *jobStream = nullptr; + // `StartXpsPrintJob()` is deprecated, but we still use it for compatibility. + // We may change to use the `Print Document Package API` in the future. + // https://learn.microsoft.com/en-us/windows/win32/printdocs/xpsprint-functions + hr = StartXpsPrintJobPtr( + printerName, + L"Print Job 1", + nullptr, + nullptr, + completionEvent, + nullptr, + 0, + &job, + &jobStream, + nullptr); + PRINT_XPS_CHECK_HR(hr, "Failed to start XPS print job."); + + std::shared_ptr jobStreamGuard(jobStream, [](IXpsPrintJobStream *jobStream) { + jobStream->Release(); + }); + BOOL jobOk = FALSE; + std::shared_ptr jobGuard(job, [&jobOk](IXpsPrintJob* job) { + if (jobOk == FALSE) + { + job->Cancel(); + } + job->Release(); + }); + + DWORD bytesWritten = 0; + hr = jobStream->Write(rawData, dataSize, &bytesWritten); + PRINT_XPS_CHECK_HR(hr, "Failed to write data to print job stream."); + + hr = jobStream->Close(); + PRINT_XPS_CHECK_HR(hr, "Failed to close print job stream."); + + // Wait about 5 minutes for the print job to complete. + DWORD waitMillis = 300 * 1000; + DWORD waitResult = WaitForSingleObject(completionEvent, waitMillis); + if (waitResult != WAIT_OBJECT_0) + { + flog("Wait for print job completion failed. Last error: %d\n", GetLastError()); + return -1; + } + jobOk = TRUE; + + return 0; + } + + void CleanupXpsPrint() + { + if (xpsPrintModule != nullptr) + { + FreeLibrary(xpsPrintModule); + xpsPrintModule = nullptr; + StartXpsPrintJobPtr = nullptr; + } + } + +#pragma warning(pop) +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index c0839dc55ac..a00e9906bfc 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -13,48 +13,71 @@ use hbb_common::{ libc::{c_int, wchar_t}, log, message_proto::{DisplayInfo, Resolution, WindowsSession}, - sleep, timeout, tokio, + sleep, + sysinfo::{Pid, System}, + timeout, tokio, }; use std::{ collections::HashMap, ffi::{CString, OsString}, - fs, io, - io::prelude::*, + fs, + io::{self, prelude::*}, mem, - os::windows::process::CommandExt, + os::{ + raw::c_ulong, + windows::{ffi::OsStringExt, process::CommandExt}, + }, path::*, - process::{Command, Stdio}, ptr::null_mut, sync::{atomic::Ordering, Arc, Mutex}, time::{Duration, Instant}, }; use wallpaper; +#[cfg(not(debug_assertions))] +use winapi::um::libloaderapi::{LoadLibraryExW, LOAD_LIBRARY_SEARCH_USER_DIRS}; use winapi::{ ctypes::c_void, shared::{minwindef::*, ntdef::NULL, windef::*, winerror::*}, - um::sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}, um::{ errhandlingapi::GetLastError, - handleapi::CloseHandle, - libloaderapi::{GetProcAddress, LoadLibraryA}, + handleapi::{CloseHandle, INVALID_HANDLE_VALUE}, + libloaderapi::{ + GetProcAddress, LoadLibraryA, LoadLibraryExA, LOAD_LIBRARY_SEARCH_SYSTEM32, + }, minwinbase::STILL_ACTIVE, processthreadsapi::{ GetCurrentProcess, GetCurrentProcessId, GetExitCodeProcess, OpenProcess, OpenProcessToken, ProcessIdToSessionId, PROCESS_INFORMATION, STARTUPINFOW, }, - securitybaseapi::GetTokenInformation, + securitybaseapi::{ + AllocateAndInitializeSid, DuplicateToken, EqualSid, FreeSid, GetTokenInformation, + }, shellapi::ShellExecuteW, + sysinfoapi::{GetNativeSystemInfo, SYSTEM_INFO}, winbase::*, wingdi::*, winnt::{ - TokenElevation, ES_AWAYMODE_REQUIRED, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, + SecurityImpersonation, TokenElevation, TokenGroups, TokenImpersonation, TokenType, + DOMAIN_ALIAS_RID_ADMINS, ES_AWAYMODE_REQUIRED, ES_CONTINUOUS, ES_DISPLAY_REQUIRED, ES_SYSTEM_REQUIRED, HANDLE, PROCESS_ALL_ACCESS, PROCESS_QUERY_LIMITED_INFORMATION, - TOKEN_ELEVATION, TOKEN_QUERY, + PSID, SECURITY_BUILTIN_DOMAIN_RID, SECURITY_NT_AUTHORITY, SID_IDENTIFIER_AUTHORITY, + TOKEN_ELEVATION, TOKEN_GROUPS, TOKEN_QUERY, TOKEN_TYPE, }, winreg::HKEY_CURRENT_USER, + winspool::{ + EnumPrintersW, GetDefaultPrinterW, PRINTER_ENUM_CONNECTIONS, PRINTER_ENUM_LOCAL, + PRINTER_INFO_1W, + }, winuser::*, }, }; +use windows::Win32::{ + Foundation::{CloseHandle as WinCloseHandle, HANDLE as WinHANDLE}, + System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, + TH32CS_SNAPPROCESS, + }, +}; use windows_service::{ define_windows_service, service::{ @@ -71,6 +94,7 @@ pub const SET_FOREGROUND_WINDOW: &'static str = "SET_FOREGROUND_WINDOW"; const REG_NAME_INSTALL_DESKTOPSHORTCUTS: &str = "DESKTOPSHORTCUTS"; const REG_NAME_INSTALL_STARTMENUSHORTCUTS: &str = "STARTMENUSHORTCUTS"; +pub const REG_NAME_INSTALL_PRINTER: &str = "PRINTER"; pub fn get_focused_display(displays: Vec) -> Option { unsafe { @@ -468,6 +492,7 @@ extern "C" { cmd: *const u16, session_id: DWORD, as_user: BOOL, + show: BOOL, token_pid: &mut DWORD, ) -> HANDLE; fn GetSessionUserTokenWin( @@ -500,6 +525,10 @@ extern "C" { fn is_service_running_w(svc_name: *const u16) -> bool; } +pub fn get_current_session_id(share_rdp: bool) -> DWORD { + unsafe { get_current_session(if share_rdp { TRUE } else { FALSE }) } +} + extern "system" { fn BlockInput(v: BOOL) -> BOOL; } @@ -554,7 +583,11 @@ async fn run_service(_arguments: Vec) -> ResultType<()> { let current_active_session = unsafe { get_current_session(share_rdp()) }; if session_id != current_active_session { session_id = current_active_session; - h_process = launch_server(session_id, true).await.unwrap_or(NULL); + // https://github.com/rustdesk/rustdesk/discussions/10039 + let count = ipc::get_port_forward_session_count(1000).await.unwrap_or(0); + if count == 0 { + h_process = launch_server(session_id, true).await.unwrap_or(NULL); + } } } let res = timeout(super::SERVICE_INTERVAL, incoming.next()).await; @@ -603,8 +636,11 @@ async fn run_service(_arguments: Vec) -> ResultType<()> { if tmp != session_id && stored_usid != Some(session_id) { log::info!("session changed from {} to {}", session_id, tmp); session_id = tmp; - send_close_async("").await.ok(); - close_sent = true; + let count = ipc::get_port_forward_session_count(1000).await.unwrap_or(0); + if count == 0 { + send_close_async("").await.ok(); + close_sent = true; + } } let mut exit_code: DWORD = 0; if h_process.is_null() @@ -653,6 +689,10 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType ResultType { use std::os::windows::ffi::OsStrExt; let wstr: Vec = std::ffi::OsStr::new(&cmd) .encode_wide() @@ -660,9 +700,12 @@ async fn launch_server(session_id: DWORD, close_first: bool) -> ResultType ResultType) -> ResultType> { - let cmd = format!( - "\"{}\" {}", - std::env::current_exe()?.to_str().unwrap_or(""), - arg.join(" "), - ); + run_exe_in_cur_session(std::env::current_exe()?.to_str().unwrap_or(""), arg, false) +} + +pub fn run_exe_in_cur_session( + exe: &str, + arg: Vec<&str>, + show: bool, +) -> ResultType> { let Some(session_id) = get_current_process_session_id() else { bail!("Failed to get current process session id"); }; + run_exe_in_session(exe, arg, session_id, show) +} + +pub fn run_exe_in_session( + exe: &str, + arg: Vec<&str>, + session_id: DWORD, + show: bool, +) -> ResultType> { use std::os::windows::ffi::OsStrExt; + let cmd = format!("\"{}\" {}", exe, arg.join(" "),); let wstr: Vec = std::ffi::OsStr::new(&cmd) .encode_wide() .chain(Some(0).into_iter()) .collect(); let wstr = wstr.as_ptr(); let mut token_pid = 0; - let h = unsafe { LaunchProcessWin(wstr, session_id, TRUE, &mut token_pid) }; + let h = unsafe { + LaunchProcessWin( + wstr, + session_id, + TRUE, + if show { TRUE } else { FALSE }, + &mut token_pid, + ) + }; if h.is_null() { if token_pid == 0 { bail!( @@ -730,7 +794,79 @@ pub fn send_sas() { } unsafe { log::info!("SAS received"); + + // Check and temporarily set SoftwareSASGeneration if needed + let mut original_value: Option = None; + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + + if let Ok(policy_key) = hklm.open_subkey_with_flags( + "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", + KEY_READ | KEY_WRITE, + ) { + // Read current value + match policy_key.get_value::("SoftwareSASGeneration") { + Ok(value) => { + /* + - 0 = None (disabled) + - 1 = Services + - 2 = Ease of Access applications + - 3 = Services and Ease of Access applications (Both) + */ + if value != 1 && value != 3 { + original_value = Some(value); + log::info!("SoftwareSASGeneration is {}, setting to 1", value); + // Set to 1 for SendSAS to work + if let Err(e) = policy_key.set_value("SoftwareSASGeneration", &1u32) { + log::error!("Failed to set SoftwareSASGeneration: {}", e); + } + } + } + Err(e) => { + log::info!( + "SoftwareSASGeneration not found or error reading: {}, setting to 1", + e + ); + original_value = Some(0); // Mark that we need to restore (delete) it + // Create and set to 1 + if let Err(e) = policy_key.set_value("SoftwareSASGeneration", &1u32) { + log::error!("Failed to set SoftwareSASGeneration: {}", e); + } + } + } + } else { + log::error!("Failed to open registry key for SoftwareSASGeneration"); + } + + // Send SAS SendSAS(FALSE); + + // Restore original value if we changed it + if let Some(original) = original_value { + if let Ok(policy_key) = hklm.open_subkey_with_flags( + "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System", + KEY_WRITE, + ) { + if original == 0 { + // It didn't exist before, delete it + if let Err(e) = policy_key.delete_value("SoftwareSASGeneration") { + log::error!("Failed to delete SoftwareSASGeneration: {}", e); + } else { + log::info!("Deleted SoftwareSASGeneration (restored to original state)"); + } + } else { + // Restore the original value + if let Err(e) = policy_key.set_value("SoftwareSASGeneration", &original) { + log::error!( + "Failed to restore SoftwareSASGeneration to {}: {}", + original, + e + ); + } else { + log::info!("Restored SoftwareSASGeneration to {}", original); + } + } + } + } } } @@ -784,8 +920,12 @@ pub fn set_share_rdp(enable: bool) { } pub fn get_current_process_session_id() -> Option { + get_session_id_of_process(unsafe { GetCurrentProcessId() }) +} + +pub fn get_session_id_of_process(pid: DWORD) -> Option { let mut sid = 0; - if unsafe { ProcessIdToSessionId(GetCurrentProcessId(), &mut sid) == TRUE } { + if unsafe { ProcessIdToSessionId(pid, &mut sid) == TRUE } { Some(sid) } else { None @@ -950,6 +1090,19 @@ pub fn is_prelogin() -> bool { username.is_empty() || username == "SYSTEM" } +// `is_logon_ui()` is regardless of multiple sessions now. +// It only check if "LogonUI.exe" exists. +// +// If there're mulitple sessions (logged in users), +// some are in the login screen, while the others are not. +// Then this function may not work fine if the session we want to handle(connect) is not in the login screen. +// But it's a rare case and cannot be simply handled, so it will not be dealt with for the time being. +#[inline] +pub fn is_logon_ui() -> ResultType { + let pids = get_pids("LogonUI.exe")?; + Ok(!pids.is_empty()) +} + pub fn is_root() -> bool { // https://stackoverflow.com/questions/4023586/correct-way-to-find-out-if-a-service-is-running-as-the-system-user unsafe { is_local_system() == TRUE } @@ -1009,6 +1162,10 @@ pub fn get_install_options() -> String { if let Some(start_menu_shortcuts) = start_menu_shortcuts { opts.insert(REG_NAME_INSTALL_STARTMENUSHORTCUTS, start_menu_shortcuts); } + let printer = get_reg_of_hkcr(&subkey, REG_NAME_INSTALL_PRINTER); + if let Some(printer) = printer { + opts.insert(REG_NAME_INSTALL_PRINTER, printer); + } serde_json::to_string(&opts).unwrap_or("{}".to_owned()) } @@ -1134,6 +1291,7 @@ fn get_after_install( exe: &str, reg_value_start_menu_shortcuts: Option, reg_value_desktop_shortcuts: Option, + reg_value_printer: Option, ) -> String { let app_name = crate::get_app_name(); let ext = app_name.to_lowercase(); @@ -1141,7 +1299,7 @@ fn get_after_install( // reg delete HKEY_CURRENT_USER\Software\Classes for // https://github.com/rustdesk/rustdesk/commit/f4bdfb6936ae4804fc8ab1cf560db192622ad01a // and https://github.com/leanflutter/uni_links_desktop/blob/1b72b0226cec9943ca8a84e244c149773f384e46/lib/src/protocol_registrar_impl_windows.dart#L30 - let hcu = winreg::RegKey::predef(HKEY_CURRENT_USER); + let hcu = RegKey::predef(HKEY_CURRENT_USER); hcu.delete_subkey_all(format!("Software\\Classes\\{}", exe)) .ok(); @@ -1157,12 +1315,20 @@ fn get_after_install( ) }) .unwrap_or_default(); + let reg_printer = reg_value_printer + .map(|v| { + format!( + "reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {REG_NAME_INSTALL_PRINTER} /t REG_SZ /d \"{v}\"" + ) + }) + .unwrap_or_default(); format!(" chcp 65001 reg add HKEY_CLASSES_ROOT\\.{ext} /f {desktop_shortcuts} {start_menu_shortcuts} + {reg_printer} reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f reg add HKEY_CLASSES_ROOT\\.{ext}\\DefaultIcon /f /ve /t REG_SZ /d \"\\\"{exe}\\\",0\" reg add HKEY_CLASSES_ROOT\\.{ext}\\shell /f @@ -1183,7 +1349,7 @@ fn get_after_install( } pub fn install_me(options: &str, path: String, silent: bool, debug: bool) -> ResultType<()> { - let uninstall_str = get_uninstall(false); + let uninstall_str = get_uninstall(false, false); let mut path = path.trim_end_matches('\\').to_owned(); let (subkey, _path, start_menu, exe) = get_default_install_info(); let mut exe = exe; @@ -1247,6 +1413,7 @@ oLink.Save let tray_shortcut = get_tray_shortcut(&exe, &tmp_path)?; let mut reg_value_desktop_shortcuts = "0".to_owned(); let mut reg_value_start_menu_shortcuts = "0".to_owned(); + let mut reg_value_printer = "0".to_owned(); let mut shortcuts = Default::default(); if options.contains("desktopicon") { shortcuts = format!( @@ -1266,6 +1433,10 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{start_menu}\\\" ); reg_value_start_menu_shortcuts = "1".to_owned(); } + let install_printer = options.contains("printer") && is_win_10_or_greater(); + if install_printer { + reg_value_printer = "1".to_owned(); + } let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; let size = meta.len() / 1024; @@ -1301,6 +1472,19 @@ copy /Y \"{tmp_path}\\{app_name} Tray.lnk\" \"%PROGRAMDATA%\\Microsoft\\Windows\ ") }; + let install_remote_printer = if install_printer { + // No need to use `|| true` here. + // The script will not exit even if `--install-remote-printer` panics. + format!("\"{}\" --install-remote-printer", &src_exe) + } else if is_win_10_or_greater() { + format!("\"{}\" --uninstall-remote-printer", &src_exe) + } else { + "".to_owned() + }; + + // Remember to check if `update_me` need to be changed if changing the `cmds`. + // No need to merge the existing dup code, because the code in these two functions are too critical. + // New code should be written in a common function. let cmds = format!( " {uninstall_str} @@ -1329,6 +1513,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" {dels} {import_config} {after_install} +{install_remote_printer} {sleep} ", version = crate::VERSION.replace("-", "."), @@ -1336,7 +1521,8 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" after_install = get_after_install( &exe, Some(reg_value_start_menu_shortcuts), - Some(reg_value_desktop_shortcuts) + Some(reg_value_desktop_shortcuts), + Some(reg_value_printer) ), sleep = if debug { "timeout 300" } else { "" }, dels = if debug { "" } else { &dels }, @@ -1350,7 +1536,11 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\" pub fn run_after_install() -> ResultType<()> { let (_, _, _, exe) = get_install_info(); - run_cmds(get_after_install(&exe, None, None), true, "after_install") + run_cmds( + get_after_install(&exe, None, None, None), + true, + "after_install", + ) } pub fn run_before_uninstall() -> ResultType<()> { @@ -1380,22 +1570,39 @@ fn get_before_uninstall(kill_self: bool) -> String { ) } -fn get_uninstall(kill_self: bool) -> String { +/// Constructs the uninstall command string for the application. +/// +/// # Parameters +/// - `kill_self`: The command will kill the process of current app name. If `true`, it will kill +/// the current process as well. If `false`, it will exclude the current process from the kill +/// command. +/// - `uninstall_printer`: If `true`, includes commands to uninstall the remote printer. +/// +/// # Details +/// The `uninstall_printer` parameter determines whether the command to uninstall the remote printer +/// is included in the generated uninstall script. If `uninstall_printer` is `false`, the printer +/// related command is omitted from the script. +fn get_uninstall(kill_self: bool, uninstall_printer: bool) -> String { let reg_uninstall_string = get_reg("UninstallString"); if reg_uninstall_string.to_lowercase().contains("msiexec.exe") { return reg_uninstall_string; } let mut uninstall_cert_cmd = "".to_string(); + let mut uninstall_printer_cmd = "".to_string(); if let Ok(exe) = std::env::current_exe() { if let Some(exe_path) = exe.to_str() { uninstall_cert_cmd = format!("\"{}\" --uninstall-cert", exe_path); + if uninstall_printer { + uninstall_printer_cmd = format!("\"{}\" --uninstall-remote-printer", &exe_path); + } } } let (subkey, path, start_menu, _) = get_install_info(); format!( " {before_uninstall} + {uninstall_printer_cmd} {uninstall_cert_cmd} reg delete {subkey} /f {uninstall_amyuni_idd} @@ -1411,7 +1618,7 @@ fn get_uninstall(kill_self: bool) -> String { } pub fn uninstall_me(kill_self: bool) -> ResultType<()> { - run_cmds(get_uninstall(kill_self), true, "uninstall") + run_cmds(get_uninstall(kill_self, true), true, "uninstall") } fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType { @@ -1460,15 +1667,13 @@ fn to_le(v: &mut [u16]) -> &[u8] { unsafe { v.align_to().1 } } -fn get_undone_file(tmp: &PathBuf) -> ResultType { - let mut tmp1 = tmp.clone(); - tmp1.set_file_name(format!( +fn get_undone_file(tmp: &Path) -> ResultType { + Ok(tmp.with_file_name(format!( "{}.undone", tmp.file_name() .ok_or(anyhow!("Failed to get filename of {:?}", tmp))? .to_string_lossy() - )); - Ok(tmp1) + ))) } fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> { @@ -1555,6 +1760,21 @@ pub fn get_license_from_exe_name() -> ResultType { get_custom_server_from_string(&exe) } +// We can't directly use `RegKey::set_value` to update the registry value, because it will fail with `ERROR_ACCESS_DENIED` +// So we have to use `run_cmds` to update the registry value. +pub fn update_install_option(k: &str, v: &str) -> ResultType<()> { + // Don't update registry if not installed or not server process. + if !is_installed() || !crate::is_server() { + return Ok(()); + } + let app_name = crate::get_app_name(); + let ext = app_name.to_lowercase(); + let cmds = + format!("chcp 65001 && reg add HKEY_CLASSES_ROOT\\.{ext} /f /v {k} /t REG_SZ /d \"{v}\""); + run_cmds(cmds, false, "update_install_option")?; + Ok(()) +} + #[inline] pub fn is_win_server() -> bool { unsafe { is_windows_server() > 0 } @@ -1565,10 +1785,78 @@ pub fn is_win_10_or_greater() -> bool { unsafe { is_windows_10_or_greater() > 0 } } -pub fn bootstrap() { +pub fn bootstrap() -> bool { if let Ok(lic) = get_license_from_exe_name() { *config::EXE_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone(); } + + #[cfg(debug_assertions)] + { + true + } + #[cfg(not(debug_assertions))] + { + // This function will cause `'sciter.dll' was not found neither in PATH nor near the current executable.` when debugging RustDesk. + // Only call set_safe_load_dll() on Windows 10 or greater + if is_win_10_or_greater() { + set_safe_load_dll() + } else { + true + } + } +} + +#[cfg(not(debug_assertions))] +fn set_safe_load_dll() -> bool { + if !unsafe { set_default_dll_directories() } { + return false; + } + + // `SetDllDirectoryW` should never fail. + // https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-setdlldirectoryw + if unsafe { SetDllDirectoryW(wide_string("").as_ptr()) == FALSE } { + eprintln!("SetDllDirectoryW failed: {}", io::Error::last_os_error()); + return false; + } + + true +} + +// https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-setdefaultdlldirectories +#[cfg(not(debug_assertions))] +unsafe fn set_default_dll_directories() -> bool { + let module = LoadLibraryExW( + wide_string("Kernel32.dll").as_ptr(), + 0 as _, + LOAD_LIBRARY_SEARCH_SYSTEM32, + ); + if module.is_null() { + return false; + } + + match CString::new("SetDefaultDllDirectories") { + Err(e) => { + eprintln!("CString::new failed: {}", e); + return false; + } + Ok(func_name) => { + let func = GetProcAddress(module, func_name.as_ptr()); + if func.is_null() { + eprintln!("GetProcAddress failed: {}", io::Error::last_os_error()); + return false; + } + type SetDefaultDllDirectories = unsafe extern "system" fn(DWORD) -> BOOL; + let func: SetDefaultDllDirectories = std::mem::transmute(func); + if func(LOAD_LIBRARY_SEARCH_SYSTEM32 | LOAD_LIBRARY_SEARCH_USER_DIRS) == FALSE { + eprintln!( + "SetDefaultDllDirectories failed: {}", + io::Error::last_os_error() + ); + return false; + } + } + } + true } pub fn create_shortcut(id: &str) -> ResultType<()> { @@ -1878,6 +2166,177 @@ pub fn send_message_to_hnwd( return true; } +pub fn get_logon_user_token(user: &str, pwd: &str) -> ResultType { + let user_split = user.split("\\").collect::>(); + let wuser = wide_string(user_split.get(1).unwrap_or(&user)); + let wpc = wide_string(user_split.get(0).unwrap_or(&"")); + let wpwd = wide_string(pwd); + let mut ph_token: HANDLE = std::ptr::null_mut(); + let res = unsafe { + LogonUserW( + wuser.as_ptr(), + wpc.as_ptr(), + wpwd.as_ptr(), + LOGON32_LOGON_INTERACTIVE, + LOGON32_PROVIDER_DEFAULT, + &mut ph_token as _, + ) + }; + if res == FALSE { + bail!( + "Failed to log on user {}: {}", + user, + std::io::Error::last_os_error() + ); + } else { + if ph_token.is_null() { + bail!( + "Failed to log on user {}: {}", + user, + std::io::Error::last_os_error() + ); + } + Ok(ph_token) + } +} + +// Ensure the token returned is a primary token. +// If the provided token is an impersonation token, it duplicates it to a primary token. +// If the provided token is already a primary token, it returns it as is. +// The caller is responsible for closing the returned token handle. +pub fn ensure_primary_token(user_token: HANDLE) -> ResultType { + if user_token.is_null() || user_token == INVALID_HANDLE_VALUE { + bail!("Invalid user token provided"); + } + + unsafe { + let mut token_type: TOKEN_TYPE = 0; + let mut return_length: DWORD = 0; + + if GetTokenInformation( + user_token, + TokenType, + &mut token_type as *mut _ as *mut _, + std::mem::size_of::() as DWORD, + &mut return_length, + ) == FALSE + { + bail!( + "Failed to get token type, error {}", + io::Error::last_os_error() + ); + } + + if token_type == TokenImpersonation { + let mut duplicate_token: HANDLE = std::ptr::null_mut(); + let dup_res = DuplicateToken(user_token, SecurityImpersonation, &mut duplicate_token); + CloseHandle(user_token); + if dup_res == FALSE { + bail!( + "Failed to duplicate token, error {}", + io::Error::last_os_error() + ); + } + Ok(duplicate_token) + } else { + Ok(user_token) + } + } +} + +pub fn is_user_token_admin(user_token: HANDLE) -> ResultType { + if user_token.is_null() || user_token == INVALID_HANDLE_VALUE { + bail!("Invalid user token provided"); + } + + unsafe { + let mut dw_size: DWORD = 0; + GetTokenInformation( + user_token, + TokenGroups, + std::ptr::null_mut(), + 0, + &mut dw_size, + ); + + let last_error = GetLastError(); + if last_error != ERROR_INSUFFICIENT_BUFFER { + bail!( + "Failed to get token groups buffer size, error: {}", + last_error + ); + } + if dw_size == 0 { + bail!("Token groups buffer size is zero"); + } + + let mut buffer = vec![0u8; dw_size as usize]; + if GetTokenInformation( + user_token, + TokenGroups, + buffer.as_mut_ptr() as *mut _, + dw_size, + &mut dw_size, + ) == FALSE + { + bail!( + "Failed to get token groups information, error: {}", + io::Error::last_os_error() + ); + } + + let p_token_groups = buffer.as_ptr() as *const TOKEN_GROUPS; + let group_count = (*p_token_groups).GroupCount; + + if group_count == 0 { + return Ok(false); + } + + let mut nt_authority: SID_IDENTIFIER_AUTHORITY = SID_IDENTIFIER_AUTHORITY { + Value: SECURITY_NT_AUTHORITY, + }; + let mut administrators_group: PSID = std::ptr::null_mut(); + if AllocateAndInitializeSid( + &mut nt_authority, + 2, + SECURITY_BUILTIN_DOMAIN_RID, + DOMAIN_ALIAS_RID_ADMINS, + 0, + 0, + 0, + 0, + 0, + 0, + &mut administrators_group, + ) == FALSE + { + bail!( + "Failed to allocate administrators group SID, error: {}", + io::Error::last_os_error() + ); + } + if administrators_group.is_null() { + bail!("Failed to create administrators group SID"); + } + + let mut is_admin = false; + let groups = + std::slice::from_raw_parts((*p_token_groups).Groups.as_ptr(), group_count as usize); + for group in groups { + if EqualSid(administrators_group, group.Sid) == TRUE { + is_admin = true; + break; + } + } + + if !administrators_group.is_null() { + FreeSid(administrators_group); + } + + Ok(is_admin) + } +} + pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> ResultType<()> { let last_error_table = HashMap::from([ ( @@ -1933,7 +2392,7 @@ pub fn create_process_with_logon(user: &str, pwd: &str, exe: &str, arg: &str) -> return Ok(()); } -pub fn set_path_permission(dir: &PathBuf, permission: &str) -> ResultType<()> { +pub fn set_path_permission(dir: &Path, permission: &str) -> ResultType<()> { std::process::Command::new("icacls") .arg(dir.as_os_str()) .arg("/grant") @@ -2216,6 +2675,186 @@ if exist \"{tray_shortcut}\" del /f /q \"{tray_shortcut}\" std::process::exit(0); } +pub fn update_me(debug: bool) -> ResultType<()> { + let app_name = crate::get_app_name(); + let src_exe = std::env::current_exe()?.to_string_lossy().to_string(); + let (subkey, path, _, exe) = get_install_info(); + let is_installed = std::fs::metadata(&exe).is_ok(); + if !is_installed { + bail!("{} is not installed.", &app_name); + } + + let app_exe_name = &format!("{}.exe", &app_name); + let main_window_pids = + crate::platform::get_pids_of_process_with_args::<_, &str>(&app_exe_name, &[]); + let main_window_sessions = main_window_pids + .iter() + .map(|pid| get_session_id_of_process(pid.as_u32())) + .flatten() + .collect::>(); + kill_process_by_pids(&app_exe_name, main_window_pids)?; + let tray_pids = crate::platform::get_pids_of_process_with_args(&app_exe_name, &["--tray"]); + let tray_sessions = tray_pids + .iter() + .map(|pid| get_session_id_of_process(pid.as_u32())) + .flatten() + .collect::>(); + kill_process_by_pids(&app_exe_name, tray_pids)?; + let is_service_running = is_self_service_running(); + + let mut version_major = "0"; + let mut version_minor = "0"; + let mut version_build = "0"; + let versions: Vec<&str> = crate::VERSION.split(".").collect(); + if versions.len() > 0 { + version_major = versions[0]; + } + if versions.len() > 1 { + version_minor = versions[1]; + } + if versions.len() > 2 { + version_build = versions[2]; + } + let meta = std::fs::symlink_metadata(std::env::current_exe()?)?; + let size = meta.len() / 1024; + + let reg_cmd = format!( + " +reg add {subkey} /f /v DisplayIcon /t REG_SZ /d \"{exe}\" +reg add {subkey} /f /v DisplayVersion /t REG_SZ /d \"{version}\" +reg add {subkey} /f /v Version /t REG_SZ /d \"{version}\" +reg add {subkey} /f /v BuildDate /t REG_SZ /d \"{build_date}\" +reg add {subkey} /f /v VersionMajor /t REG_DWORD /d {version_major} +reg add {subkey} /f /v VersionMinor /t REG_DWORD /d {version_minor} +reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build} +reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size} + ", + version = crate::VERSION.replace("-", "."), + build_date = crate::BUILD_DATE, + ); + + let filter = format!(" /FI \"PID ne {}\"", get_current_pid()); + let restore_service_cmd = if is_service_running { + format!("sc start {}", &app_name) + } else { + "".to_owned() + }; + + // No need to check the install option here, `is_rd_printer_installed` rarely fails. + let is_printer_installed = remote_printer::is_rd_printer_installed(&app_name).unwrap_or(false); + // Do nothing if the printer is not installed or failed to query if the printer is installed. + let (uninstall_printer_cmd, install_printer_cmd) = if is_printer_installed { + ( + format!("\"{}\" --uninstall-remote-printer", &src_exe), + format!("\"{}\" --install-remote-printer", &src_exe), + ) + } else { + ("".to_owned(), "".to_owned()) + }; + + // We do not try to remove all files in the old version. + // Because I don't know whether additional files will be installed here after installation, such as drivers. + // Just copy files to the installation directory works fine. + //if exist \"{path}\" rd /s /q \"{path}\" + // md \"{path}\" + // + // We need `taskkill` because: + // 1. There may be some other processes like `rustdesk --connect` are running. + // 2. Sometimes, the main window and the tray icon are showing + // while I cannot find them by `tasklist` or the methods above. + // There's should be 4 processes running: service, server, tray and main window. + // But only 2 processes are shown in the tasklist. + let cmds = format!( + " +chcp 65001 +sc stop {app_name} +taskkill /F /IM {app_name}.exe{filter} +{reg_cmd} +{copy_exe} +{restore_service_cmd} +{uninstall_printer_cmd} +{install_printer_cmd} +{sleep} + ", + app_name = app_name, + copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?, + sleep = if debug { "timeout 300" } else { "" }, + ); + + run_cmds(cmds, debug, "update")?; + + std::thread::sleep(std::time::Duration::from_millis(2000)); + if tray_sessions.is_empty() { + log::info!("No tray process found."); + } else { + log::info!("Try to restore the tray process..."); + log::info!( + "Try to restore the tray process..., sessions: {:?}", + &tray_sessions + ); + for s in tray_sessions { + if s != 0 { + allow_err!(run_exe_in_session(&exe, vec!["--tray"], s, true)); + } + } + } + if main_window_sessions.is_empty() { + log::info!("No main window process found."); + } else { + log::info!("Try to restore the main window process..."); + std::thread::sleep(std::time::Duration::from_millis(2000)); + for s in main_window_sessions { + if s != 0 { + allow_err!(run_exe_in_session(&exe, vec![], s, true)); + } + } + } + std::thread::sleep(std::time::Duration::from_millis(300)); + log::info!("Update completed."); + + Ok(()) +} + +// Double confirm the process name +fn kill_process_by_pids(name: &str, pids: Vec) -> ResultType<()> { + let name = name.to_lowercase(); + let s = System::new_all(); + // No need to check all names of `pids` first, and kill them then. + // It's rare case that they're not matched. + for pid in pids { + if let Some(process) = s.process(pid) { + if process.name().to_lowercase() != name { + bail!("Failed to kill the process, the names are mismatched."); + } + if !process.kill() { + bail!("Failed to kill the process"); + } + } else { + bail!("Failed to kill the process, the pid is not found"); + } + } + Ok(()) +} + +// Don't launch tray app when running with `\qn`. +// 1. Because `/qn` requires administrator permission and the tray app should be launched with user permission. +// Or launching the main window from the tray app will cause the main window to be launched with administrator permission. +// 2. We are not able to launch the tray app if the UI is in the login screen. +// `fn update_me()` can handle the above cases, but for msi update, we need to do more work to handle the above cases. +// 1. Record the tray app session ids. +// 2. Do the update. +// 3. Restore the tray app sessions. +// `1` and `3` must be done in custom actions. +// We need also to handle the command line parsing to find the tray processes. +pub fn update_me_msi(msi: &str, quiet: bool) -> ResultType<()> { + let cmds = format!( + "chcp 65001 && msiexec /i {msi} {}", + if quiet { "/qn LAUNCH_TRAY_APP=N" } else { "" } + ); + run_cmds(cmds, false, "update-msi")?; + Ok(()) +} + pub fn get_tray_shortcut(exe: &str, tmp_path: &str) -> ResultType { Ok(write_cmds( format!( @@ -2300,23 +2939,6 @@ pub fn try_kill_broker() { .spawn()); } -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_uninstall_cert() { - println!("uninstall driver certs: {:?}", cert::uninstall_cert()); - } - - #[test] - fn test_get_unicode_char_by_vk() { - let chr = get_char_from_vk(0x41); // VK_A - assert_eq!(chr, Some('a')); - let chr = get_char_from_vk(VK_ESCAPE as u32); // VK_ESC - assert_eq!(chr, None) - } -} - pub fn message_box(text: &str) { let mut text = text.to_owned(); let nodialog = std::env::var("NO_DIALOG").unwrap_or_default() == "Y"; @@ -2532,7 +3154,15 @@ pub fn try_kill_rustdesk_main_window_process() -> ResultType<()> { fn nt_terminate_process(process_id: DWORD) -> ResultType<()> { type NtTerminateProcess = unsafe extern "system" fn(HANDLE, DWORD) -> DWORD; unsafe { - let h_module = LoadLibraryA(CString::new("ntdll.dll")?.as_ptr()); + let h_module = if is_win_10_or_greater() { + LoadLibraryExA( + CString::new("ntdll.dll")?.as_ptr(), + std::ptr::null_mut(), + LOAD_LIBRARY_SEARCH_SYSTEM32, + ) + } else { + LoadLibraryA(CString::new("ntdll.dll")?.as_ptr()) + }; if !h_module.is_null() { let f_nt_terminate_process: NtTerminateProcess = std::mem::transmute(GetProcAddress( h_module, @@ -2633,16 +3263,21 @@ pub mod reg_display_settings { None } - pub fn restore_reg_connectivity(reg_recovery: RegRecovery) -> ResultType<()> { + pub fn restore_reg_connectivity(reg_recovery: RegRecovery, force: bool) -> ResultType<()> { let hklm = winreg::RegKey::predef(HKEY_LOCAL_MACHINE); let reg_item = hklm.open_subkey_with_flags(®_recovery.path, KEY_READ | KEY_WRITE)?; - let cur_reg_value = reg_item.get_raw_value(®_recovery.key)?; - let new_reg_value = RegValue { - bytes: reg_recovery.new.0, - vtype: isize_to_reg_type(reg_recovery.new.1), - }; - if cur_reg_value != new_reg_value { - return Ok(()); + if !force { + let cur_reg_value = reg_item.get_raw_value(®_recovery.key)?; + let new_reg_value = RegValue { + bytes: reg_recovery.new.0, + vtype: isize_to_reg_type(reg_recovery.new.1), + }; + // Compare if the current value is the same as the new value. + // If they are not the same, the registry value has been changed by other processes. + // So we do not restore the registry value. + if cur_reg_value != new_reg_value { + return Ok(()); + } } let reg_value = RegValue { bytes: reg_recovery.old.0, @@ -2671,3 +3306,459 @@ pub mod reg_display_settings { } } } + +pub fn get_printer_names() -> ResultType> { + let mut needed_bytes = 0; + let mut returned_count = 0; + + unsafe { + // First call to get required buffer size + EnumPrintersW( + PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + std::ptr::null_mut(), + 1, + std::ptr::null_mut(), + 0, + &mut needed_bytes, + &mut returned_count, + ); + + let mut buffer = vec![0u8; needed_bytes as usize]; + + if EnumPrintersW( + PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + std::ptr::null_mut(), + 1, + buffer.as_mut_ptr() as *mut _, + needed_bytes, + &mut needed_bytes, + &mut returned_count, + ) == 0 + { + return Err(anyhow!("Failed to enumerate printers")); + } + + let ptr = buffer.as_ptr() as *const PRINTER_INFO_1W; + let printers = std::slice::from_raw_parts(ptr, returned_count as usize); + + Ok(printers + .iter() + .filter_map(|p| { + let name = p.pName; + if !name.is_null() { + let mut len = 0; + while len < 500 { + if name.add(len).is_null() || *name.add(len) == 0 { + break; + } + len += 1; + } + if len > 0 && len < 500 { + Some(String::from_utf16_lossy(std::slice::from_raw_parts( + name, len, + ))) + } else { + None + } + } else { + None + } + }) + .collect()) + } +} + +extern "C" { + fn PrintXPSRawData(printer_name: *const u16, raw_data: *const u8, data_size: c_ulong) -> DWORD; +} + +pub fn send_raw_data_to_printer(printer_name: Option, data: Vec) -> ResultType<()> { + let mut printer_name = printer_name.unwrap_or_default(); + if printer_name.is_empty() { + // use GetDefaultPrinter to get the default printer name + let mut needed_bytes = 0; + unsafe { + GetDefaultPrinterW(std::ptr::null_mut(), &mut needed_bytes); + } + if needed_bytes > 0 { + let mut default_printer_name = vec![0u16; needed_bytes as usize]; + unsafe { + GetDefaultPrinterW( + default_printer_name.as_mut_ptr() as *mut _, + &mut needed_bytes, + ); + } + printer_name = String::from_utf16_lossy(&default_printer_name[..needed_bytes as usize]); + } + } else { + if let Ok(names) = crate::platform::windows::get_printer_names() { + if !names.contains(&printer_name) { + // Don't set the first printer as current printer. + // It may not be the desired printer. + bail!("Printer name \"{}\" not found", &printer_name); + } + } + } + if printer_name.is_empty() { + return Err(anyhow!("Failed to get printer name")); + } + + log::info!("Sending data to printer: {}", &printer_name); + let printer_name = wide_string(&printer_name); + unsafe { + let res = PrintXPSRawData( + printer_name.as_ptr(), + data.as_ptr() as *const u8, + data.len() as c_ulong, + ); + if res != 0 { + bail!("Failed to send data to the printer, see logs in C:\\Windows\\temp\\test_rustdesk.log for more details."); + } else { + log::info!("Successfully sent data to the printer"); + } + } + + Ok(()) +} + +fn get_pids>(name: S) -> ResultType> { + let name = name.as_ref().to_lowercase(); + let mut pids = Vec::new(); + + unsafe { + let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)?; + if snapshot == WinHANDLE::default() { + return Ok(pids); + } + + let mut entry: PROCESSENTRY32W = std::mem::zeroed(); + entry.dwSize = std::mem::size_of::() as u32; + + if Process32FirstW(snapshot, &mut entry).is_ok() { + loop { + let proc_name = OsString::from_wide(&entry.szExeFile) + .to_string_lossy() + .to_lowercase(); + + if proc_name.contains(&name) { + pids.push(entry.th32ProcessID); + } + + if !Process32NextW(snapshot, &mut entry).is_ok() { + break; + } + } + } + + let _ = WinCloseHandle(snapshot); + } + + Ok(pids) +} + +pub fn is_msi_installed() -> std::io::Result { + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let uninstall_key = hklm.open_subkey(format!( + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{}", + crate::get_app_name() + ))?; + Ok(1 == uninstall_key.get_value::("WindowsInstaller")?) +} + +pub fn is_cur_exe_the_installed() -> bool { + let (_, _, _, exe) = get_install_info(); + // Check if is installed, because `exe` is the default path if is not installed. + if !std::fs::metadata(&exe).is_ok() { + return false; + } + let mut path = std::env::current_exe().unwrap_or_default(); + if let Ok(linked) = path.read_link() { + path = linked; + } + let path = path.to_string_lossy().to_lowercase(); + path == exe.to_lowercase() +} + +#[cfg(not(target_pointer_width = "64"))] +pub fn get_pids_with_first_arg_check_session, S2: AsRef>( + name: S1, + arg: S2, + same_session_id: bool, +) -> ResultType> { + // Though `wmic` can return the sessionId, for simplicity we only return processid. + let pids = get_pids_with_first_arg_by_wmic(name, arg); + if !same_session_id { + return Ok(pids); + } + let Some(cur_sid) = get_current_process_session_id() else { + bail!("Can't get current process session id"); + }; + let mut same_session_pids = vec![]; + for pid in pids.into_iter() { + let mut sid = 0; + if unsafe { ProcessIdToSessionId(pid.as_u32(), &mut sid) == TRUE } { + if sid == cur_sid { + same_session_pids.push(pid); + } + } else { + // Only log here, because this call almost never fails. + log::warn!( + "Failed to get session id of the process id, error: {:?}", + std::io::Error::last_os_error() + ); + } + } + Ok(same_session_pids) +} + +#[cfg(not(target_pointer_width = "64"))] +fn get_pids_with_args_from_wmic_output>( + output: std::borrow::Cow<'_, str>, + name: &str, + args: &[S2], +) -> Vec { + // CommandLine= + // ProcessId=33796 + // + // CommandLine= + // ProcessId=34668 + // + // CommandLine="C:\Program Files\RustDesk\RustDesk.exe" --tray + // ProcessId=13728 + // + // CommandLine="C:\Program Files\RustDesk\RustDesk.exe" + // ProcessId=10136 + let mut pids = Vec::new(); + let mut proc_found = false; + for line in output.lines() { + if line.starts_with("ProcessId=") { + if proc_found { + if let Ok(pid) = line["ProcessId=".len()..].trim().parse::() { + pids.push(hbb_common::sysinfo::Pid::from_u32(pid)); + } + proc_found = false; + } + } else if line.starts_with("CommandLine=") { + proc_found = false; + let cmd = line["CommandLine=".len()..].trim().to_lowercase(); + if args.is_empty() { + if cmd.ends_with(&name) || cmd.ends_with(&format!("{}\"", &name)) { + proc_found = true; + } + } else { + proc_found = args.iter().all(|arg| cmd.contains(arg.as_ref())); + } + } + } + pids +} + +// Note the args are not compared strictly, only check if the args are contained in the command line. +// If we want to check the args strictly, we need to parse the command line and compare each arg. +// Maybe we have to introduce some external crate like `shell_words` to do this. +#[cfg(not(target_pointer_width = "64"))] +pub(super) fn get_pids_with_args_by_wmic, S2: AsRef>( + name: S1, + args: &[S2], +) -> Vec { + let name = name.as_ref().to_lowercase(); + std::process::Command::new("wmic.exe") + .args([ + "process", + "where", + &format!("name='{}'", name), + "get", + "commandline,processid", + "/value", + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map(|output| { + get_pids_with_args_from_wmic_output::( + String::from_utf8_lossy(&output.stdout), + &name, + args, + ) + }) + .unwrap_or_default() +} + +#[cfg(not(target_pointer_width = "64"))] +fn get_pids_with_first_arg_from_wmic_output( + output: std::borrow::Cow<'_, str>, + name: &str, + arg: &str, +) -> Vec { + let mut pids = Vec::new(); + let mut proc_found = false; + for line in output.lines() { + if line.starts_with("ProcessId=") { + if proc_found { + if let Ok(pid) = line["ProcessId=".len()..].trim().parse::() { + pids.push(hbb_common::sysinfo::Pid::from_u32(pid)); + } + proc_found = false; + } + } else if line.starts_with("CommandLine=") { + proc_found = false; + let cmd = line["CommandLine=".len()..].trim().to_lowercase(); + if cmd.is_empty() { + continue; + } + if !arg.is_empty() && cmd.starts_with(arg) { + proc_found = true; + } else { + for x in [&format!("{}\"", name), &format!("{}", name)] { + if cmd.contains(x) { + let cmd = cmd.split(x).collect::>()[1..].join(""); + if arg.is_empty() { + if cmd.trim().is_empty() { + proc_found = true; + } + } else if cmd.trim().starts_with(arg) { + proc_found = true; + } + break; + } + } + } + } + } + pids +} + +// Note the args are not compared strictly, only check if the args are contained in the command line. +// If we want to check the args strictly, we need to parse the command line and compare each arg. +// Maybe we have to introduce some external crate like `shell_words` to do this. +#[cfg(not(target_pointer_width = "64"))] +pub(super) fn get_pids_with_first_arg_by_wmic, S2: AsRef>( + name: S1, + arg: S2, +) -> Vec { + let name = name.as_ref().to_lowercase(); + let arg = arg.as_ref().to_lowercase(); + std::process::Command::new("wmic.exe") + .args([ + "process", + "where", + &format!("name='{}'", name), + "get", + "commandline,processid", + "/value", + ]) + .creation_flags(CREATE_NO_WINDOW) + .output() + .map(|output| { + get_pids_with_first_arg_from_wmic_output( + String::from_utf8_lossy(&output.stdout), + &name, + &arg, + ) + }) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_uninstall_cert() { + println!("uninstall driver certs: {:?}", cert::uninstall_cert()); + } + + #[test] + fn test_get_unicode_char_by_vk() { + let chr = get_char_from_vk(0x41); // VK_A + assert_eq!(chr, Some('a')); + let chr = get_char_from_vk(VK_ESCAPE as u32); // VK_ESC + assert_eq!(chr, None) + } + + #[cfg(not(target_pointer_width = "64"))] + #[test] + fn test_get_pids_with_args_from_wmic_output() { + let output = r#" +CommandLine= +ProcessId=33796 + +CommandLine= +ProcessId=34668 + +CommandLine="C:\Program Files\testapp\TestApp.exe" --tray +ProcessId=13728 + +CommandLine="C:\Program Files\testapp\TestApp.exe" +ProcessId=10136 +"#; + let name = "testapp.exe"; + let args = vec!["--tray"]; + let pids = super::get_pids_with_args_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + &args, + ); + assert_eq!(pids.len(), 1); + assert_eq!(pids[0].as_u32(), 13728); + + let args: Vec<&str> = vec![]; + let pids = super::get_pids_with_args_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + &args, + ); + assert_eq!(pids.len(), 1); + assert_eq!(pids[0].as_u32(), 10136); + + let args = vec!["--other"]; + let pids = super::get_pids_with_args_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + &args, + ); + assert_eq!(pids.len(), 0); + } + + #[cfg(not(target_pointer_width = "64"))] + #[test] + fn test_get_pids_with_first_arg_from_wmic_output() { + let output = r#" +CommandLine= +ProcessId=33796 + +CommandLine= +ProcessId=34668 + +CommandLine="C:\Program Files\testapp\TestApp.exe" --tray +ProcessId=13728 + +CommandLine="C:\Program Files\testapp\TestApp.exe" +ProcessId=10136 + "#; + let name = "testapp.exe"; + let arg = "--tray"; + let pids = super::get_pids_with_first_arg_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + arg, + ); + assert_eq!(pids.len(), 1); + assert_eq!(pids[0].as_u32(), 13728); + + let arg = ""; + let pids = super::get_pids_with_first_arg_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + arg, + ); + assert_eq!(pids.len(), 1); + assert_eq!(pids[0].as_u32(), 10136); + + let arg = "--other"; + let pids = super::get_pids_with_first_arg_from_wmic_output( + String::from_utf8_lossy(output.as_bytes()), + name, + arg, + ); + assert_eq!(pids.len(), 0); + } +} diff --git a/src/plugin/manager.rs b/src/plugin/manager.rs index 74a7f736f24..f59e4c9ff78 100644 --- a/src/plugin/manager.rs +++ b/src/plugin/manager.rs @@ -452,7 +452,7 @@ pub(super) mod install { use std::{ fs::File, io::{BufReader, BufWriter, Write}, - path::PathBuf, + path::Path, }; use zip::ZipArchive; @@ -488,7 +488,7 @@ pub(super) mod install { Ok(()) } - fn download_file(id: &str, url: &str, filename: &PathBuf) -> bool { + fn download_file(id: &str, url: &str, filename: &Path) -> bool { let file = match File::create(filename) { Ok(f) => f, Err(e) => { @@ -505,7 +505,7 @@ pub(super) mod install { true } - fn do_install_file(filename: &PathBuf, target_dir: &PathBuf) -> ResultType<()> { + fn do_install_file(filename: &Path, target_dir: &Path) -> ResultType<()> { let mut zip = ZipArchive::new(BufReader::new(File::open(filename)?))?; for i in 0..zip.len() { let mut file = zip.by_index(i)?; diff --git a/src/plugin/plugins.rs b/src/plugin/plugins.rs index b40ee411676..bf980ee8ca5 100644 --- a/src/plugin/plugins.rs +++ b/src/plugin/plugins.rs @@ -13,7 +13,7 @@ use serde_derive::Serialize; use std::{ collections::{HashMap, HashSet}, ffi::{c_char, c_void}, - path::PathBuf, + path::Path, sync::{Arc, RwLock}, }; @@ -186,7 +186,6 @@ macro_rules! make_plugin { $(let $field = match unsafe { lib.symbol::<$tp>(stringify!($field)) } { Ok(m) => { - log::debug!("{} method found {}", path, stringify!($field)); *m }, Err(e) => { @@ -299,7 +298,7 @@ pub(super) fn load_plugins(uninstalled_ids: &HashSet) -> ResultType<()> Ok(()) } -fn load_plugin_dir(dir: &PathBuf) { +fn load_plugin_dir(dir: &Path) { log::debug!("Begin load plugin dir: {}", dir.display()); if let Ok(rd) = std::fs::read_dir(dir) { for entry in rd { diff --git a/src/port_forward.rs b/src/port_forward.rs index 28ac624cd14..056233b00cc 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -118,7 +118,7 @@ async fn connect_and_login( } else { ConnType::PORT_FORWARD }; - let ((mut stream, direct, _pk), (feedback, rendezvous_server)) = + let ((mut stream, direct, _pk, _kcp, _stream_type), (feedback, rendezvous_server)) = Client::start(id, key, token, conn_type, interface.clone()).await?; interface.update_direct(Some(direct)); let mut buffer = Vec::new(); diff --git a/src/privacy_mode.rs b/src/privacy_mode.rs index a02b8bc93eb..d96f639fdc2 100644 --- a/src/privacy_mode.rs +++ b/src/privacy_mode.rs @@ -219,9 +219,10 @@ async fn turn_on_privacy_async(impl_key: String, conn_id: i32) -> Option match res { Ok(res) => res, Err(e) => Some(Err(anyhow!(e.to_string()))), diff --git a/src/privacy_mode/win_virtual_display.rs b/src/privacy_mode/win_virtual_display.rs index d235575fdac..f521cbacb24 100644 --- a/src/privacy_mode/win_virtual_display.rs +++ b/src/privacy_mode/win_virtual_display.rs @@ -172,6 +172,7 @@ impl PrivacyModeImpl { } fn set_primary_display(&mut self) -> ResultType { + // Multiple virtual displays with different origins are tested. let display = &self.virtual_displays[0]; let display_name = std::string::String::from_utf16(&display.name)?; @@ -194,9 +195,32 @@ impl PrivacyModeImpl { ); } + // Windows 24H2 requires the virtual display to be set first. + // No idea why, maybe the same issue: https://developercommunity.visualstudio.com/t/Windows-11-Enterprise-24H2-using-WinApi/10851936?sort=newest + let flags = CDS_UPDATEREGISTRY | CDS_NORESET; + let offx = new_primary_dm.u1.s2().dmPosition.x; + let offy = new_primary_dm.u1.s2().dmPosition.y; + new_primary_dm.u1.s2_mut().dmPosition.x = 0; + new_primary_dm.u1.s2_mut().dmPosition.y = 0; + new_primary_dm.dmFields |= DM_POSITION; + let rc = ChangeDisplaySettingsExW( + display.name.as_ptr(), + &mut new_primary_dm, + NULL as _, + flags | CDS_SET_PRIMARY, + NULL, + ); + if rc != DISP_CHANGE_SUCCESSFUL { + let err = Self::change_display_settings_ex_err_msg(rc); + log::error!( + "Failed ChangeDisplaySettingsEx, the virtual display, {}", + &err + ); + bail!("Failed ChangeDisplaySettingsEx, {}", err); + } + let mut i: DWORD = 0; loop { - let mut flags = CDS_UPDATEREGISTRY | CDS_NORESET; #[allow(invalid_value)] let mut dd: DISPLAY_DEVICEW = std::mem::MaybeUninit::uninit().assume_init(); dd.cb = std::mem::size_of::() as _; @@ -209,9 +233,9 @@ impl PrivacyModeImpl { if (dd.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) == 0 { continue; } - + // Skip the virtual display. if dd.DeviceName == display.name { - flags |= CDS_SET_PRIMARY; + continue; } #[allow(invalid_value)] @@ -228,8 +252,8 @@ impl PrivacyModeImpl { ); } - dm.u1.s2_mut().dmPosition.x -= new_primary_dm.u1.s2().dmPosition.x; - dm.u1.s2_mut().dmPosition.y -= new_primary_dm.u1.s2().dmPosition.y; + dm.u1.s2_mut().dmPosition.x -= offx; + dm.u1.s2_mut().dmPosition.y -= offy; dm.dmFields |= DM_POSITION; let rc = ChangeDisplaySettingsExW( dd.DeviceName.as_ptr(), @@ -263,6 +287,9 @@ impl PrivacyModeImpl { Ok(display_name) } + // NOTE: We can't detect if the other virtual displays are physical displays or not. + // We can only use `DeviceString` == `virtual_display_manager::get_cur_device_string()` to detect if the display is a virtual display. + // The other virtual displays can't be restored after exiting the privacy mode on Windows 24H2. fn disable_physical_displays(&self) -> ResultType<()> { for display in &self.displays { let mut dm = display.dm.clone(); @@ -303,21 +330,34 @@ impl PrivacyModeImpl { }] } - pub fn ensure_virtual_display(&mut self) -> ResultType<()> { + // This function will wait at most 6 seconds for the virtual displays to be ready. + // It's ok to wait, because: + // 1. A new thread is created to handle the async privacy mode. + // 2. The user is usually not in a hurry to turn on the privacy mode. + pub fn ensure_virtual_display(&mut self, is_async_mode: bool) -> ResultType<()> { if self.virtual_displays.is_empty() { let displays = virtual_display_manager::plug_in_peer_request(vec![Self::default_display_modes()])?; - if virtual_display_manager::is_amyuni_idd() { - thread::sleep(Duration::from_secs(3)); + if is_async_mode { + thread::sleep(Duration::from_secs(1)); } self.set_displays(); - // No physical displays, no need to use the privacy mode. if self.displays.is_empty() { virtual_display_manager::plug_out_monitor_indices(&displays, false, false)?; bail!(NO_PHYSICAL_DISPLAYS); } + if is_async_mode { + let now = std::time::Instant::now(); + while self.virtual_displays.is_empty() + && now.elapsed() < Duration::from_millis(5000) + { + thread::sleep(Duration::from_millis(500)); + self.set_displays(); + } + } + self.virtual_displays_added.extend(displays); } @@ -364,9 +404,22 @@ impl PrivacyModeImpl { Self::restore_displays(&self.displays); Self::restore_displays(&self.virtual_displays); allow_err!(Self::commit_change_display(0)); - self.restore_plug_out_monitor(); self.displays.clear(); self.virtual_displays.clear(); + let is_virtual_display_added = self.virtual_displays_added.len() > 0; + if is_virtual_display_added { + self.restore_plug_out_monitor(); + } else { + // https://github.com/rustdesk/rustdesk/pull/12114#issuecomment-2983054370 + // No virtual displays added, we need to change the display combination to force the display settings to be reloaded. + // This function changes the user behavior of the virtual displays. + // But it makes the privacy mode more stable. + // No need to restore the virtual displays. It's easy to notice that the virtual displays are plugged out. + let _ = virtual_display_manager::plug_out_monitor(-1, true, false); + + // We can't replug the virtual dislays here. + // TODO: plug out + plug in the virtual displays (`IDD_IMPL_AMYUNI`) in a short time makes the server side crash. + } } fn restore_displays(displays: &[Display]) { @@ -418,12 +471,13 @@ impl PrivacyMode for PrivacyModeImpl { bail!(NO_PHYSICAL_DISPLAYS); } + let is_async_mode = self.is_async_privacy_mode(); let mut guard = TurnOnGuard { privacy_mode: self, succeeded: false, }; - guard.ensure_virtual_display()?; + guard.ensure_virtual_display(is_async_mode)?; if guard.virtual_displays.is_empty() { log::debug!("No virtual displays"); bail!("No virtual displays."); @@ -434,7 +488,11 @@ impl PrivacyMode for PrivacyModeImpl { guard.disable_physical_displays()?; Self::commit_change_display(CDS_RESET)?; // Explicitly set the resolution(virtual display) to 1920x1080. - allow_err!(crate::platform::change_resolution(&primary_display_name, 1920, 1080)); + allow_err!(crate::platform::change_resolution( + &primary_display_name, + 1920, + 1080 + )); let reg_connectivity_2 = reg_display_settings::read_reg_connectivity()?; if let Some(reg_recovery) = @@ -466,7 +524,9 @@ impl PrivacyMode for PrivacyModeImpl { super::win_input::unhook()?; let _tmp_ignore_changed_holder = crate::display_service::temp_ignore_displays_changed(); self.restore(); - restore_reg_connectivity(false); + // We need to force restore the registry connectivity. + // This is because the registry connection may be changed by `self.restore()`, but will not be fully restored. + restore_reg_connectivity(false, true); if self.conn_id != INVALID_PRIVACY_MODE_CONN_ID { if let Some(state) = state { @@ -507,7 +567,7 @@ fn reset_config_reg_connectivity() { Config::set_option(CONFIG_KEY_REG_RECOVERY.to_owned(), "".to_owned()); } -pub fn restore_reg_connectivity(plug_out_monitors: bool) { +pub fn restore_reg_connectivity(plug_out_monitors: bool, force: bool) { let config_recovery_value = Config::get_option(CONFIG_KEY_REG_RECOVERY); if config_recovery_value.is_empty() { return; @@ -518,7 +578,7 @@ pub fn restore_reg_connectivity(plug_out_monitors: bool) { if let Ok(reg_recovery) = serde_json::from_str::(&config_recovery_value) { - if let Err(e) = reg_display_settings::restore_reg_connectivity(reg_recovery) { + if let Err(e) = reg_display_settings::restore_reg_connectivity(reg_recovery, force) { log::error!("Failed restore_reg_connectivity, error: {}", e); } } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 69fc886cac9..e17920c8a10 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -2,9 +2,9 @@ use std::{ net::SocketAddr, sync::{ atomic::{AtomicBool, Ordering}, - Arc, + Arc, RwLock, }, - time::Instant, + time::{Duration, Instant}, }; use uuid::Uuid; @@ -12,30 +12,31 @@ use uuid::Uuid; use hbb_common::{ allow_err, anyhow::{self, bail}, - config::{self, keys::*, option2bool, Config, CONNECT_TIMEOUT, REG_INTERVAL, RENDEZVOUS_PORT}, + config::{ + self, keys::*, option2bool, use_ws, Config, CONNECT_TIMEOUT, REG_INTERVAL, RENDEZVOUS_PORT, + }, futures::future::join_all, log, protobuf::Message as _, - proxy::Proxy, rendezvous_proto::*, sleep, - socket_client::{self, connect_tcp, is_ipv4}, - tcp::FramedStream, + socket_client::{self, connect_tcp, is_ipv4, new_direct_udp_for, new_udp_for}, tokio::{self, select, sync::Mutex, time::interval}, udp::FramedSocket, - AddrMangle, IntoTargetAddr, ResultType, TargetAddr, + AddrMangle, IntoTargetAddr, ResultType, Stream, TargetAddr, }; use crate::{ check_port, server::{check_zombie, new as new_server, ServerPtr}, - ui_interface::get_builtin_option, }; type Message = RendezvousMessage; lazy_static::lazy_static! { - static ref SOLVING_PK_MISMATCH: Arc> = Default::default(); + static ref SOLVING_PK_MISMATCH: Mutex = Default::default(); + static ref LAST_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); + static ref LAST_RELAY_MSG: Mutex<(SocketAddr, Instant)> = Mutex::new((SocketAddr::new([0; 4].into(), 0), Instant::now())); } static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); @@ -56,19 +57,19 @@ impl RendezvousMediator { } pub async fn start_all() { + crate::test_nat_type(); if config::is_outgoing_only() { loop { sleep(1.).await; } } crate::hbbs_http::sync::start(); - let mut nat_tested = false; + #[cfg(target_os = "windows")] + if crate::platform::is_installed() && crate::is_server() && !crate::is_custom_client() { + crate::updater::start_auto_update(); + } check_zombie(); let server = new_server(); - if Config::get_nat_type() == NatType::UNKNOWN_NAT as i32 { - crate::test_nat_type(); - nat_tested = true; - } if config::option2bool("stop-service", &Config::get_option("stop-service")) { crate::test_rendezvous_server(); } @@ -92,24 +93,30 @@ impl RendezvousMediator { } scrap::codec::test_av1(); loop { + let timeout = Arc::new(RwLock::new(CONNECT_TIMEOUT)); let conn_start_time = Instant::now(); *SOLVING_PK_MISMATCH.lock().await = "".to_owned(); if !config::option2bool("stop-service", &Config::get_option("stop-service")) && !crate::platform::installing_service() { - if !nat_tested { - crate::test_nat_type(); - nat_tested = true; - } let mut futs = Vec::new(); let servers = Config::get_rendezvous_servers(); SHOULD_EXIT.store(false, Ordering::SeqCst); MANUAL_RESTARTED.store(false, Ordering::SeqCst); for host in servers.clone() { let server = server.clone(); + let timeout = timeout.clone(); futs.push(tokio::spawn(async move { if let Err(err) = Self::start(server, host).await { - log::error!("rendezvous mediator error: {err}"); + let err = format!("rendezvous mediator error: {err}"); + // When user reboot, there might be below error, waiting too long + // (CONNECT_TIMEOUT 18s) will make user think there is bug + if err.contains("10054") || err.contains("11001") { + // No such host is known. (os error 11001) + // An existing connection was forcibly closed by the remote host. (os error 10054): also happens for UDP + *timeout.write().unwrap() = 3000; + } + log::error!("{err}"); } // SHOULD_EXIT here is to ensure once one exits, the others also exit. SHOULD_EXIT.store(true, Ordering::SeqCst); @@ -120,11 +127,15 @@ impl RendezvousMediator { server.write().unwrap().close_connections(); } Config::reset_online(); + let timeout = *timeout.read().unwrap(); if !MANUAL_RESTARTED.load(Ordering::SeqCst) { let elapsed = conn_start_time.elapsed().as_millis() as u64; - if elapsed < CONNECT_TIMEOUT { - sleep(((CONNECT_TIMEOUT - elapsed) / 1000) as _).await; + if elapsed < timeout { + sleep(((timeout - elapsed) / 1000) as _).await; } + } else { + // https://github.com/rustdesk/rustdesk/issues/12233 + sleep(0.033).await; } } } @@ -144,7 +155,8 @@ impl RendezvousMediator { pub async fn start_udp(server: ServerPtr, host: String) -> ResultType<()> { let host = check_port(&host, RENDEZVOUS_PORT); - let (mut socket, mut addr) = socket_client::new_udp_for(&host, CONNECT_TIMEOUT).await?; + log::info!("start udp: {host}"); + let (mut socket, mut addr) = new_udp_for(&host, CONNECT_TIMEOUT).await?; let mut rz = Self { addr: addr.clone(), host: host.clone(), @@ -203,7 +215,7 @@ impl RendezvousMediator { log::debug!("Non-protobuf message bytes received: {:?}", bytes); } }, - Some(Err(e)) => bail!("Failed to receive next {}", e), // maybe socks5 tcp disconnected + Some(Err(e)) => bail!("Failed to receive next: {}", e), // maybe socks5 tcp disconnected None => { bail!("Socket receive none. Maybe socks5 server is down."); }, @@ -328,6 +340,7 @@ impl RendezvousMediator { pub async fn start_tcp(server: ServerPtr, host: String) -> ResultType<()> { let host = check_port(&host, RENDEZVOUS_PORT); + log::info!("start tcp: {}", hbb_common::websocket::check_ws(&host)); let mut conn = connect_tcp(host.clone(), CONNECT_TIMEOUT).await?; let key = crate::get_key(true).await; crate::secure_tcp(&mut conn, &key).await?; @@ -341,7 +354,7 @@ impl RendezvousMediator { let mut last_register_sent: Option = None; let mut last_recv_msg = Instant::now(); // we won't support connecting to multiple rendzvous servers any more, so we can use a global variable here. - Config::set_host_key_confirmed(&host, false); + Config::set_host_key_confirmed(&rz.host_prefix, false); loop { let mut update_latency = || { let latency = last_register_sent @@ -355,6 +368,8 @@ impl RendezvousMediator { last_recv_msg = Instant::now(); let bytes = res.ok_or_else(|| anyhow::anyhow!("Rendezvous connection is reset by the peer"))??; if bytes.is_empty() { + // After fixing frequent register_pk, for websocket, nginx need to set proxy_read_timeout to more than 60 seconds, eg: 120s + // https://serverfault.com/questions/1060525/why-is-my-websocket-connection-gets-closed-in-60-seconds conn.send_bytes(bytes::Bytes::new()).await?; continue; // heartbeat } @@ -370,7 +385,7 @@ impl RendezvousMediator { bail!("Rendezvous connection is timeout"); } if (!Config::get_key_confirmed() || - !Config::get_host_key_confirmed(&host)) && + !Config::get_host_key_confirmed(&rz.host_prefix)) && last_register_sent.map(|x| x.elapsed().as_millis() as i64).unwrap_or(REG_INTERVAL) >= REG_INTERVAL { rz.register_pk(Sink::Stream(&mut conn)).await?; last_register_sent = Some(Instant::now()); @@ -384,15 +399,10 @@ impl RendezvousMediator { pub async fn start(server: ServerPtr, host: String) -> ResultType<()> { log::info!("start rendezvous mediator of {}", host); //If the investment agent type is http or https, then tcp forwarding is enabled. - let is_http_proxy = if let Some(conf) = Config::get_socks() { - let proxy = Proxy::from_conf(&conf, None)?; - proxy.is_http_or_https() - } else { - false - }; if (cfg!(debug_assertions) && option_env!("TEST_TCP").is_some()) - || is_http_proxy - || get_builtin_option(config::keys::OPTION_DISABLE_UDP) == "Y" + || Config::is_proxy() + || use_ws() + || crate::is_udp_disabled() { Self::start_tcp(server, host).await } else { @@ -401,6 +411,14 @@ impl RendezvousMediator { } async fn handle_request_relay(&self, rr: RequestRelay, server: ServerPtr) -> ResultType<()> { + let addr = AddrMangle::decode(&rr.socket_addr); + let last = *LAST_RELAY_MSG.lock().await; + *LAST_RELAY_MSG.lock().await = (addr, Instant::now()); + // skip duplicate relay request messages + if last.0 == addr && last.1.elapsed().as_millis() < 100 { + return Ok(()); + } + self.create_relay( rr.socket_addr.into(), rr.relay_server, @@ -408,6 +426,7 @@ impl RendezvousMediator { server, rr.secure, false, + Default::default(), ) .await } @@ -420,6 +439,7 @@ impl RendezvousMediator { server: ServerPtr, secure: bool, initiate: bool, + socket_addr_v6: bytes::Bytes, ) -> ResultType<()> { let peer_addr = AddrMangle::decode(&socket_addr); log::info!( @@ -436,6 +456,7 @@ impl RendezvousMediator { let mut rr = RelayResponse { socket_addr: socket_addr.into(), version: crate::VERSION.to_owned(), + socket_addr_v6, ..Default::default() }; if initiate { @@ -458,11 +479,28 @@ impl RendezvousMediator { } async fn handle_intranet(&self, fla: FetchLocalAddr, server: ServerPtr) -> ResultType<()> { + let addr = AddrMangle::decode(&fla.socket_addr); + let last = *LAST_MSG.lock().await; + *LAST_MSG.lock().await = (addr, Instant::now()); + // skip duplicate punch hole messages + if last.0 == addr && last.1.elapsed().as_millis() < 100 { + return Ok(()); + } + let peer_addr_v6 = hbb_common::AddrMangle::decode(&fla.socket_addr_v6); let relay_server = self.get_relay_server(fla.relay_server.clone()); - // nat64, go relay directly, because current hbbs will crash if demangle ipv6 address - if is_ipv4(&self.addr) && !config::is_disable_tcp_listen() && !Config::is_proxy() { + let relay = use_ws() || Config::is_proxy(); + let mut socket_addr_v6 = Default::default(); + if peer_addr_v6.port() > 0 && !relay { + socket_addr_v6 = start_ipv6(peer_addr_v6, addr, server.clone()).await; + } + if is_ipv4(&self.addr) && !relay && !config::is_disable_tcp_listen() { if let Err(err) = self - .handle_intranet_(fla.clone(), server.clone(), relay_server.clone()) + .handle_intranet_( + fla.clone(), + server.clone(), + relay_server.clone(), + socket_addr_v6.clone(), + ) .await { log::debug!("Failed to handle intranet: {:?}, will try relay", err); @@ -478,6 +516,7 @@ impl RendezvousMediator { server, true, true, + socket_addr_v6, ) .await } @@ -487,6 +526,7 @@ impl RendezvousMediator { fla: FetchLocalAddr, server: ServerPtr, relay_server: String, + socket_addr_v6: bytes::Bytes, ) -> ResultType<()> { let peer_addr = AddrMangle::decode(&fla.socket_addr); log::debug!("Handle intranet from {:?}", peer_addr); @@ -502,6 +542,7 @@ impl RendezvousMediator { local_addr: AddrMangle::encode(local_addr).into(), relay_server, version: crate::VERSION.to_owned(), + socket_addr_v6, ..Default::default() }); let bytes = msg_out.write_to_bytes()?; @@ -511,10 +552,25 @@ impl RendezvousMediator { } async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> { + let mut peer_addr = AddrMangle::decode(&ph.socket_addr); + let last = *LAST_MSG.lock().await; + *LAST_MSG.lock().await = (peer_addr, Instant::now()); + // skip duplicate punch hole messages + if last.0 == peer_addr && last.1.elapsed().as_millis() < 100 { + return Ok(()); + } + let peer_addr_v6 = hbb_common::AddrMangle::decode(&ph.socket_addr_v6); + let relay = use_ws() || Config::is_proxy() || ph.force_relay; + let mut socket_addr_v6 = Default::default(); + if peer_addr_v6.port() > 0 && !relay { + socket_addr_v6 = start_ipv6(peer_addr_v6, peer_addr, server.clone()).await; + } let relay_server = self.get_relay_server(ph.relay_server); + // for ensure, websocket go relay directly if ph.nat_type.enum_value() == Ok(NatType::SYMMETRIC) || Config::get_nat_type() == NatType::SYMMETRIC as i32 - || config::is_disable_tcp_listen() + || relay + || (config::is_disable_tcp_listen() && ph.udp_port <= 0) { let uuid = Uuid::new_v4().to_string(); return self @@ -525,11 +581,27 @@ impl RendezvousMediator { server, true, true, + socket_addr_v6.clone(), ) .await; } - let peer_addr = AddrMangle::decode(&ph.socket_addr); - log::debug!("Punch hole to {:?}", peer_addr); + use hbb_common::protobuf::Enum; + let nat_type = NatType::from_i32(Config::get_nat_type()).unwrap_or(NatType::UNKNOWN_NAT); + let msg_punch = PunchHoleSent { + socket_addr: ph.socket_addr, + id: Config::get_id(), + relay_server, + nat_type: nat_type.into(), + version: crate::VERSION.to_owned(), + socket_addr_v6, + ..Default::default() + }; + if ph.udp_port > 0 { + peer_addr.set_port(ph.udp_port as u16); + self.punch_udp_hole(peer_addr, server, msg_punch).await?; + return Ok(()); + } + log::debug!("Punch tcp hole to {:?}", peer_addr); let mut socket = { let socket = connect_tcp(&*self.host, CONNECT_TIMEOUT).await?; let local_addr = socket.local_addr(); @@ -539,22 +611,36 @@ impl RendezvousMediator { socket }; let mut msg_out = Message::new(); - use hbb_common::protobuf::Enum; - let nat_type = NatType::from_i32(Config::get_nat_type()).unwrap_or(NatType::UNKNOWN_NAT); - msg_out.set_punch_hole_sent(PunchHoleSent { - socket_addr: ph.socket_addr, - id: Config::get_id(), - relay_server, - nat_type: nat_type.into(), - version: crate::VERSION.to_owned(), - ..Default::default() - }); + msg_out.set_punch_hole_sent(msg_punch); let bytes = msg_out.write_to_bytes()?; socket.send_raw(bytes).await?; crate::accept_connection(server.clone(), socket, peer_addr, true).await; Ok(()) } + async fn punch_udp_hole( + &self, + peer_addr: SocketAddr, + server: ServerPtr, + msg_punch: PunchHoleSent, + ) -> ResultType<()> { + let mut msg_out = Message::new(); + msg_out.set_punch_hole_sent(msg_punch); + let (socket, addr) = new_direct_udp_for(&self.host).await?; + let data = msg_out.write_to_bytes()?; + socket.send_to(&data, addr).await?; + let socket_cloned = socket.clone(); + tokio::spawn(async move { + for _ in 0..2 { + let tm = (hbb_common::time_based_rand() % 20 + 10) as f32 / 1000.; + hbb_common::sleep(tm).await; + socket.send_to(&data, addr).await.ok(); + } + }); + udp_nat_listen(socket_cloned.clone(), peer_addr, peer_addr, server).await?; + Ok(()) + } + async fn register_pk(&mut self, socket: Sink<'_>) -> ResultType<()> { let mut msg_out = Message::new(); let pk = Config::get_key_pair().1; @@ -564,6 +650,7 @@ impl RendezvousMediator { id, uuid: uuid.into(), pk: pk.into(), + no_register_device: Config::no_register_device(), ..Default::default() }); socket.send(&msg_out).await?; @@ -706,7 +793,7 @@ async fn direct_server(server: ServerPtr) { enum Sink<'a> { Framed(&'a mut FramedSocket, &'a TargetAddr<'a>), - Stream(&'a mut FramedStream), + Stream(&'a mut Stream), } impl Sink<'_> { @@ -717,3 +804,49 @@ impl Sink<'_> { } } } + +async fn start_ipv6( + peer_addr_v6: SocketAddr, + peer_addr_v4: SocketAddr, + server: ServerPtr, +) -> bytes::Bytes { + crate::test_ipv6().await; + if let Some((socket, local_addr_v6)) = crate::get_ipv6_socket().await { + let server = server.clone(); + tokio::spawn(async move { + allow_err!(udp_nat_listen(socket.clone(), peer_addr_v6, peer_addr_v4, server).await); + }); + return local_addr_v6; + } + Default::default() +} + +async fn udp_nat_listen( + socket: Arc, + peer_addr: SocketAddr, + peer_addr_v4: SocketAddr, + server: ServerPtr, +) -> ResultType<()> { + let tm = Instant::now(); + let socket_cloned = socket.clone(); + let func = async { + socket.connect(peer_addr).await?; + let res = crate::punch_udp(socket.clone(), true).await?; + let stream = crate::kcp_stream::KcpStream::accept( + socket, + Duration::from_millis(CONNECT_TIMEOUT as _), + res, + ) + .await?; + crate::server::create_tcp_connection(server, stream.1, peer_addr_v4, true).await?; + Ok(()) + }; + func.await.map_err(|e: anyhow::Error| { + anyhow::anyhow!( + "Stop listening on {:?} for remote {peer_addr} with KCP, {:?} elapsed: {e}", + socket_cloned.local_addr(), + tm.elapsed() + ) + })?; + Ok(()) +} diff --git a/src/server.rs b/src/server.rs index ba1682f3d0f..39d0add862b 100644 --- a/src/server.rs +++ b/src/server.rs @@ -24,13 +24,17 @@ use hbb_common::{ sodiumoxide::crypto::{box_, sign}, timeout, tokio, ResultType, Stream, }; +use scrap::camera; #[cfg(not(any(target_os = "android", target_os = "ios")))] use service::ServiceTmpl; use service::{EmptyExtraFieldService, GenericService, Service, Subscriber}; +use video_service::VideoSource; use crate::ipc::Data; pub mod audio_service; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub mod terminal_service; cfg_if::cfg_if! { if #[cfg(not(target_os = "ios"))] { mod clipboard_service; @@ -68,6 +72,9 @@ mod service; mod video_qos; pub mod video_service; +#[cfg(all(target_os = "windows", feature = "flutter"))] +pub mod printer_service; + pub type Childs = Arc>>; type ConnMap = HashMap; @@ -76,7 +83,6 @@ const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); - pub static ref CONN_COUNT: Arc> = Default::default(); // A client server used to provide local services(audio, video, clipboard, etc.) // for all initiative connections. // @@ -106,7 +112,13 @@ pub fn new() -> ServerPtr { #[cfg(not(target_os = "ios"))] { server.add_service(Box::new(display_service::new())); - server.add_service(Box::new(clipboard_service::new())); + server.add_service(Box::new(clipboard_service::new( + clipboard_service::NAME.to_owned(), + ))); + #[cfg(feature = "unix-file-copy-paste")] + server.add_service(Box::new(clipboard_service::new( + clipboard_service::FILE_NAME.to_owned(), + ))); } #[cfg(not(any(target_os = "android", target_os = "ios")))] { @@ -122,6 +134,21 @@ pub fn new() -> ServerPtr { server.add_service(Box::new(input_service::new_window_focus())); } } + #[cfg(all(target_os = "windows", feature = "flutter"))] + { + match printer_service::init(&crate::get_app_name()) { + Ok(()) => { + log::info!("printer service initialized"); + server.add_service(Box::new(printer_service::new( + printer_service::NAME.to_owned(), + ))); + } + Err(e) => { + log::error!("printer service init failed: {}", e); + } + } + } + // Terminal service is created per connection, not globally Arc::new(RwLock::new(server)) } @@ -204,11 +231,13 @@ pub async fn create_tcp_connection( #[cfg(target_os = "macos")] { use std::process::Command; - Command::new("/usr/bin/caffeinate") + if let Ok(task) = Command::new("/usr/bin/caffeinate") .arg("-u") .arg("-t 5") .spawn() - .ok(); + { + super::CHILD_PROCESS.lock().unwrap().push(task); + } log::info!("wake up macos"); } Connection::start(addr, stream, id, Arc::downgrade(&server)).await; @@ -222,7 +251,7 @@ pub async fn accept_connection( secure: bool, ) { if let Err(err) = accept_connection_(server, socket, secure).await { - log::error!("Failed to accept connection from {}: {}", peer_addr, err); + log::warn!("Failed to accept connection from {}: {}", peer_addr, err); } } @@ -273,22 +302,53 @@ async fn create_relay_connection_( impl Server { fn is_video_service_name(name: &str) -> bool { - name.starts_with(video_service::NAME) + name.starts_with(VideoSource::Monitor.service_name_prefix()) + || name.starts_with(VideoSource::Camera.service_name_prefix()) + } + + pub fn try_add_primary_camera_service(&mut self) { + if !camera::primary_camera_exists() { + return; + } + let primary_camera_name = + video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX); + if !self.contains(&primary_camera_name) { + self.add_service(Box::new(video_service::new( + VideoSource::Camera, + camera::PRIMARY_CAMERA_IDX, + ))); + } } pub fn try_add_primay_video_service(&mut self) { - let primary_video_service_name = - video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX); + let primary_video_service_name = video_service::get_service_name( + VideoSource::Monitor, + *display_service::PRIMARY_DISPLAY_IDX, + ); if !self.contains(&primary_video_service_name) { self.add_service(Box::new(video_service::new( + VideoSource::Monitor, *display_service::PRIMARY_DISPLAY_IDX, ))); } } + pub fn add_camera_connection(&mut self, conn: ConnInner) { + if camera::primary_camera_exists() { + let primary_camera_name = + video_service::get_service_name(VideoSource::Camera, camera::PRIMARY_CAMERA_IDX); + if let Some(s) = self.services.get(&primary_camera_name) { + s.on_subscribe(conn.clone()); + } + } + self.connections.insert(conn.id(), conn); + } + pub fn add_connection(&mut self, conn: ConnInner, noperms: &Vec<&'static str>) { - let primary_video_service_name = - video_service::get_service_name(*display_service::PRIMARY_DISPLAY_IDX); + let primary_video_service_name = video_service::get_service_name( + VideoSource::Monitor, + *display_service::PRIMARY_DISPLAY_IDX, + ); for s in self.services.values() { let name = s.name(); if Self::is_video_service_name(&name) && name != primary_video_service_name { @@ -301,7 +361,6 @@ impl Server { #[cfg(target_os = "macos")] self.update_enable_retina(); self.connections.insert(conn.id(), conn); - *CONN_COUNT.lock().unwrap() = self.connections.len(); } pub fn remove_connection(&mut self, conn: &ConnInner) { @@ -309,7 +368,6 @@ impl Server { s.on_unsubscribe(conn.id()); } self.connections.remove(&conn.id()); - *CONN_COUNT.lock().unwrap() = self.connections.len(); #[cfg(target_os = "macos")] self.update_enable_retina(); } @@ -355,10 +413,15 @@ impl Server { self.id_count } - pub fn set_video_service_opt(&self, display: Option, opt: &str, value: &str) { + pub fn set_video_service_opt( + &self, + display: Option<(VideoSource, usize)>, + opt: &str, + value: &str, + ) { for (k, v) in self.services.iter() { - if let Some(display) = display { - if k != &video_service::get_service_name(display) { + if let Some((source, display)) = display { + if k != &video_service::get_service_name(source, display) { continue; } } @@ -386,13 +449,14 @@ impl Server { fn capture_displays( &mut self, conn: ConnInner, + source: VideoSource, displays: &[usize], include: bool, exclude: bool, ) { let displays = displays .iter() - .map(|d| video_service::get_service_name(*d)) + .map(|d| video_service::get_service_name(source, *d)) .collect::>(); let keys = self.services.keys().cloned().collect::>(); for name in keys.iter() { diff --git a/src/server/clipboard_service.rs b/src/server/clipboard_service.rs index 8ae48250055..1d2f0a3fb56 100644 --- a/src/server/clipboard_service.rs +++ b/src/server/clipboard_service.rs @@ -1,18 +1,27 @@ use super::*; #[cfg(not(target_os = "android"))] +use crate::clipboard::clipboard_listener; +#[cfg(not(target_os = "android"))] pub use crate::clipboard::{check_clipboard, ClipboardContext, ClipboardSide}; pub use crate::clipboard::{CLIPBOARD_INTERVAL as INTERVAL, CLIPBOARD_NAME as NAME}; #[cfg(windows)] use crate::ipc::{self, ClipboardFile, ClipboardNonFile, Data}; +#[cfg(feature = "unix-file-copy-paste")] +pub use crate::{ + clipboard::{check_clipboard_files, FILE_CLIPBOARD_NAME as FILE_NAME}, + clipboard_file::unix_file_clip, +}; +#[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] +use clipboard::platform::unix::fuse::{init_fuse_context, uninit_fuse_context}; #[cfg(not(target_os = "android"))] -use clipboard_master::{CallbackResult, ClipboardHandler}; +use clipboard_master::CallbackResult; #[cfg(target_os = "android")] use hbb_common::config::{keys, option2bool}; #[cfg(target_os = "android")] use std::sync::atomic::{AtomicBool, Ordering}; use std::{ io, - sync::mpsc::{channel, RecvTimeoutError, Sender}, + sync::mpsc::{channel, RecvTimeoutError}, time::Duration, }; #[cfg(windows)] @@ -23,9 +32,7 @@ static CLIPBOARD_SERVICE_OK: AtomicBool = AtomicBool::new(false); #[cfg(not(target_os = "android"))] struct Handler { - sp: EmptyExtraFieldService, ctx: Option, - tx_cb_result: Sender, #[cfg(target_os = "windows")] stream: Option>, #[cfg(target_os = "windows")] @@ -37,39 +44,51 @@ pub fn is_clipboard_service_ok() -> bool { CLIPBOARD_SERVICE_OK.load(Ordering::SeqCst) } -pub fn new() -> GenericService { - let svc = EmptyExtraFieldService::new(NAME.to_owned(), false); +pub fn new(name: String) -> GenericService { + let svc = EmptyExtraFieldService::new(name, false); GenericService::run(&svc.clone(), run); svc.sp } #[cfg(not(target_os = "android"))] fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + #[cfg(all(feature = "unix-file-copy-paste", target_os = "linux"))] + let _fuse_call_on_ret = { + if sp.name() == FILE_NAME { + Some(init_fuse_context(false).map(|_| crate::SimpleCallOnReturn { + b: true, + f: Box::new(|| { + uninit_fuse_context(false); + }), + })) + } else { + None + } + }; + let (tx_cb_result, rx_cb_result) = channel(); - let handler = Handler { - sp: sp.clone(), - ctx: Some(ClipboardContext::new()?), - tx_cb_result, + let ctx = Some(ClipboardContext::new().map_err(|e| io::Error::new(io::ErrorKind::Other, e))?); + clipboard_listener::subscribe(sp.name(), tx_cb_result)?; + let mut handler = Handler { + ctx, #[cfg(target_os = "windows")] stream: None, #[cfg(target_os = "windows")] rt: None, }; - let (tx_start_res, rx_start_res) = channel(); - let h = crate::clipboard::start_clipbard_master_thread(handler, tx_start_res); - let shutdown = match rx_start_res.recv() { - Ok((Some(s), _)) => s, - Ok((None, err)) => { - bail!(err); - } - Err(e) => { - bail!("Failed to create clipboard listener: {}", e); - } - }; - while sp.ok() { match rx_cb_result.recv_timeout(Duration::from_millis(INTERVAL)) { + Ok(CallbackResult::Next) => { + #[cfg(feature = "unix-file-copy-paste")] + if sp.name() == FILE_NAME { + handler.check_clipboard_file(); + continue; + } + if let Some(msg) = handler.get_clipboard_msg() { + sp.send(msg); + } + } Ok(CallbackResult::Stop) => { log::debug!("Clipboard listener stopped"); break; @@ -78,36 +97,44 @@ fn run(sp: EmptyExtraFieldService) -> ResultType<()> { bail!("Clipboard listener stopped with error: {}", err); } Err(RecvTimeoutError::Timeout) => {} - _ => {} + Err(RecvTimeoutError::Disconnected) => { + log::error!("Clipboard listener disconnected"); + break; + } } } - shutdown.signal(); - h.join().ok(); + + clipboard_listener::unsubscribe(&sp.name()); Ok(()) } #[cfg(not(target_os = "android"))] -impl ClipboardHandler for Handler { - fn on_clipboard_change(&mut self) -> CallbackResult { - if self.sp.ok() { - if let Some(msg) = self.get_clipboard_msg() { - self.sp.send(msg); +impl Handler { + #[cfg(feature = "unix-file-copy-paste")] + fn check_clipboard_file(&mut self) { + if let Some(urls) = check_clipboard_files(&mut self.ctx, ClipboardSide::Host, false) { + if !urls.is_empty() { + #[cfg(target_os = "macos")] + if crate::clipboard::is_file_url_set_by_rustdesk(&urls) { + return; + } + match clipboard::platform::unix::serv_files::sync_files(&urls) { + Ok(()) => { + // Use `send_data()` here to reuse `handle_file_clip()` in `connection.rs`. + hbb_common::allow_err!(clipboard::send_data( + 0, + unix_file_clip::get_format_list() + )); + } + Err(e) => { + log::error!("Failed to sync clipboard files: {}", e); + } + } } } - CallbackResult::Next } - fn on_clipboard_error(&mut self, error: io::Error) -> CallbackResult { - self.tx_cb_result - .send(CallbackResult::StopWithError(error)) - .ok(); - CallbackResult::Next - } -} - -#[cfg(not(target_os = "android"))] -impl Handler { fn get_clipboard_msg(&mut self) -> Option { #[cfg(target_os = "windows")] if crate::common::is_server() && crate::platform::is_root() { @@ -144,6 +171,7 @@ impl Handler { } } } + check_clipboard(&mut self.ctx, ClipboardSide::Host, false) } diff --git a/src/server/connection.rs b/src/server/connection.rs index 153740c28a3..01d84437db0 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -1,4 +1,6 @@ use super::{input_service::*, *}; +#[cfg(feature = "unix-file-copy-paste")] +use crate::clipboard::try_empty_clipboard_files; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::clipboard::{update_clipboard, ClipboardSide}; #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] @@ -6,8 +8,6 @@ use crate::clipboard_file::*; #[cfg(target_os = "android")] use crate::keyboard::client::map_key_to_control_key; #[cfg(target_os = "linux")] -use crate::platform::linux::is_x11; -#[cfg(target_os = "linux")] use crate::platform::linux_desktop_manager; #[cfg(any(target_os = "windows", target_os = "linux"))] use crate::platform::WallPaperRemover; @@ -28,11 +28,12 @@ use hbb_common::platform::linux::run_cmds; use hbb_common::protobuf::EnumOrUnknown; use hbb_common::{ config::{self, keys, Config, TrustedDevice}, - fs::{self, can_enable_overwrite_detection}, + fs::{self, can_enable_overwrite_detection, JobType}, futures::{SinkExt, StreamExt}, get_time, get_version_number, message_proto::{option_message::BoolOption, permission_info::Permission}, password_security::{self as password, ApproveMode}, + sha2::{Digest, Sha256}, sleep, timeout, tokio::{ net::TcpStream, @@ -43,9 +44,9 @@ use hbb_common::{ }; #[cfg(any(target_os = "android", target_os = "ios"))] use scrap::android::{call_main_service_key_event, call_main_service_pointer_input}; +use scrap::camera; use serde_derive::Serialize; use serde_json::{json, value::Value}; -use sha2::{Digest, Sha256}; #[cfg(not(any(target_os = "android", target_os = "ios")))] use std::sync::atomic::Ordering; use std::{ @@ -55,6 +56,8 @@ use std::{ }; #[cfg(not(any(target_os = "android", target_os = "ios")))] use system_shutdown; +#[cfg(target_os = "windows")] +use windows::Win32::Foundation::{CloseHandle, HANDLE}; #[cfg(windows)] use crate::virtual_display_manager; @@ -66,7 +69,7 @@ lazy_static::lazy_static! { static ref LOGIN_FAILURES: [Arc::>>; 2] = Default::default(); static ref SESSIONS: Arc::>> = Default::default(); static ref ALIVE_CONNS: Arc::>> = Default::default(); - pub static ref AUTHED_CONNS: Arc::>> = Default::default(); + pub static ref AUTHED_CONNS: Arc::>> = Default::default(); static ref SWITCH_SIDES_UUID: Arc::>> = Default::default(); static ref WAKELOCK_SENDER: Arc::>> = Arc::new(Mutex::new(start_wakelock_thread())); } @@ -167,8 +170,26 @@ pub enum AuthConnType { Remote, FileTransfer, PortForward, + ViewCamera, + Terminal, +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[derive(Clone, Debug)] +enum TerminalUserToken { + SelfUser, + CurrentLogonUser(crate::terminal_service::UserToken), } +#[cfg(not(any(target_os = "android", target_os = "ios")))] +impl TerminalUserToken { + fn to_terminal_service_token(&self) -> Option { + match self { + TerminalUserToken::SelfUser => None, + TerminalUserToken::CurrentLogonUser(token) => Some(*token), + } + } +} pub struct Connection { inner: ConnInner, display_idx: usize, @@ -179,6 +200,8 @@ pub struct Connection { timer: crate::RustDeskInterval, file_timer: crate::RustDeskInterval, file_transfer: Option<(String, bool)>, + view_camera: bool, + terminal: bool, port_forward_socket: Option>, port_forward_address: String, tx_to_cm: mpsc::UnboundedSender, @@ -222,13 +245,13 @@ pub struct Connection { portable: PortableState, from_switch: bool, voice_call_request_timestamp: Option, + voice_calling: bool, options_in_login: Option, #[cfg(not(any(target_os = "ios")))] pressed_modifiers: HashSet, #[cfg(target_os = "linux")] linux_headless_handle: LinuxHeadlessHandle, closed: bool, - delay_response_instant: Instant, #[cfg(not(any(target_os = "android", target_os = "ios")))] start_cm_ipc_para: Option, auto_disconnect_timer: Option<(Instant, u64)>, @@ -242,6 +265,19 @@ pub struct Connection { follow_remote_cursor: bool, follow_remote_window: bool, multi_ui_session: bool, + tx_from_authed: mpsc::UnboundedSender, + printer_data: Vec<(Instant, String, Vec)>, + // For post requests that need to be sent sequentially. + // eg. post_conn_audit + tx_post_seq: mpsc::UnboundedSender<(String, Value)>, + terminal_service_id: String, + terminal_persistent: bool, + // The user token must be set when terminal is enabled. + // 0 indicates SYSTEM user + // other values indicate current user + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal_user_token: Option, + terminal_generic_service: Option>, } impl ConnInner { @@ -306,6 +342,7 @@ impl Connection { 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 (tx_from_authed, mut rx_from_authed) = mpsc::unbounded_channel::(); let mut hbbs_rx = crate::hbbs_http::sync::signal_receiver(); #[cfg(not(any(target_os = "android", target_os = "ios")))] let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1); @@ -315,6 +352,11 @@ impl Connection { let linux_headless_handle = LinuxHeadlessHandle::new(_rx_cm_stream_ready, _tx_desktop_ready); + let (tx_post_seq, rx_post_seq) = mpsc::unbounded_channel(); + tokio::spawn(async move { + Self::post_seq_loop(rx_post_seq).await; + }); + #[cfg(not(any(target_os = "android", target_os = "ios")))] let tx_cloned = tx.clone(); let mut conn = Self { @@ -332,6 +374,8 @@ impl Connection { timer: crate::rustdesk_interval(time::interval(SEC30)), file_timer: crate::rustdesk_interval(time::interval(SEC30)), file_transfer: None, + view_camera: false, + terminal: false, port_forward_socket: None, port_forward_address: "".to_owned(), tx_to_cm, @@ -370,13 +414,13 @@ impl Connection { from_switch: false, audio_sender: None, voice_call_request_timestamp: None, + voice_calling: false, options_in_login: None, #[cfg(not(any(target_os = "ios")))] pressed_modifiers: Default::default(), #[cfg(target_os = "linux")] linux_headless_handle, closed: false, - delay_response_instant: Instant::now(), #[cfg(not(any(target_os = "android", target_os = "ios")))] start_cm_ipc_para: Some(StartCmIpcPara { rx_to_cm, @@ -392,6 +436,14 @@ impl Connection { delayed_read_dir: None, #[cfg(target_os = "macos")] retina: Retina::default(), + tx_from_authed, + printer_data: Vec::new(), + tx_post_seq, + terminal_service_id: "".to_owned(), + terminal_persistent: false, + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal_user_token: None, + terminal_generic_service: None, }; let addr = hbb_common::try_into_v4(addr); if !conn.on_open(addr).await { @@ -432,7 +484,7 @@ impl Connection { let mut last_recv_time = Instant::now(); conn.stream.set_send_timeout( - if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() { + if conn.file_transfer.is_some() || conn.port_forward_socket.is_some() || conn.terminal { SEND_TIMEOUT_OTHER } else { SEND_TIMEOUT_VIDEO @@ -443,6 +495,28 @@ impl Connection { std::thread::spawn(move || Self::handle_input(_rx_input, tx_cloned)); let mut second_timer = crate::rustdesk_interval(time::interval(Duration::from_secs(1))); + #[cfg(feature = "unix-file-copy-paste")] + let rx_clip_holder; + let mut rx_clip; + let _tx_clip: mpsc::UnboundedSender; + #[cfg(feature = "unix-file-copy-paste")] + { + rx_clip_holder = ( + clipboard::get_rx_cliprdr_server(id), + crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(id); + }), + }, + ); + rx_clip = rx_clip_holder.0.lock().await; + } + #[cfg(not(feature = "unix-file-copy-paste"))] + { + (_tx_clip, rx_clip) = mpsc::unbounded_channel::(); + } + loop { tokio::select! { // biased; // video has higher priority // causing test_delay_timer failed while transferring big file @@ -490,6 +564,12 @@ impl Connection { s.write().unwrap().subscribe( super::clipboard_service::NAME, conn.inner.clone(), conn.can_sub_clipboard_service()); + #[cfg(feature = "unix-file-copy-paste")] + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + conn.inner.clone(), + conn.can_sub_file_clipboard_service(), + ); s.write().unwrap().subscribe( NAME_CURSOR, conn.inner.clone(), enabled || conn.show_remote_cursor); @@ -507,14 +587,34 @@ impl Connection { conn.send_permission(Permission::Audio, enabled).await; if conn.authorized { if let Some(s) = conn.server.upgrade() { - s.write().unwrap().subscribe( - super::audio_service::NAME, - conn.inner.clone(), conn.audio_enabled()); + if conn.is_authed_view_camera_conn() { + if conn.voice_calling || !conn.audio_enabled() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } + } else { + s.write().unwrap().subscribe( + super::audio_service::NAME, + conn.inner.clone(), conn.audio_enabled()); + } } } } else if &name == "file" { conn.file = enabled; conn.send_permission(Permission::File, enabled).await; + #[cfg(feature = "unix-file-copy-paste")] + if !enabled { + conn.try_empty_file_clipboard(); + } + #[cfg(feature = "unix-file-copy-paste")] + if let Some(s) = conn.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + conn.inner.clone(), + conn.can_sub_file_clipboard_service(), + ); + } } else if &name == "restart" { conn.restart = enabled; conn.send_permission(Permission::Restart, enabled).await; @@ -529,7 +629,7 @@ impl Connection { ipc::Data::RawMessage(bytes) => { allow_err!(conn.stream.send_raw(bytes).await); } - #[cfg(any(target_os="windows", target_os="linux", target_os = "macos"))] + #[cfg(target_os = "windows")] ipc::Data::ClipboardFile(clip) => { allow_err!(conn.stream.send(&clip_2_msg(clip)).await); } @@ -706,6 +806,19 @@ impl Connection { break; } }, + Some(data) = rx_from_authed.recv() => { + match data { + #[cfg(all(target_os = "windows", feature = "flutter"))] + ipc::Data::PrinterData(data) => { + if config::Config::get_bool_option(config::keys::OPTION_ENABLE_REMOTE_PRINTER) { + conn.send_printer_request(data).await; + } else { + conn.send_remote_printing_disallowed().await; + } + } + _ => {} + } + } _ = second_timer.tick() => { #[cfg(windows)] conn.portable_check(); @@ -736,11 +849,32 @@ impl Connection { }); conn.send(msg_out.into()).await; } - video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(conn.inner.id(), conn.delay_response_instant.elapsed().as_millis()); + if conn.is_authed_remote_conn() || conn.view_camera { + if let Some(last_test_delay) = conn.last_test_delay { + video_service::VIDEO_QOS.lock().unwrap().user_delay_response_elapsed(id, last_test_delay.elapsed().as_millis()); + } + } } + clip_file = rx_clip.recv() => match clip_file { + Some(_clip) => { + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste(&conn.lr.version) + { + conn.handle_file_clip(_clip).await; + } + } + None => { + // + } + }, } } + #[cfg(feature = "unix-file-copy-paste")] + { + conn.try_empty_file_clipboard(); + } + if let Some(video_privacy_conn_id) = privacy_mode::get_privacy_mode_conn_id() { if video_privacy_conn_id == id { let _ = Self::turn_off_privacy_to_msg(id); @@ -869,7 +1003,14 @@ impl Connection { } #[cfg(target_os = "linux")] clear_remapped_keycode(); - log::info!("Input thread exited"); + log::debug!("Input thread exited"); + } + + async fn post_seq_loop(mut rx: mpsc::UnboundedReceiver<(String, Value)>) { + while let Some((url, v)) = rx.recv().await { + allow_err!(Self::post_audit_async(url, v).await); + } + log::debug!("post_seq_loop exited"); } async fn try_port_forward_loop( @@ -1026,9 +1167,23 @@ impl Connection { v["uuid"] = json!(crate::encode64(hbb_common::get_uuid())); v["conn_id"] = json!(self.inner.id); v["session_id"] = json!(self.lr.session_id); - tokio::spawn(async move { - allow_err!(Self::post_audit_async(url, v).await); - }); + allow_err!(self.tx_post_seq.send((url, v))); + } + + fn get_files_for_audit(job_type: fs::JobType, mut files: Vec) -> Vec<(String, i64)> { + files + .drain(..) + .map(|f| { + ( + if job_type == fs::JobType::Printer { + "Remote print".to_owned() + } else { + f.name + }, + f.size as _, + ) + }) + .collect() } fn post_file_audit( @@ -1130,6 +1285,10 @@ impl Connection { (1, AuthConnType::FileTransfer) } else if self.port_forward_socket.is_some() { (2, AuthConnType::PortForward) + } else if self.view_camera { + (3, AuthConnType::ViewCamera) + } else if self.terminal { + (4, AuthConnType::Terminal) } else { (0, AuthConnType::Remote) }; @@ -1137,6 +1296,8 @@ impl Connection { self.inner.id(), auth_conn_type, self.session_key(), + self.tx_from_authed.clone(), + self.lr.clone(), )); self.session_last_recv_time = SESSIONS .lock() @@ -1157,8 +1318,8 @@ impl Connection { #[cfg(not(target_os = "android"))] { - pi.hostname = whoami::hostname(); - pi.platform = whoami::platform().to_string(); + pi.hostname = hbb_common::whoami::hostname(); + pi.platform = hbb_common::whoami::platform().to_string(); } #[cfg(target_os = "android")] { @@ -1200,15 +1361,27 @@ impl Connection { ); } - #[cfg(any( - target_os = "windows", - all( - any(target_os = "linux", target_os = "macos"), - feature = "unix-file-copy-paste" - ) - ))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + { + let is_both_windows = cfg!(target_os = "windows") + && self.lr.my_platform == hbb_common::whoami::Platform::Windows.to_string(); + #[cfg(feature = "unix-file-copy-paste")] + let is_unix_and_peer_supported = crate::is_support_file_copy_paste(&self.lr.version); + #[cfg(not(feature = "unix-file-copy-paste"))] + let is_unix_and_peer_supported = false; + let is_both_macos = cfg!(target_os = "macos") + && self.lr.my_platform == hbb_common::whoami::Platform::MacOS.to_string(); + let is_peer_support_paste_if_macos = + crate::is_support_file_paste_if_macos(&self.lr.version); + let has_file_clipboard = is_both_windows + || (is_unix_and_peer_supported + && (!is_both_macos || is_peer_support_paste_if_macos)); + platform_additions.insert("has_file_clipboard".into(), json!(has_file_clipboard)); + } + + #[cfg(any(target_os = "windows", target_os = "linux"))] { - platform_additions.insert("has_file_clipboard".into(), json!(true)); + platform_additions.insert("support_view_camera".into(), json!(true)); } #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] @@ -1224,7 +1397,7 @@ impl Connection { return; } #[cfg(target_os = "linux")] - if !self.file_transfer.is_some() && !self.port_forward_socket.is_some() { + if self.is_remote() { let mut msg = "".to_string(); if crate::platform::linux::is_login_screen_wayland() { msg = crate::client::LOGIN_SCREEN_WAYLAND.to_owned() @@ -1255,7 +1428,8 @@ impl Connection { } #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.file_transfer.is_some() { - if crate::platform::is_prelogin() || self.tx_to_cm.send(ipc::Data::Test).is_err() { + if crate::platform::is_prelogin() { + // }|| self.tx_to_cm.send(ipc::Data::Test).is_err() { username = "".to_owned(); } } @@ -1266,10 +1440,19 @@ impl Connection { .unwrap() .insert(self.lr.my_id.clone(), self.tx_input.clone()); + // Terminal feature is supported on desktop only + #[allow(unused_mut)] + let mut terminal = cfg!(not(any(target_os = "android", target_os = "ios"))); + #[cfg(target_os = "windows")] + { + terminal = terminal && portable_pty::win::check_support().is_ok(); + } pi.username = username; pi.sas_enabled = sas_enabled; pi.features = Some(Features { privacy_mode: privacy_mode::is_privacy_mode_supported(), + #[cfg(not(any(target_os = "android", target_os = "ios")))] + terminal, ..Default::default() }) .into(); @@ -1278,9 +1461,34 @@ impl Connection { #[allow(unused_mut)] let mut wait_session_id_confirm = false; #[cfg(windows)] - self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); - if self.file_transfer.is_some() { + if !self.terminal { + self.handle_windows_specific_session(&mut pi, &mut wait_session_id_confirm); + } + if self.file_transfer.is_some() || self.terminal { res.set_peer_info(pi); + } else if self.view_camera { + let supported_encoding = scrap::codec::Encoder::supported_encoding(); + self.last_supported_encoding = Some(supported_encoding.clone()); + log::info!("peer info supported_encoding: {:?}", supported_encoding); + pi.encoding = Some(supported_encoding).into(); + + pi.displays = camera::Cameras::all_info().unwrap_or(Vec::new()); + pi.current_display = camera::PRIMARY_CAMERA_IDX as _; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + pi.resolutions = Some(SupportedResolutions { + resolutions: camera::Cameras::get_camera_resolution( + pi.current_display as usize, + ) + .ok() + .into_iter() + .collect(), + ..Default::default() + }) + .into(); + } + res.set_peer_info(pi); + self.update_codec_on_login(); } else { let supported_encoding = scrap::codec::Encoder::supported_encoding(); self.last_supported_encoding = Some(supported_encoding.clone()); @@ -1348,15 +1556,42 @@ impl Connection { } else { self.delayed_read_dir = Some((dir.to_owned(), show_hidden)); } + } else if self.terminal { + self.keyboard = false; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + self.init_terminal_service().await; + } else if self.view_camera { + if !wait_session_id_confirm { + self.try_sub_camera_displays(); + } + self.keyboard = false; + self.send_permission(Permission::Keyboard, false).await; } else if sub_service { if !wait_session_id_confirm { - self.try_sub_services(); + self.try_sub_monitor_services(); } } } - fn try_sub_services(&mut self) { - let is_remote = self.file_transfer.is_none() && self.port_forward_socket.is_none(); + fn try_sub_camera_displays(&mut self) { + if let Some(s) = self.server.upgrade() { + let mut s = s.write().unwrap(); + + s.try_add_primary_camera_service(); + s.add_camera_connection(self.inner.clone()); + } + } + + #[inline] + fn is_remote(&self) -> bool { + self.file_transfer.is_none() + && self.port_forward_socket.is_none() + && !self.view_camera + && !self.terminal + } + + fn try_sub_monitor_services(&mut self) { + let is_remote = self.is_remote(); if is_remote && !self.services_subed { self.services_subed = true; if let Some(s) = self.server.upgrade() { @@ -1373,6 +1608,10 @@ impl Connection { if !self.can_sub_clipboard_service() { noperms.push(super::clipboard_service::NAME); } + #[cfg(feature = "unix-file-copy-paste")] + if !self.can_sub_file_clipboard_service() { + noperms.push(super::clipboard_service::FILE_NAME); + } if !self.audio_enabled() { noperms.push(super::audio_service::NAME); } @@ -1396,7 +1635,7 @@ impl Connection { if let Some(current_sid) = crate::platform::get_current_process_session_id() { if crate::platform::is_installed() && crate::platform::is_share_rdp() - && raii::AuthedConnID::remote_and_file_conn_count() == 1 + && raii::AuthedConnID::non_port_forward_conn_count() == 1 && sessions.len() > 1 && sessions.iter().any(|e| e.sid == current_sid) && get_version_number(&self.lr.version) >= get_version_number("1.2.4") @@ -1453,15 +1692,24 @@ impl Connection { self.audio && !self.disable_audio } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] fn file_transfer_enabled(&self) -> bool { self.file && self.enable_file_transfer } + #[cfg(feature = "unix-file-copy-paste")] + fn can_sub_file_clipboard_service(&self) -> bool { + self.clipboard_enabled() + && self.file_transfer_enabled() + && crate::get_builtin_option(keys::OPTION_ONE_WAY_FILE_TRANSFER) != "Y" + } + fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) { self.send_to_cm(ipc::Data::Login { id: self.inner.id(), is_file_transfer: self.file_transfer.is_some(), + is_view_camera: self.view_camera, + is_terminal: self.terminal, port_forward: self.port_forward_address.clone(), peer_id, name, @@ -1668,7 +1916,7 @@ impl Connection { ) .await { - log::error!("ipc to connection manager exit: {}", err); + log::warn!("ipc to connection manager exit: {}", err); // https://github.com/rustdesk/rustdesk-server-pro/discussions/382#discussioncomment-10525725, cm may start failed #[cfg(windows)] if !crate::platform::is_prelogin() @@ -1689,6 +1937,16 @@ impl Connection { } async fn on_message(&mut self, msg: Message) -> bool { + if let Some(message::Union::Misc(misc)) = &msg.union { + // Move the CloseReason forward, as this message needs to be received when unauthorized, especially for kcp. + if let Some(misc::Union::CloseReason(s)) = &misc.union { + log::info!("receive close reason: {}", s); + self.on_close("Peer close", true).await; + raii::AuthedConnID::check_remove_session(self.inner.id(), self.session_key()); + return false; + } + } + // After handling CloseReason messages, proceed to process other message types if let Some(message::Union::LoginRequest(lr)) = msg.union { self.handle_login_request_without_validation(&lr).await; if self.authorized { @@ -1704,6 +1962,63 @@ impl Connection { } self.file_transfer = Some((ft.dir, ft.show_hidden)); } + Some(login_request::Union::ViewCamera(_vc)) => { + if !Connection::permission(keys::OPTION_ENABLE_CAMERA) { + self.send_login_error("No permission of viewing camera") + .await; + sleep(1.).await; + return false; + } + self.view_camera = true; + } + Some(login_request::Union::Terminal(terminal)) => { + if !Connection::permission(keys::OPTION_ENABLE_TERMINAL) { + self.send_login_error("No permission of terminal").await; + sleep(1.).await; + return false; + } + #[cfg(target_os = "windows")] + if !lr.os_login.username.is_empty() && !crate::platform::is_installed() { + self.send_login_error("Supported only in the installed version.") + .await; + sleep(1.).await; + return false; + } + + self.terminal = true; + if let Some(o) = self.options_in_login.as_ref() { + self.terminal_persistent = + o.terminal_persistent.enum_value() == Ok(BoolOption::Yes); + } + self.terminal_service_id = terminal.service_id; + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(msg) = + self.fill_terminal_user_token(&lr.os_login.username, &lr.os_login.password) + { + self.send_login_error(msg).await; + sleep(1.).await; + return false; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Some(is_user) = + terminal_service::is_service_specified_user(&self.terminal_service_id) + { + if let Some(user_token) = &self.terminal_user_token { + let has_service_token = + user_token.to_terminal_service_token().is_some(); + if is_user != has_service_token { + // This occurs when the service id (in the configuration) is manually changed by the user, causing a mismatch in validation. + log::error!("Terminal service user mismatch detected. The service ID may have been manually changed in the configuration, causing validation to fail."); + // No need to translate the following message, because it is in an abnormal case. + self.send_login_error("Terminal service user mismatch detected.") + .await; + sleep(1.).await; + return false; + } + } + } + } Some(login_request::Union::PortForward(mut pf)) => { if !Connection::permission("enable-tunnel") { self.send_login_error("No permission of IP tunneling").await; @@ -1762,6 +2077,27 @@ impl Connection { return true; } + // https://github.com/rustdesk/rustdesk-server-pro/discussions/646 + // `is_logon` is used to check login with `OPTION_ALLOW_LOGON_SCREEN_PASSWORD` == "Y". + // `is_logon_ui()` is used on Windows, because there's no good way to detect `is_locked()`. + // Detecting `is_logon_ui()` (if `LogonUI.exe` running) is a workaround. + #[cfg(target_os = "windows")] + let is_logon = || { + crate::platform::is_prelogin() || { + match crate::platform::is_logon_ui() { + Ok(result) => result, + Err(e) => { + log::error!("Failed to detect logon UI: {:?}", e); + false + } + } + } + }; + #[cfg(any(target_os = "linux", target_os = "macos"))] + let is_logon = || crate::platform::is_prelogin() || crate::platform::is_locked(); + #[cfg(any(target_os = "android", target_os = "ios"))] + let is_logon = || crate::platform::is_prelogin(); + if !hbb_common::is_ip_str(&lr.username) && !hbb_common::is_domain_port_str(&lr.username) && lr.username != Config::get_id() @@ -1770,8 +2106,8 @@ impl Connection { .await; return false; } else if (password::approve_mode() == ApproveMode::Click - && !(crate::platform::is_prelogin() - && crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y")) + && !(crate::get_builtin_option(keys::OPTION_ALLOW_LOGON_SCREEN_PASSWORD) == "Y" + && is_logon())) || password::approve_mode() == ApproveMode::Both && !password::has_valid_password() { self.try_start_cm(lr.my_id, lr.my_name, false); @@ -1877,7 +2213,6 @@ impl Connection { .user_network_delay(self.inner.id(), new_delay); self.network_delay = new_delay; } - self.delay_response_instant = Instant::now(); } } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union { #[cfg(feature = "flutter")] @@ -1911,6 +2246,9 @@ impl Connection { match msg.union { #[allow(unused_mut)] Some(message::Union::MouseEvent(mut me)) => { + if self.is_authed_view_camera_conn() { + return true; + } #[cfg(any(target_os = "android", target_os = "ios"))] if let Err(e) = call_main_service_pointer_input("mouse", me.mask, me.x, me.y) { log::debug!("call_main_service_pointer_input fail:{}", e); @@ -1929,6 +2267,9 @@ impl Connection { self.update_auto_disconnect_timer(); } Some(message::Union::PointerDeviceEvent(pde)) => { + if self.is_authed_view_camera_conn() { + return true; + } #[cfg(any(target_os = "android", target_os = "ios"))] if let Err(e) = match pde.union { Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union { @@ -1968,6 +2309,9 @@ impl Connection { Some(message::Union::KeyEvent(..)) => {} #[cfg(any(target_os = "android"))] Some(message::Union::KeyEvent(mut me)) => { + if self.is_authed_view_camera_conn() { + return true; + } let key = match me.mode.enum_value() { Ok(KeyboardMode::Map) => { Some(crate::keyboard::keycode_to_rdev_key(me.chr())) @@ -2020,6 +2364,9 @@ impl Connection { } #[cfg(not(any(target_os = "android", target_os = "ios")))] Some(message::Union::KeyEvent(me)) => { + if self.is_authed_view_camera_conn() { + return true; + } if self.peer_keyboard_enabled() { if is_enter(&me) { CLICK_TIME.store(get_time(), Ordering::SeqCst); @@ -2112,16 +2459,62 @@ impl Connection { #[cfg(target_os = "android")] crate::clipboard::handle_msg_multi_clipboards(_mcb); } - Some(message::Union::Cliprdr(_clip)) => - { - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - if let Some(clip) = msg_2_clip(_clip) { - log::debug!("got clipfile from client peer"); - self.send_to_cm(ipc::Data::ClipboardFile(clip)) + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + Some(message::Union::Cliprdr(clip)) => { + if let Some(clip) = msg_2_clip(clip) { + #[cfg(target_os = "windows")] + { + self.send_to_cm(ipc::Data::ClipboardFile(clip)); + } + #[cfg(feature = "unix-file-copy-paste")] + if crate::is_support_file_copy_paste(&self.lr.version) { + let mut out_msg = None; + + #[cfg(target_os = "macos")] + if clipboard::platform::unix::macos::should_handle_msg(&clip) { + if let Err(e) = clipboard::ContextSend::make_sure_enabled() { + log::error!("failed to restart clipboard context: {}", e); + } else { + let _ = + clipboard::ContextSend::proc(|context| -> ResultType<()> { + context + .server_clip_file(self.inner.id(), clip) + .map_err(|e| e.into()) + }); + } + } else { + out_msg = unix_file_clip::serve_clip_messages( + ClipboardSide::Host, + clip, + self.inner.id(), + ); + } + + #[cfg(not(target_os = "macos"))] + { + out_msg = unix_file_clip::serve_clip_messages( + ClipboardSide::Host, + clip, + self.inner.id(), + ); + } + + if let Some(msg) = out_msg { + self.send(msg).await; + } + } } } Some(message::Union::FileAction(fa)) => { - if self.file_transfer.is_some() { + let mut handle_fa = self.file_transfer.is_some(); + if !handle_fa { + if let Some(file_action::Union::Send(s)) = fa.union.as_ref() { + if JobType::from_proto(s.file_type) == JobType::Printer { + handle_fa = true; + } + } + } + if handle_fa { if self.delayed_read_dir.is_some() { if let Some(file_action::Union::ReadDir(rd)) = fa.union { self.delayed_read_dir = Some((rd.path, rd.include_hidden)); @@ -2178,10 +2571,34 @@ impl Connection { &self.lr.version, )); let path = s.path.clone(); + let r#type = JobType::from_proto(s.file_type); + let data_source; + match r#type { + JobType::Generic => { + data_source = + fs::DataSource::FilePath(PathBuf::from(&path)); + } + JobType::Printer => { + if let Some((_, _, data)) = self + .printer_data + .iter() + .position(|(_, p, _)| *p == path) + .map(|index| self.printer_data.remove(index)) + { + data_source = fs::DataSource::MemoryCursor( + std::io::Cursor::new(data), + ); + } else { + // Ignore this message if the printer data is not found + return true; + } + } + }; match fs::TransferJob::new_read( id, + r#type, "".to_string(), - path.clone(), + data_source, s.file_num, s.include_hidden, false, @@ -2193,19 +2610,21 @@ impl Connection { Ok(mut job) => { self.send(fs::new_dir(id, path, job.files().to_vec())) .await; - let mut files = job.files().to_owned(); + let files = job.files().to_owned(); job.is_remote = true; job.conn_id = self.inner.id(); + let job_type = job.r#type; self.read_jobs.push(job); self.file_timer = crate::rustdesk_interval(time::interval(MILLI1)); self.post_file_audit( FileAuditType::RemoteSend, - &s.path, - files - .drain(..) - .map(|f| (f.name, f.size as _)) - .collect(), + if job_type == fs::JobType::Printer { + "Remote print" + } else { + &s.path + }, + Self::get_files_for_audit(job_type, files), json!({}), ); } @@ -2236,11 +2655,7 @@ impl Connection { self.post_file_audit( FileAuditType::RemoteReceive, &r.path, - r.files - .to_vec() - .drain(..) - .map(|f| (f.name, f.size as _)) - .collect(), + Self::get_files_for_audit(fs::JobType::Generic, r.files), json!({}), ); self.file_transferred = true; @@ -2279,13 +2694,12 @@ impl Connection { } Some(file_action::Union::Cancel(c)) => { self.send_fs(ipc::FS::CancelWrite { id: c.id }); - if let Some(job) = fs::get_job_immutable(c.id, &self.read_jobs) { + if let Some(job) = fs::remove_job(c.id, &mut self.read_jobs) { self.send_to_cm(ipc::Data::FileTransferLog(( "transfer".to_string(), - fs::serialize_transfer_job(job, false, true, ""), + fs::serialize_transfer_job(&job, false, true, ""), ))); } - fs::remove_job(c.id, &mut self.read_jobs); } Some(file_action::Union::SendConfirm(r)) => { if let Some(job) = fs::get_job(r.id, &mut self.read_jobs) { @@ -2386,15 +2800,6 @@ impl Connection { Some(Instant::now().into()), ); } - Some(misc::Union::CloseReason(_)) => { - self.on_close("Peer close", true).await; - raii::AuthedConnID::check_remove_session( - self.inner.id(), - self.session_key(), - ); - return false; - } - Some(misc::Union::RestartRemoteDevice(_)) => { #[cfg(not(any(target_os = "android", target_os = "ios")))] if self.restart { @@ -2478,7 +2883,7 @@ impl Connection { let sessions = crate::platform::get_available_sessions(false); if crate::platform::is_installed() && crate::platform::is_share_rdp() - && raii::AuthedConnID::remote_and_file_conn_count() == 1 + && raii::AuthedConnID::non_port_forward_conn_count() == 1 && sessions.len() > 1 && current_process_sid != sid && sessions.iter().any(|e| e.sid == sid) @@ -2492,15 +2897,19 @@ impl Connection { if let Some((dir, show_hidden)) = self.delayed_read_dir.take() { self.read_dir(&dir, show_hidden); } - } else { - self.try_sub_services(); + } else if self.view_camera { + self.try_sub_camera_displays(); + } else if !self.terminal { + self.try_sub_monitor_services(); } } } Some(misc::Union::MessageQuery(mq)) => { - if let Some(msg_out) = - video_service::make_display_changed_msg(mq.switch_display as _, None) - { + if let Some(msg_out) = video_service::make_display_changed_msg( + mq.switch_display as _, + None, + self.video_source(), + ) { self.send(msg_out).await; } } @@ -2532,12 +2941,126 @@ impl Connection { Some(message::Union::VoiceCallResponse(_response)) => { // TODO: Maybe we can do a voice call from cm directly. } + Some(message::Union::ScreenshotRequest(request)) => { + if let Some(tx) = self.inner.tx.clone() { + crate::video_service::set_take_screenshot( + request.display as _, + request.sid.clone(), + tx, + ); + self.refresh_video_display(Some(request.display as usize)); + } + } + Some(message::Union::TerminalAction(action)) => { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(self.handle_terminal_action(action).await); + #[cfg(any(target_os = "android", target_os = "ios"))] + log::warn!("Terminal action received but not supported on this platform"); + } _ => {} } } true } + #[cfg(any(target_os = "linux", target_os = "macos"))] + fn fill_terminal_user_token( + &mut self, + _username: &str, + _password: &str, + ) -> Option<&'static str> { + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } + + // Try to fill user token for terminal connection. + // If username is empty, use the user token of the current session. + // If username is not empty, try to logon and check if the user is an administrator. + // If the user is an administrator, use the user token of current process (SYSTEM). + // If the user is not an administrator, return an error message. + // Note: Only local and domain users are supported, Microsoft account (online account) not supported for now. + #[cfg(target_os = "windows")] + fn fill_terminal_user_token(&mut self, username: &str, password: &str) -> Option<&'static str> { + // No need to check if the password is empty. + if !username.is_empty() { + return self.handle_administrator_check(username, password); + } + + if crate::platform::is_prelogin() { + self.terminal_user_token = None; + return Some("No active console user logged on, please connect and logon first."); + } + + if crate::platform::is_installed() { + return self.handle_installed_user(); + } + + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } + + #[cfg(target_os = "windows")] + fn handle_administrator_check( + &mut self, + username: &str, + password: &str, + ) -> Option<&'static str> { + let check_admin_res = + crate::platform::get_logon_user_token(username, password).map(|token| { + let is_token_admin = crate::platform::is_user_token_admin(token); + unsafe { + hbb_common::allow_err!(CloseHandle(HANDLE(token as _))); + }; + is_token_admin + }); + match check_admin_res { + Ok(Ok(b)) => { + if b { + self.terminal_user_token = Some(TerminalUserToken::SelfUser); + None + } else { + Some("The user is not an administrator.") + } + } + Ok(Err(e)) => { + log::error!("Failed to check if the user is an administrator: {}", e); + Some("Failed to check if the user is an administrator.") + } + Err(e) => { + log::error!("Failed to get logon user token: {}", e); + Some("Incorrect username or password.") + } + } + } + + #[cfg(target_os = "windows")] + fn handle_installed_user(&mut self) -> Option<&'static str> { + let session_id = crate::platform::get_current_session_id(true); + if session_id == 0xFFFFFFFF { + return Some("Failed to get current session id."); + } + let token = crate::platform::get_user_token(session_id, true); + if !token.is_null() { + match crate::platform::ensure_primary_token(token) { + Ok(t) => { + self.terminal_user_token = Some(TerminalUserToken::CurrentLogonUser(t as _)); + } + Err(e) => { + log::error!("Failed to ensure primary token: {}", e); + self.terminal_user_token = + Some(TerminalUserToken::CurrentLogonUser(token as _)); + } + } + None + } else { + log::error!( + "Failed to get user token for terminal action, {}", + std::io::Error::last_os_error() + ); + Some("Failed to get user token.") + } + } + fn update_failure(&self, (mut failure, time): ((i32, i32, i32), i32), remove: bool, i: usize) { if remove { if failure.0 != 0 { @@ -2599,7 +3122,7 @@ impl Connection { video_service::refresh(); self.server.upgrade().map(|s| { s.read().unwrap().set_video_service_opt( - display, + display.map(|d| (self.video_source(), d)), video_service::OPTION_REFRESH, super::service::SERVICE_OPTION_VALUE_TRUE, ); @@ -2629,19 +3152,33 @@ impl Connection { // 1. For compatibility with old versions ( < 1.2.4 ). // 2. Sciter version. // 3. Update `SupportedResolutions`. - if let Some(msg_out) = video_service::make_display_changed_msg(self.display_idx, None) { + if let Some(msg_out) = + video_service::make_display_changed_msg(self.display_idx, None, self.video_source()) + { self.send(msg_out).await; } } } + fn video_source(&self) -> VideoSource { + if self.view_camera { + VideoSource::Camera + } else { + VideoSource::Monitor + } + } + fn switch_display_to(&mut self, display_idx: usize, server: Arc>) { - let new_service_name = video_service::get_service_name(display_idx); - let old_service_name = video_service::get_service_name(self.display_idx); + let new_service_name = video_service::get_service_name(self.video_source(), display_idx); + let old_service_name = + video_service::get_service_name(self.video_source(), self.display_idx); let mut lock = server.write().unwrap(); if display_idx != *display_service::PRIMARY_DISPLAY_IDX { if !lock.contains(&new_service_name) { - lock.add_service(Box::new(video_service::new(display_idx))); + lock.add_service(Box::new(video_service::new( + self.video_source(), + display_idx, + ))); } } // For versions greater than 1.2.4, a `CaptureDisplays` message will be sent immediately. @@ -2676,26 +3213,27 @@ impl Connection { } async fn capture_displays(&mut self, add: &[usize], sub: &[usize], set: &[usize]) { + let video_source = self.video_source(); if let Some(sever) = self.server.upgrade() { let mut lock = sever.write().unwrap(); for display in add.iter() { - let service_name = video_service::get_service_name(*display); + let service_name = video_service::get_service_name(video_source, *display); if !lock.contains(&service_name) { - lock.add_service(Box::new(video_service::new(*display))); + lock.add_service(Box::new(video_service::new(video_source, *display))); } } for display in set.iter() { - let service_name = video_service::get_service_name(*display); + let service_name = video_service::get_service_name(video_source, *display); if !lock.contains(&service_name) { - lock.add_service(Box::new(video_service::new(*display))); + lock.add_service(Box::new(video_service::new(video_source, *display))); } } if !add.is_empty() { - lock.capture_displays(self.inner.clone(), add, true, false); + lock.capture_displays(self.inner.clone(), video_source, add, true, false); } else if !sub.is_empty() { - lock.capture_displays(self.inner.clone(), sub, false, true); + lock.capture_displays(self.inner.clone(), video_source, sub, false, true); } else { - lock.capture_displays(self.inner.clone(), set, true, true); + lock.capture_displays(self.inner.clone(), video_source, set, true, true); } self.multi_ui_session = lock.get_subbed_displays_count(self.inner.id()) > 1; if self.follow_remote_window { @@ -2781,10 +3319,18 @@ impl Connection { if virtual_display_manager::amyuni_idd::is_my_display(&name) { record_changed = false; } + #[cfg(not(target_os = "macos"))] + let scale = 1.0; + #[cfg(target_os = "macos")] + let scale = display.scale(); + let original = ( + ((display.width() as f64) / scale).round() as _, + (display.height() as f64 / scale).round() as _, + ); if record_changed { display_service::set_last_changed_resolution( &name, - (display.width() as _, display.height() as _), + original, (r.width, r.height), ); } @@ -2817,6 +3363,16 @@ impl Connection { self.send_to_cm(Data::CloseVoiceCall("".to_owned())); } self.send(msg).await; + self.voice_calling = accepted; + if self.is_authed_view_camera_conn() { + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled() && accepted, + ); + } + } } else { log::warn!("Possible a voice call attack."); } @@ -2826,6 +3382,14 @@ impl Connection { crate::audio_service::set_voice_call_input_device(None, true); // Notify the connection manager that the voice call has been closed. self.send_to_cm(Data::CloseVoiceCall("".to_owned())); + self.voice_calling = false; + if self.is_authed_view_camera_conn() { + if let Some(s) = self.server.upgrade() { + s.write() + .unwrap() + .subscribe(super::audio_service::NAME, self.inner.clone(), false); + } + } } async fn update_options(&mut self, o: &OptionMessage) { @@ -2902,21 +3466,44 @@ impl Connection { if q != BoolOption::NotSet { self.disable_audio = q == BoolOption::Yes; if let Some(s) = self.server.upgrade() { - s.write().unwrap().subscribe( - super::audio_service::NAME, - self.inner.clone(), - self.audio_enabled(), - ); + if self.is_authed_view_camera_conn() { + if self.voice_calling || !self.audio_enabled() { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled(), + ); + } + } else { + s.write().unwrap().subscribe( + super::audio_service::NAME, + self.inner.clone(), + self.audio_enabled(), + ); + } } } } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] if let Ok(q) = o.enable_file_transfer.enum_value() { if q != BoolOption::NotSet { self.enable_file_transfer = q == BoolOption::Yes; + #[cfg(target_os = "windows")] self.send_to_cm(ipc::Data::ClipboardFileEnabled( self.file_transfer_enabled(), )); + #[cfg(feature = "unix-file-copy-paste")] + if !self.enable_file_transfer { + self.try_empty_file_clipboard(); + } + #[cfg(feature = "unix-file-copy-paste")] + if let Some(s) = self.server.upgrade() { + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + self.inner.clone(), + self.can_sub_file_clipboard_service(), + ); + } } } if let Ok(q) = o.disable_clipboard.enum_value() { @@ -2940,6 +3527,12 @@ impl Connection { self.inner.clone(), self.can_sub_clipboard_service(), ); + #[cfg(feature = "unix-file-copy-paste")] + s.write().unwrap().subscribe( + super::clipboard_service::FILE_NAME, + self.inner.clone(), + self.can_sub_file_clipboard_service(), + ); s.write().unwrap().subscribe( NAME_CURSOR, self.inner.clone(), @@ -2990,6 +3583,12 @@ impl Connection { } } } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(q) = o.terminal_persistent.enum_value() { + if q != BoolOption::NotSet { + self.update_terminal_persistence(q == BoolOption::Yes).await; + } + } } async fn turn_on_privacy(&mut self, impl_key: String) { @@ -3181,11 +3780,7 @@ impl Connection { #[cfg(windows)] fn portable_check(&mut self) { - if self.portable.is_installed - || self.file_transfer.is_some() - || self.port_forward_socket.is_some() - || !self.keyboard - { + if self.portable.is_installed || !self.is_remote() || !self.keyboard { return; } let running = portable_client::running(); @@ -3322,6 +3917,143 @@ impl Connection { session_id: self.lr.session_id, } } + + fn is_authed_remote_conn(&self) -> bool { + if let Some(id) = self.authed_conn_id.as_ref() { + return id.conn_type() == AuthConnType::Remote; + } + false + } + + fn is_authed_view_camera_conn(&self) -> bool { + if let Some(id) = self.authed_conn_id.as_ref() { + return id.conn_type() == AuthConnType::ViewCamera; + } + false + } + + #[cfg(feature = "unix-file-copy-paste")] + async fn handle_file_clip(&mut self, clip: clipboard::ClipboardFile) { + let is_stopping_allowed = clip.is_stopping_allowed(); + let is_keyboard_enabled = self.peer_keyboard_enabled(); + let file_transfer_enabled = self.file_transfer_enabled(); + let stop = is_stopping_allowed && !file_transfer_enabled; + log::debug!( + "Process clipboard message from clip, stop: {}, is_stopping_allowed: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, file_transfer_enabled); + if !stop { + use hbb_common::config::keys::OPTION_ONE_WAY_FILE_TRANSFER; + // Note: Code will not reach here if `crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y"` is true. + // Because `file-clipboard` service will not be subscribed. + // But we still check it here to keep the same logic to windows version in `ui_cm_interface.rs`. + if clip.is_beginning_message() + && crate::get_builtin_option(OPTION_ONE_WAY_FILE_TRANSFER) == "Y" + { + // If one way file transfer is enabled, don't send clipboard file to client + } else { + // Maybe we should end the connection, because copy&paste files causes everything to wait. + allow_err!( + self.stream + .send(&crate::clipboard_file::clip_2_msg(clip)) + .await + ); + } + } + } + + #[inline] + #[cfg(feature = "unix-file-copy-paste")] + fn try_empty_file_clipboard(&mut self) { + try_empty_clipboard_files(ClipboardSide::Host, self.inner.id()); + } + + #[cfg(all(target_os = "windows", feature = "flutter"))] + async fn send_printer_request(&mut self, data: Vec) { + // This path is only used to identify the printer job. + let path = format!("RustDesk://FsJob//Printer/{}", get_time()); + + let msg = fs::new_send(0, fs::JobType::Printer, path.clone(), 1, false); + self.send(msg).await; + self.printer_data + .retain(|(t, _, _)| t.elapsed().as_secs() < 60); + self.printer_data.push((Instant::now(), path, data)); + } + + #[cfg(all(target_os = "windows", feature = "flutter"))] + async fn send_remote_printing_disallowed(&mut self) { + let mut msg_out = Message::new(); + let res = MessageBox { + msgtype: "custom-nook-nocancel-hasclose".to_owned(), + title: "remote-printing-disallowed-tile-tip".to_owned(), + text: "remote-printing-disallowed-text-tip".to_owned(), + link: "".to_owned(), + ..Default::default() + }; + msg_out.set_message_box(res); + self.send(msg_out).await; + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn update_terminal_persistence(&mut self, persistent: bool) { + self.terminal_persistent = persistent; + terminal_service::set_persistent(&self.terminal_service_id, persistent).ok(); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn init_terminal_service(&mut self) { + debug_assert!(self.terminal_user_token.is_some()); + let Some(user_token) = self.terminal_user_token.clone() else { + // unreachable, but keep it for safety + log::error!("Terminal user token is not set."); + return; + }; + if self.terminal_service_id.is_empty() { + self.terminal_service_id = terminal_service::generate_service_id(); + } + let s = Box::new(terminal_service::new( + self.terminal_service_id.clone(), + self.terminal_persistent, + user_token.to_terminal_service_token(), + )); + s.on_subscribe(self.inner.clone()); + self.terminal_generic_service = Some(s); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + async fn handle_terminal_action(&mut self, action: TerminalAction) -> ResultType<()> { + debug_assert!(self.terminal_user_token.is_some()); + let Some(user_token) = self.terminal_user_token.clone() else { + // unreacheable, but keep it for safety + bail!("Terminal user token is not set."); + }; + let mut proxy = terminal_service::TerminalServiceProxy::new( + self.terminal_service_id.clone(), + Some(self.terminal_persistent), + user_token.to_terminal_service_token(), + ); + + match proxy.handle_action(&action) { + Ok(Some(response)) => { + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + self.send(msg_out).await; + } + Ok(None) => { + // No response needed + } + Err(err) => { + let mut response = TerminalResponse::new(); + let mut error = TerminalError::new(); + error.message = format!("Failed to handle action: {}", err); + response.set_error(error); + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + self.send(msg_out).await; + } + } + + Ok(()) + } } pub fn insert_switch_sides_uuid(id: String, uuid: uuid::Uuid) { @@ -3657,6 +4389,19 @@ fn start_wakelock_thread() -> std::sync::mpsc::Sender<(usize, usize)> { tx } +#[cfg(all(target_os = "windows", feature = "flutter"))] +pub fn on_printer_data(data: Vec) { + crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.printer) + .next() + .map(|c| { + c.sender.send(Data::PrinterData(data)).ok(); + }); +} + #[cfg(windows)] pub struct PortableState { pub last_uac: bool, @@ -3681,6 +4426,19 @@ impl Drop for Connection { fn drop(&mut self) { #[cfg(not(any(target_os = "android", target_os = "ios")))] self.release_pressed_modifiers(); + + if let Some(s) = self.terminal_generic_service.as_ref() { + s.join(); + } + + #[cfg(target_os = "windows")] + if let Some(TerminalUserToken::CurrentLogonUser(token)) = self.terminal_user_token.take() { + if token != 0 { + unsafe { + hbb_common::allow_err!(CloseHandle(HANDLE(token as _))); + }; + } + } } } @@ -3790,8 +4548,15 @@ impl Retina { } } +pub struct AuthedConn { + pub conn_id: i32, + pub conn_type: AuthConnType, + pub session_key: SessionKey, + pub sender: mpsc::UnboundedSender, + pub printer: bool, +} + mod raii { - // CONN_COUNT: remote connection count in fact // ALIVE_CONNS: all connections, including unauthorized connections // AUTHED_CONNS: all authorized connections @@ -3809,27 +4574,41 @@ mod raii { fn drop(&mut self) { let mut active_conns_lock = ALIVE_CONNS.lock().unwrap(); active_conns_lock.retain(|&c| c != self.0); - video_service::VIDEO_QOS - .lock() - .unwrap() - .on_connection_close(self.0); } } pub struct AuthedConnID(i32, AuthConnType); impl AuthedConnID { - pub fn new(conn_id: i32, conn_type: AuthConnType, session_key: SessionKey) -> Self { - AUTHED_CONNS - .lock() - .unwrap() - .push((conn_id, conn_type, session_key)); + pub fn new( + conn_id: i32, + conn_type: AuthConnType, + session_key: SessionKey, + sender: mpsc::UnboundedSender, + lr: LoginRequest, + ) -> Self { + let printer = conn_type == crate::server::AuthConnType::Remote + && crate::is_support_remote_print(&lr.version) + && lr.my_platform == hbb_common::whoami::Platform::Windows.to_string(); + AUTHED_CONNS.lock().unwrap().push(AuthedConn { + conn_id, + conn_type, + session_key, + sender, + printer, + }); Self::check_wake_lock(); use std::sync::Once; static _ONCE: Once = Once::new(); _ONCE.call_once(|| { shutdown_hooks::add_shutdown_hook(connection_shutdown_hook); }); + if conn_type == AuthConnType::Remote || conn_type == AuthConnType::ViewCamera { + video_service::VIDEO_QOS + .lock() + .unwrap() + .on_connection_open(conn_id); + } Self(conn_id, conn_type) } @@ -3839,7 +4618,7 @@ mod raii { .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote) + .filter(|c| c.conn_type == AuthConnType::Remote) .count(); allow_err!(WAKELOCK_SENDER .lock() @@ -3847,12 +4626,12 @@ mod raii { .send((conn_count, remote_count))); } - pub fn remote_and_file_conn_count() -> usize { + pub fn non_port_forward_conn_count() -> usize { AUTHED_CONNS .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote || c.1 == AuthConnType::FileTransfer) + .filter(|c| c.conn_type != AuthConnType::PortForward) .count() } @@ -3865,16 +4644,16 @@ mod raii { .lock() .unwrap() .iter() - .any(|c| c.0 == conn_id && c.1 == AuthConnType::Remote); + .any(|c| c.conn_id == conn_id && c.conn_type == AuthConnType::Remote); // If there are 2 connections with the same peer_id and session_id, a remote connection and a file transfer or port forward connection, // If any of the connections is closed allowing retry, this will not be called; // If the file transfer/port forward connection is closed with no retry, the session should be kept for remote control menu action; // If the remote connection is closed with no retry, keep the session is not reasonable in case there is a retry button in the remote side, and ignore network fluctuations. - let another_remote = AUTHED_CONNS - .lock() - .unwrap() - .iter() - .any(|c| c.0 != conn_id && c.2 == key && c.1 == AuthConnType::Remote); + let another_remote = AUTHED_CONNS.lock().unwrap().iter().any(|c| { + c.conn_id != conn_id + && c.session_key == key + && c.conn_type == AuthConnType::Remote + }); if is_remote || !another_remote { lock.remove(&key); log::info!("remove session"); @@ -3927,19 +4706,27 @@ mod raii { ); } } + + pub fn conn_type(&self) -> AuthConnType { + self.1 + } } impl Drop for AuthedConnID { fn drop(&mut self) { - if self.1 == AuthConnType::Remote { + if self.1 == AuthConnType::Remote || self.1 == AuthConnType::ViewCamera { scrap::codec::Encoder::update(scrap::codec::EncodingUpdate::Remove(self.0)); + video_service::VIDEO_QOS + .lock() + .unwrap() + .on_connection_close(self.0); } - AUTHED_CONNS.lock().unwrap().retain(|c| c.0 != self.0); + AUTHED_CONNS.lock().unwrap().retain(|c| c.conn_id != self.0); let remote_count = AUTHED_CONNS .lock() .unwrap() .iter() - .filter(|c| c.1 == AuthConnType::Remote) + .filter(|c| c.conn_type == AuthConnType::Remote) .count(); if remote_count == 0 { #[cfg(any(target_os = "windows", target_os = "linux"))] @@ -3947,7 +4734,7 @@ mod raii { *WALLPAPER_REMOVER.lock().unwrap() = None; } #[cfg(not(any(target_os = "android", target_os = "ios")))] - display_service::reset_resolutions(); + display_service::restore_resolutions(); #[cfg(windows)] let _ = virtual_display_manager::reset_all(); #[cfg(target_os = "linux")] diff --git a/src/server/display_service.rs b/src/server/display_service.rs index 98b42a5face..6a52cbbea9e 100644 --- a/src/server/display_service.rs +++ b/src/server/display_service.rs @@ -133,12 +133,13 @@ pub fn set_last_changed_resolution(display_name: &str, original: (i32, i32), cha #[inline] #[cfg(not(any(target_os = "android", target_os = "ios")))] -pub fn reset_resolutions() { +pub fn restore_resolutions() { for (name, res) in CHANGED_RESOLUTIONS.read().unwrap().iter() { let (w, h) = res.original; + log::info!("Restore resolution of display '{}' to ({}, {})", name, w, h); if let Err(e) = crate::platform::change_resolution(name, w as _, h as _) { log::error!( - "Failed to reset resolution of display '{}' to ({},{}): {}", + "Failed to restore resolution of display '{}' to ({},{}): {}", name, w, h, @@ -146,7 +147,7 @@ pub fn reset_resolutions() { ); } } - // Can be cleared because reset resolutions is called when there is no client connected. + // Can be cleared because restore resolutions is called when there is no client connected. CHANGED_RESOLUTIONS.write().unwrap().clear(); } diff --git a/src/server/input_service.rs b/src/server/input_service.rs index c7f651e9ac7..1b0a248d4b6 100644 --- a/src/server/input_service.rs +++ b/src/server/input_service.rs @@ -457,7 +457,7 @@ static RECORD_CURSOR_POS_RUNNING: AtomicBool = AtomicBool::new(false); // We need to do some special handling for macOS when using the legacy mode. #[cfg(target_os = "macos")] static LAST_KEY_LEGACY_MODE: AtomicBool = AtomicBool::new(true); -// We use enigo to +// We use enigo to // 1. Simulate mouse events // 2. Simulate the legacy mode key events // 3. Simulate the functioin key events, like LockScreen @@ -501,8 +501,13 @@ pub fn try_start_record_cursor_pos() -> Option> { } pub fn try_stop_record_cursor_pos() { - let count_lock = CONN_COUNT.lock().unwrap(); - if *count_lock > 0 { + let remote_count = AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.conn_type == AuthConnType::Remote) + .count(); + if remote_count > 0 { return; } RECORD_CURSOR_POS_RUNNING.store(false, Ordering::SeqCst); @@ -659,10 +664,20 @@ fn is_pressed(key: &Key, en: &mut Enigo) -> bool { get_modifier_state(key.clone(), en) } +// Sleep for 8ms is enough in my tests, but we sleep 12ms to be safe. +// sleep 12ms In my test, the characters are already output in real time. #[inline] #[cfg(target_os = "macos")] fn key_sleep() { - std::thread::sleep(Duration::from_millis(20)); + // https://www.reddit.com/r/rustdesk/comments/1kn1w5x/typing_lags_when_connecting_to_macos_clients/ + // + // There's a strange bug when running by `launchctl load -w /Library/LaunchAgents/abc.plist` + // `std::thread::sleep(Duration::from_millis(20));` may sleep 90ms or more. + // Though `/Applications/RustDesk.app/Contents/MacOS/rustdesk --server` in terminal is ok. + let now = Instant::now(); + while now.elapsed() < Duration::from_millis(12) { + std::thread::sleep(Duration::from_millis(1)); + } } #[inline] @@ -686,8 +701,8 @@ fn get_modifier_state(key: Key, en: &mut Enigo) -> bool { pub fn handle_mouse(evt: &MouseEvent, conn: i32) { #[cfg(target_os = "macos")] - if !is_server() { - // having GUI, run main GUI thread, otherwise crash + { + // having GUI (--server has tray, it is GUI too), run main GUI thread, otherwise crash let evt = evt.clone(); QUEUE.exec_async(move || handle_mouse_(&evt, conn)); return; @@ -701,7 +716,7 @@ pub fn handle_mouse(evt: &MouseEvent, conn: i32) { // to-do: merge handle_mouse and handle_pointer pub fn handle_pointer(evt: &PointerDeviceEvent, conn: i32) { #[cfg(target_os = "macos")] - if !is_server() { + { // having GUI, run main GUI thread, otherwise crash let evt = evt.clone(); QUEUE.exec_async(move || handle_pointer_(&evt, conn)); @@ -1186,6 +1201,13 @@ pub fn handle_key(evt: &KeyEvent) { // having GUI, run main GUI thread, otherwise crash let evt = evt.clone(); QUEUE.exec_async(move || handle_key_(&evt)); + // Key sleep is required for macOS. + // If we don't sleep, the key press/release events may not take effect. + // + // For example, the controlled side osx `12.7.6` or `15.1.1` + // If we input characters quickly and continuously, and press or release "Shift" for a short period of time, + // it is possible that after releasing "Shift", the controlled side will still print uppercase characters. + // Though it is not very easy to reproduce. key_sleep(); } @@ -1200,11 +1222,7 @@ fn reset_input() { #[cfg(target_os = "macos")] pub fn reset_input_ondisconn() { - if !is_server() { - QUEUE.exec_async(reset_input); - } else { - reset_input(); - } + QUEUE.exec_async(reset_input); } fn sim_rdev_rawkey_position(code: KeyCode, keydown: bool) { diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs index ca86e48e792..4a4eaaad10f 100644 --- a/src/server/portable_service.rs +++ b/src/server/portable_service.rs @@ -15,7 +15,7 @@ use shared_memory::*; use std::{ mem::size_of, ops::{Deref, DerefMut}, - path::PathBuf, + path::Path, sync::{Arc, Mutex}, time::Duration, }; @@ -92,7 +92,7 @@ impl SharedMemory { } }; log::info!("Create shared memory, size: {}, flink: {}", size, flink); - set_path_permission(&PathBuf::from(flink), "F").ok(); + set_path_permission(Path::new(&flink), "F").ok(); Ok(SharedMemory { inner: shmem }) } @@ -586,8 +586,8 @@ pub mod client { let mut exe = std::env::current_exe()?.to_string_lossy().to_string(); #[cfg(feature = "flutter")] { - if let Some(dir) = PathBuf::from(&exe).parent() { - if set_path_permission(&PathBuf::from(dir), "RX").is_err() { + if let Some(dir) = Path::new(&exe).parent() { + if set_path_permission(Path::new(dir), "RX").is_err() { *SHMEM.lock().unwrap() = None; bail!("Failed to set permission of {:?}", dir); } @@ -717,7 +717,7 @@ pub mod client { } let frame_ptr = base.add(ADDR_CAPTURE_FRAME); let data = slice::from_raw_parts(frame_ptr, (*frame_info).length); - Ok(Frame::PixelBuffer(PixelBuffer::new( + Ok(Frame::PixelBuffer(PixelBuffer::with_BGRA( data, self.width, self.height, @@ -808,8 +808,13 @@ pub mod client { }, ConnCount(None) => { if !quick_support { - let cnt = crate::server::CONN_COUNT.lock().unwrap().clone(); - stream.send(&Data::DataPortableService(ConnCount(Some(cnt)))).await.ok(); + let remote_count = crate::server::AUTHED_CONNS + .lock() + .unwrap() + .iter() + .filter(|c| c.conn_type == crate::server::AuthConnType::Remote) + .count(); + stream.send(&Data::DataPortableService(ConnCount(Some(remote_count)))).await.ok(); } }, WillClose => { diff --git a/src/server/printer_service.rs b/src/server/printer_service.rs new file mode 100644 index 00000000000..edf5f3c1dbb --- /dev/null +++ b/src/server/printer_service.rs @@ -0,0 +1,163 @@ +use super::service::{EmptyExtraFieldService, GenericService, Service}; +use hbb_common::{bail, dlopen::symbor::Library, log, ResultType}; +use std::{ + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +pub const NAME: &'static str = "remote-printer"; + +const LIB_NAME_PRINTER_DRIVER_ADAPTER: &str = "printer_driver_adapter"; + +// Return 0 if success, otherwise return error code. +pub type Init = fn(tag_name: *const i8) -> i32; +pub type Uninit = fn(); +// dur_mills: Get the file generated in the last `dur_mills` milliseconds. +// data: The raw prn data, xps format. +// data_len: The length of the raw prn data. +pub type GetPrnData = fn(dur_mills: u32, data: *mut *mut i8, data_len: *mut u32); +// Free the prn data allocated by GetPrnData(). +pub type FreePrnData = fn(data: *mut i8); + +macro_rules! make_lib_wrapper { + ($($field:ident : $tp:ty),+) => { + struct LibWrapper { + _lib: Option, + $($field: Option<$tp>),+ + } + + impl LibWrapper { + fn new() -> Self { + let lib_name = match get_lib_name() { + Ok(name) => name, + Err(e) => { + log::warn!("Failed to get lib name, {}", e); + return Self { + _lib: None, + $( $field: None ),+ + }; + } + }; + let lib = match Library::open(&lib_name) { + Ok(lib) => Some(lib), + Err(e) => { + log::warn!("Failed to load library {}, {}", &lib_name, e); + None + } + }; + + $(let $field = if let Some(lib) = &lib { + match unsafe { lib.symbol::<$tp>(stringify!($field)) } { + Ok(m) => { + Some(*m) + }, + Err(e) => { + log::warn!("Failed to load func {}, {}", stringify!($field), e); + None + } + } + } else { + None + };)+ + + Self { + _lib: lib, + $( $field ),+ + } + } + } + + impl Default for LibWrapper { + fn default() -> Self { + Self::new() + } + } + } +} + +make_lib_wrapper!( + init: Init, + uninit: Uninit, + get_prn_data: GetPrnData, + free_prn_data: FreePrnData +); + +lazy_static::lazy_static! { + static ref LIB_WRAPPER: Arc> = Default::default(); +} + +fn get_lib_name() -> ResultType { + let exe_file = std::env::current_exe()?; + if let Some(cur_dir) = exe_file.parent() { + let dll_name = format!("{}.dll", LIB_NAME_PRINTER_DRIVER_ADAPTER); + let full_path = cur_dir.join(dll_name); + if !full_path.exists() { + bail!("{} not found", full_path.to_string_lossy().as_ref()); + } else { + Ok(full_path.to_string_lossy().into_owned()) + } + } else { + bail!( + "Invalid exe parent for {}", + exe_file.to_string_lossy().as_ref() + ); + } +} + +pub fn init(app_name: &str) -> ResultType<()> { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + let Some(fn_init) = lib_wrapper.init.as_ref() else { + bail!("Failed to load func init"); + }; + + let tag_name = std::ffi::CString::new(app_name)?; + let ret = fn_init(tag_name.as_ptr()); + if ret != 0 { + bail!("Failed to init printer driver"); + } + Ok(()) +} + +pub fn uninit() { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + if let Some(fn_uninit) = lib_wrapper.uninit.as_ref() { + fn_uninit(); + } +} + +fn get_prn_data(dur_mills: u32) -> ResultType> { + let lib_wrapper = LIB_WRAPPER.lock().unwrap(); + if let Some(fn_get_prn_data) = lib_wrapper.get_prn_data.as_ref() { + let mut data = std::ptr::null_mut(); + let mut data_len = 0u32; + fn_get_prn_data(dur_mills, &mut data, &mut data_len); + if data.is_null() || data_len == 0 { + return Ok(Vec::new()); + } + let bytes = + Vec::from(unsafe { std::slice::from_raw_parts(data as *const u8, data_len as usize) }); + lib_wrapper.free_prn_data.map(|f| f(data)); + Ok(bytes) + } else { + bail!("Failed to load func get_prn_file"); + } +} + +pub fn new(name: String) -> GenericService { + let svc = EmptyExtraFieldService::new(name, false); + GenericService::run(&svc.clone(), run); + svc.sp +} + +fn run(sp: EmptyExtraFieldService) -> ResultType<()> { + while sp.ok() { + let bytes = get_prn_data(1000)?; + if !bytes.is_empty() { + log::info!("Got prn data, data len: {}", bytes.len()); + crate::server::on_printer_data(bytes); + } + thread::sleep(Duration::from_millis(300)); + } + Ok(()) +} diff --git a/src/server/terminal_service.rs b/src/server/terminal_service.rs new file mode 100644 index 00000000000..945ae27bd09 --- /dev/null +++ b/src/server/terminal_service.rs @@ -0,0 +1,1136 @@ +use super::*; +use hbb_common::{ + anyhow::{anyhow, Context, Result}, + compress, +}; +use portable_pty::{Child, CommandBuilder, PtySize}; +use std::{ + collections::{HashMap, VecDeque}, + io::{Read, Write}, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{self, Receiver, SyncSender}, + Arc, Mutex, + }, + thread, + time::{Duration, Instant}, +}; + +const MAX_OUTPUT_BUFFER_SIZE: usize = 1024 * 1024; // 1MB per terminal +const MAX_BUFFER_LINES: usize = 10000; +const MAX_SERVICES: usize = 100; // Maximum number of persistent terminal services +const SERVICE_IDLE_TIMEOUT: Duration = Duration::from_secs(3600); // 1 hour idle timeout +const CHANNEL_BUFFER_SIZE: usize = 100; // Number of messages to buffer in channel +const COMPRESS_THRESHOLD: usize = 512; // Compress terminal data larger than this + +lazy_static::lazy_static! { + // Global registry of persistent terminal services indexed by service_id + static ref TERMINAL_SERVICES: Arc>>>> = + Arc::new(Mutex::new(HashMap::new())); + + // Cleanup task handle + static ref CLEANUP_TASK: Arc>>> = Arc::new(Mutex::new(None)); + + // List of terminal child processes to check for zombies + static ref TERMINAL_TASKS: Arc>>> = Arc::new(Mutex::new(Vec::new())); +} + +/// Service metadata that is sent to clients +#[derive(Clone, Debug)] +pub struct ServiceMetadata { + pub service_id: String, + pub created_at: Instant, + pub terminal_count: usize, + pub is_persistent: bool, +} + +/// Generate a new persistent service ID +pub fn generate_service_id() -> String { + format!("ts_{}", uuid::Uuid::new_v4()) +} + +fn get_default_shell() -> String { + #[cfg(target_os = "windows")] + { + // Try PowerShell Core first (cross-platform version) + // Common installation paths for PowerShell Core + let pwsh_paths = [ + "pwsh.exe", + r"C:\Program Files\PowerShell\7\pwsh.exe", + r"C:\Program Files\PowerShell\6\pwsh.exe", + ]; + + for path in &pwsh_paths { + if std::path::Path::new(path).exists() { + return path.to_string(); + } + } + + // Try Windows PowerShell (should be available on all Windows systems) + let powershell_path = r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"; + if std::path::Path::new(powershell_path).exists() { + return powershell_path.to_string(); + } + + // Final fallback to cmd.exe + std::env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string()) + } + #[cfg(not(target_os = "windows"))] + { + // First try the SHELL environment variable + if let Ok(shell) = std::env::var("SHELL") { + if !shell.is_empty() { + return shell; + } + } + + // Check for common shells in order of preference + let shells = ["/bin/bash", "/bin/zsh", "/bin/sh"]; + for shell in &shells { + if std::path::Path::new(shell).exists() { + return shell.to_string(); + } + } + + // Final fallback to /bin/sh which should exist on all POSIX systems + "/bin/sh".to_string() + } +} + +pub fn is_service_specified_user(service_id: &str) -> Option { + get_service(service_id).map(|s| s.lock().unwrap().is_specified_user) +} + +/// Get or create a persistent terminal service +fn get_or_create_service( + service_id: String, + is_persistent: bool, + is_specified_user: bool, +) -> Result>> { + let mut services = TERMINAL_SERVICES.lock().unwrap(); + + // Check service limit + if !services.contains_key(&service_id) && services.len() >= MAX_SERVICES { + return Err(anyhow!( + "Maximum number of terminal services ({}) reached", + MAX_SERVICES + )); + } + + let service = services + .entry(service_id.clone()) + .or_insert_with(|| { + log::info!( + "Creating new terminal service: {} (persistent: {})", + service_id, + is_persistent + ); + Arc::new(Mutex::new(PersistentTerminalService::new( + service_id.clone(), + is_persistent, + is_specified_user, + ))) + }) + .clone(); + + // Ensure cleanup task is running + ensure_cleanup_task(); + + service.lock().unwrap().reset_status(is_persistent); + + Ok(service) +} + +/// Remove a service from the global registry +fn remove_service(service_id: &str) { + let mut services = TERMINAL_SERVICES.lock().unwrap(); + if let Some(service) = services.remove(service_id) { + log::info!("Removed service: {}", service_id); + // Close all terminals in the service + let sessions = service.lock().unwrap().sessions.clone(); + for (_, session) in sessions.iter() { + let mut session = session.lock().unwrap(); + session.stop(); + } + } +} + +/// List all active terminal services +pub fn list_services() -> Vec { + let services = TERMINAL_SERVICES.lock().unwrap(); + services + .iter() + .filter_map(|(id, service)| { + service.lock().ok().map(|svc| ServiceMetadata { + service_id: id.clone(), + created_at: svc.created_at, + terminal_count: svc.sessions.len(), + is_persistent: svc.is_persistent, + }) + }) + .collect() +} + +/// Get service by ID +pub fn get_service(service_id: &str) -> Option>> { + let services = TERMINAL_SERVICES.lock().unwrap(); + services.get(service_id).cloned() +} + +/// Clean up inactive services +pub fn cleanup_inactive_services() { + let services = TERMINAL_SERVICES.lock().unwrap(); + let now = Instant::now(); + let mut to_remove = Vec::new(); + + for (service_id, service) in services.iter() { + if let Ok(svc) = service.lock() { + // Remove non-persistent services after idle timeout + if !svc.is_persistent && now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT { + to_remove.push(service_id.clone()); + log::info!("Cleaning up idle non-persistent service: {}", service_id); + } + // Remove persistent services with no active terminals after longer timeout + else if svc.is_persistent + && svc.sessions.is_empty() + && now.duration_since(svc.last_activity) > SERVICE_IDLE_TIMEOUT * 2 + { + to_remove.push(service_id.clone()); + log::info!("Cleaning up empty persistent service: {}", service_id); + } + } + } + + // Remove outside of iteration to avoid deadlock + drop(services); + for id in to_remove { + remove_service(&id); + } +} + +/// Add a child process to the zombie reaper +fn add_to_reaper(child: Box) { + if let Ok(mut tasks) = TERMINAL_TASKS.lock() { + tasks.push(child); + } +} + +/// Check and reap zombie terminal processes +fn check_zombie_terminals() { + let mut tasks = match TERMINAL_TASKS.lock() { + Ok(t) => t, + Err(_) => return, + }; + + let mut i = 0; + while i < tasks.len() { + match tasks[i].try_wait() { + Ok(Some(_)) => { + // Process has exited, remove it + log::info!("Process exited: {:?}", tasks[i].process_id()); + tasks.remove(i); + } + Ok(None) => { + // Still running + i += 1; + } + Err(err) => { + // Error checking status, remove it + log::info!( + "Process exited with error: {:?}, err: {err}", + tasks[i].process_id() + ); + tasks.remove(i); + } + } + } +} + +/// Ensure the cleanup task is running +fn ensure_cleanup_task() { + let mut task_handle = CLEANUP_TASK.lock().unwrap(); + if task_handle.is_none() { + let handle = std::thread::spawn(|| { + log::info!("Started cleanup task"); + let mut last_service_cleanup = Instant::now(); + loop { + // Check for zombie processes every 100ms + check_zombie_terminals(); + + // Check for inactive services every 5 minutes + if last_service_cleanup.elapsed() > Duration::from_secs(300) { + cleanup_inactive_services(); + last_service_cleanup = Instant::now(); + } + + std::thread::sleep(Duration::from_millis(100)); + } + }); + *task_handle = Some(handle); + } +} + +#[cfg(target_os = "linux")] +pub fn get_terminal_session_count(include_zombie_tasks: bool) -> usize { + let mut c = TERMINAL_SERVICES.lock().unwrap().len(); + if include_zombie_tasks { + c += TERMINAL_TASKS.lock().unwrap().len(); + } + c +} + +pub type UserToken = u64; + +#[derive(Clone)] +pub struct TerminalService { + sp: GenericService, + user_token: Option, +} + +impl Deref for TerminalService { + type Target = ServiceTmpl; + + fn deref(&self) -> &Self::Target { + &self.sp + } +} + +impl DerefMut for TerminalService { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.sp + } +} + +pub fn get_service_name(source: VideoSource, idx: usize) -> String { + format!("{}{}", source.service_name_prefix(), idx) +} + +pub fn new( + service_id: String, + is_persistent: bool, + user_token: Option, +) -> GenericService { + // Create the service with initial persistence setting + allow_err!(get_or_create_service( + service_id.clone(), + is_persistent, + user_token.is_some() + )); + let svc = TerminalService { + sp: GenericService::new(service_id.clone(), false), + user_token, + }; + GenericService::run(&svc.clone(), move |sp| run(sp, service_id.clone())); + svc.sp +} + +fn run(sp: TerminalService, service_id: String) -> ResultType<()> { + while sp.ok() { + let responses = TerminalServiceProxy::new(service_id.clone(), None, sp.user_token.clone()) + .read_outputs(); + for response in responses { + let mut msg_out = Message::new(); + msg_out.set_terminal_response(response); + sp.send(msg_out); + } + + thread::sleep(Duration::from_millis(30)); // Read at ~33fps for responsive terminal + } + + // Clean up non-persistent service when loop exits + if let Some(service) = get_service(&service_id) { + let should_remove = !service.lock().unwrap().is_persistent; + if should_remove { + remove_service(&service_id); + } + } + + Ok(()) +} + +/// Output buffer for terminal session +struct OutputBuffer { + lines: VecDeque>, + total_size: usize, + last_line_incomplete: bool, +} + +impl OutputBuffer { + fn new() -> Self { + Self { + lines: VecDeque::new(), + total_size: 0, + last_line_incomplete: false, + } + } + + fn append(&mut self, data: &[u8]) { + if data.is_empty() { + return; + } + + // Handle incomplete lines + let mut start = 0; + if self.last_line_incomplete { + if let Some(last_line) = self.lines.back_mut() { + // Find first newline in new data + if let Some(newline_pos) = data.iter().position(|&b| b == b'\n') { + last_line.extend_from_slice(&data[..=newline_pos]); + start = newline_pos + 1; + self.last_line_incomplete = false; + } else { + // Still no newline, append all + last_line.extend_from_slice(data); + self.total_size += data.len(); + return; + } + } + } + + // Process remaining data + let remaining = &data[start..]; + let ends_with_newline = remaining.last() == Some(&b'\n'); + + // Split by lines + let lines: Vec<&[u8]> = remaining.split(|&b| b == b'\n').collect(); + + for (i, line) in lines.iter().enumerate() { + if i == lines.len() - 1 && !ends_with_newline && !line.is_empty() { + // Last line without newline + self.last_line_incomplete = true; + } + + if !line.is_empty() || i < lines.len() - 1 { + let mut line_data = line.to_vec(); + if i < lines.len() - 1 || ends_with_newline { + line_data.push(b'\n'); + } + + self.total_size += line_data.len(); + self.lines.push_back(line_data); + } + } + + // Trim old data if buffer is too large + while self.total_size > MAX_OUTPUT_BUFFER_SIZE || self.lines.len() > MAX_BUFFER_LINES { + if let Some(removed) = self.lines.pop_front() { + self.total_size -= removed.len(); + } + } + } + + fn get_recent(&self, max_bytes: usize) -> Vec { + let mut result = Vec::new(); + let mut size = 0; + + // Get recent lines up to max_bytes + for line in self.lines.iter().rev() { + if size + line.len() > max_bytes { + break; + } + size += line.len(); + result.splice(0..0, line.iter().cloned()); + } + + result + } +} + +pub struct TerminalSession { + pub created_at: Instant, + last_activity: Instant, + pty_pair: Option, + child: Option>, + // Channel for sending input to the writer thread + input_tx: Option>>, + // Channel for receiving output from the reader thread + output_rx: Option>>, + exiting: Arc, + // Thread handles + reader_thread: Option>, + writer_thread: Option>, + output_buffer: OutputBuffer, + title: String, + pid: u32, + rows: u16, + cols: u16, + // Track if we've already sent the closed message + closed_message_sent: bool, + is_opened: bool, +} + +impl TerminalSession { + fn new(terminal_id: i32, rows: u16, cols: u16) -> Self { + Self { + created_at: Instant::now(), + last_activity: Instant::now(), + pty_pair: None, + child: None, + input_tx: None, + output_rx: None, + exiting: Arc::new(AtomicBool::new(false)), + reader_thread: None, + writer_thread: None, + output_buffer: OutputBuffer::new(), + title: format!("Terminal {}", terminal_id), + pid: 0, + rows, + cols, + closed_message_sent: false, + is_opened: false, + } + } + + fn update_activity(&mut self) { + self.last_activity = Instant::now(); + } + + // This helper function is to ensure that the threads are joined before the child process is dropped. + // Though this is not strictly necessary on macOS. + fn stop(&mut self) { + self.is_opened = false; + self.exiting.store(true, Ordering::SeqCst); + + // Drop the input channel to signal writer thread to exit + if let Some(input_tx) = self.input_tx.take() { + // Send a final newline to ensure the reader can read some data, and then exit. + // This is required on Windows and Linux. + // Although `self.pty_pair = None;` is called below, we can still send a final newline here. + if let Err(e) = input_tx.send(b"\r\n".to_vec()) { + log::warn!("Failed to send final newline to the terminal: {}", e); + } + drop(input_tx); + } + self.output_rx = None; + + // 1. Windows + // `pty_pair` uses pipe. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/win/conpty.rs#L16 + // `read()` may stuck at https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/filedescriptor/src/windows.rs#L345 + // We can close the pipe to signal the reader thread to exit. + // After https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/win/psuedocon.rs#L86, the reader reads `[27, 91, 63, 57, 48, 48, 49, 108, 27, 91, 63, 49, 48, 48, 52, 108]` in my tests. + // 2. Linux + // `pty_pair` uses `libc::openpty`. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/unix.rs#L32 + // We can also call the drop method first. https://github.com/rustdesk-org/wezterm/blob/80174f8009f41565f0fa8c66dab90d4f9211ae16/pty/src/unix.rs#L352 + // The reader will get [13, 10] after dropping the `pty_pair`. + // 3. macOS + // No stuck cases have been found so far, more testing is needed. + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + self.pty_pair = None; + } + + // Wait for threads to finish + // The reader thread should join before the writer thread on Windows. + if let Some(reader_thread) = self.reader_thread.take() { + let _ = reader_thread.join(); + } + + // The read can read the last "\r\n" after the writer thread (not the child process) exits + // on Linux in my tests. + // But we still send "\r\n" to the writer thread and let the reader thread exit first for safety. + if let Some(writer_thread) = self.writer_thread.take() { + let _ = writer_thread.join(); + } + + if let Some(mut child) = self.child.take() { + // Kill the process + let _ = child.kill(); + add_to_reaper(child); + } + } +} + +impl Drop for TerminalSession { + fn drop(&mut self) { + // Ensure child process is properly handled when session is dropped + self.stop(); + } +} + +/// Persistent terminal service that can survive connection drops +pub struct PersistentTerminalService { + service_id: String, + sessions: HashMap>>, + pub created_at: Instant, + last_activity: Instant, + pub is_persistent: bool, + needs_session_sync: bool, + is_specified_user: bool, +} + +impl PersistentTerminalService { + pub fn new(service_id: String, is_persistent: bool, is_specified_user: bool) -> Self { + Self { + service_id, + sessions: HashMap::new(), + created_at: Instant::now(), + last_activity: Instant::now(), + is_persistent, + needs_session_sync: false, + is_specified_user, + } + } + + fn update_activity(&mut self) { + self.last_activity = Instant::now(); + } + + /// Get list of terminal metadata + pub fn list_terminals(&self) -> Vec<(i32, String, u32, Instant)> { + self.sessions + .iter() + .map(|(id, session)| { + let s = session.lock().unwrap(); + (*id, s.title.clone(), s.pid, s.created_at) + }) + .collect() + } + + /// Get buffered output for a terminal + pub fn get_terminal_buffer(&self, terminal_id: i32, max_bytes: usize) -> Option> { + self.sessions.get(&terminal_id).map(|session| { + let session = session.lock().unwrap(); + session.output_buffer.get_recent(max_bytes) + }) + } + + /// Get terminal info for recovery + pub fn get_terminal_info(&self, terminal_id: i32) -> Option<(u16, u16, Vec)> { + self.sessions.get(&terminal_id).map(|session| { + let session = session.lock().unwrap(); + ( + session.rows, + session.cols, + session.output_buffer.get_recent(4096), + ) + }) + } + + /// Check if service has active terminals + pub fn has_active_terminals(&self) -> bool { + !self.sessions.is_empty() + } + + fn reset_status(&mut self, is_persistent: bool) { + self.is_persistent = is_persistent; + self.needs_session_sync = true; + for session in self.sessions.values() { + let mut session = session.lock().unwrap(); + session.is_opened = false; + } + } +} + +pub struct TerminalServiceProxy { + service_id: String, + is_persistent: bool, + #[cfg(target_os = "windows")] + user_token: Option, +} + +pub fn set_persistent(service_id: &str, is_persistent: bool) -> Result<()> { + if let Some(service) = get_service(service_id) { + service.lock().unwrap().is_persistent = is_persistent; + Ok(()) + } else { + Err(anyhow!("Service {} not found", service_id)) + } +} + +impl TerminalServiceProxy { + pub fn new( + service_id: String, + is_persistent: Option, + _user_token: Option, + ) -> Self { + // Get persistence from the service if it exists + let is_persistent = + is_persistent.unwrap_or(if let Some(service) = get_service(&service_id) { + service.lock().unwrap().is_persistent + } else { + false + }); + TerminalServiceProxy { + service_id, + is_persistent, + #[cfg(target_os = "windows")] + user_token: _user_token, + } + } + + pub fn get_service_id(&self) -> &str { + &self.service_id + } + + pub fn handle_action(&mut self, action: &TerminalAction) -> Result> { + let service = match get_service(&self.service_id) { + Some(s) => s, + None => { + let mut response = TerminalResponse::new(); + let mut error = TerminalError::new(); + error.message = format!("Terminal service {} not found", self.service_id); + response.set_error(error); + return Ok(Some(response)); + } + }; + service.lock().unwrap().update_activity(); + match &action.union { + Some(terminal_action::Union::Open(open)) => { + self.handle_open(&mut service.lock().unwrap(), open) + } + Some(terminal_action::Union::Resize(resize)) => { + let session = service + .lock() + .unwrap() + .sessions + .get(&resize.terminal_id) + .cloned(); + self.handle_resize(session, resize) + } + Some(terminal_action::Union::Data(data)) => { + let session = service + .lock() + .unwrap() + .sessions + .get(&data.terminal_id) + .cloned(); + self.handle_data(session, data) + } + Some(terminal_action::Union::Close(close)) => { + self.handle_close(&mut service.lock().unwrap(), close) + } + _ => Ok(None), + } + } + + fn handle_open( + &self, + service: &mut PersistentTerminalService, + open: &OpenTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + // Check if terminal already exists + if let Some(session_arc) = service.sessions.get(&open.terminal_id) { + // Reconnect to existing terminal + let mut session = session_arc.lock().unwrap(); + session.is_opened = true; + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Reconnected to existing terminal".to_string(); + opened.pid = session.pid; + opened.service_id = self.service_id.clone(); + if service.needs_session_sync { + if service.sessions.len() > 1 { + // No need to include the current terminal in the list. + // Because the `persistent_sessions` is used to restore the other sessions. + opened.persistent_sessions = service + .sessions + .keys() + .filter(|&id| *id != open.terminal_id) + .cloned() + .collect(); + } + service.needs_session_sync = false; + } + response.set_opened(opened); + + // Send buffered output + let buffer = session.output_buffer.get_recent(4096); + if !buffer.is_empty() { + // We'll need to send this separately or extend the protocol + // For now, just acknowledge the reconnection + } + + return Ok(Some(response)); + } + + // Create new terminal session + log::info!( + "Creating new terminal {} for service: {}", + open.terminal_id, + service.service_id + ); + let mut session = + TerminalSession::new(open.terminal_id, open.rows as u16, open.cols as u16); + + let pty_size = PtySize { + rows: open.rows as u16, + cols: open.cols as u16, + pixel_width: 0, + pixel_height: 0, + }; + + log::debug!("Opening PTY with size: {}x{}", open.rows, open.cols); + let pty_system = portable_pty::native_pty_system(); + let pty_pair = pty_system.openpty(pty_size).context("Failed to open PTY")?; + + // Use default shell for the platform + let shell = get_default_shell(); + log::debug!("Using shell: {}", shell); + + #[allow(unused_mut)] + let mut cmd = CommandBuilder::new(&shell); + + #[cfg(target_os = "windows")] + if let Some(token) = &self.user_token { + cmd.set_user_token(*token as _); + } + + log::debug!("Spawning shell process..."); + let child = pty_pair + .slave + .spawn_command(cmd) + .context("Failed to spawn command")?; + + let writer = pty_pair + .master + .take_writer() + .context("Failed to get writer")?; + + let reader = pty_pair + .master + .try_clone_reader() + .context("Failed to get reader")?; + + session.pid = child.process_id().unwrap_or(0) as u32; + + // Create channels for input/output + let (input_tx, input_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + let (output_tx, output_rx) = mpsc::sync_channel::>(CHANNEL_BUFFER_SIZE); + + // Spawn writer thread + let terminal_id = open.terminal_id; + let writer_thread = thread::spawn(move || { + let mut writer = writer; + // Write initial carriage return: + // 1. Windows requires at least one carriage return for `drop()` to work properly. + // Without this, the reader may fail to read the buffer after `input_tx.send(b"\r\n".to_vec()).ok();`. + // 2. This also refreshes the terminal interface on the controlling side (workaround for blank content on connect). + if let Err(e) = writer.write_all(b"\r") { + log::error!("Terminal {} initial write error: {}", terminal_id, e); + } else { + if let Err(e) = writer.flush() { + log::error!("Terminal {} initial flush error: {}", terminal_id, e); + } + } + while let Ok(data) = input_rx.recv() { + if let Err(e) = writer.write_all(&data) { + log::error!("Terminal {} write error: {}", terminal_id, e); + break; + } + if let Err(e) = writer.flush() { + log::error!("Terminal {} flush error: {}", terminal_id, e); + } + } + log::debug!("Terminal {} writer thread exiting", terminal_id); + }); + + let exiting = session.exiting.clone(); + // Spawn reader thread + let terminal_id = open.terminal_id; + let reader_thread = thread::spawn(move || { + let mut reader = reader; + let mut buf = vec![0u8; 4096]; + loop { + match reader.read(&mut buf) { + Ok(0) => { + // EOF + // This branch can be reached when the child process exits on macOS. + // But not on Linux and Windows in my tests. + break; + } + Ok(n) => { + if exiting.load(Ordering::SeqCst) { + break; + } + let data = buf[..n].to_vec(); + // Try to send, if channel is full, drop the data + match output_tx.try_send(data) { + Ok(_) => {} + Err(mpsc::TrySendError::Full(_)) => { + log::debug!( + "Terminal {} output channel full, dropping data", + terminal_id + ); + } + Err(mpsc::TrySendError::Disconnected(_)) => { + log::debug!("Terminal {} output channel disconnected", terminal_id); + break; + } + } + } + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { + // This branch is not reached in my tests, but we still add `exiting` check to ensure we can exit. + if exiting.load(Ordering::SeqCst) { + break; + } + // For non-blocking I/O, sleep briefly + thread::sleep(Duration::from_millis(10)); + } + Err(e) => { + log::error!("Terminal {} read error: {}", terminal_id, e); + break; + } + } + } + log::debug!("Terminal {} reader thread exiting", terminal_id); + }); + + session.pty_pair = Some(pty_pair); + session.child = Some(child); + session.input_tx = Some(input_tx); + session.output_rx = Some(output_rx); + session.reader_thread = Some(reader_thread); + session.writer_thread = Some(writer_thread); + session.is_opened = true; + + let mut opened = TerminalOpened::new(); + opened.terminal_id = open.terminal_id; + opened.success = true; + opened.message = "Terminal opened".to_string(); + opened.pid = session.pid; + opened.service_id = service.service_id.clone(); + if service.needs_session_sync { + if !service.sessions.is_empty() { + opened.persistent_sessions = service.sessions.keys().cloned().collect(); + } + service.needs_session_sync = false; + } + response.set_opened(opened); + + log::info!( + "Terminal {} opened successfully with PID {}", + open.terminal_id, + session.pid + ); + + // Store the session + service + .sessions + .insert(open.terminal_id, Arc::new(Mutex::new(session))); + + Ok(Some(response)) + } + + fn handle_resize( + &self, + session: Option>>, + resize: &ResizeTerminal, + ) -> Result> { + if let Some(session_arc) = session { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + session.rows = resize.rows as u16; + session.cols = resize.cols as u16; + + if let Some(pty_pair) = &session.pty_pair { + pty_pair.master.resize(PtySize { + rows: resize.rows as u16, + cols: resize.cols as u16, + pixel_width: 0, + pixel_height: 0, + })?; + } + } + Ok(None) + } + + fn handle_data( + &self, + session: Option>>, + data: &TerminalData, + ) -> Result> { + if let Some(session_arc) = session { + let mut session = session_arc.lock().unwrap(); + session.update_activity(); + if let Some(input_tx) = &session.input_tx { + // Send data to writer thread + if let Err(e) = input_tx.send(data.data.to_vec()) { + log::error!( + "Failed to send data to terminal {}: {}", + data.terminal_id, + e + ); + } + } + } + + Ok(None) + } + + fn handle_close( + &self, + service: &mut PersistentTerminalService, + close: &CloseTerminal, + ) -> Result> { + let mut response = TerminalResponse::new(); + + // Always close and remove the terminal + if let Some(session_arc) = service.sessions.remove(&close.terminal_id) { + let mut session = session_arc.lock().unwrap(); + let exit_code = if let Some(mut child) = session.child.take() { + child.kill()?; + add_to_reaper(child); + -1 // -1 indicates forced termination + } else { + 0 + }; + + let mut closed = TerminalClosed::new(); + closed.terminal_id = close.terminal_id; + closed.exit_code = exit_code; + response.set_closed(closed); + Ok(Some(response)) + } else { + Ok(None) + } + } + + pub fn read_outputs(&self) -> Vec { + let service = match get_service(&self.service_id) { + Some(s) => s, + None => { + return vec![]; + } + }; + + // Get session references with minimal service lock time + let sessions: Vec<(i32, Arc>)> = { + let service = service.lock().unwrap(); + service + .sessions + .iter() + .map(|(id, session)| (*id, session.clone())) + .collect() + }; + + let mut responses = Vec::new(); + let mut closed_terminals = Vec::new(); + + // Process each session with its own lock + for (terminal_id, session_arc) in sessions { + if let Ok(mut session) = session_arc.try_lock() { + // Check if reader thread is still alive and we haven't sent closed message yet + let mut should_send_closed = false; + if !session.closed_message_sent { + if let Some(thread) = &session.reader_thread { + if thread.is_finished() { + should_send_closed = true; + session.closed_message_sent = true; + } + } + } + // It's Ok to put the closed message here. + // Because the `reader_thread` is joined in `stop()`, + // and `stop()` is called before the session is dropped. + if should_send_closed { + closed_terminals.push(terminal_id); + } + + if !session.is_opened { + // Skip the session if it is not opened. + continue; + } + + // Read from output channel + let mut has_activity = false; + let mut received_data = Vec::new(); + if let Some(output_rx) = &session.output_rx { + // Try to read all available data + while let Ok(data) = output_rx.try_recv() { + has_activity = true; + received_data.push(data); + } + } + + // Update buffer after reading + for data in &received_data { + session.output_buffer.append(data); + } + + // Process received data for responses + for data in received_data { + let mut response = TerminalResponse::new(); + let mut terminal_data = TerminalData::new(); + terminal_data.terminal_id = terminal_id; + + // Compress data if it exceeds threshold + if data.len() > COMPRESS_THRESHOLD { + let compressed = compress::compress(&data); + if compressed.len() < data.len() { + terminal_data.data = bytes::Bytes::from(compressed); + terminal_data.compressed = true; + } else { + // Compression didn't help, send uncompressed + terminal_data.data = bytes::Bytes::from(data); + } + } else { + terminal_data.data = bytes::Bytes::from(data); + } + + response.set_data(terminal_data); + responses.push(response); + } + + if has_activity { + session.update_activity(); + } + } + } + + // Clean up closed terminals (requires service lock briefly) + if !closed_terminals.is_empty() { + let mut sessions = service.lock().unwrap().sessions.clone(); + for terminal_id in closed_terminals { + let mut exit_code = 0; + + if !self.is_persistent { + if let Some(session_arc) = sessions.remove(&terminal_id) { + service.lock().unwrap().sessions.remove(&terminal_id); + let mut session = session_arc.lock().unwrap(); + // Take the child and add to zombie reaper + if let Some(mut child) = session.child.take() { + // Try to get exit code if available + if let Ok(Some(status)) = child.try_wait() { + exit_code = status.exit_code() as i32; + } + add_to_reaper(child); + } + } + } else { + // For persistent sessions, just clear the child reference + if let Some(session_arc) = sessions.get(&terminal_id) { + let mut session = session_arc.lock().unwrap(); + if let Some(mut child) = session.child.take() { + // Try to get exit code if available + if let Ok(Some(status)) = child.try_wait() { + exit_code = status.exit_code() as i32; + } + add_to_reaper(child); + } + } + } + + let mut response = TerminalResponse::new(); + let mut closed = TerminalClosed::new(); + closed.terminal_id = terminal_id; + closed.exit_code = exit_code; + response.set_closed(closed); + responses.push(response); + } + } + + responses + } + + /// Cleanup when connection drops + pub fn on_disconnect(&self) { + if !self.is_persistent { + // Remove non-persistent service + remove_service(&self.service_id); + } + } +} diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs index 5d3aeca85ea..344fc8548f9 100644 --- a/src/server/video_qos.rs +++ b/src/server/video_qos.rs @@ -1,287 +1,222 @@ use super::*; -use scrap::codec::Quality; -use std::time::Duration; +use scrap::codec::{Quality, BR_BALANCED, BR_BEST, BR_SPEED}; +use std::{ + collections::VecDeque, + time::{Duration, Instant}, +}; + +/* +FPS adjust: +a. new user connected =>set to INIT_FPS +b. TestDelay receive => update user's fps according to network delay + When network delay < DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and increase fps; + When network delay >= DELAY_THRESHOLD_150MS, set minimum fps according to image quality, and decrease fps; +c. second timeout / TestDelay receive => update real fps to the minimum fps from all users + +ratio adjust: +a. user set image quality => update to the maximum ratio of the latest quality +b. 3 seconds timeout => update ratio according to network delay + When network delay < DELAY_THRESHOLD_150MS, increase ratio, max 150kbps; + When network delay >= DELAY_THRESHOLD_150MS, decrease ratio; + +adjust betwen FPS and ratio: + When network delay < DELAY_THRESHOLD_150MS, fps is always higher than the minimum fps, and ratio is increasing; + When network delay >= DELAY_THRESHOLD_150MS, fps is always lower than the minimum fps, and ratio is decreasing; + +delay: + use delay minus RTT as the actual network delay +*/ + +// Constants pub const FPS: u32 = 30; pub const MIN_FPS: u32 = 1; pub const MAX_FPS: u32 = 120; -trait Percent { - fn as_percent(&self) -> u32; +pub const INIT_FPS: u32 = 15; + +// Bitrate ratio constants for different quality levels +const BR_MAX: f32 = 40.0; // 2000 * 2 / 100 +const BR_MIN: f32 = 0.2; +const BR_MIN_HIGH_RESOLUTION: f32 = 0.1; // For high resolution, BR_MIN is still too high, so we set a lower limit +const MAX_BR_MULTIPLE: f32 = 1.0; + +const HISTORY_DELAY_LEN: usize = 2; +const ADJUST_RATIO_INTERVAL: usize = 3; // Adjust quality ratio every 3 seconds +const DYNAMIC_SCREEN_THRESHOLD: usize = 2; // Allow increase quality ratio if encode more than 2 times in one second +const DELAY_THRESHOLD_150MS: u32 = 150; // 150ms is the threshold for good network condition + +#[derive(Default, Debug, Clone)] +struct UserDelay { + response_delayed: bool, + delay_history: VecDeque, + fps: Option, + rtt_calculator: RttCalculator, + quick_increase_fps_count: usize, + increase_fps_count: usize, } -impl Percent for ImageQuality { - fn as_percent(&self) -> u32 { - match self { - ImageQuality::NotSet => 0, - ImageQuality::Low => 50, - ImageQuality::Balanced => 66, - ImageQuality::Best => 100, +impl UserDelay { + fn add_delay(&mut self, delay: u32) { + self.rtt_calculator.update(delay); + if self.delay_history.len() > HISTORY_DELAY_LEN { + self.delay_history.pop_front(); } + self.delay_history.push_back(delay); } -} -#[derive(Default, Debug, Copy, Clone)] -struct Delay { - state: DelayState, - staging_state: DelayState, - delay: u32, - counter: u32, - slower_than_old_state: Option, + // Average delay minus RTT + fn avg_delay(&self) -> u32 { + let len = self.delay_history.len(); + if len > 0 { + let avg_delay = self.delay_history.iter().sum::() / len as u32; + + // If RTT is available, subtract it from average delay to get actual network latency + if let Some(rtt) = self.rtt_calculator.get_rtt() { + if avg_delay > rtt { + avg_delay - rtt + } else { + avg_delay + } + } else { + avg_delay + } + } else { + DELAY_THRESHOLD_150MS + } + } } -#[derive(Default, Debug, Copy, Clone)] +// User session data structure +#[derive(Default, Debug, Clone)] struct UserData { auto_adjust_fps: Option, // reserve for compatibility custom_fps: Option, quality: Option<(i64, Quality)>, // (time, quality) - delay: Option, - response_delayed: bool, + delay: UserDelay, record: bool, } +#[derive(Default, Debug, Clone)] +struct DisplayData { + send_counter: usize, // Number of times encode during period + support_changing_quality: bool, +} + +// Main QoS controller structure pub struct VideoQoS { fps: u32, - quality: Quality, + ratio: f32, users: HashMap, + displays: HashMap, bitrate_store: u32, - support_abr: HashMap, -} - -#[derive(PartialEq, Debug, Clone, Copy)] -enum DelayState { - Normal = 0, - LowDelay = 200, - HighDelay = 500, - Broken = 1000, -} - -impl Default for DelayState { - fn default() -> Self { - DelayState::Normal - } -} - -impl DelayState { - fn from_delay(delay: u32) -> Self { - if delay > DelayState::Broken as u32 { - DelayState::Broken - } else if delay > DelayState::HighDelay as u32 { - DelayState::HighDelay - } else if delay > DelayState::LowDelay as u32 { - DelayState::LowDelay - } else { - DelayState::Normal - } - } + adjust_ratio_instant: Instant, + abr_config: bool, + new_user_instant: Instant, } impl Default for VideoQoS { fn default() -> Self { VideoQoS { fps: FPS, - quality: Default::default(), + ratio: BR_BALANCED, users: Default::default(), + displays: Default::default(), bitrate_store: 0, - support_abr: Default::default(), + adjust_ratio_instant: Instant::now(), + abr_config: true, + new_user_instant: Instant::now(), } } } -#[derive(Debug, PartialEq, Eq)] -pub enum RefreshType { - SetImageQuality, -} - +// Basic functionality impl VideoQoS { + // Calculate seconds per frame based on current FPS pub fn spf(&self) -> Duration { Duration::from_secs_f32(1. / (self.fps() as f32)) } + // Get current FPS within valid range pub fn fps(&self) -> u32 { - if self.fps >= MIN_FPS && self.fps <= MAX_FPS { - self.fps + let fps = self.fps; + if fps >= MIN_FPS && fps <= MAX_FPS { + fps } else { FPS } } + // Store bitrate for later use pub fn store_bitrate(&mut self, bitrate: u32) { self.bitrate_store = bitrate; } + // Get stored bitrate pub fn bitrate(&self) -> u32 { self.bitrate_store } - pub fn quality(&self) -> Quality { - self.quality + // Get current bitrate ratio with bounds checking + pub fn ratio(&mut self) -> f32 { + if self.ratio < BR_MIN_HIGH_RESOLUTION || self.ratio > BR_MAX { + self.ratio = BR_BALANCED; + } + self.ratio } + // Check if any user is in recording mode pub fn record(&self) -> bool { self.users.iter().any(|u| u.1.record) } - pub fn set_support_abr(&mut self, display_idx: usize, support: bool) { - self.support_abr.insert(display_idx, support); + pub fn set_support_changing_quality(&mut self, video_service_name: &str, support: bool) { + if let Some(display) = self.displays.get_mut(video_service_name) { + display.support_changing_quality = support; + } } + // Check if variable bitrate encoding is supported and enabled pub fn in_vbr_state(&self) -> bool { - Config::get_option("enable-abr") != "N" && self.support_abr.iter().all(|e| *e.1) + self.abr_config && self.displays.iter().all(|e| e.1.support_changing_quality) } +} - pub fn refresh(&mut self, typ: Option) { - // fps - let user_fps = |u: &UserData| { - // custom_fps - let mut fps = u.custom_fps.unwrap_or(FPS); - // auto adjust fps - if let Some(auto_adjust_fps) = u.auto_adjust_fps { - if fps == 0 || auto_adjust_fps < fps { - fps = auto_adjust_fps; - } - } - // delay - if let Some(delay) = u.delay { - fps = match delay.state { - DelayState::Normal => fps, - DelayState::LowDelay => fps * 3 / 4, - DelayState::HighDelay => fps / 2, - DelayState::Broken => fps / 4, - } - } - // delay response - if u.response_delayed { - if fps > MIN_FPS + 2 { - fps = MIN_FPS + 2; - } - } - return fps; - }; - let mut fps = self - .users - .iter() - .map(|(_, u)| user_fps(u)) - .filter(|u| *u >= MIN_FPS) - .min() - .unwrap_or(FPS); - if fps > MAX_FPS { - fps = MAX_FPS; - } - self.fps = fps; - - // quality - // latest image quality - let latest_quality = self - .users - .iter() - .map(|(_, u)| u.quality) - .filter(|q| *q != None) - .max_by(|a, b| a.unwrap_or_default().0.cmp(&b.unwrap_or_default().0)) - .unwrap_or_default() - .unwrap_or_default() - .1; - let mut quality = latest_quality; +// User session management +impl VideoQoS { + // Initialize new user session + pub fn on_connection_open(&mut self, id: i32) { + self.users.insert(id, UserData::default()); + self.abr_config = Config::get_option("enable-abr") != "N"; + self.new_user_instant = Instant::now(); + } - // network delay - let abr_enabled = self.in_vbr_state(); - if abr_enabled && typ != Some(RefreshType::SetImageQuality) { - // max delay - let delay = self - .users - .iter() - .map(|u| u.1.delay) - .filter(|d| d.is_some()) - .max_by(|a, b| { - (a.unwrap_or_default().state as u32).cmp(&(b.unwrap_or_default().state as u32)) - }); - let delay = delay.unwrap_or_default().unwrap_or_default().state; - if delay != DelayState::Normal { - match self.quality { - Quality::Best => { - quality = if delay == DelayState::Broken { - Quality::Low - } else { - Quality::Balanced - }; - } - Quality::Balanced => { - quality = Quality::Low; - } - Quality::Low => { - quality = Quality::Low; - } - Quality::Custom(b) => match delay { - DelayState::LowDelay => { - quality = - Quality::Custom(if b >= 150 { 100 } else { std::cmp::min(50, b) }); - } - DelayState::HighDelay => { - quality = - Quality::Custom(if b >= 100 { 50 } else { std::cmp::min(25, b) }); - } - DelayState::Broken => { - quality = - Quality::Custom(if b >= 50 { 25 } else { std::cmp::min(10, b) }); - } - DelayState::Normal => {} - }, - } - } else { - match self.quality { - Quality::Low => { - if latest_quality == Quality::Best { - quality = Quality::Balanced; - } - } - Quality::Custom(current_b) => { - if let Quality::Custom(latest_b) = latest_quality { - if current_b < latest_b / 2 { - quality = Quality::Custom(latest_b / 2); - } - } - } - _ => {} - } - } + // Clean up user session + pub fn on_connection_close(&mut self, id: i32) { + self.users.remove(&id); + if self.users.is_empty() { + *self = Default::default(); } - self.quality = quality; } pub fn user_custom_fps(&mut self, id: i32, fps: u32) { - if fps < MIN_FPS { + if fps < MIN_FPS || fps > MAX_FPS { return; } if let Some(user) = self.users.get_mut(&id) { user.custom_fps = Some(fps); - } else { - self.users.insert( - id, - UserData { - custom_fps: Some(fps), - ..Default::default() - }, - ); } - self.refresh(None); } pub fn user_auto_adjust_fps(&mut self, id: i32, fps: u32) { + if fps < MIN_FPS || fps > MAX_FPS { + return; + } if let Some(user) = self.users.get_mut(&id) { user.auto_adjust_fps = Some(fps); - } else { - self.users.insert( - id, - UserData { - auto_adjust_fps: Some(fps), - ..Default::default() - }, - ); } - self.refresh(None); } pub fn user_image_quality(&mut self, id: i32, image_quality: i32) { - // https://github.com/rustdesk/rustdesk/blob/d716e2b40c38737f1aa3f16de0dec67394a6ac68/src/server/video_service.rs#L493 - let convert_quality = |q: i32| { + let convert_quality = |q: i32| -> Quality { if q == ImageQuality::Balanced.value() { Quality::Balanced } else if q == ImageQuality::Low.value() { @@ -289,103 +224,372 @@ impl VideoQoS { } else if q == ImageQuality::Best.value() { Quality::Best } else { - let mut b = (q >> 8 & 0xFFF) * 2; - b = std::cmp::max(b, 20); - b = std::cmp::min(b, 8000); - Quality::Custom(b as u32) + let b = ((q >> 8 & 0xFFF) * 2) as f32 / 100.0; + Quality::Custom(b.clamp(BR_MIN, BR_MAX)) } }; let quality = Some((hbb_common::get_time(), convert_quality(image_quality))); if let Some(user) = self.users.get_mut(&id) { user.quality = quality; - } else { - self.users.insert( - id, - UserData { - quality, - ..Default::default() - }, - ); + // update ratio directly + self.ratio = self.latest_quality().ratio(); + } + } + + pub fn user_record(&mut self, id: i32, v: bool) { + if let Some(user) = self.users.get_mut(&id) { + user.record = v; } - self.refresh(Some(RefreshType::SetImageQuality)); } pub fn user_network_delay(&mut self, id: i32, delay: u32) { - let state = DelayState::from_delay(delay); - let debounce = 3; + let highest_fps = self.highest_fps(); + let target_ratio = self.latest_quality().ratio(); + + // For bad network, small fps means quick reaction and high quality + let (min_fps, normal_fps) = if target_ratio >= BR_BEST { + (8, 16) + } else if target_ratio >= BR_BALANCED { + (10, 20) + } else { + (12, 24) + }; + + // Calculate minimum acceptable delay-fps product + let dividend_ms = DELAY_THRESHOLD_150MS * min_fps; + + let mut adjust_ratio = false; if let Some(user) = self.users.get_mut(&id) { - if let Some(d) = &mut user.delay { - d.delay = (delay + d.delay) / 2; - let new_state = DelayState::from_delay(d.delay); - let slower_than_old_state = new_state as i32 - d.staging_state as i32; - let slower_than_old_state = if slower_than_old_state > 0 { - Some(true) - } else if slower_than_old_state < 0 { - Some(false) + let delay = delay.max(10); + let old_avg_delay = user.delay.avg_delay(); + user.delay.add_delay(delay); + let mut avg_delay = user.delay.avg_delay(); + avg_delay = avg_delay.max(10); + let mut fps = self.fps; + + // Adaptive FPS adjustment based on network delay: + if avg_delay < 50 { + user.delay.quick_increase_fps_count += 1; + let mut step = if fps < normal_fps { 1 } else { 0 }; + if user.delay.quick_increase_fps_count >= 3 { + // After 3 consecutive good samples, increase more aggressively + user.delay.quick_increase_fps_count = 0; + step = 5; + } + fps = min_fps.max(fps + step); + } else if avg_delay < 100 { + let step = if avg_delay < old_avg_delay { + if fps < normal_fps { + 1 + } else { + 0 + } } else { - None + 0 }; - if d.slower_than_old_state == slower_than_old_state { - let old_counter = d.counter; - d.counter += delay / 1000 + 1; - if old_counter < debounce && d.counter >= debounce { - d.counter = 0; - d.state = d.staging_state; - d.staging_state = new_state; - } - if d.counter % debounce == 0 { - self.refresh(None); - } + fps = min_fps.max(fps + step); + } else if avg_delay < DELAY_THRESHOLD_150MS { + fps = min_fps.max(fps); + } else { + let devide_fps = ((fps as f32) / (avg_delay as f32 / DELAY_THRESHOLD_150MS as f32)) + .ceil() as u32; + if avg_delay < 200 { + fps = min_fps.max(devide_fps); + } else if avg_delay < 300 { + fps = min_fps.min(devide_fps); + } else if avg_delay < 600 { + fps = dividend_ms / avg_delay; } else { - d.counter = 0; - d.staging_state = new_state; - d.slower_than_old_state = slower_than_old_state; + fps = (dividend_ms / avg_delay).min(devide_fps); } + } + + if avg_delay < DELAY_THRESHOLD_150MS { + user.delay.increase_fps_count += 1; } else { - user.delay = Some(Delay { - state: DelayState::Normal, - staging_state: state, - delay, - counter: 0, - slower_than_old_state: None, - }); + user.delay.increase_fps_count = 0; } - } else { - self.users.insert( - id, - UserData { - delay: Some(Delay { - state: DelayState::Normal, - staging_state: state, - delay, - counter: 0, - slower_than_old_state: None, - }), - ..Default::default() - }, - ); + if user.delay.increase_fps_count >= 3 { + // After 3 stable samples, try increasing FPS + user.delay.increase_fps_count = 0; + fps += 1; + } + + // Reset quick increase counter if network condition worsens + if avg_delay > 50 { + user.delay.quick_increase_fps_count = 0; + } + + fps = fps.clamp(MIN_FPS, highest_fps); + // first network delay message + adjust_ratio = user.delay.fps.is_none(); + user.delay.fps = Some(fps); + } + self.adjust_fps(); + if adjust_ratio && !cfg!(target_os = "linux") { + //Reduce the possibility of vaapi being created twice + self.adjust_ratio(false); } } pub fn user_delay_response_elapsed(&mut self, id: i32, elapsed: u128) { if let Some(user) = self.users.get_mut(&id) { - let old = user.response_delayed; - user.response_delayed = elapsed > 3000; - if old != user.response_delayed { - self.refresh(None); + user.delay.response_delayed = elapsed > 2000; + if user.delay.response_delayed { + user.delay.add_delay(elapsed as u32); + self.adjust_fps(); } } } +} - pub fn user_record(&mut self, id: i32, v: bool) { - if let Some(user) = self.users.get_mut(&id) { - user.record = v; +// Common adjust functions +impl VideoQoS { + pub fn new_display(&mut self, video_service_name: String) { + self.displays + .insert(video_service_name, DisplayData::default()); + } + + pub fn remove_display(&mut self, video_service_name: &str) { + self.displays.remove(video_service_name); + } + + pub fn update_display_data(&mut self, video_service_name: &str, send_counter: usize) { + if let Some(display) = self.displays.get_mut(video_service_name) { + display.send_counter += send_counter; + } + self.adjust_fps(); + let abr_enabled = self.in_vbr_state(); + if abr_enabled { + if self.adjust_ratio_instant.elapsed().as_secs() >= ADJUST_RATIO_INTERVAL as u64 { + let dynamic_screen = self + .displays + .iter() + .any(|d| d.1.send_counter >= ADJUST_RATIO_INTERVAL * DYNAMIC_SCREEN_THRESHOLD); + self.displays.iter_mut().for_each(|d| { + d.1.send_counter = 0; + }); + self.adjust_ratio(dynamic_screen); + } + } else { + self.ratio = self.latest_quality().ratio(); } } - pub fn on_connection_close(&mut self, id: i32) { - self.users.remove(&id); - self.refresh(None); + #[inline] + fn highest_fps(&self) -> u32 { + let user_fps = |u: &UserData| { + let mut fps = u.custom_fps.unwrap_or(FPS); + if let Some(auto_adjust_fps) = u.auto_adjust_fps { + if fps == 0 || auto_adjust_fps < fps { + fps = auto_adjust_fps; + } + } + fps + }; + + let fps = self + .users + .iter() + .map(|(_, u)| user_fps(u)) + .filter(|u| *u >= MIN_FPS) + .min() + .unwrap_or(FPS); + + fps.clamp(MIN_FPS, MAX_FPS) + } + + // Get latest quality settings from all users + pub fn latest_quality(&self) -> Quality { + self.users + .iter() + .map(|(_, u)| u.quality) + .filter(|q| *q != None) + .max_by(|a, b| a.unwrap_or_default().0.cmp(&b.unwrap_or_default().0)) + .flatten() + .unwrap_or((0, Quality::Balanced)) + .1 + } + + // Adjust quality ratio based on network delay and screen changes + fn adjust_ratio(&mut self, dynamic_screen: bool) { + if !self.in_vbr_state() { + return; + } + // Get maximum delay from all users + let max_delay = self.users.iter().map(|u| u.1.delay.avg_delay()).max(); + let Some(max_delay) = max_delay else { + return; + }; + + let target_quality = self.latest_quality(); + let target_ratio = self.latest_quality().ratio(); + let current_ratio = self.ratio; + let current_bitrate = self.bitrate(); + + // Calculate minimum ratio for high resolution (1Mbps baseline) + let ratio_1mbps = if current_bitrate > 0 { + Some((current_ratio * 1000.0 / current_bitrate as f32).max(BR_MIN_HIGH_RESOLUTION)) + } else { + None + }; + + // Calculate ratio for adding 150kbps bandwidth + let ratio_add_150kbps = if current_bitrate > 0 { + Some((current_bitrate + 150) as f32 * current_ratio / current_bitrate as f32) + } else { + None + }; + + // Set minimum ratio based on quality mode + let min = match target_quality { + Quality::Best => { + // For Best quality, ensure minimum 1Mbps for high resolution + let mut min = BR_BEST / 2.5; + if let Some(ratio_1mbps) = ratio_1mbps { + if min > ratio_1mbps { + min = ratio_1mbps; + } + } + min.max(BR_MIN) + } + Quality::Balanced => { + let mut min = (BR_BALANCED / 2.0).min(0.4); + if let Some(ratio_1mbps) = ratio_1mbps { + if min > ratio_1mbps { + min = ratio_1mbps; + } + } + min.max(BR_MIN_HIGH_RESOLUTION) + } + Quality::Low => BR_MIN_HIGH_RESOLUTION, + Quality::Custom(_) => BR_MIN_HIGH_RESOLUTION, + }; + let max = target_ratio * MAX_BR_MULTIPLE; + + let mut v = current_ratio; + + // Adjust ratio based on network delay thresholds + if max_delay < 50 { + if dynamic_screen { + v = current_ratio * 1.15; + } + } else if max_delay < 100 { + if dynamic_screen { + v = current_ratio * 1.1; + } + } else if max_delay < DELAY_THRESHOLD_150MS { + if dynamic_screen { + v = current_ratio * 1.05; + } + } else if max_delay < 200 { + v = current_ratio * 0.95; + } else if max_delay < 300 { + v = current_ratio * 0.9; + } else if max_delay < 500 { + v = current_ratio * 0.85; + } else { + v = current_ratio * 0.8; + } + + // Limit quality increase rate for better stability + if let Some(ratio_add_150kbps) = ratio_add_150kbps { + if v > ratio_add_150kbps + && ratio_add_150kbps > current_ratio + && current_ratio >= BR_SPEED + { + v = ratio_add_150kbps; + } + } + + self.ratio = v.clamp(min, max); + self.adjust_ratio_instant = Instant::now(); + } + + // Adjust fps based on network delay and user response time + fn adjust_fps(&mut self) { + let highest_fps = self.highest_fps(); + // Get minimum fps from all users + let mut fps = self + .users + .iter() + .map(|u| u.1.delay.fps.unwrap_or(INIT_FPS)) + .min() + .unwrap_or(INIT_FPS); + + if self.users.iter().any(|u| u.1.delay.response_delayed) { + if fps > MIN_FPS + 1 { + fps = MIN_FPS + 1; + } + } + + // For new connections (within 1 second), cap fps to INIT_FPS to ensure stability + if self.new_user_instant.elapsed().as_secs() < 1 { + if fps > INIT_FPS { + fps = INIT_FPS; + } + } + + // Ensure fps stays within valid range + self.fps = fps.clamp(MIN_FPS, highest_fps); + } +} + +#[derive(Default, Debug, Clone)] +struct RttCalculator { + min_rtt: Option, // Historical minimum RTT ever observed + window_min_rtt: Option, // Minimum RTT within last 60 samples + smoothed_rtt: Option, // Smoothed RTT estimation + samples: VecDeque, // Last 60 RTT samples +} + +impl RttCalculator { + const WINDOW_SAMPLES: usize = 60; // Keep last 60 samples + const MIN_SAMPLES: usize = 10; // Require at least 10 samples + const ALPHA: f32 = 0.5; // Smoothing factor for weighted average + + /// Update RTT estimates with a new sample + pub fn update(&mut self, delay: u32) { + // 1. Update historical minimum RTT + match self.min_rtt { + Some(min_rtt) if delay < min_rtt => self.min_rtt = Some(delay), + None => self.min_rtt = Some(delay), + _ => {} + } + + // 2. Update sample window + if self.samples.len() >= Self::WINDOW_SAMPLES { + self.samples.pop_front(); + } + self.samples.push_back(delay); + + // 3. Calculate minimum RTT within the window + self.window_min_rtt = self.samples.iter().min().copied(); + + // 4. Calculate smoothed RTT + // Use weighted average if we have enough samples + if self.samples.len() >= Self::WINDOW_SAMPLES { + if let (Some(min), Some(window_min)) = (self.min_rtt, self.window_min_rtt) { + // Weighted average of historical minimum and window minimum + let new_srtt = + ((1.0 - Self::ALPHA) * min as f32 + Self::ALPHA * window_min as f32) as u32; + self.smoothed_rtt = Some(new_srtt); + } + } + } + + /// Get current RTT estimate + /// Returns None if no valid estimation is available + pub fn get_rtt(&self) -> Option { + if let Some(rtt) = self.smoothed_rtt { + return Some(rtt); + } + if self.samples.len() >= Self::MIN_SAMPLES { + if let Some(rtt) = self.min_rtt { + return Some(rtt); + } + } + None } } diff --git a/src/server/video_service.rs b/src/server/video_service.rs index 733405a37f7..a9474db7445 100644 --- a/src/server/video_service.rs +++ b/src/server/video_service.rs @@ -18,12 +18,7 @@ // to-do: // https://slhck.info/video/2017/03/01/rate-control.html -use super::{ - display_service::{check_display_changed, get_display_info}, - service::ServiceTmpl, - video_qos::VideoQoS, - *, -}; +use super::{display_service::check_display_changed, service::ServiceTmpl, video_qos::VideoQoS, *}; #[cfg(target_os = "linux")] use crate::common::SimpleCallOnReturn; #[cfg(target_os = "linux")] @@ -51,10 +46,10 @@ use scrap::vram::{VRamEncoder, VRamEncoderConfig}; use scrap::Capturer; use scrap::{ aom::AomEncoderConfig, - codec::{Encoder, EncoderCfg, Quality}, + codec::{Encoder, EncoderCfg}, record::{Recorder, RecorderContext}, vpxcodec::{VpxEncoderConfig, VpxVideoCodecId}, - CodecFormat, Display, EncodeInput, TraitCapturer, + CodecFormat, Display, EncodeInput, TraitCapturer, TraitPixelBuffer, }; #[cfg(windows)] use std::sync::Once; @@ -65,7 +60,6 @@ use std::{ time::{self, Duration, Instant}, }; -pub const NAME: &'static str = "video"; pub const OPTION_REFRESH: &'static str = "refresh"; lazy_static::lazy_static! { @@ -76,6 +70,13 @@ lazy_static::lazy_static! { pub static ref VIDEO_QOS: Arc> = Default::default(); pub static ref IS_UAC_RUNNING: Arc> = Default::default(); pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc> = Default::default(); + static ref SCREENSHOTS: Mutex> = Default::default(); +} + +struct Screenshot { + sid: String, + tx: Sender, + restore_vram: bool, } #[inline] @@ -133,10 +134,34 @@ impl VideoFrameController { } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum VideoSource { + Monitor, + Camera, +} + +impl VideoSource { + pub fn service_name_prefix(&self) -> &'static str { + match self { + VideoSource::Monitor => "monitor", + VideoSource::Camera => "camera", + } + } + + pub fn is_monitor(&self) -> bool { + matches!(self, VideoSource::Monitor) + } + + pub fn is_camera(&self) -> bool { + matches!(self, VideoSource::Camera) + } +} + #[derive(Clone)] pub struct VideoService { sp: GenericService, idx: usize, + source: VideoSource, } impl Deref for VideoService { @@ -153,14 +178,15 @@ impl DerefMut for VideoService { } } -pub fn get_service_name(idx: usize) -> String { - format!("{}{}", NAME, idx) +pub fn get_service_name(source: VideoSource, idx: usize) -> String { + format!("{}{}", source.service_name_prefix(), idx) } -pub fn new(idx: usize) -> GenericService { +pub fn new(source: VideoSource, idx: usize) -> GenericService { let vs = VideoService { - sp: GenericService::new(get_service_name(idx), true), + sp: GenericService::new(get_service_name(source, idx), true), idx, + source, }; GenericService::run(&vs, run); vs.sp @@ -292,7 +318,10 @@ impl DerefMut for CapturerInfo { } } -fn get_capturer(current: usize, portable_service_running: bool) -> ResultType { +fn get_capturer_monitor( + current: usize, + portable_service_running: bool, +) -> ResultType { #[cfg(target_os = "linux")] { if !is_x11() { @@ -309,6 +338,7 @@ fn get_capturer(current: usize, portable_service_running: bool) -> ResultType ResultType ResultType { + let cameras = camera::Cameras::get_sync_cameras(); + let ncamera = cameras.len(); + if ncamera <= current { + bail!("Failed to get camera {}, cameras len: {}", current, ncamera,); + } + let Some(camera) = cameras.get(current) else { + bail!( + "Camera of index {} doesn't exist or platform not supported", + current + ); + }; + let capturer = camera::Cameras::get_capturer(current)?; + let (width, height) = (camera.width as usize, camera.height as usize); + let origin = (camera.x as i32, camera.y as i32); + let name = &camera.name; + let privacy_mode_id = get_privacy_mode_conn_id().unwrap_or(INVALID_PRIVACY_MODE_CONN_ID); + let _capturer_privacy_mode_id = privacy_mode_id; + log::debug!( + "#cameras={}, current={}, origin: {:?}, width={}, height={}, cpus={}/{}, name:{}", + ncamera, + current, + &origin, + width, + height, + num_cpus::get_physical(), + num_cpus::get(), + name, + ); + return Ok(CapturerInfo { + origin, + width, + height, + ndisplay: ncamera, + current, + privacy_mode_id, + _capturer_privacy_mode_id: privacy_mode_id, + capturer, + }); +} +fn get_capturer( + source: VideoSource, + current: usize, + portable_service_running: bool, +) -> ResultType { + match source { + VideoSource::Monitor => get_capturer_monitor(current, portable_service_running), + VideoSource::Camera => get_capturer_camera(current), + } +} + fn run(vs: VideoService) -> ResultType<()> { - let _raii = Raii::new(vs.idx); + let mut _raii = Raii::new(vs.sp.name()); // Wayland only support one video capturer for now. It is ok to call ensure_inited() here. // // ensure_inited() is needed because clear() may be called. @@ -406,16 +487,15 @@ fn run(vs: VideoService) -> ResultType<()> { let display_idx = vs.idx; let sp = vs.sp; - let mut c = get_capturer(display_idx, last_portable_service_running)?; + let mut c = get_capturer(vs.source, display_idx, last_portable_service_running)?; #[cfg(windows)] if !scrap::codec::enable_directx_capture() && !c.is_gdi() { log::info!("disable dxgi with option, fall back to gdi"); c.set_gdi(); } let mut video_qos = VIDEO_QOS.lock().unwrap(); - video_qos.refresh(None); - let mut spf; - let mut quality = video_qos.quality(); + let mut spf = video_qos.spf(); + let mut quality = video_qos.ratio(); let record_incoming = config::option2bool( "allow-auto-record-incoming", &Config::get_option("allow-auto-record-incoming"), @@ -424,11 +504,13 @@ fn run(vs: VideoService) -> ResultType<()> { drop(video_qos); let (mut encoder, encoder_cfg, codec_format, use_i444, recorder) = match setup_encoder( &c, - display_idx, + sp.name(), quality, client_record, record_incoming, last_portable_service_running, + vs.source, + display_idx, ) { Ok(result) => result, Err(err) => { @@ -442,26 +524,30 @@ fn run(vs: VideoService) -> ResultType<()> { })); setup_encoder( &c, - display_idx, + sp.name(), quality, client_record, record_incoming, last_portable_service_running, + vs.source, + display_idx, )? } }; #[cfg(feature = "vram")] c.set_output_texture(encoder.input_texture()); #[cfg(target_os = "android")] - if let Err(e) = check_change_scale(encoder.is_hardware()) { - try_broadcast_display_changed(&sp, display_idx, &c, true).ok(); - bail!(e); + if vs.source.is_monitor() { + if let Err(e) = check_change_scale(encoder.is_hardware()) { + try_broadcast_display_changed(&sp, display_idx, &c, true).ok(); + bail!(e); + } } VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate()); VIDEO_QOS .lock() .unwrap() - .set_support_abr(display_idx, encoder.support_abr()); + .set_support_changing_quality(&sp.name(), encoder.support_changing_quality()); log::info!("initial quality: {quality:?}"); if sp.is_option_true(OPTION_REFRESH) { @@ -489,34 +575,24 @@ fn run(vs: VideoService) -> ResultType<()> { let mut first_frame = true; let capture_width = c.width; let capture_height = c.height; + let (mut second_instant, mut send_counter) = (Instant::now(), 0); while sp.ok() { #[cfg(windows)] check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?; - - let mut video_qos = VIDEO_QOS.lock().unwrap(); - spf = video_qos.spf(); - if quality != video_qos.quality() { - log::debug!("quality: {:?} -> {:?}", quality, video_qos.quality()); - quality = video_qos.quality(); - if encoder.support_changing_quality() { - allow_err!(encoder.set_quality(quality)); - video_qos.store_bitrate(encoder.bitrate()); - } else { - if !video_qos.in_vbr_state() && !quality.is_custom() { - log::info!("switch to change quality"); - bail!("SWITCH"); - } - } - } - if client_record != video_qos.record() { - log::info!("switch due to record changed"); - bail!("SWITCH"); - } - drop(video_qos); - + check_qos( + &mut encoder, + &mut quality, + &mut spf, + client_record, + &mut send_counter, + &mut second_instant, + &sp.name(), + )?; if sp.is_option_true(OPTION_REFRESH) { - let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); + if vs.source.is_monitor() { + let _ = try_broadcast_display_changed(&sp, display_idx, &c, true); + } log::info!("switch to refresh"); bail!("SWITCH"); } @@ -540,10 +616,12 @@ fn run(vs: VideoService) -> ResultType<()> { #[cfg(all(windows, feature = "vram"))] if c.is_gdi() && encoder.input_texture() { log::info!("changed to gdi when using vram"); - VRamEncoder::set_fallback_gdi(display_idx, true); + VRamEncoder::set_fallback_gdi(sp.name(), true); bail!("SWITCH"); } - check_privacy_mode_changed(&sp, display_idx, &c)?; + if vs.source.is_monitor() { + check_privacy_mode_changed(&sp, display_idx, &c)?; + } #[cfg(windows)] { if crate::platform::windows::desktop_changed() @@ -553,7 +631,7 @@ fn run(vs: VideoService) -> ResultType<()> { } } let now = time::Instant::now(); - if last_check_displays.elapsed().as_millis() > 1000 { + if vs.source.is_monitor() && last_check_displays.elapsed().as_millis() > 1000 { last_check_displays = now; // This check may be redundant, but it is better to be safe. // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. @@ -568,6 +646,49 @@ fn run(vs: VideoService) -> ResultType<()> { Ok(frame) => { repeat_encode_counter = 0; if frame.valid() { + let screenshot = SCREENSHOTS.lock().unwrap().remove(&display_idx); + if let Some(mut screenshot) = screenshot { + let restore_vram = screenshot.restore_vram; + let (msg, w, h, data) = match &frame { + scrap::Frame::PixelBuffer(f) => match get_rgba_from_pixelbuf(f) { + Ok(rgba) => ("".to_owned(), f.width(), f.height(), rgba), + Err(e) => { + let serr = e.to_string(); + log::error!( + "Failed to convert the pix format into rgba, {}", + &serr + ); + (format!("Convert pixfmt: {}", serr), 0, 0, vec![]) + } + }, + scrap::Frame::Texture(_) => { + if restore_vram { + // Already set one time, just ignore to break infinite loop. + // Though it's unreachable, this branch is kept to avoid infinite loop. + ( + "Please change codec and try again.".to_owned(), + 0, + 0, + vec![], + ) + } else { + #[cfg(all(windows, feature = "vram"))] + VRamEncoder::set_not_use(sp.name(), true); + screenshot.restore_vram = true; + SCREENSHOTS.lock().unwrap().insert(display_idx, screenshot); + _raii.try_vram = false; + bail!("SWITCH"); + } + } + }; + std::thread::spawn(move || { + handle_screenshot(screenshot, msg, w, h, data); + }); + if restore_vram { + bail!("SWITCH"); + } + } + let frame = frame.to(encoder.yuvfmt(), &mut yuv, &mut mid_data)?; let send_conn_ids = handle_one_frame( display_idx, @@ -582,12 +703,13 @@ fn run(vs: VideoService) -> ResultType<()> { capture_height, )?; frame_controller.set_send(now, send_conn_ids); + send_counter += 1; } #[cfg(windows)] { #[cfg(feature = "vram")] if try_gdi == 1 && !c.is_gdi() { - VRamEncoder::set_fallback_gdi(display_idx, false); + VRamEncoder::set_fallback_gdi(sp.name(), false); } try_gdi = 0; } @@ -640,13 +762,16 @@ fn run(vs: VideoService) -> ResultType<()> { capture_height, )?; frame_controller.set_send(now, send_conn_ids); + send_counter += 1; } } } Err(err) => { // This check may be redundant, but it is better to be safe. // The previous check in `sp.is_option_true(OPTION_REFRESH)` block may be enough. - try_broadcast_display_changed(&sp, display_idx, &c, true)?; + if vs.source.is_monitor() { + try_broadcast_display_changed(&sp, display_idx, &c, true)?; + } #[cfg(windows)] if !c.is_gdi() { @@ -668,7 +793,9 @@ fn run(vs: VideoService) -> ResultType<()> { let timeout_millis = 3_000u64; let wait_begin = Instant::now(); while wait_begin.elapsed().as_millis() < timeout_millis as _ { - check_privacy_mode_changed(&sp, display_idx, &c)?; + if vs.source.is_monitor() { + check_privacy_mode_changed(&sp, display_idx, &c)?; + } 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() { @@ -687,31 +814,44 @@ fn run(vs: VideoService) -> ResultType<()> { Ok(()) } -struct Raii(usize); +struct Raii { + name: String, + try_vram: bool, +} impl Raii { - fn new(display_idx: usize) -> Self { - Raii(display_idx) + fn new(name: String) -> Self { + log::info!("new video service: {}", name); + VIDEO_QOS.lock().unwrap().new_display(name.clone()); + Raii { + name, + try_vram: true, + } } } impl Drop for Raii { fn drop(&mut self) { + log::info!("stop video service: {}", self.name); #[cfg(feature = "vram")] - VRamEncoder::set_not_use(self.0, false); + if self.try_vram { + VRamEncoder::set_not_use(self.name.clone(), false); + } #[cfg(feature = "vram")] Encoder::update(scrap::codec::EncodingUpdate::Check); - VIDEO_QOS.lock().unwrap().set_support_abr(self.0, true); + VIDEO_QOS.lock().unwrap().remove_display(&self.name); } } fn setup_encoder( c: &CapturerInfo, - display_idx: usize, - quality: Quality, + name: String, + quality: f32, client_record: bool, record_incoming: bool, last_portable_service_running: bool, + source: VideoSource, + display_idx: usize, ) -> ResultType<( Encoder, EncoderCfg, @@ -721,14 +861,15 @@ fn setup_encoder( )> { let encoder_cfg = get_encoder_config( &c, - display_idx, + name.to_string(), quality, client_record || record_incoming, last_portable_service_running, + source, ); Encoder::set_fallback(&encoder_cfg); let codec_format = Encoder::negotiated_codec(); - let recorder = get_recorder(record_incoming, display_idx); + let recorder = get_recorder(record_incoming, display_idx, source == VideoSource::Camera); let use_i444 = Encoder::use_i444(&encoder_cfg); let encoder = Encoder::new(encoder_cfg.clone(), use_i444)?; Ok((encoder, encoder_cfg, codec_format, use_i444, recorder)) @@ -736,15 +877,16 @@ fn setup_encoder( fn get_encoder_config( c: &CapturerInfo, - _display_idx: usize, - quality: Quality, + _name: String, + quality: f32, record: bool, _portable_service: bool, + _source: VideoSource, ) -> EncoderCfg { #[cfg(all(windows, feature = "vram"))] - if _portable_service || c.is_gdi() { + if _portable_service || c.is_gdi() || _source == VideoSource::Camera { log::info!("gdi:{}, portable:{}", c.is_gdi(), _portable_service); - VRamEncoder::set_not_use(_display_idx, true); + VRamEncoder::set_not_use(_name, true); } #[cfg(feature = "vram")] Encoder::update(scrap::codec::EncodingUpdate::Check); @@ -810,7 +952,11 @@ fn get_encoder_config( } } -fn get_recorder(record_incoming: bool, display: usize) -> Arc>> { +fn get_recorder( + record_incoming: bool, + display_idx: usize, + camera: bool, +) -> Arc>> { #[cfg(windows)] let root = crate::platform::is_root(); #[cfg(not(windows))] @@ -829,7 +975,8 @@ fn get_recorder(record_incoming: bool, display: usize) -> Arc, + source: VideoSource, ) -> Option { let display = match opt_display { Some(d) => d, - None => get_display_info(display_idx)?, + None => match source { + VideoSource::Monitor => display_service::get_display_info(display_idx)?, + VideoSource::Camera => camera::Cameras::get_sync_cameras() + .get(display_idx)? + .clone(), + }, }; let mut misc = Misc::new(); misc.set_switch_display(SwitchDisplay { @@ -1043,13 +1198,24 @@ pub fn make_display_changed_msg( y: display.y, width: display.width, height: display.height, - cursor_embedded: display_service::capture_cursor_embedded(), + cursor_embedded: match source { + VideoSource::Monitor => display_service::capture_cursor_embedded(), + VideoSource::Camera => false, + }, #[cfg(not(target_os = "android"))] resolutions: Some(SupportedResolutions { - resolutions: if display.name.is_empty() { - vec![] - } else { - crate::platform::resolutions(&display.name) + resolutions: match source { + VideoSource::Monitor => { + if display.name.is_empty() { + vec![] + } else { + crate::platform::resolutions(&display.name) + } + } + VideoSource::Camera => camera::Cameras::get_camera_resolution(display_idx) + .ok() + .into_iter() + .collect(), }, ..SupportedResolutions::default() }) @@ -1061,3 +1227,114 @@ pub fn make_display_changed_msg( msg_out.set_misc(misc); Some(msg_out) } + +fn check_qos( + encoder: &mut Encoder, + ratio: &mut f32, + spf: &mut Duration, + client_record: bool, + send_counter: &mut usize, + second_instant: &mut Instant, + name: &str, +) -> ResultType<()> { + let mut video_qos = VIDEO_QOS.lock().unwrap(); + *spf = video_qos.spf(); + if *ratio != video_qos.ratio() { + *ratio = video_qos.ratio(); + if encoder.support_changing_quality() { + allow_err!(encoder.set_quality(*ratio)); + video_qos.store_bitrate(encoder.bitrate()); + } else { + // Now only vaapi doesn't support changing quality + if !video_qos.in_vbr_state() && !video_qos.latest_quality().is_custom() { + log::info!("switch to change quality"); + bail!("SWITCH"); + } + } + } + if client_record != video_qos.record() { + log::info!("switch due to record changed"); + bail!("SWITCH"); + } + if second_instant.elapsed() > Duration::from_secs(1) { + *second_instant = Instant::now(); + video_qos.update_display_data(&name, *send_counter); + *send_counter = 0; + } + drop(video_qos); + Ok(()) +} + +pub fn set_take_screenshot(display_idx: usize, sid: String, tx: Sender) { + SCREENSHOTS.lock().unwrap().insert( + display_idx, + Screenshot { + sid, + tx, + restore_vram: false, + }, + ); +} + +// We need to this function, because the `stride` may be larger than `width * 4`. +fn get_rgba_from_pixelbuf<'a>(pixbuf: &scrap::PixelBuffer<'a>) -> ResultType> { + let w = pixbuf.width(); + let h = pixbuf.height(); + let stride = pixbuf.stride(); + let Some(s) = stride.get(0) else { + bail!("Invalid pixel buf stride.") + }; + + if *s == w * 4 { + let mut rgba = vec![]; + scrap::convert(pixbuf, scrap::Pixfmt::RGBA, &mut rgba)?; + Ok(rgba) + } else { + let bgra = pixbuf.data(); + let mut bit_flipped = Vec::with_capacity(w * h * 4); + for y in 0..h { + for x in 0..w { + let i = s * y + 4 * x; + bit_flipped.extend_from_slice(&[bgra[i + 2], bgra[i + 1], bgra[i], bgra[i + 3]]); + } + } + Ok(bit_flipped) + } +} + +fn handle_screenshot(screenshot: Screenshot, msg: String, w: usize, h: usize, data: Vec) { + let mut response = ScreenshotResponse::new(); + response.sid = screenshot.sid; + if msg.is_empty() { + if data.is_empty() { + response.msg = "Failed to take screenshot, please try again later.".to_owned(); + } else { + fn encode_png(width: usize, height: usize, rgba: Vec) -> ResultType> { + let mut png = Vec::new(); + let mut encoder = + repng::Options::smallest(width as _, height as _).build(&mut png)?; + encoder.write(&rgba)?; + encoder.finish()?; + Ok(png) + } + match encode_png(w as _, h as _, data) { + Ok(png) => { + response.data = png.into(); + } + Err(e) => { + response.msg = format!("Error encoding png: {}", e); + } + } + } + } else { + response.msg = msg; + } + let mut msg_out = Message::new(); + msg_out.set_screenshot_response(response); + if let Err(e) = screenshot + .tx + .send((hbb_common::tokio::time::Instant::now(), Arc::new(msg_out))) + { + log::error!("Failed to send screenshot, {}", e); + } +} diff --git a/src/server/wayland.rs b/src/server/wayland.rs index 5560cb95ed3..42c6132777e 100644 --- a/src/server/wayland.rs +++ b/src/server/wayland.rs @@ -1,5 +1,8 @@ use super::*; -use hbb_common::{allow_err, platform::linux::DISTRO}; +use hbb_common::{ + allow_err, + platform::linux::{CMD_SH, DISTRO}, +}; use scrap::{is_cursor_embedded, set_map_err, Capturer, Display, Frame, TraitCapturer}; use std::io; use std::process::{Command, Output}; @@ -21,12 +24,6 @@ pub fn init() { } fn map_err_scrap(err: String) -> io::Error { - // to-do: Remove this the following log - log::error!( - "REMOVE ME ===================================== wayland scrap error {}", - &err - ); - // to-do: Handle error better, do not restart server if err.starts_with("Did not receive a reply") { log::error!("Fatal pipewire error, {}", &err); @@ -115,7 +112,7 @@ pub(super) fn is_inited() -> Option { fn get_max_desktop_resolution() -> Option { // works with Xwayland - let output: Output = Command::new("sh") + let output: Output = Command::new(CMD_SH.as_str()) .arg("-c") .arg("xrandr | awk '/current/ { print $8,$9,$10 }'") .output() diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 00000000000..ce1855bdb8b --- /dev/null +++ b/src/service.rs @@ -0,0 +1,11 @@ +use librustdesk::*; + +#[cfg(not(target_os = "macos"))] +fn main() {} + +#[cfg(target_os = "macos")] +fn main() { + crate::common::load_custom_client(); + hbb_common::init_log(false, "service"); + crate::start_os_service(); +} diff --git a/src/tray.rs b/src/tray.rs index 3a3ae92f37f..f36da2cec41 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -21,6 +21,10 @@ pub fn start_tray() { return; } } + + #[cfg(target_os = "linux")] + crate::server::check_zombie(); + allow_err!(make_tray()); } @@ -56,7 +60,7 @@ fn make_tray() -> hbb_common::ResultType<()> { let mut event_loop = EventLoopBuilder::new().build(); let tray_menu = Menu::new(); - let quit_i = MenuItem::new(translate("Exit".to_owned()), true, None); + let quit_i = MenuItem::new(translate("Stop service".to_owned()), true, None); let open_i = MenuItem::new(translate("Open".to_owned()), true, None); tray_menu.append_items(&[&open_i, &quit_i]).ok(); let tooltip = |count: usize| { @@ -99,9 +103,11 @@ fn make_tray() -> hbb_common::ResultType<()> { } #[cfg(target_os = "linux")] { - // Do not use "xdg-open", it won't read config + // Do not use "xdg-open", it won't read the config. if crate::dbus::invoke_new_connection(crate::get_uri_prefix()).is_err() { - crate::run_me::<&str>(vec![]).ok(); + if let Ok(task) = crate::run_me::<&str>(vec![]) { + crate::server::CHILD_PROCESS.lock().unwrap().push(task); + } } } }; diff --git a/src/ui.rs b/src/ui.rs index d3d291433ba..6bf7c68dadc 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -42,7 +42,7 @@ pub fn start(args: &mut [String]) { #[cfg(all(target_os = "linux", feature = "inline"))] { let app_dir = std::env::var("APPDIR").unwrap_or("".to_string()); - let mut so_path = "/usr/lib/rustdesk/libsciter-gtk.so".to_owned(); + let mut so_path = "/usr/share/rustdesk/libsciter-gtk.so".to_owned(); for (prefix, dir) in [ ("", "/usr"), ("", "/app"), @@ -51,7 +51,7 @@ pub fn start(args: &mut [String]) { ] .iter() { - let path = format!("{prefix}{dir}/lib/rustdesk/libsciter-gtk.so"); + let path = format!("{prefix}{dir}/share/rustdesk/libsciter-gtk.so"); if std::path::Path::new(&path).exists() { so_path = path; break; @@ -118,6 +118,11 @@ pub fn start(args: &mut [String]) { Box::new(cm::SciterConnectionManager::new()) }); page = "cm.html"; + *cm::HIDE_CM.lock().unwrap() = crate::ipc::get_config("hide_cm") + .ok() + .flatten() + .unwrap_or_default() + == "true"; } else if (args[0] == "--connect" || args[0] == "--file-transfer" || args[0] == "--port-forward" @@ -178,6 +183,13 @@ pub fn start(args: &mut [String]) { .unwrap_or("".to_owned()), page )); + let hide_cm = *cm::HIDE_CM.lock().unwrap(); + if !args.is_empty() && args[0] == "--cm" && hide_cm { + // run_app calls expand(show) + run_loop, we use collapse(hide) + run_loop instead to create a hidden window + frame.collapse(true); + frame.run_loop(); + return; + } frame.run_app(); } @@ -634,6 +646,10 @@ impl UI { verify2fa(code) } + fn verify_login(&self, raw: String, id: String) -> bool { + crate::verify_login(&raw, &id) + } + fn generate_2fa_img_src(&self, data: String) -> String { let v = qrcode_generator::to_png_to_vec(data, qrcode_generator::QrCodeEcc::Low, 128) .unwrap_or_default(); @@ -739,6 +755,7 @@ impl sciter::EventHandler for UI { fn generate_2fa_img_src(String); fn verify2fa(String); fn check_hwcodec(); + fn verify_login(String, String); } } diff --git a/src/ui/cm.rs b/src/ui/cm.rs index c8c8c657fb7..92cd2e2f22b 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -7,6 +7,10 @@ use sciter::{make_args, Element, Value, HELEMENT}; use std::sync::Mutex; use std::{ops::Deref, sync::Arc}; +lazy_static::lazy_static! { + pub static ref HIDE_CM: Arc> = Arc::new(Mutex::new(false)); +} + #[derive(Clone, Default)] pub struct SciterHandler { pub element: Arc>>, @@ -19,6 +23,8 @@ impl InvokeUiCM for SciterHandler { &make_args!( client.id, client.is_file_transfer, + client.is_view_camera, + client.is_terminal, client.port_forward.clone(), client.peer_id.clone(), client.name.clone(), @@ -149,6 +155,10 @@ impl SciterConnectionManager { fn get_option(&self, key: String) -> String { crate::ui_interface::get_option(key) } + + fn hide_cm(&self) -> bool { + *crate::ui::cm::HIDE_CM.lock().unwrap() + } } impl sciter::EventHandler for SciterConnectionManager { @@ -170,5 +180,6 @@ impl sciter::EventHandler for SciterConnectionManager { fn can_elevate(); fn elevate_portable(i32); fn get_option(String); + fn hide_cm(); } } diff --git a/src/ui/cm.tis b/src/ui/cm.tis index 38f5c5c2d80..0b0165b7374 100644 --- a/src/ui/cm.tis +++ b/src/ui/cm.tis @@ -6,6 +6,13 @@ var show_chat = false; var show_elevation = true; var svg_elevate = ; +var hide_cm = undefined; +function setWindowState(state) { + if (hide_cm == undefined) hide_cm = handler.hide_cm(); + if (hide_cm) return; + view.windowState = state; +} + class Body: Reactor.Component { this var cur = 0; @@ -29,7 +36,7 @@ class Body: Reactor.Component }; var right_style = show_chat ? "" : "display: none"; var disconnected = c.disconnected; - var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && c.port_forward.length == 0; + var show_elevation_btn = handler.can_elevate() && show_elevation && !c.is_file_transfer && !c.is_view_camera && !c.is_terminal && c.port_forward.length == 0; var show_accept_btn = handler.get_option('approve-mode') != 'password'; // below size:* is a workaround for Linux, it already set in css, but not work, shit sciter return

@@ -48,8 +55,8 @@ class Body: Reactor.Component
- {c.is_file_transfer || c.port_forward || disconnected ? "" :
{translate('Permissions')}
} - {c.is_file_transfer || c.port_forward || disconnected ? "" :
+ {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
{translate('Permissions')}
} + {c.is_file_transfer || c.is_terminal || c.port_forward || disconnected ? "" :
@@ -60,6 +67,9 @@ class Body: Reactor.Component
} + {c.is_file_transfer ?
{translate('Transfer file')}
: ""} + {c.is_view_camera ?
{translate('View camera')}
: ""} + {c.is_terminal ?
{translate('Terminal')}
: ""} {c.port_forward ?
Port Forwarding: {c.port_forward}
: ""}
@@ -72,10 +82,10 @@ class Body: Reactor.Component {auth && !disconnected ? : "" } {auth && disconnected ? : "" }
- {c.is_file_transfer || c.port_forward ? "" :
{svg_chat}
} + {c.is_file_transfer || c.is_terminal || c.port_forward ? "" :
{svg_chat}
}
- {c.is_file_transfer || c.port_forward ? "" : } + {c.is_file_transfer || c.is_terminal || c.port_forward ? "" : }
; } @@ -160,7 +170,7 @@ class Body: Reactor.Component body.update(); handler.authorize(cid); self.timer(30ms, function() { - view.windowState = View.WINDOW_MINIMIZED; + setWindowState(View.WINDOW_MINIMIZED); }); }); } @@ -174,7 +184,7 @@ class Body: Reactor.Component handler.elevate_portable(cid); handler.authorize(cid); self.timer(30ms, function() { - view.windowState = View.WINDOW_MINIMIZED; + setWindowState(View.WINDOW_MINIMIZED); }); }); } @@ -186,7 +196,7 @@ class Body: Reactor.Component body.update(); handler.elevate_portable(cid); self.timer(30ms, function() { - view.windowState = View.WINDOW_MINIMIZED; + setWindowState(View.WINDOW_MINIMIZED); }); }); } @@ -347,7 +357,7 @@ function bring_to_top(idx=-1) { if (is_linux) { view.focus = self; } else { - view.windowState = View.WINDOW_SHOWN; + setWindowState(View.WINDOW_SHOWN); } if (idx >= 0) body.cur = idx; } else { @@ -356,7 +366,7 @@ function bring_to_top(idx=-1) { } } -handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { +handler.addConnection = function(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input) { stdout.println("new connection #" + id + ": " + peer_id); var conn; connections.map(function(c) { @@ -373,7 +383,7 @@ handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, na }); if (!name) name = "NA"; conn = { - id: id, is_file_transfer: is_file_transfer, peer_id: peer_id, + id: id, is_file_transfer: is_file_transfer, is_view_camera: is_view_camera, is_terminal: is_terminal, peer_id: peer_id, port_forward: port_forward, name: name, authorized: authorized, time: new Date(), now: new Date(), keyboard: keyboard, clipboard: clipboard, msgs: [], unreaded: 0, @@ -393,7 +403,7 @@ handler.addConnection = function(id, is_file_transfer, port_forward, peer_id, na self.timer(1ms, adjustHeader); if (authorized) { self.timer(3s, function() { - view.windowState = View.WINDOW_MINIMIZED; + setWindowState(View.WINDOW_MINIMIZED); }); } } @@ -506,7 +516,7 @@ var tm0 = getTime(); function self.closing() { if (connections.length == 0 && getTime() - tm0 > 30000) return true; - view.windowState = View.WINDOW_HIDDEN; + setWindowState(View.WINDOW_HIDDEN); return false; } @@ -550,7 +560,7 @@ function adjustHeader() { view.on("size", adjustHeader); -// handler.addConnection(0, false, 0, "", "test1", true, false, false, true, true); -// handler.addConnection(1, false, 0, "", "test2--------", true, false, false, false, false); -// handler.addConnection(2, false, 0, "", "test3", true, false, false, false, false); +// handler.addConnection(0, false, false, 0, "", "test1", true, false, false, true, true); +// handler.addConnection(1, false, false, 0, "", "test2--------", true, false, false, false, false); +// handler.addConnection(2, false, false, 0, "", "test3", true, false, false, false, false); // handler.newMessage(0, 'h'); diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 6c741b31f4b..0b60cf748a0 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -69,8 +69,6 @@ function getExt(name) { return ""; } -var jobIdCounter = 1; - class JobTable: Reactor.Component { this var jobs = []; this var job_map = {}; @@ -126,8 +124,7 @@ class JobTable: Reactor.Component { } if (!to) return; to += handler.get_path_sep(!is_remote) + getFileName(is_remote, path); - var id = jobIdCounter; - jobIdCounter += 1; + var id = handler.get_next_job_id(); this.jobs.push({ type: "transfer", id: id, path: path, to: to, include_hidden: show_hidden, @@ -135,7 +132,7 @@ class JobTable: Reactor.Component { is_last: false }); this.job_map[id] = this.jobs[this.jobs.length - 1]; - handler.send_files(id, path, to, 0, show_hidden, is_remote); + handler.send_files(id, 0, path, to, 0, show_hidden, is_remote); var self = this; self.timer(30ms, function() { self.update(); }); } @@ -147,8 +144,8 @@ class JobTable: Reactor.Component { is_remote: is_remote, is_last: true, file_num: file_num }; this.jobs.push(job); this.job_map[id] = this.jobs[this.jobs.length - 1]; - jobIdCounter = id + 1; - handler.add_job(id, path, to, file_num, show_hidden, is_remote); + handler.update_next_job_id(id + 1); + handler.add_job(id, 0, path, to, file_num, show_hidden, is_remote); stdout.println(JSON.stringify(job)); } @@ -162,16 +159,14 @@ class JobTable: Reactor.Component { } function addDelDir(path, is_remote) { - var id = jobIdCounter; - jobIdCounter += 1; + var id = handler.get_next_job_id(); this.jobs.push({ type: "del-dir", id: id, path: path, is_remote: is_remote }); this.job_map[id] = this.jobs[this.jobs.length - 1]; this.update(); } function addDelFile(path, is_remote) { - var id = jobIdCounter; - jobIdCounter += 1; + var id = handler.get_next_job_id(); this.jobs.push({ type: "del-file", id: id, path: path, is_remote: is_remote }); this.job_map[id] = this.jobs[this.jobs.length - 1]; this.update(); @@ -552,9 +547,9 @@ class FolderView : Reactor.Component { return; } var path = me.joinPath(name); - handler.create_dir(jobIdCounter, path, me.is_remote); - create_dir_jobs[jobIdCounter] = { is_remote: me.is_remote, path: path }; - jobIdCounter += 1; + var id = handler.get_next_job_id(); + handler.create_dir(id, path, me.is_remote); + create_dir_jobs[id] = { is_remote: me.is_remote, path: path }; }); } diff --git a/src/ui/header.tis b/src/ui/header.tis index 4b634cf54c5..17efe698267 100644 --- a/src/ui/header.tis +++ b/src/ui/header.tis @@ -117,6 +117,13 @@ class Header: Reactor.Component { icon_conn = svg_insecure_relay; title_conn = translate("Relayed and unencrypted connection"); } + var stream_type = this.stream_type; + if (stream_type == "Relay") { + stream_type = "TCP"; + } + if (stream_type) { + title_conn += " (" + stream_type + ")"; + } var title = get_id(); if (pi.hostname) title += "(" + pi.username + "@" + pi.hostname + ")"; if ((pi.displays || []).length == 0) { @@ -174,6 +181,13 @@ class Header: Reactor.Component { } } + var is_file_copy_paste_supported = false; + if (handler.version_cmp(pi.version, '1.2.4') < 0) { + is_file_copy_paste_supported = is_win && pi.platform == "Windows"; + } else { + is_file_copy_paste_supported = handler.has_file_clipboard() && pi.platform_additions?.has_file_clipboard; + } + return
  • {translate('Adjust Window')}
  • @@ -201,7 +215,7 @@ class Header: Reactor.Component { {
  • {svg_checkmark}{translate('Follow remote window focus')}
  • }
  • {svg_checkmark}{translate('Show quality monitor')}
  • {audio_enabled ?
  • {svg_checkmark}{translate('Mute')}
  • : ""} - {(is_win && pi.platform == "Windows") && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} + {is_file_copy_paste_supported && file_enabled ?
  • {svg_checkmark}{translate('Enable file copy and paste')}
  • : ""} {keyboard_enabled && clipboard_enabled ?
  • {svg_checkmark}{translate('Disable clipboard')}
  • : ""} {keyboard_enabled ?
  • {svg_checkmark}{translate('Lock after session end')}
  • : ""} {keyboard_enabled && pi.platform == "Windows" ?
  • {svg_checkmark}{translate('Privacy mode')}
  • : ""} @@ -223,6 +237,7 @@ class Header: Reactor.Component { {restart_enabled && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS") ?
  • {translate('Restart remote device')}
  • : ""} {keyboard_enabled ?
  • {translate('Insert Lock')}
  • : ""} {keyboard_enabled && pi.platform == "Windows" && pi.sas_enabled ?
  • {translate("Block user input")}
  • : ""} + {handler.is_screenshot_supported() ?
  • {translate('Take screenshot')}
  • : "" }
  • {translate('Refresh')}
  • ; @@ -369,6 +384,10 @@ class Header: Reactor.Component { event click $(#lock-screen) { handler.lock_screen(); } + + event click $(#take-screenshot) { + handler.take_screenshot(pi.current_display, ""); + } event click $(#refresh) { // 0 is just a dummy value. It will be ignored by the handler. @@ -408,7 +427,7 @@ class Header: Reactor.Component { adaptDisplay(); } else if (type == "codec-preference") { handler.set_option("codec-preference", me.id); - handler.change_prefer_codec(); + handler.update_supported_decodings(); } toggleMenuState(); } @@ -539,6 +558,26 @@ handler.setCurrentDisplay = function(v) { } } +handler.screenshot = function(msg) { + if (msg) { + msgbox( + "custom-nocancel-nook-hasclose-error", + translate("Take screenshot"), + msg, + "", + function() {} + ); + } else { + msgbox( + "custom-take-screenshot-nocancel-nook", + translate("Take screenshot"), + translate("screenshot-action-tip"), + "", + function() {} + ); + } +} + function updatePrivacyMode() { var el = $(li#privacy-mode); if (el) { @@ -580,7 +619,7 @@ function toggleQualityMonitor(name) { function toggleI444(name) { handler.toggle_option(name); - handler.change_prefer_codec(); + handler.update_supported_decodings(); toggleMenuState(); } @@ -663,10 +702,11 @@ function startChat() { chatbox = view.window(params); } -handler.setConnectionType = function(secured, direct) { +handler.setConnectionType = function(secured, direct, stream_type) { header.update({ secure_connection: secured, direct_connection: direct, + stream_type: stream_type, }); } diff --git a/src/ui/index.tis b/src/ui/index.tis index 2c9b0f9835b..570abe18004 100644 --- a/src/ui/index.tis +++ b/src/ui/index.tis @@ -64,7 +64,7 @@ function createNewConnect(id, type) { if (!id) return; var old_id = id; id = handler.handle_relay_id(id); - var force_relay = old_id != id; + var force_relay = old_id != id; if (id == my_id) { msgbox("custom-error", "Error", "You cannot connect to your own computer"); return; @@ -79,7 +79,7 @@ class ShareRdp: Reactor.Component { var cls = handler.is_share_rdp() ? "selected" : "line-through"; return
  • {svg_checkmark}{rdp_shared_string}
  • ; } - + function onClick() { handler.set_share_rdp(!handler.is_share_rdp()); this.update(); @@ -98,7 +98,7 @@ class DirectServer: Reactor.Component { var cls = enabled ? "selected" : "line-through"; return
  • {svg_checkmark}{text}{enabled && }
  • ; } - + function onClick() { if (is_edit_rdp_port) { is_edit_rdp_port = false; @@ -313,8 +313,10 @@ class MyIdMenu: Reactor.Component {
  • {svg_checkmark}{translate('Enable keyboard/mouse')}
  • {svg_checkmark}{translate('Enable clipboard')}
  • -
  • {svg_checkmark}{translate('Enable file transfer')}
  • -
  • {svg_checkmark}{translate('Enable remote restart')}
  • +
  • {svg_checkmark}{translate('Enable file transfer')}
  • +
  • {svg_checkmark}{translate('Enable camera')}
  • +
  • {svg_checkmark}{translate('Enable terminal')}
  • +
  • {svg_checkmark}{translate('Enable remote restart')}
  • {svg_checkmark}{translate('Enable TCP tunneling')}
  • {is_win ?
  • {svg_checkmark}{translate('Enable blocking user input')}
  • : ""}
  • {svg_checkmark}{translate('Enable LAN discovery')}
  • @@ -325,19 +327,21 @@ class MyIdMenu: Reactor.Component {
  • {translate('ID/Relay Server')}
  • {translate('IP Whitelisting')}
  • {translate('Socks5 Proxy')}
  • + { false &&
  • {svg_checkmark}{translate('Use WebSocket')}
  • }
  • {svg_checkmark}{translate("Enable service")}
  • {is_win && handler.is_installed() ? : ""} {false && handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } {handler.is_ok_change_id() ?
    : ""} - {username ? + {username ?
  • {translate('Logout')} ({username})
  • :
  • {translate('Login')}
  • } {handler.is_ok_change_id() && key_confirmed && connect_status > 0 ?
  • {translate('Change ID')}
  • : ""}
  • {svg_checkmark}{translate('Dark Theme')}
  • +
  • {svg_checkmark}{translate('Auto update')}
  • {translate('About')} {" "}{handler.get_app_name()}
  • ; @@ -385,7 +389,7 @@ class MyIdMenu: Reactor.Component {
    Fingerprint: " + handler.get_fingerprint() + " \
    " + translate("Privacy Statement") + "
    \
    " + translate("Website") + "
    \ -
    Copyright © 2024 Purslane Ltd.\ +
    Copyright © 2025 Purslane Ltd.\
    " + handler.get_license() + " \

    " + translate("Slogan_tip") + "

    \
    \ @@ -469,7 +473,7 @@ class MyIdMenu: Reactor.Component { var old_proxy = socks5[0] || ""; var old_username = socks5[1] || ""; var old_password = socks5[2] || ""; - msgbox("custom-server", "Socks5 Proxy",
    + msgbox("custom-server", "Socks5 Proxy",
    {translate("Server")}:
    {translate("Username")}:
    {translate("Password")}:
    @@ -574,8 +578,6 @@ class App: Reactor.Component
    {!is_win || handler.is_installed() ? "": } - {software_update_url ? : ""} - {is_win && handler.is_installed() && !software_update_url && handler.is_installed_lower_version() ? : ""} {is_can_screen_recording ? "": } {is_can_screen_recording && !handler.is_process_trusted(false) ? : ""} {!service_stopped && is_can_screen_recording && handler.is_process_trusted(false) && handler.is_installed() && !handler.is_installed_daemon(false) ? : ""} @@ -585,14 +587,6 @@ class App: Reactor.Component
    -
    -
    {translate('Control Remote Desktop')}
    - -
    - - -
    -
    @@ -644,7 +638,7 @@ function download(from, to, args..) { case 1: rqp.params = p; break; case 2: rqp.headers = p; break; } - } + } } view.request(rqp); } @@ -690,7 +684,7 @@ class UpdateMe: Reactor.Component { handler.update_me(path); }; var onerror = function(err) { - msgbox("custom-error", "Download Error", "Failed to download"); + msgbox("custom-error", "Download Error", "Failed to download"); }; var onprogress = function(loaded, total) { if (!total) total = 5 * 1024 * 1024; @@ -728,7 +722,7 @@ class TrustMe: Reactor.Component { handler.is_process_trusted(true); watch_trust(); } - + event click $(#help-me) { handler.open_url(translate("doc_mac_permission")); } @@ -748,7 +742,7 @@ class CanScreenRecording: Reactor.Component { handler.is_can_screen_recording(true); watch_screen_recording(); } - + event click $(#help-me) { handler.open_url(translate("doc_mac_permission")); } @@ -896,7 +890,7 @@ class PasswordArea: Reactor.Component { function render() { var me = this; self.timer(1ms, function() { me.toggleMenuState() }); - return + return
    {translate('One-time Password')}
    @@ -954,7 +948,7 @@ class PasswordArea: Reactor.Component { var approve_mode= handler.get_option('approve-mode'); var show_password = approve_mode != 'click'; if(show_password && temporaryPasswordLengthMenu) temporaryPasswordLengthMenu.update({show: true }); - var menu = $(menu#edit-password-context); + var menu = $(menu#edit-password-context); me.popup(menu); } @@ -1053,7 +1047,7 @@ function updatePasswordArea() { } if (update) passwordArea.update(); updatePasswordArea(); - }); + }); } updatePasswordArea(); @@ -1193,14 +1187,14 @@ function checkConnectStatus() { app.update(); } check_if_overlay(); - checkConnectStatus(); - }); -} - -var enter = false; -function self.onMouse(evt) { - switch(evt.type) { - case Event.MOUSE_ENTER: + checkConnectStatus(); + }); +} + +var enter = false; +function self.onMouse(evt) { + switch(evt.type) { + case Event.MOUSE_ENTER: enter = true; check_if_overlay(); break; @@ -1236,9 +1230,9 @@ function set_local_user_info(user) { function login() { var name0 = getUserName(); var pass0 = ''; - msgbox("custom-login", translate('Login'),
    -
    {translate('Username')}:
    -
    {translate('Password')}:
    + msgbox("custom-login", translate('Login'),
    +
    {translate('Username')}:
    +
    {translate('Password')}:
    , "", function(res=null, show_progress) { if (!res) return; show_progress(); @@ -1287,11 +1281,11 @@ function on_2fa_check(last_msg) { const secret = last_msg.secret; const emailHint = last_msg.user.email; - msgbox("custom-2fa-verification-code", translate('Verification code'),
    + msgbox("custom-2fa-verification-code", translate('Verification code'),
    { isEmailCheck &&
    {translate('Email')}:{emailHint}
    } -
    {translate(isEmailCheck ? 'Verification code' : '2FA code')}:
    +
    {translate(isEmailCheck ? 'Verification code' : '2FA code')}:
    { isEmailCheck &&
    {translate('verification_tip')}
    } -
    , "", +
    , "", function(res=null, show_progress) { if (!res) return; show_progress(); @@ -1312,7 +1306,7 @@ function on_2fa_check(last_msg) { secret: secret, deviceInfo: getDeviceInfo() }; - httpRequest(url + "/api/login", #post, loginData, + httpRequest(url + "/api/login", #post, loginData, function(data) { if (data.error) { abLoading = false; @@ -1358,7 +1352,8 @@ function logout() { } function refreshCurrentUser() { - if (!handler.get_local_option("access_token")) return; + var token = handler.get_local_option("access_token"); + if (!token) { return; } abLoading = true; abError = ""; app.update(); @@ -1370,6 +1365,10 @@ function refreshCurrentUser() { handleAbError(data.error); return; } + if (!handler.verify_login(data.verifier, token)) { + handleAbError("Please update your self-hosting server Pro to latest version"); + return; + } set_local_user_info(data); myIdMenu.update(); getAb(); @@ -1379,7 +1378,7 @@ function refreshCurrentUser() { } handleAbError(err); }, getHttpHeaders()); -} +} function getHttpHeaders() { return "Authorization: Bearer " + handler.get_local_option("access_token"); diff --git a/src/ui/msgbox.tis b/src/ui/msgbox.tis index 34f6b04432d..542691f5fd7 100644 --- a/src/ui/msgbox.tis +++ b/src/ui/msgbox.tis @@ -132,6 +132,17 @@ class MsgboxComponent: Reactor.Component { return this.type.indexOf("skip") >= 0; } + function getScreenshotButtons() { + var isScreenshot = this.type.indexOf("take-screenshot") >= 0; + return isScreenshot + ?
    + + + +
    + : ""; + } + function render() { this.set_outline_focus(); var color = this.getColor(); @@ -170,6 +181,7 @@ class MsgboxComponent: Reactor.Component { {hasOk || this.hasRetry ? : ""} {hasLink ? : ""} {hasClose ? : ""} + {this.getScreenshotButtons()}
    @@ -245,6 +257,39 @@ class MsgboxComponent: Reactor.Component { this.close(); } } + + event click $(button#screenshotSaveAs) { + this.close(); + + handler.leave(handler.get_keyboard_mode()); + const filter = "Png file (*.png)"; + const defaultExt = "png"; + const initialPath = System.path(#USER_DOCUMENTS, "screenshot"); + const caption = "Save as"; + var url = view.selectFile(#save, filter, defaultExt, initialPath, caption); + handler.enter(handler.get_keyboard_mode()); + if(url) { + var res = handler.handle_screenshot("0:" + URL.toPath(url)); + if (res) { + msgbox("custom-error-nocancel-nook-hasclose", "Take screenshot", res, "", function() {}); + } + } else { + handler.handle_screenshot("2"); + } + } + + event click $(button#screenshotCopyToClip) { + this.close(); + var res = handler.handle_screenshot("1"); + if (res) { + msgbox("custom-error-nocancel-nook-hasclose", "Take screenshot", res, "", function() {}); + } + } + + event click $(button#screenshotCancel) { + this.close(); + handler.handle_screenshot("2"); + } event keydown (evt) { if (!evt.shortcutKey) { @@ -317,6 +362,9 @@ class MsgboxComponent: Reactor.Component { if (this.type == "multiple-sessions-nocancel") { values.sid = (this.$$(select))[0].value; } + if (this.type == "remote-printer-selector") { + values.name = (this.$$(select))[0].value; + } return values; } diff --git a/src/ui/printer.tis b/src/ui/printer.tis new file mode 100644 index 00000000000..c2848260191 --- /dev/null +++ b/src/ui/printer.tis @@ -0,0 +1,41 @@ +include "sciter:reactor.tis"; + +handler.printerRequest = function(id, path) { + show_printer_selector(id, path); +}; + +function show_printer_selector(id, path) +{ + var names = handler.get_printer_names(); + msgbox("remote-printer-selector", "Incoming Print Job", , "", function(res=null) { + if (res && res.name) { + handler.on_printer_selected(id, path, res.name); + } + }, 180); +} + +class PrinterComponent extends Reactor.Component { + this var names = []; + this var jobTip = translate("print-incoming-job-confirm-tip"); + + function this(params) { + if (params && params.names) { + this.names = params.names; + } + } + + function render() { + return
    +
    {translate("print-incoming-job-confirm-tip")}
    +
    +
    + +
    +
    +
    ; + } +} diff --git a/src/ui/remote.html b/src/ui/remote.html index d58c3449bd8..70e909d17d2 100644 --- a/src/ui/remote.html +++ b/src/ui/remote.html @@ -15,6 +15,7 @@ include "port_forward.tis"; include "grid.tis"; include "header.tis"; + include "printer.tis";
    diff --git a/src/ui/remote.rs b/src/ui/remote.rs index 0296d82bda5..f67f3790294 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -66,6 +66,39 @@ impl SciterHandler { } displays_value } + + fn make_platform_additions(data: &str) -> Option { + if let Ok(v2) = serde_json::from_str::>(data) { + let mut value = Value::map(); + for (k, v) in v2 { + match v { + serde_json::Value::String(s) => { + value.set_item(k, s); + } + serde_json::Value::Number(n) => { + if let Some(n) = n.as_i64() { + value.set_item(k, n as i32); + } else if let Some(n) = n.as_f64() { + value.set_item(k, n); + } + } + serde_json::Value::Bool(b) => { + value.set_item(k, b); + } + _ => { + // ignore for now + } + } + } + if value.len() > 0 { + return Some(value); + } else { + None + } + } else { + None + } + } } impl InvokeUiSession for SciterHandler { @@ -145,8 +178,11 @@ impl InvokeUiSession for SciterHandler { self.call("setCursorPosition", &make_args!(cp.x, cp.y)); } - fn set_connection_type(&self, is_secured: bool, direct: bool) { - self.call("setConnectionType", &make_args!(is_secured, direct)); + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str) { + self.call( + "setConnectionType", + &make_args!(is_secured, direct, stream_type.to_string()), + ); } fn set_fingerprint(&self, _fingerprint: String) {} @@ -245,6 +281,9 @@ impl InvokeUiSession for SciterHandler { pi_sciter.set_item("displays", Self::make_displays_array(&pi.displays)); pi_sciter.set_item("current_display", pi.current_display); pi_sciter.set_item("version", pi.version.clone()); + if let Some(v) = Self::make_platform_additions(&pi.platform_additions) { + pi_sciter.set_item("platform_additions", v); + } self.call("updatePi", &make_args!(pi_sciter)); } @@ -277,12 +316,10 @@ impl InvokeUiSession for SciterHandler { 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(); } + _ => {} } } @@ -339,6 +376,19 @@ impl InvokeUiSession for SciterHandler { fn update_record_status(&self, start: bool) { self.call("updateRecordStatus", &make_args!(start)); } + + fn printer_request(&self, id: i32, path: String) { + self.call("printerRequest", &make_args!(id, path)); + } + + fn handle_screenshot_resp(&self, _sid: String, msg: String) { + self.call("screenshot", &make_args!(msg)); + } + + fn handle_terminal_response(&self, _response: TerminalResponse) { + // Terminal support is not implemented for Sciter UI + // This is a stub implementation to satisfy the trait requirements + } } pub struct SciterSession(Session); @@ -451,6 +501,8 @@ impl sciter::EventHandler for SciterSession { fn get_chatbox(); fn get_icon(); fn get_home_dir(); + fn get_next_job_id(); + fn update_next_job_id(i32); fn read_dir(String, bool); fn remove_dir(i32, String, bool); fn create_dir(i32, String, bool); @@ -462,8 +514,8 @@ impl sciter::EventHandler for SciterSession { fn confirm_delete_files(i32, i32); fn set_no_confirm(i32); fn cancel_job(i32); - fn send_files(i32, String, String, i32, bool, bool); - fn add_job(i32, String, String, i32, bool, bool); + fn send_files(i32, i32, String, String, i32, bool, bool); + fn add_job(i32, i32, String, String, i32, bool, bool); fn resume_job(i32, bool); fn get_platform(bool); fn get_path_sep(bool); @@ -483,6 +535,9 @@ impl sciter::EventHandler for SciterSession { fn save_custom_image_quality(i32); fn refresh_video(i32); fn record_screen(bool); + fn is_screenshot_supported(); + fn take_screenshot(i32, String); + fn handle_screenshot(String); fn get_toggle_option(String); fn is_privacy_mode_supported(); fn toggle_option(String); @@ -493,13 +548,16 @@ impl sciter::EventHandler for SciterSession { fn is_keyboard_mode_supported(String); fn save_keyboard_mode(String); fn alternative_codecs(); - fn change_prefer_codec(); + fn update_supported_decodings(); fn restart_remote_device(); fn request_voice_call(); fn close_voice_call(); fn version_cmp(String, String); fn set_selected_windows_session_id(String); fn is_recording(); + fn has_file_clipboard(); + fn get_printer_names(); + fn on_printer_selected(i32, String, String); } } @@ -517,6 +575,8 @@ impl SciterSession { let conn_type = if cmd.eq("--file-transfer") { ConnType::FILE_TRANSFER + } else if cmd.eq("--view-camera") { + ConnType::VIEW_CAMERA } else if cmd.eq("--port-forward") { ConnType::PORT_FORWARD } else if cmd.eq("--rdp") { @@ -607,6 +667,10 @@ impl SciterSession { self.send_selected_session_id(u_sid); } + fn has_file_clipboard(&self) -> bool { + cfg!(any(target_os = "windows", feature = "unix-file-copy-paste")) + } + fn get_port_forwards(&mut self) -> Value { let port_forwards = self.lc.read().unwrap().port_forwards.clone(); let mut v = Value::array(0); @@ -795,6 +859,26 @@ impl SciterSession { fn version_cmp(&self, v1: String, v2: String) -> i32 { (hbb_common::get_version_number(&v1) - hbb_common::get_version_number(&v2)) as i32 } + + fn get_printer_names(&self) -> Value { + #[cfg(target_os = "windows")] + let printer_names = crate::platform::windows::get_printer_names().unwrap_or_default(); + #[cfg(not(target_os = "windows"))] + let printer_names: Vec = vec![]; + let mut v = Value::array(0); + for name in printer_names { + v.push(name); + } + v + } + + fn on_printer_selected(&self, id: i32, path: String, printer_name: String) { + self.printer_response(id, path, printer_name); + } + + fn handle_screenshot(&self, action: String) -> String { + crate::client::screenshot::handle_screenshot(action) + } } pub fn make_fd(id: i32, entries: &Vec, only_count: bool) -> Value { diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs index a3373f8ccd7..880f0ca61df 100644 --- a/src/ui_cm_interface.rs +++ b/src/ui_cm_interface.rs @@ -3,8 +3,11 @@ use crate::ipc::ClipboardNonFile; #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::ipc::Connection; #[cfg(not(any(target_os = "ios")))] -use crate::ipc::{self, Data}; -#[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] +use crate::{ + clipboard::ClipboardSide, + ipc::{self, Data}, +}; +#[cfg(target_os = "windows")] use clipboard::ContextSend; #[cfg(not(any(target_os = "android", target_os = "ios")))] use hbb_common::tokio::sync::mpsc::unbounded_channel; @@ -44,6 +47,8 @@ pub struct Client { pub authorized: bool, pub disconnected: bool, pub is_file_transfer: bool, + pub is_view_camera: bool, + pub is_terminal: bool, pub port_forward: String, pub name: String, pub peer_id: String, @@ -71,9 +76,9 @@ struct IpcTaskRunner { close: bool, running: bool, conn_id: i32, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled: bool, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled_peer: bool, } @@ -125,6 +130,8 @@ impl ConnectionManager { &self, id: i32, is_file_transfer: bool, + is_view_camera: bool, + is_terminal: bool, port_forward: String, peer_id: String, name: String, @@ -144,6 +151,8 @@ impl ConnectionManager { authorized, disconnected: false, is_file_transfer, + is_view_camera, + is_terminal, port_forward, name: name.clone(), peer_id: peer_id.clone(), @@ -169,7 +178,7 @@ impl ConnectionManager { } #[inline] - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] fn is_authorized(&self, id: i32) -> bool { CLIENTS .read() @@ -190,12 +199,9 @@ impl ConnectionManager { .map(|c| c.disconnected = true); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { - let _ = ContextSend::proc(|context| -> ResultType<()> { - context.empty_clipboard(id)?; - Ok(()) - }); + crate::clipboard::try_empty_clipboard_files(ClipboardSide::Host, id); } #[cfg(any(target_os = "android"))] @@ -203,7 +209,7 @@ impl ConnectionManager { .read() .unwrap() .iter() - .filter(|(_k, v)| !v.is_file_transfer) + .filter(|(_k, v)| !v.is_file_transfer && !v.is_terminal) .next() .is_none() { @@ -345,31 +351,40 @@ impl IpcTaskRunner { // for tmp use, without real conn id let mut write_jobs: Vec = Vec::new(); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] let is_authorized = self.cm.is_authorized(self.conn_id); - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] - let rx_clip1; + #[cfg(target_os = "windows")] + let rx_clip_holder; let mut rx_clip; let _tx_clip; - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] if self.conn_id > 0 && is_authorized { log::debug!("Clipboard is enabled from client peer: type 1"); - rx_clip1 = clipboard::get_rx_cliprdr_server(self.conn_id); - rx_clip = rx_clip1.lock().await; + let conn_id = self.conn_id; + rx_clip_holder = ( + clipboard::get_rx_cliprdr_server(conn_id), + Some(crate::SimpleCallOnReturn { + b: true, + f: Box::new(move || { + clipboard::remove_channel_by_conn_id(conn_id); + }), + }), + ); + rx_clip = rx_clip_holder.0.lock().await; } else { log::debug!("Clipboard is enabled from client peer, actually useless: type 2"); let rx_clip2; (_tx_clip, rx_clip2) = unbounded_channel::(); - rx_clip1 = Arc::new(TokioMutex::new(rx_clip2)); - rx_clip = rx_clip1.lock().await; + rx_clip_holder = (Arc::new(TokioMutex::new(rx_clip2)), None); + rx_clip = rx_clip_holder.0.lock().await; } - #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))] + #[cfg(not(target_os = "windows"))] { (_tx_clip, rx_clip) = unbounded_channel::(); } - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { if ContextSend::is_enabled() { log::debug!("Clipboard is enabled"); @@ -393,11 +408,11 @@ 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, block_input, from_switch} => { + Data::Login{id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, block_input, 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, block_input, from_switch, self.tx.clone()); + self.cm.add_connection(id, is_file_transfer, is_view_camera, is_terminal, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, block_input, from_switch, self.tx.clone()); self.conn_id = id; - #[cfg(any(target_os = "linux", target_os = "windows", target_os = "macos"))] + #[cfg(target_os = "windows")] { self.file_transfer_enabled = _file_transfer_enabled; } @@ -438,34 +453,31 @@ impl IpcTaskRunner { Data::FileTransferLog((action, log)) => { self.cm.ui_handler.file_transfer_log(&action, &log); } - #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[cfg(target_os = "windows")] Data::ClipboardFile(_clip) => { - #[cfg(any(target_os = "windows", target_os="linux", target_os = "macos"))] - { - let is_stopping_allowed = _clip.is_beginning_message(); - let is_clipboard_enabled = ContextSend::is_enabled(); - let file_transfer_enabled = self.file_transfer_enabled; - let stop = !is_stopping_allowed && !(is_clipboard_enabled && file_transfer_enabled); - log::debug!( - "Process clipboard message from client peer, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}", - stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled); - if stop { - ContextSend::set_is_stopped(); - } else { - if !is_authorized { - log::debug!("Clipboard message from client peer, but not authorized"); - continue; - } - let conn_id = self.conn_id; - let _ = ContextSend::proc(|context| -> ResultType<()> { - context.server_clip_file(conn_id, _clip) - .map_err(|e| e.into()) - }); + let is_stopping_allowed = _clip.is_beginning_message(); + let is_clipboard_enabled = ContextSend::is_enabled(); + let file_transfer_enabled = self.file_transfer_enabled; + let stop = !is_stopping_allowed && !(is_clipboard_enabled && file_transfer_enabled); + log::debug!( + "Process clipboard message from client peer, stop: {}, is_stopping_allowed: {}, is_clipboard_enabled: {}, file_transfer_enabled: {}", + stop, is_stopping_allowed, is_clipboard_enabled, file_transfer_enabled); + if stop { + ContextSend::set_is_stopped(); + } else { + if !is_authorized { + log::debug!("Clipboard message from client peer, but not authorized"); + continue; } + let conn_id = self.conn_id; + let _ = ContextSend::proc(|context| -> ResultType<()> { + context.server_clip_file(conn_id, _clip) + .map_err(|e| e.into()) + }); } } Data::ClipboardFileEnabled(_enabled) => { - #[cfg(any(target_os= "windows",target_os ="linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { self.file_transfer_enabled_peer = _enabled; } @@ -543,7 +555,7 @@ impl IpcTaskRunner { } match &data { Data::SwitchPermission{name: _name, enabled: _enabled} => { - #[cfg(any(target_os="linux", target_os="windows", target_os = "macos"))] + #[cfg(target_os = "windows")] if _name == "file" { self.file_transfer_enabled = *_enabled; } @@ -558,7 +570,7 @@ impl IpcTaskRunner { }, clip_file = rx_clip.recv() => match clip_file { Some(_clip) => { - #[cfg(any(target_os = "windows", target_os ="linux", target_os = "macos"))] + #[cfg(target_os = "windows")] { let is_stopping_allowed = _clip.is_stopping_allowed(); let is_clipboard_enabled = ContextSend::is_enabled(); @@ -602,9 +614,9 @@ impl IpcTaskRunner { close: true, running: true, conn_id: 0, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled: false, - #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "windows")] file_transfer_enabled_peer: false, }; @@ -623,13 +635,7 @@ impl IpcTaskRunner { #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main(flavor = "current_thread")] pub async fn start_ipc(cm: ConnectionManager) { - #[cfg(any( - target_os = "windows", - all( - any(target_os = "linux", target_os = "macos"), - feature = "unix-file-copy-paste" - ), - ))] + #[cfg(target_os = "windows")] ContextSend::enable(option2bool( OPTION_ENABLE_FILE_TRANSFER, &Config::get_option(OPTION_ENABLE_FILE_TRANSFER), @@ -672,6 +678,8 @@ pub async fn start_listen( Some(Data::Login { id, is_file_transfer, + is_view_camera, + is_terminal, port_forward, peer_id, name, @@ -690,6 +698,8 @@ pub async fn start_listen( cm.add_connection( id, is_file_transfer, + is_view_camera, + is_terminal, port_forward, peer_id, name, @@ -739,6 +749,8 @@ async fn handle_fs( tx: &UnboundedSender, tx_log: Option<&UnboundedSender>, ) { + use std::path::PathBuf; + use hbb_common::fs::serialize_transfer_job; match fs { @@ -780,8 +792,9 @@ async fn handle_fs( // dummy remote, show_hidden, is_remote let mut job = fs::TransferJob::new_write( id, + fs::JobType::Generic, "".to_string(), - path, + fs::DataSource::FilePath(PathBuf::from(&path)), file_num, false, false, @@ -800,27 +813,24 @@ async fn handle_fs( write_jobs.push(job); } ipc::FS::CancelWrite { id } => { - if let Some(job) = fs::get_job(id, write_jobs) { + if let Some(job) = fs::remove_job(id, write_jobs) { job.remove_download_file(); tx_log.map(|tx: &UnboundedSender| { - tx.send(serialize_transfer_job(job, false, true, "")) + tx.send(serialize_transfer_job(&job, false, true, "")) }); - fs::remove_job(id, write_jobs); } } ipc::FS::WriteDone { id, file_num } => { - if let Some(job) = fs::get_job(id, write_jobs) { + if let Some(job) = fs::remove_job(id, write_jobs) { job.modify_time(); send_raw(fs::new_done(id, file_num), tx); - tx_log.map(|tx| tx.send(serialize_transfer_job(job, true, false, ""))); - fs::remove_job(id, write_jobs); + tx_log.map(|tx| tx.send(serialize_transfer_job(&job, true, false, ""))); } } ipc::FS::WriteError { id, file_num, err } => { - if let Some(job) = fs::get_job(id, write_jobs) { - tx_log.map(|tx| tx.send(serialize_transfer_job(job, false, false, &err))); + if let Some(job) = fs::remove_job(id, write_jobs) { + tx_log.map(|tx| tx.send(serialize_transfer_job(&job, false, false, &err))); send_raw(fs::new_error(job.id(), err, file_num), tx); - fs::remove_job(job.id(), write_jobs); } } ipc::FS::WriteBlock { @@ -866,32 +876,34 @@ async fn handle_fs( ..Default::default() }; if let Some(file) = job.files().get(file_num as usize) { - let path = get_string(&job.join(&file.name)); - match is_write_need_confirmation(&path, &digest) { - Ok(digest_result) => { - match digest_result { - DigestCheckResult::IsSame => { - req.set_skip(true); - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); - } - DigestCheckResult::NeedConfirm(mut digest) => { - // upload to server, but server has the same file, request - digest.is_upload = is_upload; - let mut msg_out = Message::new(); - let mut fr = FileResponse::new(); - fr.set_digest(digest); - msg_out.set_file_response(fr); - send_raw(msg_out, &tx); - } - DigestCheckResult::NoSuchFile => { - let msg_out = new_send_confirm(req); - send_raw(msg_out, &tx); + if let fs::DataSource::FilePath(p) = &job.data_source { + let path = get_string(&fs::TransferJob::join(p, &file.name)); + match is_write_need_confirmation(&path, &digest) { + Ok(digest_result) => { + match digest_result { + DigestCheckResult::IsSame => { + req.set_skip(true); + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } + DigestCheckResult::NeedConfirm(mut digest) => { + // upload to server, but server has the same file, request + digest.is_upload = is_upload; + let mut msg_out = Message::new(); + let mut fr = FileResponse::new(); + fr.set_digest(digest); + msg_out.set_file_response(fr); + send_raw(msg_out, &tx); + } + DigestCheckResult::NoSuchFile => { + let msg_out = new_send_confirm(req); + send_raw(msg_out, &tx); + } } } - } - Err(err) => { - send_raw(fs::new_error(id, err, file_num), &tx); + Err(err) => { + send_raw(fs::new_error(id, err, file_num), &tx); + } } } } diff --git a/src/ui_interface.rs b/src/ui_interface.rs index 323b651fedb..e17e82fcee6 100644 --- a/src/ui_interface.rs +++ b/src/ui_interface.rs @@ -3,10 +3,7 @@ use hbb_common::password_security; use hbb_common::{ allow_err, bytes::Bytes, - config::{ - self, keys::*, option2bool, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, - RENDEZVOUS_PORT, - }, + config::{self, keys::*, Config, LocalConfig, PeerConfig, CONNECT_TIMEOUT, RENDEZVOUS_PORT}, directories_next, futures::future::join_all, log, @@ -23,7 +20,6 @@ use serde_derive::Serialize; use std::process::Child; use std::{ collections::HashMap, - sync::atomic::{AtomicUsize, Ordering}, sync::{Arc, Mutex}, }; @@ -209,10 +205,11 @@ pub fn use_texture_render() -> bool { #[inline] pub fn get_local_option(key: String) -> String { - LocalConfig::get_option(&key) + crate::get_local_option(&key) } #[inline] +#[cfg(feature = "flutter")] pub fn get_hard_option(key: String) -> String { config::HARD_SETTINGS .read() @@ -429,7 +426,10 @@ pub fn set_option(key: String, value: String) { ipc::set_options(options.clone()).ok(); } #[cfg(any(target_os = "android", target_os = "ios"))] - Config::set_option(key, value); + { + let _nat = crate::CheckTestNatType::new(); + Config::set_option(key, value); + } } #[inline] @@ -479,18 +479,19 @@ pub fn set_socks(proxy: String, username: String, password: String) { ipc::set_socks(socks).ok(); #[cfg(target_os = "android")] { + let _nat = crate::CheckTestNatType::new(); if socks.proxy.is_empty() { Config::set_socks(None); } else { Config::set_socks(Some(socks)); } - crate::common::test_nat_type(); crate::RendezvousMediator::restart(); log::info!("socks updated"); } } #[inline] +#[cfg(feature = "flutter")] pub fn get_proxy_status() -> bool { #[cfg(not(any(target_os = "android", target_os = "ios")))] return ipc::get_proxy_status(); @@ -859,7 +860,7 @@ pub fn video_save_directory(root: bool) -> String { { let drive = std::env::var("SystemDrive").unwrap_or("C:".to_owned()); let dir = - std::path::PathBuf::from(format!("{drive}\\ProgramData\\RustDesk\\recording",)); + std::path::PathBuf::from(format!("{drive}\\ProgramData\\{appname}\\recording",)); return dir.to_string_lossy().to_string(); } } @@ -874,7 +875,7 @@ pub fn video_save_directory(root: bool) -> String { #[cfg(any(target_os = "android", target_os = "ios"))] if let Ok(home) = config::APP_HOME_DIR.read() { let mut path = home.to_owned(); - path.push_str("/RustDesk/ScreenRecord"); + path.push_str(format!("/{appname}/ScreenRecord").as_str()); let dir = try_create(&std::path::Path::new(&path)); if !dir.is_empty() { return dir; @@ -1150,13 +1151,7 @@ async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver String { pub async fn change_id_shared_(id: String, old_id: String) -> &'static str { if !hbb_common::is_valid_custom_id(&id) { + log::debug!( + "debugging invalid id: \"{id}\", len: {}, base64: \"{}\"", + id.len(), + crate::encode64(&id) + ); + let bom = id.trim_start_matches('\u{FEFF}'); + log::debug!("bom: {}", hbb_common::is_valid_custom_id(&bom)); return INVALID_FORMAT; } diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index 17642646414..fcb84da2194 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -20,7 +20,7 @@ use uuid::Uuid; use hbb_common::fs; use hbb_common::{ allow_err, - config::{Config, LocalConfig, PeerConfig}, + config::{keys, Config, LocalConfig, PeerConfig}, get_version_number, log, message_proto::*, rendezvous_proto::ConnType, @@ -29,7 +29,7 @@ use hbb_common::{ sync::mpsc, time::{Duration as TokioDuration, Instant}, }, - Stream, + whoami, Stream, }; use crate::client::io_loop::Remote; @@ -58,6 +58,7 @@ pub struct Session { pub server_clipboard_enabled: Arc>, pub last_change_display: Arc>, pub connection_round_state: Arc>, + pub printer_names: Arc>>, } #[derive(Clone)] @@ -162,6 +163,13 @@ impl SessionPermissionConfig { && *self.server_keyboard_enabled.read().unwrap() && !self.lc.read().unwrap().disable_clipboard.v } + + #[cfg(feature = "unix-file-copy-paste")] + pub fn is_file_clipboard_required(&self) -> bool { + *self.server_keyboard_enabled.read().unwrap() + && *self.server_file_transfer_enabled.read().unwrap() + && self.lc.read().unwrap().enable_file_copy_paste.v + } } impl Session { @@ -183,6 +191,22 @@ impl Session { .eq(&ConnType::FILE_TRANSFER) } + pub fn is_default(&self) -> bool { + self.lc + .read() + .unwrap() + .conn_type + .eq(&ConnType::DEFAULT_CONN) + } + + pub fn is_view_camera(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::VIEW_CAMERA) + } + + pub fn is_terminal(&self) -> bool { + self.lc.read().unwrap().conn_type.eq(&ConnType::TERMINAL) + } + pub fn is_port_forward(&self) -> bool { let conn_type = self.lc.read().unwrap().conn_type; conn_type == ConnType::PORT_FORWARD || conn_type == ConnType::RDP @@ -218,6 +242,10 @@ impl Session { self.lc.read().unwrap().version.clone() } + pub fn get_trackpad_speed(&self) -> i32 { + self.lc.read().unwrap().trackpad_speed + } + pub fn fallback_keyboard_mode(&self) -> String { let peer_version = self.get_peer_version(); let platform = self.peer_platform(); @@ -324,8 +352,8 @@ impl Session { pub fn toggle_option(&self, name: String) { let msg = self.lc.write().unwrap().toggle_option(name.clone()); - #[cfg(not(feature = "flutter"))] - if name == hbb_common::config::keys::OPTION_ENABLE_FILE_COPY_PASTE { + #[cfg(all(target_os = "windows", not(feature = "flutter")))] + if name == keys::OPTION_ENABLE_FILE_COPY_PASTE { self.send(Data::ToggleClipboardFile); } if let Some(msg) = msg { @@ -361,6 +389,13 @@ impl Session { && !self.lc.read().unwrap().disable_clipboard.v } + #[cfg(any(target_os = "windows", feature = "unix-file-copy-paste"))] + pub fn is_file_clipboard_required(&self) -> bool { + *self.server_keyboard_enabled.read().unwrap() + && *self.server_file_transfer_enabled.read().unwrap() + && self.lc.read().unwrap().enable_file_copy_paste.v + } + #[cfg(feature = "flutter")] pub fn refresh_video(&self, display: i32) { if crate::common::is_support_multi_ui_session_num(self.lc.read().unwrap().version) { @@ -390,16 +425,19 @@ impl Session { } pub fn record_screen(&self, start: bool) { - let mut misc = Misc::new(); - misc.set_client_record_status(start); - let mut msg = Message::new(); - msg.set_misc(misc); - self.send(Data::Message(msg)); self.send(Data::RecordScreen(start)); } + pub fn is_screenshot_supported(&self) -> bool { + crate::common::is_support_screenshot_num(self.lc.read().unwrap().version) + } + + pub fn take_screenshot(&self, display: i32, sid: String) { + self.send(Data::TakeScreenshot((display, sid))); + } + pub fn is_recording(&self) -> bool { - self.lc.read().unwrap().record + self.lc.read().unwrap().record_state } pub fn save_custom_image_quality(&self, custom_image_quality: i32) { @@ -426,6 +464,10 @@ impl Session { } } + pub fn save_trackpad_speed(&self, trackpad_speed: i32) { + self.lc.write().unwrap().save_trackpad_speed(trackpad_speed); + } + pub fn set_custom_fps(&self, custom_fps: i32) { let msg = self.lc.write().unwrap().set_custom_fps(custom_fps, true); self.send(Data::Message(msg)); @@ -475,14 +517,14 @@ impl Session { (vp8, av1, h264, h265) } - pub fn change_prefer_codec(&self) { + pub fn update_supported_decodings(&self) { let msg = self.lc.write().unwrap().update_supported_decodings(); self.send(Data::Message(msg)); } pub fn use_texture_render_changed(&self) { self.send(Data::ResetDecoder(None)); - self.change_prefer_codec(); + self.update_supported_decodings(); self.send(Data::Message(LoginConfigHandler::refresh())); } @@ -716,6 +758,56 @@ impl Session { self.send(Data::Message(msg_out)); } + // Terminal methods + pub fn open_terminal(&self, terminal_id: i32, rows: u32, cols: u32) { + let mut action = TerminalAction::new(); + action.set_open(OpenTerminal { + terminal_id, + rows, + cols, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn send_terminal_input(&self, terminal_id: i32, data: String) { + let mut action = TerminalAction::new(); + action.set_data(TerminalData { + terminal_id, + data: bytes::Bytes::from(data.into_bytes()), + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn resize_terminal(&self, terminal_id: i32, rows: u32, cols: u32) { + let mut action = TerminalAction::new(); + action.set_resize(ResizeTerminal { + terminal_id, + rows, + cols, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + + pub fn close_terminal(&self, terminal_id: i32) { + let mut action = TerminalAction::new(); + action.set_close(CloseTerminal { + terminal_id, + ..Default::default() + }); + let mut msg_out = Message::new(); + msg_out.set_terminal_action(action); + self.send(Data::Message(msg_out)); + } + pub fn capture_displays(&self, add: Vec, sub: Vec, set: Vec) { let mut misc = Misc::new(); misc.set_capture_displays(CaptureDisplays { @@ -1394,9 +1486,10 @@ impl Session { #[inline] fn try_change_init_resolution(&self, display: i32) { - if let Some((w, h)) = self.lc.read().unwrap().get_custom_resolution(display) { - self.change_resolution(display, w, h); - } + let Some((w, h)) = self.lc.read().unwrap().get_custom_resolution(display) else { + return; + }; + self.change_resolution(display, w, h); } fn do_change_resolution(&self, display: i32, width: i32, height: i32) { @@ -1457,7 +1550,7 @@ impl Session { self.read_remote_dir(remote_dir, show_hidden); } } - } else { + } else if !self.is_terminal() { self.msgbox( "success", "Successful", @@ -1491,6 +1584,20 @@ impl Session { pub fn get_conn_token(&self) -> Option { self.lc.read().unwrap().get_conn_token() } + + pub fn printer_response(&self, id: i32, path: String, printer_name: String) { + self.printer_names.write().unwrap().insert(id, printer_name); + let to = std::env::temp_dir().join(format!("rustdesk_printer_{id}")); + self.send(Data::SendFiles(( + id, + hbb_common::fs::JobType::Printer, + path, + to.to_string_lossy().to_string(), + 0, + false, + true, + ))); + } } pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { @@ -1507,7 +1614,7 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn set_permission(&self, name: &str, value: bool); fn close_success(&self); fn update_quality_status(&self, qs: QualityStatus); - fn set_connection_type(&self, is_secured: bool, direct: bool); + fn set_connection_type(&self, is_secured: bool, direct: bool, stream_type: &str); fn set_fingerprint(&self, fingerprint: String); fn job_error(&self, id: i32, err: String, file_num: i32); fn job_done(&self, id: i32, file_num: i32); @@ -1556,6 +1663,9 @@ pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default { fn is_multi_ui_session(&self) -> bool; fn update_record_status(&self, start: bool); fn update_empty_dirs(&self, _res: ReadEmptyDirsResponse) {} + fn printer_request(&self, id: i32, path: String); + fn handle_screenshot_resp(&self, sid: String, msg: String); + fn handle_terminal_response(&self, response: TerminalResponse); } impl Deref for Session { @@ -1616,11 +1726,16 @@ impl Interface for Session { self.on_error("No active console user logged on, please connect and logon first."); return; } - } else if !self.is_port_forward() { + } else if !self.is_port_forward() && !self.is_terminal() { if pi.displays.is_empty() { self.lc.write().unwrap().handle_peer_info(&pi); self.update_privacy_mode(); - self.msgbox("error", "Remote Error", "No Displays", ""); + let msg = if self.is_view_camera() { + "No cameras" + } else { + "No displays" + }; + self.msgbox("error", "Error", msg, ""); return; } self.try_change_init_resolution(pi.current_display); @@ -1643,7 +1758,7 @@ impl Interface for Session { self.set_peer_info(&pi); if self.is_file_transfer() { self.close_success(); - } else if !self.is_port_forward() { + } else if !self.is_port_forward() && !self.is_terminal() { self.msgbox( "success", "Successful", @@ -1744,18 +1859,6 @@ impl Session { #[tokio::main(flavor = "current_thread")] pub async fn io_loop(handler: Session, round: u32) { - // It is ok to call this function multiple times. - #[cfg(any( - target_os = "windows", - all( - any(target_os = "linux", target_os = "macos"), - feature = "unix-file-copy-paste" - ) - ))] - if !handler.is_file_transfer() && !handler.is_port_forward() { - clipboard::ContextSend::enable(true); - } - #[cfg(any(target_os = "android", target_os = "ios"))] let (sender, receiver) = mpsc::unbounded_channel::(); #[cfg(not(any(target_os = "android", target_os = "ios")))] diff --git a/src/updater.rs b/src/updater.rs new file mode 100644 index 00000000000..3570130a851 --- /dev/null +++ b/src/updater.rs @@ -0,0 +1,249 @@ +use crate::{common::do_check_software_update, hbbs_http::create_http_client}; +use hbb_common::{bail, config, log, ResultType}; +use std::{ + io::{self, Write}, + path::PathBuf, + sync::{ + atomic::{AtomicUsize, Ordering}, + mpsc::{channel, Receiver, Sender}, + Mutex, + }, + time::{Duration, Instant}, +}; + +enum UpdateMsg { + CheckUpdate, + Exit, +} + +lazy_static::lazy_static! { + static ref TX_MSG : Mutex> = Mutex::new(start_auto_update_check()); +} + +static CONTROLLING_SESSION_COUNT: AtomicUsize = AtomicUsize::new(0); + +const DUR_ONE_DAY: Duration = Duration::from_secs(60 * 60 * 24); + +pub fn update_controlling_session_count(count: usize) { + CONTROLLING_SESSION_COUNT.store(count, Ordering::SeqCst); +} + +pub fn start_auto_update() { + let _sender = TX_MSG.lock().unwrap(); +} + +#[allow(dead_code)] +pub fn manually_check_update() -> ResultType<()> { + let sender = TX_MSG.lock().unwrap(); + sender.send(UpdateMsg::CheckUpdate)?; + Ok(()) +} + +#[allow(dead_code)] +pub fn stop_auto_update() { + let sender = TX_MSG.lock().unwrap(); + sender.send(UpdateMsg::Exit).unwrap_or_default(); +} + +#[inline] +fn has_no_active_conns() -> bool { + let conns = crate::Connection::alive_conns(); + conns.is_empty() && has_no_controlling_conns() +} + +#[cfg(any(not(target_os = "windows"), feature = "flutter"))] +fn has_no_controlling_conns() -> bool { + CONTROLLING_SESSION_COUNT.load(Ordering::SeqCst) == 0 +} + +#[cfg(not(any(not(target_os = "windows"), feature = "flutter")))] +fn has_no_controlling_conns() -> bool { + let app_exe = format!("{}.exe", crate::get_app_name().to_lowercase()); + for arg in [ + "--connect", + "--play", + "--file-transfer", + "--view-camera", + "--port-forward", + "--rdp", + ] { + if !crate::platform::get_pids_of_process_with_first_arg(&app_exe, arg).is_empty() { + return false; + } + } + true +} + +fn start_auto_update_check() -> Sender { + let (tx, rx) = channel(); + std::thread::spawn(move || start_auto_update_check_(rx)); + return tx; +} + +fn start_auto_update_check_(rx_msg: Receiver) { + std::thread::sleep(Duration::from_secs(30)); + if let Err(e) = check_update(false) { + log::error!("Error checking for updates: {}", e); + } + + const MIN_INTERVAL: Duration = Duration::from_secs(60 * 10); + const RETRY_INTERVAL: Duration = Duration::from_secs(60 * 30); + let mut last_check_time = Instant::now(); + let mut check_interval = DUR_ONE_DAY; + loop { + let recv_res = rx_msg.recv_timeout(check_interval); + match &recv_res { + Ok(UpdateMsg::CheckUpdate) | Err(_) => { + if last_check_time.elapsed() < MIN_INTERVAL { + // log::debug!("Update check skipped due to minimum interval."); + continue; + } + // Don't check update if there are alive connections. + if !has_no_active_conns() { + check_interval = RETRY_INTERVAL; + continue; + } + if let Err(e) = check_update(matches!(recv_res, Ok(UpdateMsg::CheckUpdate))) { + log::error!("Error checking for updates: {}", e); + check_interval = RETRY_INTERVAL; + } else { + last_check_time = Instant::now(); + check_interval = DUR_ONE_DAY; + } + } + Ok(UpdateMsg::Exit) => break, + } + } +} + +fn check_update(manually: bool) -> ResultType<()> { + #[cfg(target_os = "windows")] + let is_msi = crate::platform::is_msi_installed()?; + if !(manually || config::Config::get_bool_option(config::keys::OPTION_ALLOW_AUTO_UPDATE)) { + return Ok(()); + } + if !do_check_software_update().is_ok() { + // ignore + return Ok(()); + } + + let update_url = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone(); + if update_url.is_empty() { + log::debug!("No update available."); + } else { + let download_url = update_url.replace("tag", "download"); + let version = download_url.split('/').last().unwrap_or_default(); + #[cfg(target_os = "windows")] + let download_url = if cfg!(feature = "flutter") { + format!( + "{}/rustdesk-{}-x86_64.{}", + download_url, + version, + if is_msi { "msi" } else { "exe" } + ) + } else { + format!("{}/rustdesk-{}-x86-sciter.exe", download_url, version) + }; + log::debug!("New version available: {}", &version); + let client = create_http_client(); + let Some(file_path) = get_download_file_from_url(&download_url) else { + bail!("Failed to get the file path from the URL: {}", download_url); + }; + let mut is_file_exists = false; + if file_path.exists() { + // Check if the file size is the same as the server file size + // If the file size is the same, we don't need to download it again. + let file_size = std::fs::metadata(&file_path)?.len(); + let response = client.head(&download_url).send()?; + if !response.status().is_success() { + bail!("Failed to get the file size: {}", response.status()); + } + let total_size = response + .headers() + .get(reqwest::header::CONTENT_LENGTH) + .and_then(|ct_len| ct_len.to_str().ok()) + .and_then(|ct_len| ct_len.parse::().ok()); + let Some(total_size) = total_size else { + bail!("Failed to get content length"); + }; + if file_size == total_size { + is_file_exists = true; + } else { + std::fs::remove_file(&file_path)?; + } + } + if !is_file_exists { + let response = client.get(&download_url).send()?; + if !response.status().is_success() { + bail!( + "Failed to download the new version file: {}", + response.status() + ); + } + let file_data = response.bytes()?; + let mut file = std::fs::File::create(&file_path)?; + file.write_all(&file_data)?; + } + // We have checked if the `conns`` is empty before, but we need to check again. + // No need to care about the downloaded file here, because it's rare case that the `conns` are empty + // before the download, but not empty after the download. + if has_no_active_conns() { + #[cfg(target_os = "windows")] + update_new_version(is_msi, &version, &file_path); + } + } + Ok(()) +} + +#[cfg(target_os = "windows")] +fn update_new_version(is_msi: bool, version: &str, file_path: &PathBuf) { + log::debug!("New version is downloaded, update begin, is msi: {is_msi}, version: {version}, file: {:?}", file_path.to_str()); + if let Some(p) = file_path.to_str() { + if let Some(session_id) = crate::platform::get_current_process_session_id() { + if is_msi { + match crate::platform::update_me_msi(p, true) { + Ok(_) => { + log::debug!("New version \"{}\" updated.", version); + } + Err(e) => { + log::error!( + "Failed to install the new msi version \"{}\": {}", + version, + e + ); + } + } + } else { + match crate::platform::launch_privileged_process( + session_id, + &format!("{} --update", p), + ) { + Ok(h) => { + if h.is_null() { + log::error!("Failed to update to the new version: {}", version); + } + } + Err(e) => { + log::error!("Failed to run the new version: {}", e); + } + } + } + } else { + log::error!( + "Failed to get the current process session id, Error {}", + io::Error::last_os_error() + ); + } + } else { + // unreachable!() + log::error!( + "Failed to convert the file path to string: {}", + file_path.display() + ); + } +} + +pub fn get_download_file_from_url(url: &str) -> Option { + let filename = url.split('/').last()?; + Some(std::env::temp_dir().join(filename)) +} diff --git a/vcpkg.json b/vcpkg.json index 75cee85b150..394b94614db 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -48,6 +48,16 @@ "name": "libyuv", "host": false }, + { + "name": "mfx-dispatch", + "host": true, + "platform": "((x86 | x64) & (android | linux)) | (windows & !uwp)" + }, + { + "name": "mfx-dispatch", + "host": false, + "platform": "((x86 | x64) & (android | linux)) | (windows & !uwp)" + }, { "name": "ffmpeg", "host": true, @@ -76,7 +86,7 @@ "vcpkg-configuration": { "default-registry": { "kind": "builtin", - "baseline": "f7423ee180c4b7f40d43402c2feb3859161ef625" + "baseline": "6f29f12e82a8293156836ad81cc9bf5af41fe836" }, "overlay-ports": [ "./res/vcpkg" @@ -89,11 +99,7 @@ }, { "name": "amd-amf", - "version": "1.4.29" - }, - { - "name": "mfx-dispatch", - "version": "1.35.1" + "version": "1.4.35" } ] -} \ No newline at end of file +} diff --git a/vdi/README.md b/vdi/README.md deleted file mode 100644 index 85e6ff194b9..00000000000 --- a/vdi/README.md +++ /dev/null @@ -1 +0,0 @@ -# WIP diff --git a/vdi/host/.cargo/config.toml b/vdi/host/.cargo/config.toml deleted file mode 100644 index 70f9eaeb270..00000000000 --- a/vdi/host/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[registries.crates-io] -protocol = "sparse" diff --git a/vdi/host/.gitignore b/vdi/host/.gitignore deleted file mode 100644 index ea8c4bf7f35..00000000000 --- a/vdi/host/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target diff --git a/vdi/host/Cargo.lock b/vdi/host/Cargo.lock deleted file mode 100644 index 0b2e8ca2b8d..00000000000 --- a/vdi/host/Cargo.lock +++ /dev/null @@ -1,2543 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "addr2line" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" - -[[package]] -name = "async-broadcast" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b" -dependencies = [ - "easy-parallel", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-broadcast" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" -dependencies = [ - "event-listener", - "futures-core", - "parking_lot", -] - -[[package]] -name = "async-channel" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" -dependencies = [ - "concurrent-queue", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-executor" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" -dependencies = [ - "async-lock", - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" -dependencies = [ - "async-lock", - "autocfg", - "concurrent-queue", - "futures-lite", - "libc", - "log", - "parking", - "polling", - "slab", - "socket2 0.4.7", - "waker-fn", - "windows-sys 0.42.0", -] - -[[package]] -name = "async-lock" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" -dependencies = [ - "event-listener", - "futures-lite", -] - -[[package]] -name = "async-recursion" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-task" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" - -[[package]] -name = "async-trait" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "backtrace" -version = "0.3.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" -dependencies = [ - "addr2line", - "cc", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - -[[package]] -name = "bit_field" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" - -[[package]] -name = "bumpalo" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" - -[[package]] -name = "bytemuck" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" -dependencies = [ - "serde", -] - -[[package]] -name = "cc" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" -dependencies = [ - "jobserver", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-integer", - "num-traits", - "time", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "clap" -version = "4.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098" -dependencies = [ - "bitflags 2.0.2", - "clap_derive", - "clap_lex", - "is-terminal", - "once_cell", - "strsim", - "termcolor", -] - -[[package]] -name = "clap_derive" -version = "4.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fddf67631444a3a3e3e5ac51c36a5e01335302de677bd78759eaa90ab1f46644" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "codespan-reporting" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[package]] -name = "concurrent-queue" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "confy" -version = "0.4.0" -source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" -dependencies = [ - "directories-next", - "serde", - "thiserror", - "toml", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" -dependencies = [ - "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset 0.7.1", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "cxx" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "directories-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys", -] - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "easy-parallel" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" - -[[package]] -name = "ed25519" -version = "1.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" -dependencies = [ - "signature", -] - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "enumflags2" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "env_logger" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "exr" -version = "1.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd2162b720141a91a054640662d3edce3d50a944a50ffca5313cd951abb35b4" -dependencies = [ - "bit_field", - "flume", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] - -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - -[[package]] -name = "filetime" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "windows-sys 0.45.0", -] - -[[package]] -name = "flate2" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "flexi_logger" -version = "0.25.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eae57842a8221ef13f1f207632d786a175dd13bd8fbdc8be9d852f7c9cf1046" -dependencies = [ - "chrono", - "crossbeam-channel", - "crossbeam-queue", - "glob", - "is-terminal", - "lazy_static", - "log", - "nu-ansi-term", - "regex", - "thiserror", -] - -[[package]] -name = "flume" -version = "0.10.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" -dependencies = [ - "futures-core", - "futures-sink", - "nanorand", - "pin-project", - "spin", -] - -[[package]] -name = "futures" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" - -[[package]] -name = "futures-executor" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" - -[[package]] -name = "futures-lite" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-macro" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" - -[[package]] -name = "futures-task" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" - -[[package]] -name = "futures-util" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", -] - -[[package]] -name = "gif" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "gimli" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" - -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - -[[package]] -name = "half" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" -dependencies = [ - "crunchy", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] - -[[package]] -name = "hbb_common" -version = "0.1.0" -dependencies = [ - "anyhow", - "backtrace", - "bytes", - "chrono", - "confy", - "directories-next", - "dirs-next", - "env_logger", - "filetime", - "flexi_logger", - "futures", - "futures-util", - "lazy_static", - "libc", - "log", - "mac_address", - "machine-uid", - "osascript", - "protobuf", - "protobuf-codegen", - "rand", - "regex", - "serde", - "serde_derive", - "socket2 0.3.19", - "sodiumoxide", - "sysinfo", - "tokio", - "tokio-socks", - "tokio-util", - "winapi", - "zstd", -] - -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - -[[package]] -name = "hermit-abi" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "iana-time-zone" -version = "0.1.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" -dependencies = [ - "cxx", - "cxx-build", -] - -[[package]] -name = "image" -version = "0.24.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "exr", - "gif", - "jpeg-decoder", - "num-rational", - "num-traits", - "png", - "scoped_threadpool", - "tiff", -] - -[[package]] -name = "indexmap" -version = "1.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "is-terminal" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" -dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", - "rustix", - "windows-sys 0.45.0", -] - -[[package]] -name = "itoa" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" - -[[package]] -name = "jobserver" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" -dependencies = [ - "libc", -] - -[[package]] -name = "jpeg-decoder" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" -dependencies = [ - "rayon", -] - -[[package]] -name = "js-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lebe" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" - -[[package]] -name = "libc" -version = "0.2.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" - -[[package]] -name = "libsodium-sys" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" -dependencies = [ - "cc", - "libc", - "pkg-config", - "walkdir", -] - -[[package]] -name = "libusb1-sys" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] - -[[package]] -name = "linux-raw-sys" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" - -[[package]] -name = "lock_api" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "mac_address" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" -dependencies = [ - "nix 0.23.2", - "winapi", -] - -[[package]] -name = "machine-uid" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f1595709b0a7386bcd56ba34d250d626e5503917d05d32cdccddcd68603e212" -dependencies = [ - "winreg", -] - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - -[[package]] -name = "miniz_oxide" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" -dependencies = [ - "adler", -] - -[[package]] -name = "mio" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", -] - -[[package]] -name = "nanorand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" -dependencies = [ - "getrandom", -] - -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - -[[package]] -name = "nix" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", - "memoffset 0.6.5", -] - -[[package]] -name = "nom8" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8" -dependencies = [ - "memchr", -] - -[[package]] -name = "ntapi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi 0.2.6", - "libc", -] - -[[package]] -name = "object" -version = "0.30.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" - -[[package]] -name = "ordered-stream" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "os_str_bytes" -version = "6.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" - -[[package]] -name = "osascript" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc" -dependencies = [ - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys 0.45.0", -] - -[[package]] -name = "pin-project" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" - -[[package]] -name = "png" -version = "0.17.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "flate2", - "miniz_oxide", -] - -[[package]] -name = "polling" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" -dependencies = [ - "autocfg", - "cfg-if", - "libc", - "log", - "wepoll-ffi", - "windows-sys 0.42.0", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro-crate" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66618389e4ec1c7afe67d51a9bf34ff9236480f8d51e7489b7d5ab0303c13f34" -dependencies = [ - "once_cell", - "toml_edit", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "protobuf" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" -dependencies = [ - "bytes", - "once_cell", - "protobuf-support", - "thiserror", -] - -[[package]] -name = "protobuf-codegen" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" -dependencies = [ - "anyhow", - "once_cell", - "protobuf", - "protobuf-parse", - "regex", - "tempfile", - "thiserror", -] - -[[package]] -name = "protobuf-parse" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" -dependencies = [ - "anyhow", - "indexmap", - "log", - "protobuf", - "protobuf-support", - "tempfile", - "thiserror", - "which", -] - -[[package]] -name = "protobuf-support" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" -dependencies = [ - "thiserror", -] - -[[package]] -name = "qemu-display" -version = "0.1.0" -source = "git+https://github.com/rustdesk/qemu-display#e8a0925c2e804aa1eb07ee3027deaf8dd1c71b1d" -dependencies = [ - "async-broadcast 0.3.4", - "async-lock", - "async-trait", - "cfg-if", - "derivative", - "enumflags2", - "futures", - "futures-util", - "libc", - "log", - "once_cell", - "serde", - "serde_bytes", - "serde_repr", - "uds_windows", - "usbredirhost", - "windows", - "zbus", - "zvariant", -] - -[[package]] -name = "qemu-rustdesk" -version = "0.1.0" -dependencies = [ - "async-trait", - "clap", - "hbb_common", - "image", - "qemu-display", - "zbus", -] - -[[package]] -name = "quote" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rayon" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_users" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" -dependencies = [ - "getrandom", - "redox_syscall", - "thiserror", -] - -[[package]] -name = "regex" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "rusb" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703aa035c21c589b34fb5136b12e68fc8dcf7ea46486861381361dd8ebf5cee0" -dependencies = [ - "libc", - "libusb1-sys", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" - -[[package]] -name = "rustix" -version = "0.36.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys", - "windows-sys 0.45.0", -] - -[[package]] -name = "ryu" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped_threadpool" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" - -[[package]] -name = "serde" -version = "1.0.152" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde-xml-rs" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" -dependencies = [ - "log", - "serde", - "thiserror", - "xml-rs", -] - -[[package]] -name = "serde_bytes" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.152" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sha1" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -dependencies = [ - "sha1_smol", -] - -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - -[[package]] -name = "signal-hook-registry" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" - -[[package]] -name = "simd-adler32" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" - -[[package]] -name = "slab" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" - -[[package]] -name = "socket2" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" -dependencies = [ - "cfg-if", - "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "sodiumoxide" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26be3acb6c2d9a7aac28482586a7856436af4cfe7100031d219de2d2ecb0028" -dependencies = [ - "ed25519", - "libc", - "libsodium-sys", - "serde", -] - -[[package]] -name = "spin" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5d6e0250b93c8427a177b849d144a96d5acc57006149479403d7861ab721e34" -dependencies = [ - "lock_api", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sysinfo" -version = "0.28.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f69e0d827cce279e61c2f3399eb789271a8f136d8245edef70f06e3c9601a670" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "ntapi", - "once_cell", - "rayon", - "winapi", -] - -[[package]] -name = "tempfile" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" -dependencies = [ - "cfg-if", - "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "termcolor" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tiff" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" -dependencies = [ - "flate2", - "jpeg-decoder", - "weezl", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "tokio" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" -dependencies = [ - "autocfg", - "bytes", - "libc", - "memchr", - "mio", - "num_cpus", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.4.7", - "tokio-macros", - "windows-sys 0.42.0", -] - -[[package]] -name = "tokio-macros" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-socks" -version = "0.5.1-1" -source = "git+https://github.com/open-trade/tokio-socks#7034e79263ce25c348be072808d7601d82cd892d" -dependencies = [ - "bytes", - "either", - "futures-core", - "futures-sink", - "futures-util", - "pin-project", - "thiserror", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "futures-util", - "hashbrown", - "pin-project-lite", - "slab", - "tokio", - "tracing", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5" - -[[package]] -name = "toml_edit" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c59d8dd7d0dcbc6428bf7aa2f0e823e26e43b3c9aca15bbc9475d23e5fa12b" -dependencies = [ - "indexmap", - "nom8", - "toml_datetime", -] - -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "uds_windows" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" -dependencies = [ - "tempfile", - "winapi", -] - -[[package]] -name = "unicode-ident" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" - -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - -[[package]] -name = "usbredirhost" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87485e4dfeb0176203afd1086f11ed2ead837053143b12b6eed55c598e9393d5" -dependencies = [ - "libc", - "rusb", - "usbredirhost-sys", - "usbredirparser", -] - -[[package]] -name = "usbredirhost-sys" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b27c305da1f7601b665d68948bcfaf9909d443bec94510ab776118ab8afc2c7d" -dependencies = [ - "libusb1-sys", - "pkg-config", - "usbredirparser-sys", -] - -[[package]] -name = "usbredirparser" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f8b5241d7cbb3e08b4677212a9ac001f116f50731c2737d16129a84ecf6a56" -dependencies = [ - "libc", - "usbredirparser-sys", -] - -[[package]] -name = "usbredirparser-sys" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b0e834e187916fc762bccdc9d64e454a0ee58b134f8f7adab321141e8e0d91" -dependencies = [ - "pkg-config", -] - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "waker-fn" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" -dependencies = [ - "bumpalo", - "log", - "once_cell", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.84" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" - -[[package]] -name = "weezl" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" - -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - -[[package]] -name = "which" -version = "4.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" -dependencies = [ - "either", - "libc", - "once_cell", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" - -[[package]] -name = "winreg" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" -dependencies = [ - "winapi", -] - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - -[[package]] -name = "zbus" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ce2de393c874ba871292e881bf3c13a0d5eb38170ebab2e50b4c410eaa222b" -dependencies = [ - "async-broadcast 0.4.1", - "async-channel", - "async-executor", - "async-io", - "async-lock", - "async-recursion", - "async-task", - "async-trait", - "byteorder", - "derivative", - "dirs", - "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "nix 0.24.3", - "once_cell", - "ordered-stream", - "rand", - "serde", - "serde-xml-rs", - "serde_repr", - "sha1", - "static_assertions", - "tracing", - "uds_windows", - "winapi", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13d08f5dc6cf725b693cb6ceacd43cd430ec0664a879188f29e7d7dcd98f96d" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "syn", -] - -[[package]] -name = "zbus_names" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" -dependencies = [ - "serde", - "static_assertions", - "zvariant", -] - -[[package]] -name = "zstd" -version = "0.9.2+zstd.1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2390ea1bf6c038c39674f22d95f0564725fc06034a47129179810b2fc58caa54" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "4.1.3+zstd.1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e99d81b99fb3c2c2c794e3fe56c305c63d5173a16a46b5850b07c935ffc7db79" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "1.6.2+zstd.1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2daf2f248d9ea44454bfcb2516534e8b8ad2fc91bf818a1885495fc42bc8ac9f" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "zune-inflate" -version = "0.2.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01728b79fb9b7e28a8c11f715e1cd8dc2cda7416a007d66cac55cebb3a8ac6b" -dependencies = [ - "simd-adler32", -] - -[[package]] -name = "zvariant" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "903169c05b9ab948ee93fefc9127d08930df4ce031d46c980784274439803e51" -dependencies = [ - "byteorder", - "enumflags2", - "libc", - "serde", - "serde_bytes", - "static_assertions", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cce76636e8fab7911be67211cf378c252b115ee7f2bae14b18b84821b39260b5" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] diff --git a/vdi/host/Cargo.toml b/vdi/host/Cargo.toml deleted file mode 100644 index 0584b469018..00000000000 --- a/vdi/host/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "qemu-rustdesk" -version = "0.1.0" -authors = ["rustdesk "] -edition = "2021" - -[dependencies] -qemu-display = { git = "https://github.com/rustdesk/qemu-display" } -hbb_common = { path = "../../libs/hbb_common" } -clap = { version = "4.1", features = ["derive"] } -zbus = { version = "3.14.1" } -image = "0.24" -async-trait = "0.1" diff --git a/vdi/host/README.md b/vdi/host/README.md deleted file mode 100644 index 0283266bf7d..00000000000 --- a/vdi/host/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# RustDesk protocol on QEMU D-Bus display - -``` -sudo apt install libusbredirparser-dev libusbredirhost-dev libusb-1.0-0-dev -``` diff --git a/vdi/host/src/connection.rs b/vdi/host/src/connection.rs deleted file mode 100644 index 9f856fa2e5e..00000000000 --- a/vdi/host/src/connection.rs +++ /dev/null @@ -1,11 +0,0 @@ -use hbb_common::{message_proto::*, tokio, ResultType}; -pub use tokio::sync::{mpsc, Mutex}; -pub struct Connection { - pub tx: mpsc::UnboundedSender, -} - -impl Connection { - pub async fn on_message(&mut self, message: Message) -> ResultType { - Ok(true) - } -} diff --git a/vdi/host/src/console.rs b/vdi/host/src/console.rs deleted file mode 100644 index a342f1a9afc..00000000000 --- a/vdi/host/src/console.rs +++ /dev/null @@ -1,119 +0,0 @@ -use hbb_common::{tokio, ResultType}; -use image::GenericImage; -use qemu_display::{Console, ConsoleListenerHandler, MouseButton}; -use std::{collections::HashSet, sync::Arc}; -pub use tokio::sync::{mpsc, Mutex}; - -#[derive(Debug)] -pub enum Event { - ConsoleUpdate((i32, i32, i32, i32)), - Disconnected, -} - -const PIXMAN_X8R8G8B8: u32 = 0x20020888; -pub type BgraImage = image::ImageBuffer, Vec>; -#[derive(Debug)] -pub struct ConsoleListener { - pub image: Arc>, - pub tx: mpsc::UnboundedSender, -} - -#[async_trait::async_trait] -impl ConsoleListenerHandler for ConsoleListener { - async fn scanout(&mut self, s: qemu_display::Scanout) { - *self.image.lock().await = image_from_vec(s.format, s.width, s.height, s.stride, s.data); - } - - async fn update(&mut self, u: qemu_display::Update) { - let update = image_from_vec(u.format, u.w as _, u.h as _, u.stride, u.data); - let mut image = self.image.lock().await; - if (u.x, u.y) == (0, 0) && update.dimensions() == image.dimensions() { - *image = update; - } else { - image.copy_from(&update, u.x as _, u.y as _).unwrap(); - } - self.tx - .send(Event::ConsoleUpdate((u.x, u.y, u.w, u.h))) - .ok(); - } - - async fn scanout_dmabuf(&mut self, _scanout: qemu_display::ScanoutDMABUF) { - unimplemented!() - } - - async fn update_dmabuf(&mut self, _update: qemu_display::UpdateDMABUF) { - unimplemented!() - } - - async fn mouse_set(&mut self, set: qemu_display::MouseSet) { - dbg!(set); - } - - async fn cursor_define(&mut self, cursor: qemu_display::Cursor) { - dbg!(cursor); - } - - fn disconnected(&mut self) { - self.tx.send(Event::Disconnected).ok(); - } -} - -pub async fn key_event(console: &mut Console, qnum: u32, down: bool) -> ResultType<()> { - if down { - console.keyboard.press(qnum).await?; - } else { - console.keyboard.release(qnum).await?; - } - Ok(()) -} - -fn image_from_vec(format: u32, width: u32, height: u32, stride: u32, data: Vec) -> BgraImage { - if format != PIXMAN_X8R8G8B8 { - todo!("unhandled pixman format: {}", format) - } - if cfg!(target_endian = "big") { - todo!("pixman/image in big endian") - } - let layout = image::flat::SampleLayout { - channels: 4, - channel_stride: 1, - width, - width_stride: 4, - height, - height_stride: stride as _, - }; - let samples = image::flat::FlatSamples { - samples: data, - layout, - color_hint: None, - }; - samples - .try_into_buffer::>() - .or_else::<&str, _>(|(_err, samples)| { - let view = samples.as_view::>().unwrap(); - let mut img = BgraImage::new(width, height); - img.copy_from(&view, 0, 0).unwrap(); - Ok(img) - }) - .unwrap() -} - -fn button_mask_to_set(mask: u8) -> HashSet { - let mut set = HashSet::new(); - if mask & 0b0000_0001 != 0 { - set.insert(MouseButton::Left); - } - if mask & 0b0000_0010 != 0 { - set.insert(MouseButton::Middle); - } - if mask & 0b0000_0100 != 0 { - set.insert(MouseButton::Right); - } - if mask & 0b0000_1000 != 0 { - set.insert(MouseButton::WheelUp); - } - if mask & 0b0001_0000 != 0 { - set.insert(MouseButton::WheelDown); - } - set -} diff --git a/vdi/host/src/lib.rs b/vdi/host/src/lib.rs deleted file mode 100644 index e9f8d7ed3cf..00000000000 --- a/vdi/host/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod server; -mod console; -mod connection; diff --git a/vdi/host/src/main.rs b/vdi/host/src/main.rs deleted file mode 100644 index ea32a028a3c..00000000000 --- a/vdi/host/src/main.rs +++ /dev/null @@ -1,6 +0,0 @@ -fn main() { - hbb_common::init_log(false, ""); - if let Err(err) = qemu_rustdesk::server::run() { - hbb_common::log::error!("{err}"); - } -} diff --git a/vdi/host/src/server.rs b/vdi/host/src/server.rs deleted file mode 100644 index b43bd364f46..00000000000 --- a/vdi/host/src/server.rs +++ /dev/null @@ -1,172 +0,0 @@ -use clap::Parser; -use hbb_common::{ - allow_err, - anyhow::{bail, Context}, - log, - message_proto::*, - protobuf::Message as _, - tokio, - tokio::net::TcpListener, - ResultType, Stream, -}; -use qemu_display::{Console, VMProxy}; -use std::{borrow::Borrow, sync::Arc}; - -use crate::connection::*; -use crate::console::*; - -#[derive(Parser, Debug)] -pub struct SocketAddrArgs { - /// IP address - #[clap(short, long, default_value = "0.0.0.0")] - address: std::net::IpAddr, - /// IP port number - #[clap(short, long, default_value = "21116")] - port: u16, -} - -impl From for std::net::SocketAddr { - fn from(args: SocketAddrArgs) -> Self { - (args.address, args.port).into() - } -} - -#[derive(Parser, Debug)] -struct Cli { - #[clap(flatten)] - address: SocketAddrArgs, - #[clap(short, long)] - dbus_address: Option, -} - -#[derive(Debug)] -struct Server { - vm_name: String, - rx_console: mpsc::UnboundedReceiver, - tx_console: mpsc::UnboundedSender, - rx_conn: mpsc::UnboundedReceiver, - tx_conn: mpsc::UnboundedSender, - image: Arc>, - console: Arc>, -} - -impl Server { - async fn new(vm_name: String, console: Console) -> ResultType { - let width = console.width().await?; - let height = console.height().await?; - let image = BgraImage::new(width as _, height as _); - let (tx_console, rx_console) = mpsc::unbounded_channel(); - let (tx_conn, rx_conn) = mpsc::unbounded_channel(); - Ok(Self { - vm_name, - rx_console, - tx_console, - rx_conn, - tx_conn, - image: Arc::new(Mutex::new(image)), - console: Arc::new(Mutex::new(console)), - }) - } - - async fn stop_console(&self) -> ResultType<()> { - self.console.lock().await.unregister_listener(); - Ok(()) - } - - async fn run_console(&self) -> ResultType<()> { - self.console - .lock() - .await - .register_listener(ConsoleListener { - image: self.image.clone(), - tx: self.tx_console.clone(), - }) - .await?; - Ok(()) - } - - async fn dimensions(&self) -> (u16, u16) { - let image = self.image.lock().await; - (image.width() as u16, image.height() as u16) - } - - async fn handle_connection(&mut self, stream: Stream) -> ResultType<()> { - let mut stream = stream; - self.run_console().await?; - let mut conn = Connection { - tx: self.tx_conn.clone(), - }; - - loop { - tokio::select! { - Some(evt) = self.rx_console.recv() => { - match evt { - _ => {} - } - } - Some(msg) = self.rx_conn.recv() => { - allow_err!(stream.send(&msg).await); - } - res = stream.next() => { - if let Some(res) = res { - match res { - Err(err) => { - bail!(err); - } - Ok(bytes) => { - if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { - match conn.on_message(msg_in).await { - Ok(false) => { - break; - } - Err(err) => { - log::error!("{err}"); - } - _ => {} - } - } - } - } - } else { - bail!("Reset by the peer"); - } - } - } - } - - self.stop_console().await?; - Ok(()) - } -} - -#[tokio::main] -pub async fn run() -> ResultType<()> { - let args = Cli::parse(); - - let listener = TcpListener::bind::(args.address.into()) - .await - .unwrap(); - let dbus = if let Some(addr) = args.dbus_address { - zbus::ConnectionBuilder::address(addr.borrow())? - .build() - .await - } else { - zbus::Connection::session().await - } - .context("Failed to connect to DBus")?; - - let vm_name = VMProxy::new(&dbus).await?.name().await?; - let console = Console::new(&dbus.into(), 0) - .await - .context("Failed to get the console")?; - let mut server = Server::new(format!("qemu-rustdesk ({})", vm_name), console).await?; - loop { - let (stream, addr) = listener.accept().await?; - stream.set_nodelay(true).ok(); - let laddr = stream.local_addr()?; - let stream = Stream::from(stream, laddr); - if let Err(err) = server.handle_connection(stream).await { - log::error!("Connection from {addr} closed: {err}"); - } - } -}